Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,21 @@ jobs:
strategy:
matrix:
platform: [amd64]
action: [archive2disk,cexec,grub2disk,image2disk,kexec,oci2disk,qemuimg2disk,rootio,slurp,syslinux,writefile]
action:
[
archive2disk,
cexec,
cidataio,
grub2disk,
image2disk,
kexec,
oci2disk,
qemuimg2disk,
rootio,
slurp,
syslinux,
writefile,
]
steps:
- uses: actions/checkout@v4

Expand Down
46 changes: 37 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,62 @@ on:
- "main"
workflow_dispatch: {}

env:
REGISTRY: quay.io

jobs:
build:
name: Release
runs-on: ubuntu-latest
strategy:
matrix:
action: [archive2disk,cexec,grub2disk,image2disk,kexec,oci2disk,qemuimg2disk,rootio,slurp,syslinux,writefile]
action:
[
archive2disk,
cexec,
cidataio,
grub2disk,
image2disk,
kexec,
oci2disk,
qemuimg2disk,
rootio,
slurp,
syslinux,
writefile,
]
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v4

- name: Set Registry
id: registry
run: |
if [ -n "${{ secrets.QUAY_USERNAME }}" ] && [ -n "${{ secrets.QUAY_PASSWORD }}" ]; then
echo "registry=quay.io" >> $GITHUB_OUTPUT
else
echo "registry=ghcr.io" >> $GITHUB_OUTPUT
fi

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Quay.io
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
registry: ${{ steps.registry.outputs.registry }}
username: ${{ secrets.QUAY_USERNAME || github.actor }}
password: ${{ secrets.QUAY_PASSWORD || github.token }}

- name: Prepare Release
run: make prepare-release
env:
CONTAINER_REPOSITORY: ${{ steps.registry.outputs.registry }}/${{ github.repository }}

- name: Run Release
run: make release-${{ matrix.action }} -j $(nprox)
env:
CONTAINER_REPOSITORY: ${{ steps.registry.outputs.registry }}/${{ github.repository }}
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ SHELL := bash
.SECONDEXPANSION:

# Define the list of actions that can be built.
ACTIONS := archive2disk cexec grub2disk image2disk kexec oci2disk qemuimg2disk rootio slurp syslinux writefile
ACTIONS := archive2disk cidataio cexec grub2disk image2disk kexec oci2disk qemuimg2disk rootio slurp syslinux writefile ubootenv

# Define the commit for tagging images.
GIT_COMMIT := $(shell git rev-parse HEAD)

# Define container registry details.
CONTAINER_REPOSITORY := quay.io/tinkerbell/actions
CONTAINER_REPOSITORY ?= quay.io/tinkerbell/actions

include Rules.mk

Expand All @@ -28,7 +28,7 @@ help: ## Print this help

.PHONY: $(ACTIONS)
$(ACTIONS): ## Build a specific action image.
docker buildx build --platform linux/amd64 --load -t $@:latest -f ./$@/Dockerfile .
docker buildx build --platform linux/arm64 --load -t $@:latest -f ./$@/Dockerfile .

.PHONY: images
images: ## Build all action images.
Expand Down
2 changes: 1 addition & 1 deletion archive2disk/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1

FROM golang:1.21-alpine AS archive2disk
FROM golang:1.23-alpine AS archive2disk
RUN apk add --no-cache git ca-certificates gcc musl-dev
COPY . /src
WORKDIR /src/archive2disk
Expand Down
12 changes: 12 additions & 0 deletions cidataio/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.24-alpine AS cidataio
RUN apk add --no-cache git ca-certificates gcc linux-headers musl-dev
COPY . /src
WORKDIR /src/cidataio
RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
--mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
CGO_ENABLED=1 GOOS=linux go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o cidataio

FROM alpine:latest
RUN apk add --no-cache sgdisk dosfstools util-linux
COPY --from=cidataio /src/cidataio/cidataio /usr/bin/cidataio
ENTRYPOINT ["/usr/bin/cidataio"]
47 changes: 47 additions & 0 deletions cidataio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Tinkerbell `cidataio` Action

This action creates a `cidata` partition and writes cloud-init data to it.

## Description

The `cidataio` action is a Go-based tool designed to run in a Tinkerbell workflow. It prepares a disk that has just been flashed with an OS image (e.g., Talos "nocloud") by adding a new partition for cloud-init data.

It performs the following steps:
1. Finds the target disk from the `DEST_DISK` environment variable.
2. Creates a new partition using all remaining free space on the disk.
3. Formats this new partition as `vfat` with the label `cidata`.
4. Mounts the partition.
5. Writes the contents of `USER_DATA`, `META_DATA`, and `VENDOR_DATA` environment variables to `user-data`, `meta-data`, and `vendor-data` files, respectively.
6. Unmounts the partition.

## Environment Variables

* **`DEST_DISK`** (Required): The block device to operate on (e.g., `/dev/sda`, `/dev/nvme0n1`).
* **`USER_DATA`** (Optional): The content for the `user-data` file.
* **`META_DATA`** (Optional): The content for the `meta-data` file.
* **`NETWORK_CONFIG`** (Optional): The content for the `network-config` file.

## Example Workflow YAML

This action is typically run immediately after `image2disk`.

