Skip to content

feat: Added action to write Cloud Init volumes from hardware data - #164

Open
appkins wants to merge 17 commits into
tinkerbell:mainfrom
tinkerbell-community:main
Open

appkins wants to merge 17 commits into
tinkerbell:mainfrom
tinkerbell-community:main

Conversation

@appkins

@appkins appkins commented Nov 19, 2025

Copy link
Copy Markdown

Description

Adds a workflow action to write user data, meta-data and user-data to a cloud init volume - to be consumed by nocloud provisioning.

Note: The hardware data is templated - meaning it does not rely on a specific version of Tinkerbell or the CRDs. Instead, it plays nicely with the workflow template design.

Example

The following template fully implements the TalosControlPlane requirements using predictable image paths. No additional configuration is needed to pair Tinkerbell and Talos bootstrap/control plane.

version: "0.1"
name: talos-install-template
global_timeout: 9800
tasks:
  - name: "install-talos"
    worker: "{{.device_1}}"
    volumes:
      - /dev:/dev
      - /dev/console:/dev/console
      - /lib/firmware:/lib/firmware:ro
    actions:
      - name: "stream talos nocloud image"
        image: quay.io/tinkerbell/actions/image2disk:latest
        timeout: 9600
        environment:
          DEST_DISK: {{ index .Hardware.Disks 0 }}
          # Use the nocloud platform image instead of metal
          IMG_URL: "https://factory.talos.dev/image/{{ .Hardware.Metadata.Instance.OperatingSystem.Slug }}/{{ .Hardware.Metadata.Instance.OperatingSystem.Version }}/nocloud-{{ eq (index .Hardware.Interfaces 0).DHCP.Arch "aarch64" | ternary "arm64" "amd64" }}.raw.zst"
          COMPRESSED: "true"

      - name: "create cidata partition and write files"
        image: ghcr.io/tinkerbell-community/actions/cidataio:latest
        timeout: 300
        environment:
          DEST_DISK: {{ index .Hardware.Disks 0 }}
          META_DATA: |
            hostname: {{ coalesce (index .Hardware.Interfaces 0).DHCP.Hostname ((index .Hardware.Interfaces 0).DHCP.MAC | replace ":" "-" | printf "talos-%s") }}
            local-hostname: {{ coalesce (index .Hardware.Interfaces 0).DHCP.Hostname ((index .Hardware.Interfaces 0).DHCP.MAC | replace ":" "-" | printf "talos-%s") }}
            instance-id: {{ coalesce (index .Hardware.Interfaces 0).DHCP.Hostname ((index .Hardware.Interfaces 0).DHCP.MAC | replace ":" "-" | printf "talos-%s") }}
            provider-id: {{ coalesce (index .Hardware.Interfaces 0).DHCP.Hostname ((index .Hardware.Interfaces 0).DHCP.MAC | replace ":" "-" | printf "talos-%s") | printf "tinkerbell://tinkerbell/%s" }}
          NETWORK_CONFIG: |
            version: 2
            ethernets:
              {{- range $index, $iface := .Hardware.Interfaces }}
              {{ coalesce $iface.DHCP.IfaceName (printf "eth%d" $index) }}:
                match:
                  macaddress: "{{ $iface.DHCP.MAC }}"
                dhcp4: true
                dhcp6: false
                nameservers:
                  addresses:
                    {{- range $dns := $iface.DHCP.NameServers }}
                    - {{ $dns }}
                    {{- end }}
              {{- end }}
          USER_DATA: |
            {{- .Hardware.UserData | trim | nindent 12 }}

      - name: "reboot"
        image: ghcr.io/jacobweinstock/waitdaemon:latest
        timeout: 90
        pid: host
        environment:
            IMAGE: alpine
            WAIT_SECONDS: 10
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
        command: ["reboot"]

Fixes: #

How Has This Been Tested?

Extensively. Using it in a real world environment to write TalosConfig to a fleet of edge devices.

How are existing users impacted? What migration steps/scripts do we need?

No impact, new action.

Checklist:

This pull request introduces a new Tinkerbell action called cidataio, which automates the creation of a cloud-init (cidata) partition and writes configuration files to it. The changes include adding the new action to the build system, updating CI/CD workflows, and providing documentation and implementation for cidataio. The most important changes are grouped below:

Addition of the cidataio Action:

  • Added a new cidataio action, including its implementation in cidataio/main.go, which creates a cidata partition, formats it, mounts it, and writes cloud-init data files from environment variables.
  • Added a Dockerfile for cidataio to build and package the action as a container image.
  • Added documentation for cidataio in cidataio/README.md, describing its usage, environment variables, and example workflow.

Build System and Workflow Updates:

  • Updated the Makefile to include cidataio in the list of buildable actions and made the container repository configurable.
  • Changed the build platform for actions in the Makefile from linux/amd64 to linux/arm64.

