diff --git a/CHANGES.md b/CHANGES.md index 9d741aed..c973ab97 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,24 @@ Notable changes between releases. ## Latest +* Upgrade Ignition from v0.35.0 (spec v2.x) to v2.14.0 (spec v3.x) +* Parse Ignition and render forward to the current Ignition spec version + * Ignition is [forward](https://github.com/coreos/ignition/blob/main/config/v3_3/config.go#L61) compatible (a `v3.1` spec can be rendered as `v3.3` safely) +* Recommend preparing Ignition externally (**action required**) + * Use Terraform with [poseidon/terraform-provider-ct](https://github.com/poseidon/terraform-provider-ct) + * For a CLI, use Butane +* Remove support for Container Linux Configs (**action required**) + * Flatcar Linux now supports Ignition v2 (spec v3.x) + * Butane is a suitable YAML format that renders Ignition v2 (spec v3.x) +* Add limited support for Matchbox rendering Butane configs + +For those still templating Container Linux Configs, you may be able to convert to Butane by prepending: + +```yaml +variant: flatcar +version: 1.0.0 +``` + ## v0.9.1 * Add dependabot Go module update automation ([#833](https://github.com/poseidon/matchbox/pull/833)) diff --git a/docs/api-http.md b/docs/api-http.md index 6ad9e983..155917ad 100644 --- a/docs/api-http.md +++ b/docs/api-http.md @@ -101,9 +101,9 @@ coreos: command: start ``` -## Container Linux Config / Ignition Config +## Ignition Config -Finds the profile matching the machine and renders the corresponding Ignition Config with group metadata, selectors, and query params. +Finds the profile matching the machine and renders the corresponding Ignition for machine consumption. ``` GET http://matchbox.foo/ignition?label=value @@ -121,11 +121,11 @@ GET http://matchbox.foo/ignition?label=value ```json { - "ignition": { "version": "2.0.0" }, + "ignition": { "version": "3.3.0" }, "systemd": { "units": [{ "name": "example.service", - "enable": true, + "enabled": true, "contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target" }] } diff --git a/docs/cloud-config.md b/docs/cloud-config.md index f76ce14e..08e18585 100644 --- a/docs/cloud-config.md +++ b/docs/cloud-config.md @@ -1,7 +1,7 @@ # Cloud Config !!! warning - Migrate to [Container Linux Configs](container-linux-config.md). Cloud-Config support will be removed in the future. + Migrate to [Ignition configs](ignition.md). Cloud-Config support will be removed in the future. CoreOS Cloud-Config is a system for configuring machines with a Cloud-Config file or executable script from user-data. Cloud-Config runs in userspace on each boot and implements a subset of the [cloud-init spec](http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data). See the cloud-config [docs](https://coreos.com/os/docs/latest/cloud-config.html) for details. diff --git a/docs/container-linux-config.md b/docs/container-linux-config.md deleted file mode 100644 index 2d88fc55..00000000 --- a/docs/container-linux-config.md +++ /dev/null @@ -1,144 +0,0 @@ -# Container Linux Configs - -A Container Linux Config is a YAML document which declares how Container Linux instances' disks should be provisioned on network boot and first-boot from disk. Configs can declare disk partitions, write files (regular files, systemd units, networkd units, etc.), and configure users. See the Container Linux Config [spec](https://coreos.com/os/docs/latest/configuration.html). - -### Ignition - -Container Linux Configs are validated and converted to *machine-friendly* Ignition configs (JSON) by matchbox when serving to booting machines. [Ignition](https://coreos.com/ignition/docs/latest/), the provisioning utility shipped in Container Linux, will parse and execute the Ignition config to realize the desired configuration. Matchbox users usually only need to write Container Linux Configs. - -*Note: Container Linux directory names are still named "ignition" for historical reasons as outlined below. A future breaking change will rename to "container-linux-config".* - -## Adding Container Linux Configs - -Container Linux Config templates can be added to the `/var/lib/matchbox/ignition` directory or in an `ignition` subdirectory of a custom `-data-path`. Template files may contain [Go template](https://golang.org/pkg/text/template/) elements which will be evaluated with group metadata, selectors, and query params. - -``` -/var/lib/matchbox - ├── cloud - ├── ignition - │   └── k8s-controller.yaml - │   └── etcd.yaml - │   └── k8s-worker.yaml - │   └── raw.ign - └── profiles -``` - -## Referencing in Profiles - -Profiles can include a Container Linux Config for provisioning machines. Specify the Container Linux Config in a [Profile](matchbox.md#profiles) with `ignition_id`. When PXE booting, use the kernel option `coreos.first_boot=1` and `coreos.config.url` to point to the `matchbox` [Ignition endpoint](api-http.md#ignition-config). - -## Examples - -Here is an example Container Linux Config template. Variables will be interpreted using group metadata, selectors, and query params. Matchbox will convert the config to Ignition to serve Container Linux machines. - -ignition/format-disk.yaml.tmpl: - - -```yaml - ---- -storage: - disks: - - device: /dev/sda - wipe_table: true - partitions: - - label: ROOT - filesystems: - - name: root - mount: - device: "/dev/sda1" - format: "ext4" - create: - force: true - options: - - "-LROOT" - files: - - filesystem: root - path: /home/core/foo - mode: 0644 - user: - id: 500 - group: - id: 500 - contents: - inline: | - {{.example_contents}} -{{ if index . "ssh_authorized_keys" }} -passwd: - users: - - name: core - ssh_authorized_keys: - {{ range $element := .ssh_authorized_keys }} - - {{$element}} - {{end}} -{{end}} -``` - - -The Ignition config response (formatted) to a query `/ignition?label=value` for a Container Linux instance supporting Ignition 2.0.0 would be: - -```json -{ - "ignition": { - "version": "2.0.0", - "config": {} - }, - "storage": { - "disks": [ - { - "device": "/dev/sda", - "wipeTable": true, - "partitions": [ - { - "label": "ROOT", - "number": 0, - "size": 0, - "start": 0 - } - ] - } - ], - "filesystems": [ - { - "name": "root", - "mount": { - "device": "/dev/sda1", - "format": "ext4", - "create": { - "force": true, - "options": [ - "-LROOT" - ] - } - } - } - ], - "files": [ - { - "filesystem": "root", - "path": "/home/core/foo", - "contents": { - "source": "data:,Example%20file%20contents%0A", - "verification": {} - }, - "mode": 420, - "user": { - "id": 500 - }, - "group": { - "id": 500 - } - } - ] - }, - "systemd": {}, - "networkd": {}, - "passwd": {} -} -``` - -See [examples/ignition](../examples/ignition) for numerous Container Linux Config template examples. - -### Raw Ignition - -If you prefer to design your own templating solution, raw Ignition files (suffixed with `.ign` or `.ignition`) are served directly. diff --git a/docs/getting-started.md b/docs/getting-started.md index 1812e7f0..b68d391f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -91,7 +91,7 @@ terraform { ### Profiles -Machine profiles specify the kernel, initrd, kernel args, Ignition Config, and other configs (e.g. templated Container Linux Config, Cloud-config, generic) used to network boot and provision a bare-metal machine. The profile below would PXE boot machines using a Fedora CoreOS kernel and initrd (see [assets](api-http.md#assets) to learn about caching for speed), perform a disk install, reboot (first boot from disk), and use a [Fedora CoreOS Config](https://github.com/coreos/fcct/blob/master/docs/configuration-v1_1.md) to generate an Ignition config to provision. +Machine profiles specify the kernel, initrd, kernel args, Ignition Config, or other configs (e.g. templated Butane Config, generic) used to network boot and provision a bare-metal machine. The profile below would PXE boot machines using a Fedora CoreOS kernel and initrd (see [assets](api-http.md#assets) to learn about caching for speed), perform a disk install, reboot (first boot from disk), and use a [Fedora CoreOS Config](https://github.com/coreos/fcct/blob/master/docs/configuration-v1_1.md) to generate an Ignition config to provision. ```tf // Fedora CoreOS profile diff --git a/docs/ignition.md b/docs/ignition.md new file mode 100644 index 00000000..e61211b2 --- /dev/null +++ b/docs/ignition.md @@ -0,0 +1,160 @@ +# Ignition Configs + +[Ignition](https://coreos.github.io/ignition/) configs define how disks should be provisioned (on network boot and first-boot from disk) to partition disks, write files (regular files, systemd units, networkd units, etc.), and configure users. Ignition is used by: + +* Fedora CoreOS +* RHEL CoreOS +* Flatcar Linux + +See the Ignition Config v3.x [specs](https://coreos.github.io/ignition/specs/) for details. + +## Usage + +Ignition configs can be added to the `/var/lib/matchbox/ignition` directory or in an `ignition` subdirectory of a custom `-data-path`. Ignition configs must end in `.ign` or `ignition`. + +``` +/var/lib/matchbox + ├── ignition + │   └── k8s-controller.ign + │   └── k8s-worker.ign + └── profiles +``` + +Matchbox Profiles can set an Ignition config for provisioning machines. Specify the Ignition config in a [Profile](matchbox.md#profiles) with `ignition_id`. + +```json +{ + "id": "worker", + "name": "My Profile", + "boot": { + ... + }, + "ignition_id": "my-ignition.ign" +} +``` + +When PXE booting, set kernel arguments depending on the OS (e.g. `ignition.firstboot` on FCOS, `flatcar.first_boot=yes` on Flatcar). + +* [Fedora CoreOS](https://github.com/poseidon/matchbox/blob/main/examples/profiles/fedora-coreos.json) +* [Flatcar Linux](https://github.com/poseidon/matchbox/blob/main/examples/profiles/flatcar.json) + +Point the `ignition.config.url` or `flatcar.config.url` to point to the `matchbox` [Ignition endpoint](api-http.md#ignition-config). + +Matchbox parses Ignition configs (e.g. `.ign` or `.ignition`) at spec v3.3 or below and renders to the current supported version (v3.3). This relies on Ignition's [forward compatibility](https://github.com/coreos/ignition/blob/main/config/v3_3/config.go#L61). + +## Writing Configs + +Ignition configs can be prepared externally and loaded via the gRPC API, rather than writing Ignition by hand. + +### Terraform + +Terraform can be used to prepare Ignition configs, while providing integrations with external systems and rich templating. Using tools like [poseidon/terraform-provider-ct](https://github.com/poseidon/terraform-provider-ct), you can write Butane config (an easier YAML format), validate configs, and load Ignition into Matchbox ([examples](https://github.com/poseidon/matchbox/tree/main/examples/terraform)). + +Define a Butane config for Fedora CoreOS or Flatcar Linux: + +```yaml +variant: fcos +version: 1.4.0 +passwd: + users: + - name: core + ssh_authorized_keys: + - ssh-key foo +``` + +```yaml +variant: flatcar +version: 1.0.0 +passwd: + users: + - name: core + ssh_authorized_keys: + - ssh-key foo +``` + +Define a `ct_config` data source with strict validation. Optionally use Terraform [templating](https://github.com/poseidon/terraform-provider-ct). + +```tf +data "ct_config" "worker" { + content = file("worker.yaml") + strict = true + pretty_print = false + + snippets = [ + file("units.yaml"), + file("storage.yaml"), + ] +} +``` + +Then render the Butane config to Ignition and use it in a Matchbox Profile. + +```tf +resource "matchbox_profile" "fedora-coreos-install" { + name = "worker" + kernel = "/assets/fedora-coreos/fedora-coreos-${var.os_version}-live-kernel-x86_64" + initrd = [ + "--name main /assets/fedora-coreos/fedora-coreos-${var.os_version}-live-initramfs.x86_64.img" + ] + + args = [ + "initrd=main", + "coreos.live.rootfs_url=${var.matchbox_http_endpoint}/assets/fedora-coreos/fedora-coreos-${var.os_version}-live-rootfs.x86_64.img", + "coreos.inst.install_dev=/dev/vda", + "coreos.inst.ignition_url=${var.matchbox_http_endpoint}/ignition?uuid=$${uuid}&mac=$${mac:hexhyp}", + ] + + raw_ignition = data.ct_config.worker.rendered +} +``` + +See the Terraform [examples](https://github.com/poseidon/matchbox/tree/main/examples#terraform-examples) for details. + +### Butane + +The [Butane](https://coreos.github.io/butane/) command line tool can be used to convert Butane configs (an easier YAML format) to Ignition. Then you can use the Matchbox gRPC API to upload the rendered Ignition to Matchbox for serving to machines on boot. + +See [examples/ignition](../examples/ignition) for Butane config examples. + +### Matchbox Rendering + +While Matchbox recommends preparing Ignition configs externally (e.g. using Terraform's rich templating), Matchbox does still support limited templating and translation features with a builtin Butane converter. + +Specify a Butane config in a [Profile](matchbox.md#profiles) with `ignition_id` (file must not end in `.ign` or `.ignition`). + +```json +{ + "id": "worker", + "name": "My Profile", + "boot": { + ... + }, + "ignition_id": "butane.yaml" +} +``` + +Here is an example Butane config with Matchbox template elements. Template files may contain [Go template](https://golang.org/pkg/text/template/) elements which will be interpreted using group metadata, selectors, and query params. + +```yaml +variant: flatcar +version: 1.0.0 +storage: + files: + - path: /var/home/core/foo + mode: 0644 + contents: + inline: | + {{.example_contents}} + +{{ if index . "ssh_authorized_keys" }} +passwd: + users: + - name: core + ssh_authorized_keys: + {{ range $element := .ssh_authorized_keys }} + - {{$element}} + {{end}} +{{end}} +``` + +Matchbox will use the Butane library to config to the current supported Ignition version. This relies on Ignition's [forward compatibility](https://github.com/coreos/ignition/blob/main/config/v3_3/config.go#L61). diff --git a/docs/matchbox.md b/docs/matchbox.md index d58b8acd..f8458b33 100644 --- a/docs/matchbox.md +++ b/docs/matchbox.md @@ -33,9 +33,9 @@ Prepare `/var/lib/matchbox` with `groups`, `profile`, `ignition`, `cloud`, and ` │   ├── cloud.yaml.tmpl │   └── worker.sh.tmpl ├── ignition - │   └── raw.ign - │   └── etcd.yaml.tmpl - │   └── simple.yaml.tmpl + │   └── worker.ign + │   └── butane.yaml.tmpl + │   └── butane.yaml ├── generic │   └── config.yaml │   └── setup.cfg @@ -53,14 +53,14 @@ The [examples](../examples) directory is a valid data directory with some pre-de ### Profiles -Profiles reference an Ignition config, Cloud-Config, and/or generic config by name and define network boot settings. +Profiles reference an Ignition config, Butane Config, Cloud-Config, and/or generic config by name and define network boot settings. ```json { "id": "etcd", "name": "Container Linux with etcd2", "cloud_id": "", - "ignition_id": "etcd.yaml", + "ignition_id": "worker.ign", "generic_id": "some-service.cfg", "boot": { "kernel": "/assets/coreos/1967.3.0/coreos_production_pxe.vmlinuz", @@ -128,16 +128,16 @@ Group selectors can use any key/value pairs you find useful. However, several la ### Config templates -Profiles can reference various templated configs. Ignition JSON configs can be generated from [Container Linux Config](https://github.com/coreos/container-linux-config-transpiler/blob/master/doc/configuration.md) template files. Cloud-Config templates files can be used to render a script or Cloud-Config. Generic template files can be used to render arbitrary untyped configs (experimental). Each template may contain [Go template](https://golang.org/pkg/text/template/) elements which will be rendered with machine group metadata, selectors, and query params. +Profiles can reference various templated configs. Ignition configs can be provided directly or rendered fro [Butane Config](https://coreos.github.io/butane/) template files. Cloud-Config templates files can be used to render a script or Cloud-Config. Generic template files can be used to render arbitrary untyped configs (experimental). Each template may contain [Go template](https://golang.org/pkg/text/template/) elements which will be rendered with machine group metadata, selectors, and query params. For details and examples: -* [Container Linux Config](container-linux-config.md) +* [Ignition (or Butane)](ignition.md) * [Cloud-Config](cloud-config.md) #### Variables -Within Container Linux Config templates, Cloud-Config templates, or generic templates, you can use group metadata, selectors, or request-scoped query params. For example, a request `/generic?mac=52-54-00-89-d8-10&foo=some-param&bar=b` would match the `node1.json` machine group shown above. If the group's profile ("etcd") referenced a generic template, the following variables could be used. +Within Butane Config templates, Cloud-Config templates, or generic templates, you can use group metadata, selectors, or request-scoped query params. For example, a request `/generic?mac=52-54-00-89-d8-10&foo=some-param&bar=b` would match the `node1.json` machine group shown above. If the group's profile ("etcd") referenced a generic template, the following variables could be used. ``` diff --git a/go.mod b/go.mod index fd817c44..3c309aed 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/poseidon/matchbox go 1.16 require ( + github.com/coreos/butane v0.15.0 github.com/coreos/coreos-cloudinit v1.14.0 github.com/coreos/ignition/v2 v2.14.0 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f @@ -17,5 +18,4 @@ require ( golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect google.golang.org/grpc v1.49.0 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 33586a87..5aecd9a1 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clarketm/json v1.14.1 h1:43bkbTTKKdDx7crs3WHzkrnH6S1EvAF1VZrdFGMmmz4= +github.com/clarketm/json v1.14.1/go.mod h1:ynr2LRfb0fQU34l07csRNBTcivjySLLiY1YzQqKVfdo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -51,20 +53,25 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/butane v0.15.0 h1:PKN1tL5t4iGLrSiJ3gDpf/pPZMQ6JSeVNS811F3tmpM= +github.com/coreos/butane v0.15.0/go.mod h1:5b/piru1RoNVuHCgtvmLTFXPRK2AOziSBt0mX7u6aYI= github.com/coreos/coreos-cloudinit v1.14.0 h1:3bQRJaie3QC8EovAVbxiimLQgdxM6DxP1vJfUCEEl9w= github.com/coreos/coreos-cloudinit v1.14.0/go.mod h1:hV3swhSwq+bRX5apuk57gG+3fsQacgbrZVxjPTqo0zo= github.com/coreos/go-json v0.0.0-20211020211907-c63f628265de h1:qZvNu52Tv7Jfbgxdw3ONHf0BK9UpuSxi9FA9Y+qU5VU= github.com/coreos/go-json v0.0.0-20211020211907-c63f628265de/go.mod h1:lryFBkhadOfv8Jue2Vr/f/Yviw8h1DQPQojbXqEChY0= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/ignition/v2 v2.14.0 h1:KfkCCnA6AK0kts/1zxzzNH5lDMCQN9sqqGcGs+RJVX4= github.com/coreos/ignition/v2 v2.14.0/go.mod h1:wxc4qdYEIHLygzWbVVEuoD7lQGTZmMgX0VjAPYBbeEQ= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/vcontext v0.0.0-20211021162308-f1dbbca7bef4 h1:pfSsrvbjUFGINaPGy0mm2QKQKTdq7IcbUa+nQwsz2UM= github.com/coreos/vcontext v0.0.0-20211021162308-f1dbbca7bef4/go.mod h1:HckqHnP/HI41vS0bfVjJ20u6jD0biI5+68QwZm5Xb9U= +github.com/coreos/vcontext v0.0.0-20220603180515-2076d8d16945 h1:AsQHFyYGc0SwzpQQonNT0WmvtXiok5HK3CNNx2zymP0= +github.com/coreos/vcontext v0.0.0-20220603180515-2076d8d16945/go.mod h1:fLd7QpFpxRdPBbwum8cptYO8RclJJHhJUq1v9V9+ZKw= github.com/coreos/yaml v0.0.0-20141224210557-6b16a5714269 h1:/1sjrpK5Mb6IwyFOKd+u7321tXfNAsj0Ci8CivZmSlo= github.com/coreos/yaml v0.0.0-20141224210557-6b16a5714269/go.mod h1:Bl1D/T9QJhVdu6eFoLrGxN90+admDLGaLz2HXH/VzDc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/matchbox/http/ignition.go b/matchbox/http/ignition.go index 3bddf3a1..fd714a35 100644 --- a/matchbox/http/ignition.go +++ b/matchbox/http/ignition.go @@ -1,9 +1,12 @@ package http import ( + "bytes" "net/http" "strings" + butane "github.com/coreos/butane/config" + "github.com/coreos/butane/config/common" ignition "github.com/coreos/ignition/v2/config/v3_3" "github.com/sirupsen/logrus" @@ -68,44 +71,38 @@ func (s *Server) ignitionHandler(core server.Server) http.Handler { return } - // Container Linux Config template - /* + // Butane Config template (discouraged) - // collect data for rendering - data, err := collectVariables(req, group) - if err != nil { - s.logger.Errorf("error collecting variables: %v", err) - http.NotFound(w, req) - return - } + // collect data for rendering + data, err := collectVariables(req, group) + if err != nil { + s.logger.Errorf("error collecting variables: %v", err) + http.NotFound(w, req) + return + } - // render the template for an Ignition config with data - var buf bytes.Buffer - err = s.renderTemplate(&buf, data, contents) - if err != nil { - http.NotFound(w, req) - return - } + // render the template + var buf bytes.Buffer + err = s.renderTemplate(&buf, data, contents) + if err != nil { + http.NotFound(w, req) + return + } - // Parse bytes into a Container Linux Config - config, ast, report := ct.Parse(buf.Bytes()) - if report.IsFatal() { - s.logger.Errorf("error parsing Container Linux config: %s", report.String()) - http.NotFound(w, req) - return - } + // translate to Ignition + ignBytes, report, err := butane.TranslateBytes(buf.Bytes(), common.TranslateBytesOptions{}) + if err != nil { + s.logger.Errorf("error translating Butane Config: %s", report.String()) + http.NotFound(w, req) + return + } - // Convert Container Linux Config into an Ignition Config - ign, report := ct.Convert(config, "", ast) - if report.IsFatal() { - s.logger.Errorf("error converting Container Linux config: %s", report.String()) - http.NotFound(w, req) - return - } - - s.renderJSON(w, ign) - */ - return + // validate + ign, report, err := ignition.ParseCompatibleVersion(ignBytes) + if err != nil { + s.logger.Warningf("warning parsing Ignition: %s", report.String()) + } + s.renderJSON(w, ign) } return http.HandlerFunc(fn) } diff --git a/matchbox/http/ignition_test.go b/matchbox/http/ignition_test.go index 76994ead..1997fa79 100644 --- a/matchbox/http/ignition_test.go +++ b/matchbox/http/ignition_test.go @@ -15,60 +15,6 @@ import ( fake "github.com/poseidon/matchbox/matchbox/storage/testfakes" ) -/* -func TestIgnitionHandler_V21(t *testing.T) { - content := `{"ignition":{"version":"2.1.0","config":{}},"storage":{},"systemd":{"units":[{"name":"etcd2.service","enable":true}]},"networkd":{},"passwd":{}}` - profile := &storagepb.Profile{ - Id: fake.Group.Profile, - IgnitionId: "file.ign", - } - store := &fake.FixedStore{ - Profiles: map[string]*storagepb.Profile{fake.Group.Profile: profile}, - IgnitionConfigs: map[string]string{"file.ign": content}, - } - logger, _ := logtest.NewNullLogger() - srv := NewServer(&Config{Logger: logger}) - core := server.NewServer(&server.Config{Store: store}) - h := srv.ignitionHandler(core) - - ctx := withGroup(context.Background(), fake.Group) - w := httptest.NewRecorder() - req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) - h.ServeHTTP(w, req) - // assert that: - // - raw Ignition config served directly - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, jsonContentType, w.Header().Get(contentType)) - assert.Equal(t, content, w.Body.String()) -} - -func TestIgnitionHandler_V2_2_JSON(t *testing.T) { - content := `{"ignition":{"version":"2.2.0","config":{}},"storage":{},"systemd":{"units":[{"name":"etcd2.service","enable":true}]},"networkd":{},"passwd":{}}` - profile := &storagepb.Profile{ - Id: fake.Group.Profile, - IgnitionId: "file.ign", - } - store := &fake.FixedStore{ - Profiles: map[string]*storagepb.Profile{fake.Group.Profile: profile}, - IgnitionConfigs: map[string]string{"file.ign": content}, - } - logger, _ := logtest.NewNullLogger() - srv := NewServer(&Config{Logger: logger}) - c := server.NewServer(&server.Config{Store: store}) - h := srv.ignitionHandler(c) - - ctx := withGroup(context.Background(), fake.Group) - w := httptest.NewRecorder() - req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) - h.ServeHTTP(w, req) - // assert that: - // - raw Ignition config served directly - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, jsonContentType, w.Header().Get(contentType)) - assert.Equal(t, content, w.Body.String()) -} -*/ - func TestIgnitionHandler_V33(t *testing.T) { const content = `{"ignition":{"config":{"replace":{"verification":{}}},"proxy":{},"security":{"tls":{}},"timeouts":{},"version":"3.3.0"},"kernelArguments":{},"passwd":{"users":[{"name":"core","sshAuthorizedKeys":["key"]}]},"storage":{},"systemd":{"units":[{"enabled":false,"name":"docker.service"}]}}` profile := &storagepb.Profile{ @@ -89,11 +35,44 @@ func TestIgnitionHandler_V33(t *testing.T) { req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) h.ServeHTTP(w, req) // assert that: - // - raw Ignition config served directly + // - serve Ignition JSON assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, jsonContentType, w.Header().Get(contentType)) assert.Equal(t, content, w.Body.String()) } + +func TestIgnitionHandler_V31(t *testing.T) { + const ign31 = `{"ignition":{"config":{"replace":{"verification":{}}},"proxy":{},"security":{"tls":{}},"timeouts":{},"version":"3.1.0"},"kernelArguments":{},"passwd":{"users":[{"name":"core","sshAuthorizedKeys":["key"]}]},"storage":{},"systemd":{"units":[{"enabled":false,"name":"docker.service"}]}}` + const ign33 = `{"ignition":{"config":{"replace":{"verification":{}}},"proxy":{},"security":{"tls":{}},"timeouts":{},"version":"3.3.0"},"kernelArguments":{},"passwd":{"users":[{"name":"core","sshAuthorizedKeys":["key"]}]},"storage":{},"systemd":{"units":[{"enabled":false,"name":"docker.service"}]}}` + profile := &storagepb.Profile{ + Id: fake.Group.Profile, + IgnitionId: "file.ign", + } + store := &fake.FixedStore{ + Profiles: map[string]*storagepb.Profile{ + fake.Group.Profile: profile, + }, + IgnitionConfigs: map[string]string{ + "file.ign": ign31, + }, + } + logger, _ := logtest.NewNullLogger() + srv := NewServer(&Config{Logger: logger}) + core := server.NewServer(&server.Config{Store: store}) + h := srv.ignitionHandler(core) + + ctx := withGroup(context.Background(), fake.Group) + w := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) + h.ServeHTTP(w, req) + // assert that: + // - older Ignition v3.x converted to compatible latest version (e.g. v3.3) + // - serve Ignition JSON + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, jsonContentType, w.Header().Get(contentType)) + assert.Equal(t, ign33, w.Body.String()) +} + func TestIgnitionHandler_MissingIgnition(t *testing.T) { logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) @@ -107,77 +86,86 @@ func TestIgnitionHandler_MissingIgnition(t *testing.T) { assert.Equal(t, http.StatusNotFound, w.Code) } -/* -func TestIgnitionHandler_CL_YAML(t *testing.T) { - // exercise templating features, not a realistic Container Linux Config template - content := ` +func TestIgnitionHandler_Butane(t *testing.T) { + // exercise templating features, not a realistic Butane template + butane := ` +variant: flatcar +version: 1.0.0 systemd: units: - - name: {{.service_name}}.service - enable: true - name: {{.uuid}}.service - enable: true + enabled: true + contents: {{.pod_network}} - name: {{.request.query.foo}}.service - enable: true + enabled: true contents: {{.request.raw_query}} ` - expectedIgnition := `{"ignition":{"config":{},"security":{"tls":{}},"timeouts":{},"version":"2.2.0"},"networkd":{},"passwd":{},"storage":{},"systemd":{"units":[{"enable":true,"name":"etcd2.service"},{"enable":true,"name":"a1b2c3d4.service"},{"contents":"foo=some-param\u0026bar=b","enable":true,"name":"some-param.service"}]}}` + expectedIgnition := `{"ignition":{"config":{"replace":{"verification":{}}},"proxy":{},"security":{"tls":{}},"timeouts":{},"version":"3.3.0"},"kernelArguments":{},"passwd":{},"storage":{},"systemd":{"units":[{"contents":"10.2.0.0/16","enabled":true,"name":"a1b2c3d4.service"},{"contents":"foo=some-param\u0026bar=b","enabled":true,"name":"some-param.service"}]}}` + store := &fake.FixedStore{ - Profiles: map[string]*storagepb.Profile{fake.Group.Profile: testProfileIgnitionYAML}, - IgnitionConfigs: map[string]string{testProfileIgnitionYAML.IgnitionId: content}, + Profiles: map[string]*storagepb.Profile{ + fake.Group.Profile: testProfileWithButane, + }, + IgnitionConfigs: map[string]string{ + testProfileWithButane.IgnitionId: butane, + }, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) - c := server.NewServer(&server.Config{Store: store}) - h := srv.ignitionHandler(c) + core := server.NewServer(&server.Config{Store: store}) + h := srv.ignitionHandler(core) + ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/?foo=some-param&bar=b", nil) - h.ServeHTTP(w, req.WithContext(ctx)) + req, _ := http.NewRequestWithContext(ctx, "GET", "/?foo=some-param&bar=b", nil) + h.ServeHTTP(w, req) // assert that: - // - Container Linux Config template rendered with Group selectors, metadata, and query variables - // - Transformed to an Ignition config (JSON) + // - Template rendered with Group selectors, metadata, and query variables + // - Butane translated to an Ignition config assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType)) + assert.Equal(t, jsonContentType, w.Header().Get(contentType)) assert.Equal(t, expectedIgnition, w.Body.String()) } func TestIgnitionHandler_MissingCtxProfile(t *testing.T) { logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) - c := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) - h := srv.ignitionHandler(c) + core := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) + h := srv.ignitionHandler(core) + w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } -*/ -/* func TestIgnitionHandler_MissingTemplateMetadata(t *testing.T) { - content := ` -ignition_version: 1 + butane := ` +variant: flatcar +version: 1.0.0 systemd: units: - name: {{.missing_key}} - enable: true + enabled: true ` store := &fake.FixedStore{ - Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, - IgnitionConfigs: map[string]string{fake.Profile.IgnitionId: content}, + Profiles: map[string]*storagepb.Profile{ + fake.Group.Profile: fake.Profile, + }, + IgnitionConfigs: map[string]string{ + fake.Profile.IgnitionId: butane, + }, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) - c := server.NewServer(&server.Config{Store: store}) - h := srv.ignitionHandler(c) + core := server.NewServer(&server.Config{Store: store}) + h := srv.ignitionHandler(core) + ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - h.ServeHTTP(w, req.WithContext(ctx)) + req, _ := http.NewRequestWithContext(ctx, "GET", "/", nil) + h.ServeHTTP(w, req) // assert that: - // - Ignition template rendering errors because "missing_key" is not - // present in the template variables + // - Template rendering errors because "missing_key" is not present assert.Equal(t, http.StatusNotFound, w.Code) } -*/ diff --git a/matchbox/http/test_fixtures.go b/matchbox/http/test_fixtures.go index 5bc3744e..f512ca17 100644 --- a/matchbox/http/test_fixtures.go +++ b/matchbox/http/test_fixtures.go @@ -7,9 +7,9 @@ import ( var ( validMACStr = "52:da:00:89:d8:10" - testProfileIgnitionYAML = &storagepb.Profile{ + testProfileWithButane = &storagepb.Profile{ Id: "g1h2i3j4", - IgnitionId: "ignition.yaml", + IgnitionId: "butane.yaml", } testProfileGeneric = &storagepb.Profile{