```yaml
actions:
- name: "stream talos nocloud image"
image: quay.io/tinkerbell/actions/image2disk:latest
timeout: 9600
environment:
DEST_DISK: {{ index .Hardware.Disks 0 }}
IMG_URL: "..."
COMPRESSED: "true"

- name: "create cidata partition and write files"
image: ghcr.io/tinkerbell/cidataio:latest
timeout: 120
environment:
DEST_DISK: {{ index .Hardware.Disks 0 }}
USER_DATA: |
# user-data content here
META_DATA: |
local-hostname: my-node-1
```
Binary file added cidataio/cidataio
Binary file not shown.
128 changes: 128 additions & 0 deletions cidataio/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)

const (
configISOLabel = "cidata"
configNetworkConfigPath = "network-config"
configMetaDataPath = "meta-data"
configUserDataPath = "user-data"
)

// run is a helper to run a shell command and log it.
func run(cmdStr string, args ...string) {
log.Printf("Running: %s %s", cmdStr, strings.Join(args, " "))
cmd := exec.Command(cmdStr, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}

// runWithOutput runs a command and returns its stdout.
func runWithOutput(cmdStr string, args ...string) string {
log.Printf("Running (for output): %s %s", cmdStr, strings.Join(args, " "))
out, err := exec.Command(cmdStr, args...).CombinedOutput()
if err != nil {
log.Printf("Command failed: %s - %v", string(out), err)
// Don't fatalf, as some commands (like ls) might fail gracefully
}
return strings.TrimSpace(string(out))
}

// findNewPartition compares a list of partitions before and after an operation.
func findNewPartition(before, after string) string {
beforeSet := make(map[string]bool)
for _, p := range strings.Split(before, "\n") {
if p != "" {
beforeSet[p] = true
}
}

for _, p := range strings.Split(after, "\n") {
if p != "" && !beforeSet[p] {
return p // Found the new one
}
}
return ""
}

// writeFileIfEnv writes content from an env var to a file.
func writeFileIfEnv(envVar, path string) {
content := os.Getenv(envVar)
if content == "" {
log.Printf("Env var %s not set, skipping file.", envVar)
return
}

log.Printf("Writing content from %s to %s", envVar, path)
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
log.Fatalf("Failed to write file %s: %v", path, err)
}
}

func main() {
log.Println("Starting cidataio action...")

// 1. Get DEST_DISK
disk := os.Getenv("DEST_DISK")
if disk == "" {
log.Fatalf("DEST_DISK environment variable not set.")
}

// 2. Force kernel to read partition table and get "before" list
run("partprobe", disk)
time.Sleep(1 * time.Second) // Give udev time to create devices

// List all partitions for this disk using regex to match both:
// - Standard devices: /dev/sda1, /dev/sdb2, /dev/vda3
// - NVMe/MMC devices: /dev/nvme0n1p1, /dev/mmcblk0p2
globPattern := fmt.Sprintf("ls -1 %s* 2>/dev/null | grep -E '%sp?[0-9]+$' || true", disk, disk)
partsBefore := runWithOutput("sh", "-c", globPattern)

// 3. Create the new partition
log.Printf("Creating new partition on %s", disk)
run("sgdisk", "-n", "0:0:+2M", "-t", "0:0700", disk)

// 4. Force kernel to re-read and find the new partition
run("partprobe", disk)
time.Sleep(2 * time.Second) // Give udev time to settle
partsAfter := runWithOutput("sh", "-c", globPattern)

newPart := findNewPartition(partsBefore, partsAfter)
if newPart == "" {
log.Fatalf("Could not find a new partition. Before: [%s], After: [%s]", partsBefore, partsAfter)
}
log.Printf("Found new partition: %s", newPart)

// 5. Format the new partition
log.Printf("Formatting %s as vfat with label cidata", newPart)
run("mkfs.vfat", "-n", configISOLabel, newPart)

// 6. Mount, Write, Unmount
mountPoint := "/mnt/cidata"
log.Printf("Mounting %s to %s", newPart, mountPoint)
run("mkdir", "-p", mountPoint)
run("mount", newPart, mountPoint)

// 7. Write data from Env Vars
writeFileIfEnv("USER_DATA", filepath.Join(mountPoint, configUserDataPath))
writeFileIfEnv("META_DATA", filepath.Join(mountPoint, configMetaDataPath))
writeFileIfEnv("NETWORK_CONFIG", filepath.Join(mountPoint, configNetworkConfigPath))

// 8. Unmount
log.Printf("Unmounting %s", mountPoint)
run("umount", mountPoint)

log.Println("cidataio action completed successfully.")
}
19 changes: 4 additions & 15 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,43 +1,32 @@
module github.com/tinkerbell/actions

go 1.21
go 1.23.0

require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/containerd/containerd v1.4.4
github.com/deislabs/oras v0.11.1
github.com/diskfs/go-diskfs v1.4.1
github.com/dustin/go-humanize v1.0.1
github.com/klauspost/compress v1.17.9
github.com/lmittmann/tint v1.0.5
github.com/mattn/go-isatty v0.0.3
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/peterbourgon/ff/v3 v3.4.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
github.com/spf13/cobra v1.8.1
github.com/ulikunitz/xz v0.5.12
golang.org/x/sys v0.24.0
oras.land/oras-go/v2 v2.6.0
)

require (
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Microsoft/hcsshim v0.8.14 // indirect
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.22.0 // indirect
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 // indirect
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a // indirect
google.golang.org/grpc v1.23.1 // indirect
golang.org/x/sync v0.14.0 // indirect
)
Loading
Loading