CI/CD Pipeline Enhancements:

  • Updated .github/workflows/ci.yml and .github/workflows/release.yml to include cidataio in the matrix of actions to build and release. [1] [2]
  • Improved the release workflow to dynamically select the container registry (Quay or GHCR) based on available credentials and set appropriate permissions and environment variables for publishing.
    I have:
  • updated the documentation and/or roadmap (if required)
  • added unit or e2e tests
  • provided instructions on how to upgrade

@appkins appkins mentioned this pull request Nov 19, 2025
3 tasks
@appkins
appkins force-pushed the main branch 8 times, most recently from 072f412 to 8aa2632 Compare January 15, 2026 02:14
Signed-off-by: appkins <nbatkins@gmail.com>
Signed-off-by: appkins <nbatkins@gmail.com>
Signed-off-by: appkins <nbatkins@gmail.com>
Signed-off-by: appkins <nbatkins@gmail.com>
The file was a macOS arm64 build output, not something the image build
uses.

Signed-off-by: appkins <nbatkins@gmail.com>
Signed-off-by: appkins <nbatkins@gmail.com>
Upstream's CI now runs golangci-lint. Annotate the gosec taint findings
on operator-supplied values, return errors from ubootenv's run() so the
deferred unmount is not skipped by os.Exit, drop an always-constant test
parameter, and use unix.BLKRRPART instead of a local constant.

Signed-off-by: appkins <nbatkins@gmail.com>
The fork hard-coded linux/arm64 for local builds while upstream and CI
use linux/amd64. Use the host's GOARCH by default and allow overriding
with BUILD_PLATFORM.

Signed-off-by: appkins <nbatkins@gmail.com>
Write Talos Linux metal platform network configuration into the META
partition of a freshly imaged disk. Hardware data is retrieved from the
Tinkerbell metadata service like rootio does, mapped onto Talos'
PlatformConfigSpec document (addresses, links, routes, hostname,
resolvers, time servers, external IPs) and stored under META key 0xa
using the siderolabs/go-adv module, which is the extracted Talos ADV
code behind talosctl meta write.

Link names resolve from the Hardware iface_name, else from the MAC via
sysfs with udev-style predictable naming (LINK_NAMING=kernel keeps the
HookOS name). NETWORK_CONFIG passes a document through verbatim.

Also register the cidataio and ubootenv actions in the CI matrices and
README table where they were missing.

Signed-off-by: appkins <nbatkins@gmail.com>
Upstream added REGISTRY_USERNAME, REGISTRY_PASSWORD and SKIP_VERIFY, and
later fixed anonymous pulls from registries that always issue a bearer
challenge. The fork's oras v2 rewrite dropped those during the upstream
sync; bring them back on the v2 client: static credentials bound to the
image's registry host, an always-present token cache so anonymous pulls
work, and an opt-in insecure TLS transport.

Signed-off-by: appkins <nbatkins@gmail.com>
Set kernel command line arguments inside Talos Linux unified kernel
images, for example talos.config=http://<tinkerbell>:7080/2009-04-04/user-data
so a node fetches its machine configuration from Tinkerbell.

Talos 1.10+ metal images boot through systemd-boot on UEFI and keep the
command line in the .cmdline PE section of EFI/Linux/Talos-*.efi, one
for the default boot and one per boot profile. The action finds the EFI
partition through the GPT, mounts it, and rewrites every .cmdline the
way Talos' own assembler lays sections out: everything before the first
.cmdline stays byte-identical, later data sections are relocated with
the same alignment rules, and header sizes are recomputed. Signed UKIs
are refused unless STRIP_SIGNATURE=true.

Verified against the Talos v1.14.0 metal image: both command lines
updated, the rerun is a no-op, and the image boots under OVMF with the
new command line.

Signed-off-by: appkins <nbatkins@gmail.com>
Report the PE section index, file offset and size of every .cmdline the
action finds, and document the layout of the Image Factory v1.13.10 and
upstream v1.14.0 metal images.

Signed-off-by: appkins <nbatkins@gmail.com>
Tinkerbell exposes the Hardware object to Workflow templates as
.hardware, so a template can render spec.interfaces and
metadata.instance to JSON and hand them to the action directly. This
removes the dependency on the /metadata endpoint, which today only
returns storage fields. Precedence: NETWORK_CONFIG, HARDWARE_SPEC,
then MIRROR_HOST.

Signed-off-by: appkins <nbatkins@gmail.com>
…ng releases

go.mod requires Go 1.26.3 since go-adv was added, but the two
fork-authored Dockerfiles still pinned golang:1.24-alpine with
GOTOOLCHAIN=local, so their Release jobs failed and fail-fast then
cancelled every other action's release. Build them with the same base
image as the rest and let each action publish independently.

Signed-off-by: appkins <nbatkins@gmail.com>
Both only need a static Go binary: cidataio shells out to sgdisk,
partprobe, mkfs.vfat and mount from its Alpine runtime image, and
ubootenv is pure Go. Drop the CGO external-linking recipe and the C
toolchain packages, build with CGO_ENABLED=0 like upstream, and pin
the cidataio runtime image to alpine:3.22.

Signed-off-by: appkins <nbatkins@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant