diff --git a/.gitignore b/.gitignore index 124293e..30c4b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.swp .vscode/ Kyuafile +docs/*.[1-9] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1fba95b..1dfb477 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ test-install: image: alpine:latest stage: test script: - - apk add make + - apk add make scdoc - make install PREFIX=/tmp/tiny-cloud tags: - docker-alpine diff --git a/Makefile b/Makefile index 98e32fe..453d7a2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ PREFIX ?= / CLOUDS = $(sort $(notdir $(patsubst %/,%,$(wildcard lib/tiny-cloud/cloud/*/)))) +MAN1PAGES = docs/imds.1 +MAN5PAGES = docs/tiny-cloud.conf.5 docs/cloud-config.5 docs/alpine-config.5 +MAN8PAGES = docs/tiny-cloud.8 SUBPACKAGES = core openrc $(CLOUDS) @@ -8,9 +11,15 @@ SUBPACKAGES = core openrc $(CLOUDS) install: $(SUBPACKAGES) -core: +core: $(MAN1PAGES) $(MAN5PAGES) $(MAN8PAGES) install -Dm755 -t "$(PREFIX)"/usr/bin \ bin/imds + install -Dm644 -t "$(PREFIX)"/usr/share/man/man1 \ + $(MAN1PAGES) + install -Dm644 -t "$(PREFIX)"/usr/share/man/man5 \ + $(MAN5PAGES) + install -Dm644 -t "$(PREFIX)"/usr/share/man/man8 \ + $(MAN8PAGES) install -Dm644 -t "$(PREFIX)"/usr/lib/tiny-cloud \ lib/tiny-cloud/common \ lib/tiny-cloud/init \ @@ -48,3 +57,12 @@ Kyuafile: echo "test_suite('tiny-cloud')" >> $@.tmp echo "include('tests/Kyuafile')" >> $@.tmp mv $@.tmp $@ + +docs/%.1: docs/%.1.scd + scdoc < $< > $@ + +docs/%.5: docs/%.5.scd + scdoc < $< > $@ + +docs/%.8: docs/%.8.scd + scdoc < $< > $@ diff --git a/README.md b/README.md index 3ca948b..046c835 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,17 @@ tiny-cloud --enable That's it! On the next boot, Tiny Cloud will bootstrap the instance. +## Documentation + +Tiny Cloud ships with man pages for its installed commands, configuration, and +supported user-data formats: + +* [`imds(1)`](docs/imds.1.scd) - query instance metadata +* [`tiny-cloud(8)`](docs/tiny-cloud.8.scd) - first-boot initialization entry point +* [`tiny-cloud.conf(5)`](docs/tiny-cloud.conf.5.scd) - system configuration +* [`cloud-config(5)`](docs/cloud-config.5.scd) - tiny-cloud-supported `#cloud-config` subset +* [`alpine-config(5)`](docs/alpine-config.5.scd) - Alpine-specific extension of `cloud-config(5)` + ## Configuration By default, Tiny Cloud expects configuration at `/etc/tiny-cloud.conf`, diff --git a/TODO.md b/TODO.md index ad8487c..deb83ee 100644 --- a/TODO.md +++ b/TODO.md @@ -9,8 +9,6 @@ * Test improvements - each user-data handler should be tested with each cloud. -* `man` pages for `imds` and `tiny-cloud`. - ## FUTURE * Support `vendor-data`? In theory this is a baseline, and `user-data` diff --git a/docs/alpine-config.5.scd b/docs/alpine-config.5.scd new file mode 100644 index 0000000..dde28a0 --- /dev/null +++ b/docs/alpine-config.5.scd @@ -0,0 +1,89 @@ +ALPINE-CONFIG(5) + +# NAME + +alpine-config - Alpine-specific tiny-cloud user-data format + +# DESCRIPTION + +*alpine-config* extends the tiny-cloud-supported *cloud-config*(5) subset with +Alpine-specific package repository and autoinstall features. + +The user-data document must begin with the header: + + #alpine-config + +All keys documented in *cloud-config*(5) are also supported in +*alpine-config*. + +# ALPINE EXTENSIONS + +*apk.cache* + Set the APK cache directory. tiny-cloud creates the directory and points + _/etc/apk/cache_ at it. + +*apk.repositories* + Define repository entries to write to _/etc/apk/repositories_. Supported + fields are *base_url*, *repos*, and *version*. + + If *version* is omitted, tiny-cloud derives it from the running Alpine + version when possible. When *apk* is absent and no repository file exists, + tiny-cloud falls back to *setup-apkrepos -1 -c*. + +*autoinstall* + Control unattended Alpine installation. + + If set to *true*, tiny-cloud selects the largest empty disk automatically and + reboots after installation. + + When expressed as a mapping, supported keys are: + + *disk* + Target disk path, or *auto*. + + *reboot* + Whether to reboot after installation. Defaults to *true*. + + *swapsize* + Swap size in megabytes or gigabytes using *M* or *G* suffixes. + + *lvm* + When set to *true*, request LVM mode for *setup-disk*. + +# NOTES + +Because *alpine-config* includes the *cloud-config*(5) subset, you should use +*cloud-config*(5) for shared keys such as *users*, *write_files*, *packages*, +and *runcmd*, and this page for Alpine-specific additions. + +# EXAMPLES + +A more complete example: + +``` +#alpine-config +user: + name: alpine +ssh_authorized_keys: + - ssh-ed25519 AAAA... user@example.com +apk: + cache: /var/cache/apk + repositories: + - base_url: https://dl-cdn.alpinelinux.org/alpine + version: edge + repos: [ "main", "community" ] +package_update: true +packages: + - curl + - jq +autoinstall: + disk: auto + reboot: false +``` + +This example combines shared *cloud-config*(5) keys with Alpine-specific APK +repository configuration and unattended installation settings. + +# SEE ALSO + +*cloud-config*(5), *tiny-cloud*(8) diff --git a/docs/cloud-config.5.scd b/docs/cloud-config.5.scd new file mode 100644 index 0000000..b4e1c10 --- /dev/null +++ b/docs/cloud-config.5.scd @@ -0,0 +1,101 @@ +CLOUD-CONFIG(5) + +# NAME + +cloud-config - tiny-cloud-supported cloud-config user-data format + +# DESCRIPTION + +*cloud-config* describes the subset of *cloud-init* style user-data supported by +tiny-cloud. + +This is not a full implementation of upstream cloud-init semantics. Only the +keys documented here are supported by tiny-cloud's *#cloud-config* handler. + +The user-data document must begin with the header: + + #cloud-config + +# SUPPORTED KEYS + +*user* + Configure the default login user. tiny-cloud recognizes scalar and mapping + forms, including *name*, *homedir*, *shell*, *primary_group*, and *gecos*. + +*users* + Configure additional users. The first entry may be *default* to keep the + default user. Supported per-user keys are *name*, *gecos*, *homedir*, + *shell*, *primary_group*, *system*, *no_create_home*, *lock_passwd*, + *groups*, *doas*, and *ssh_authorized_keys*. + +*ssh_authorized_keys* + Install SSH keys for the default user selected by *user* or *users*. + +*groups* + Create additional groups. + +*bootcmd* + Run commands during the main phase before later package and file actions. + +*write_files* + Write files to disk. tiny-cloud supports *path*, *content*, *permissions*, + *owner*, *encoding*, and *append*. Supported encodings are *text/plain*, + *base64*, *b64*, *gzip*, *gz*, *gz+base64*, *gzip+base64*, *gz+b64*, and + *gzip+b64*. + +*ntp* + Enable and configure NTP. Supported keys are *enabled* and *ntp_client*. + Recognized client values are *busybox*, *chrony*, and *openntpd*. + +*package_update* + When set to *true*, run *apk update*. + +*package_upgrade* + When set to *true*, run *apk upgrade*. + +*packages* + Install packages with *apk add*. + +*runcmd* + Run commands during the final phase. + +# NOTES + +The implementation is Alpine-focused. Package management and service control are +performed with Alpine tools such as *apk*(8), *rc-update*(8), and +*rc-service*(8). + +For Alpine-specific extensions beyond this subset, see *alpine-config*(5). + +# EXAMPLES + +A more complete example: + +``` +#cloud-config +user: + name: alpine + shell: /bin/ash +ssh_authorized_keys: + - ssh-ed25519 AAAA... user@example.com +groups: + - wheel +package_update: true +packages: + - curl + - jq +write_files: + - path: /etc/motd + permissions: '0644' + content: | + tiny-cloud configured this host +runcmd: + - echo ready >/var/tmp/cloud-ready +``` + +This example sets the default user, installs SSH keys, creates a group, +installs packages, writes a file, and runs a final command. + +# SEE ALSO + +*alpine-config*(5), *tiny-cloud*(8) diff --git a/docs/imds.1.scd b/docs/imds.1.scd new file mode 100644 index 0000000..190a4e8 --- /dev/null +++ b/docs/imds.1.scd @@ -0,0 +1,129 @@ +IMDS(1) + +# NAME + +imds - query instance metadata + +# SYNOPSIS + +*imds* [*-h*|*--help*] { *-e* | *+e* | *-E* | *+E* | *+n* | *+s* | *+t* | *@alias* | _imds-path_ } ... + +# DESCRIPTION + +*imds* queries the instance metadata service used by the current cloud +provider. It loads cloud-specific metadata conventions from tiny-cloud's +provider library and prints the requested metadata values to standard output. + +Arguments are processed from left to right. Output control tokens may be mixed +with metadata queries in a single invocation. + +# OPTIONS + +*-h*, *--help* + Show a short usage summary and exit. + +*-e* + Ignore errors from subsequent metadata queries. + +*+e* + Return to the default behavior of stopping on query errors. + +*-E* + Hide standard error from subsequent metadata queries. + +*+E* + Return to the default behavior of showing standard error. + +*+n* + Write a newline to standard output. + +*+s* + Write a space to standard output. + +*+t* + Write a tab to standard output. + +# ALIASES + +*@hostname* + Print the instance hostname. + +*@local-hostname* + Print the local hostname. + +*@ssh-keys* + Print SSH public keys associated with the instance. + +*@userdata* + Print instance user data. + +*@nics* + Print the provider-specific list of network interfaces. + +*@nic:*[, ...] + Query metadata for a specific network interface. The interface is looked up by + its local link-layer address, then one or more NIC-specific keys are resolved. + +# NIC KEYS + +The following aliases are accepted after *@nic:*: + +*@mac* + Print the interface MAC address as exposed by the metadata service. + +*@ipv4* + Print IPv4 address data for the interface. + +*@ipv6* + Print IPv6 address data for the interface. + +*@ipv4-net* + Print IPv4 subnet information for the interface. + +*@ipv6-net* + Print IPv6 subnet information for the interface. + +*@ipv4-prefix* + Print delegated IPv4 prefix information for the interface. + +*@ipv6-prefix* + Print delegated IPv6 prefix information for the interface. + +In addition to these aliases, raw provider-specific metadata paths may be used. +The output control tokens *-e*, *+e*, *+n*, *+s*, and *+t* are also accepted +inside *@nic:* queries. + +# ENVIRONMENT + +*CLOUD* + Cloud provider name. When set to *auto*, the autodetected provider is used. + +# EXIT STATUS + +*0* + All requested operations succeeded. + +*1* + A query failed and error handling was enabled. + +# EXAMPLES + +Print the instance hostname: + + $ imds @hostname + +Print user data and terminate with a newline: + + $ imds @userdata +n + +Print IPv4 and IPv6 metadata for interface _eth1_: + + $ imds @nic:eth1,@ipv4,@ipv6 + +Query a raw metadata path: + + $ imds meta-data/hostname + +# SEE ALSO + +*tiny-cloud*(8) diff --git a/docs/tiny-cloud.8.scd b/docs/tiny-cloud.8.scd new file mode 100644 index 0000000..243d1d8 --- /dev/null +++ b/docs/tiny-cloud.8.scd @@ -0,0 +1,115 @@ +TINY-CLOUD(8) + +# NAME + +tiny-cloud - perform first-boot initialization for cloud instances + +# SYNOPSIS + +*tiny-cloud* [*-h*|*--help*] { *boot* | *early* | *main* | *final* | *-b*|*--bootstrap* { *complete* | *incomplete* | *status* } | *-E*|*--enable* | *-D*|*--disable* } + +# DESCRIPTION + +*tiny-cloud* performs the initialization steps needed to prepare a cloud +instance during first boot. + +The command operates in phases. Each phase runs a configured list of init +actions from tiny-cloud's provider and user-data libraries. After bootstrap has +been marked complete, later phase invocations exit without doing further work. + +# OPTIONS + +*-h*, *--help* + Show a short usage summary and exit. + +*-b*, *--bootstrap* { *complete* | *incomplete* | *status* } + Manage bootstrap state. + + *complete* + Mark bootstrap as complete. + + *incomplete* + Clear the bootstrap-complete marker. + + *status* + Print *complete* or *incomplete*. + +*-E*, *--enable* + Enable tiny-cloud OpenRC services by recreating the runlevel symlinks for + *tiny-cloud-boot*, *tiny-cloud-early*, *tiny-cloud-main*, and + *tiny-cloud-final*. + +*-D*, *--disable* + Disable tiny-cloud OpenRC services by removing existing *tiny-cloud* runlevel + symlinks. + +# PHASES + +*boot* + Run default early boot actions. By default this includes expanding the root + filesystem, bringing up an ephemeral network when needed, writing a default + network interface configuration, and enabling *sshd*. + +*early* + Run early metadata actions. By default this saves instance user data locally. + +*main* + Run the main configuration actions. By default this creates the default user, + sets the hostname, and installs SSH keys. + +*final* + Run finalization actions. By default this marks bootstrap complete after any + additional configured final actions have succeeded. + +# OPERATION + +When invoked with a phase argument, *tiny-cloud* first checks whether +_/etc/tiny-cloud.disabled_ exists. If it does, the command logs that +tiny-cloud is disabled and exits successfully. + +If bootstrap has already completed, the command logs that the system is already +bootstrapped and exits successfully. + +Otherwise, the selected phase action list is loaded from the tiny-cloud init +library. Each action is run in order. If an action fails, *tiny-cloud* exits +with a non-zero status. + +# FILES + +_/etc/tiny-cloud.conf_ + Main tiny-cloud configuration file. + +_/etc/tiny-cloud.disabled_ + When present, suppresses phase execution. + +_/var/lib/cloud/.bootstrap-complete_ + Bootstrap completion marker. + +_/usr/lib/tiny-cloud/init_ + Init library defining the default phase actions. + +# EXIT STATUS + +*0* + Success. + +*1* + Usage error or action failure. + +# EXAMPLES + +Run the main configuration phase: + + # tiny-cloud main + +Show bootstrap status: + + $ tiny-cloud --bootstrap status + +Disable tiny-cloud OpenRC services: + + # tiny-cloud --disable + +# SEE ALSO + +*imds*(1) diff --git a/docs/tiny-cloud.conf.5.scd b/docs/tiny-cloud.conf.5.scd new file mode 100644 index 0000000..e6d0bbc --- /dev/null +++ b/docs/tiny-cloud.conf.5.scd @@ -0,0 +1,68 @@ +TINY-CLOUD.CONF(5) + +# NAME + +tiny-cloud.conf - configuration for tiny-cloud + +# DESCRIPTION + +*tiny-cloud.conf* configures *tiny-cloud*(8). The file is sourced by the +tiny-cloud shell code during startup and therefore uses shell-style variable +assignments. + +The default system configuration file is _/etc/tiny-cloud.conf_. + +# FORMAT + +The file consists of shell variable assignments of the form: + + NAME=value + +Blank lines and shell comments are ignored. + +# VARIABLES + +*CLOUD*= + Select the cloud provider. The default is *auto*, which enables provider + autodetection. + + Supported values currently include *aws*, *azure*, *gcp*, *hetzner*, + *incus*, *nocloud*, *oci*, and *scaleway*. + +*CLOUD_USER*= + Default user account for instance SSH keys and default-user setup. The default + is *alpine*. + +*IMDS_TOKEN_TTL*= + Metadata token lifetime in seconds for AWS metadata access. This is only used + by the AWS provider. The default is *5*. + +*TINY_CLOUD_VAR*= + State directory used by tiny-cloud. The default is _/var/lib/cloud_. + +*TINY_CLOUD_LOGS*= + Log directory used by tiny-cloud. The default is _/var/log_. + +*SKIP_INIT_ACTIONS*= ... + Whitespace-separated list of init actions to skip during phase execution. + + Common action names include *expand_root*, *set_hostname*, *set_ssh_keys*, + *save_userdata*, *decompress_userdata*, and *run_userdata*. + +# EXAMPLES + +Force the AWS provider: + + CLOUD=aws + +Use a different default login account: + + CLOUD_USER=ec2-user + +Skip hostname and SSH key setup: + + SKIP_INIT_ACTIONS="set_hostname set_ssh_keys" + +# SEE ALSO + +*tiny-cloud*(8), *imds*(1) diff --git a/tests/install.test b/tests/install.test new file mode 100755 index 0000000..313088d --- /dev/null +++ b/tests/install.test @@ -0,0 +1,31 @@ +#!/usr/bin/env atf-sh +# vim:set filetype=sh: +# shellcheck shell=sh + +. $(atf_get_srcdir)/test_env.sh + +init_tests install_layout + +install_layout_body() { + atf_require_prog make + atf_require_prog scdoc + + local prefix="$PWD/stage" + + atf_check -s exit:0 -o ignore -e ignore \ + make -C "$srcdir" install PREFIX="$prefix" + + for path in \ + "$prefix/usr/bin/imds" \ + "$prefix/usr/sbin/tiny-cloud" \ + "$prefix/usr/share/man/man1/imds.1" \ + "$prefix/usr/share/man/man5/cloud-config.5" \ + "$prefix/usr/share/man/man5/alpine-config.5" \ + "$prefix/usr/share/man/man5/tiny-cloud.conf.5" \ + "$prefix/usr/share/man/man8/tiny-cloud.8" + do + if [ ! -f "$path" ]; then + atf_fail "missing installed file: $path" + fi + done +}