Skip to content

Commit 1b19865

Browse files
authored
Add image-builder utility (#66)
* internal/incus: add required helpers for image builder * implement cmd/image-builder * remove links for files that are not needed anymore * update documentation to use image-builder * fixup LXD install flaky on CI * add github action workflow to build kubeadm images * do not lint long lines on cmd package * allow override haproxy image on clusterclass --------- Signed-off-by: Angelos Kolaitis <[email protected]>
1 parent 27fc416 commit 1b19865

38 files changed

+991
-285
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build Kubeadm Images
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
required: true
8+
description: Kubernetes version, e.g. "v1.33.0"
9+
type: string
10+
11+
jobs:
12+
build:
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
include:
17+
# amd64 images
18+
- { infrastructure: incus, type: container, arch: amd64 }
19+
- { infrastructure: incus, type: virtual-machine, arch: amd64 }
20+
- { infrastructure: lxd, type: virtual-machine, arch: amd64 }
21+
22+
# arm64 images
23+
- { infrastructure: incus, type: container, arch: arm64 }
24+
# - { infrastructure: incus, type: virtual-machine, arch: arm64 } # arm64 runners do not support kvm
25+
# - { infrastructure: lxd, type: virtual-machine, arch: arm64 } # arm64 runners do not support kvm
26+
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Setup Go
32+
uses: actions/setup-go@v5
33+
with:
34+
go-version-file: go.mod
35+
36+
- name: Setup infrastructure
37+
run: |
38+
./hack/scripts/ci/setup-${{ matrix.infrastructure }}.sh
39+
40+
- name: Build image
41+
run: |
42+
go run ./cmd/image-builder kubeadm --v=4 --kubernetes-version ${{ inputs.version }} --instance-type=${{ matrix.type }} --output image.tar.gz
43+
44+
- name: Upload image
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: kubeadm-${{ inputs.version }}-${{ matrix.infrastructure }}-${{ matrix.type }}-${{ matrix.arch }}
48+
path: image.tar.gz

.golangci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ issues:
1212
- path: "api/*"
1313
linters:
1414
- lll
15+
- path: "cmd/*"
16+
linters:
17+
- lll
18+
- goconst
1519
- path: "internal/*"
1620
linters:
1721
- dupl

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,18 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
233233
ENVTEST ?= $(LOCALBIN)/setup-envtest
234234
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
235235
GINKGO = $(LOCALBIN)/ginkgo
236+
IMAGE_BUILDER = $(LOCALBIN)/image-builder
236237

237238
## Tool Versions
238239
KUSTOMIZE_VERSION ?= v5.5.0
239240
CONTROLLER_TOOLS_VERSION ?= v0.16.4
240241
ENVTEST_VERSION ?= release-0.19
241242
GOLANGCI_LINT_VERSION ?= v1.61.0
242243

244+
.PHONY: image-builder
245+
image-builder: $(LOCALBIN)
246+
go build -o $(IMAGE_BUILDER) ./cmd/image-builder
247+
243248
.PHONY: kustomize
244249
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
245250
$(KUSTOMIZE): $(LOCALBIN)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"sigs.k8s.io/controller-runtime/pkg/log"
8+
)
9+
10+
var (
11+
haproxyCmd = &cobra.Command{
12+
Use: "haproxy",
13+
GroupID: "build",
14+
Short: "Build haproxy images for cluster-api-provider-incus",
15+
SilenceUsage: true,
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
if cfg.imageAlias == "" {
18+
cfg.imageAlias = fmt.Sprintf("haproxy-u%s", cfg.ubuntuVersion)
19+
}
20+
21+
log.FromContext(gCtx).WithValues(
22+
"ubuntu-version", cfg.ubuntuVersion,
23+
"instance-type", cfg.instanceType,
24+
"image-alias", cfg.imageAlias,
25+
).Info("Building haproxy image")
26+
27+
return runStages(
28+
&stageCreateInstance{},
29+
&stagePreRunCommands{},
30+
&stageInstallHaproxy{},
31+
&stagePostRunCommands{},
32+
&stageCleanupInstance{},
33+
&stageStopInstance{},
34+
&stagePublishHaproxyImage{},
35+
&stageExportImage{},
36+
&stageRemoveInstance{},
37+
)
38+
},
39+
}
40+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/blang/semver/v4"
7+
"github.com/spf13/cobra"
8+
"sigs.k8s.io/controller-runtime/pkg/log"
9+
)
10+
11+
var (
12+
kubeadmCmd = &cobra.Command{
13+
Use: "kubeadm",
14+
GroupID: "build",
15+
Short: "Build kubeadm images for cluster-api-provider-incus",
16+
SilenceUsage: true,
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
if _, err := semver.ParseTolerant(kubeadmCfg.kubernetesVersion); err != nil {
19+
return fmt.Errorf("--kubernetes-version %q is not valid semver: %w", kubeadmCfg.kubernetesVersion, err)
20+
}
21+
22+
if cfg.imageAlias == "" {
23+
cfg.imageAlias = fmt.Sprintf("kubeadm-%s-u%s-%s", kubeadmCfg.kubernetesVersion, cfg.ubuntuVersion, cfg.instanceType)
24+
}
25+
26+
log.FromContext(gCtx).WithValues(
27+
"kubernetes-version", kubeadmCfg.kubernetesVersion,
28+
"ubuntu-version", cfg.ubuntuVersion,
29+
"instance-type", cfg.instanceType,
30+
"image-alias", cfg.imageAlias,
31+
).Info("Building kubeadm image")
32+
33+
return runStages(
34+
&stageCreateInstance{},
35+
&stagePreRunCommands{},
36+
&stageInstallKubeadm{},
37+
&stagePullExtraImages{},
38+
&stageGenerateManifest{},
39+
&stagePostRunCommands{},
40+
&stageCleanupInstance{},
41+
&stageStopInstance{},
42+
&stagePublishKubeadmImage{},
43+
&stageExportImage{},
44+
&stageRemoveInstance{},
45+
)
46+
},
47+
}
48+
)
49+
50+
func init() {
51+
kubeadmCmd.Flags().StringVar(&kubeadmCfg.kubernetesVersion, "kubernetes-version", "",
52+
"Kubernetes version to create image for")
53+
54+
kubeadmCmd.Flags().StringSliceVar(&kubeadmCfg.pullExtraImages, "pull-extra-images", defaultPullExtraImages,
55+
"Extra OCI images to pull in the image")
56+
57+
_ = kubeadmCmd.MarkFlagRequired("kubernetes-version")
58+
}

cmd/image-builder/command_root.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
cliflag "k8s.io/component-base/cli/flag"
9+
logsv1 "k8s.io/component-base/logs/api/v1"
10+
11+
"github.com/lxc/cluster-api-provider-incus/internal/incus"
12+
)
13+
14+
var (
15+
rootCmd = &cobra.Command{
16+
Use: "image-builder",
17+
SilenceUsage: true,
18+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
19+
if err := logsv1.ValidateAndApply(logOptions, nil); err != nil {
20+
return fmt.Errorf("failed to configure logging: %w", err)
21+
}
22+
23+
switch cfg.instanceType {
24+
case "container", "virtual-machine":
25+
default:
26+
return fmt.Errorf("invalid value for --instance-type argument %q, must be one of [container, virtual-machine]", cfg.instanceType)
27+
}
28+
29+
switch cfg.ubuntuVersion {
30+
case "22.04", "24.04":
31+
default:
32+
return fmt.Errorf("invalid value for --ubuntu-version argument %q, must be one of [22.04, 24.04]", cfg.ubuntuVersion)
33+
}
34+
35+
opts, err := incus.NewOptionsFromConfigFile(cfg.configFile, cfg.configRemoteName, false)
36+
if err != nil {
37+
return fmt.Errorf("failed to read client credentials: %w", err)
38+
}
39+
40+
client, err = incus.New(gCtx, opts)
41+
if err != nil {
42+
return fmt.Errorf("failed to create incus client: %w", err)
43+
}
44+
45+
return nil
46+
},
47+
}
48+
)
49+
50+
func init() {
51+
rootCmd.AddGroup(&cobra.Group{ID: "build", Title: "Available Image Types:"})
52+
rootCmd.AddCommand(kubeadmCmd, haproxyCmd)
53+
54+
logsv1.AddFlags(logOptions, rootCmd.PersistentFlags())
55+
rootCmd.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc)
56+
rootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine)
57+
58+
rootCmd.PersistentFlags().StringVar(&cfg.configFile, "config-file", "",
59+
"Read client configuration from file")
60+
rootCmd.PersistentFlags().StringVar(&cfg.configRemoteName, "config-remote-name", "",
61+
"Override remote to use from configuration file")
62+
63+
rootCmd.PersistentFlags().StringVar(&cfg.ubuntuVersion, "ubuntu-version", defaultUbuntuVersion,
64+
"Ubuntu version to use to launch instance (one of 22.04|24.04)")
65+
66+
rootCmd.PersistentFlags().StringVar(&cfg.instanceName, "instance-name", defaultInstanceName,
67+
"Name for the builder instance")
68+
rootCmd.PersistentFlags().StringVar(&cfg.instanceType, "instance-type", defaultInstanceType,
69+
"Type of image to build (one of container|virtual-machine)")
70+
rootCmd.PersistentFlags().StringSliceVar(&cfg.instanceProfiles, "instance-profile", defaultInstanceProfiles,
71+
"Profiles to use to launch the builder instance")
72+
73+
rootCmd.PersistentFlags().StringVar(&cfg.imageAlias, "image-alias", "",
74+
"Create image with alias. If not specified, a default is used based on config")
75+
76+
rootCmd.PersistentFlags().StringVar(&cfg.outputFile, "output", "image.tar.gz",
77+
"Output file for exported image")
78+
79+
}

cmd/image-builder/config.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"context"
5+
6+
"k8s.io/component-base/logs"
7+
"k8s.io/klog/v2"
8+
ctrl "sigs.k8s.io/controller-runtime"
9+
10+
"github.com/lxc/cluster-api-provider-incus/internal/incus"
11+
)
12+
13+
var (
14+
gCtx context.Context
15+
gLogger = ctrl.Log
16+
logOptions = logs.NewOptions()
17+
18+
// command-line arguments
19+
cfg struct {
20+
// client configuration
21+
configFile string
22+
configRemoteName string
23+
24+
// base image configuration
25+
ubuntuVersion string
26+
27+
// builder configuration
28+
instanceName string
29+
instanceProfiles []string
30+
instanceType string
31+
32+
// image alias configuration
33+
imageAlias string
34+
35+
// output
36+
outputFile string
37+
}
38+
39+
kubeadmCfg struct {
40+
kubernetesVersion string
41+
pullExtraImages []string
42+
}
43+
44+
// runtime configuration
45+
client *incus.Client
46+
)
47+
48+
func init() {
49+
gCtx = ctrl.SetupSignalHandler()
50+
ctrl.SetLogger(klog.Background())
51+
gCtx = ctrl.LoggerInto(gCtx, gLogger)
52+
}

cmd/image-builder/default.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package main
2+
3+
var (
4+
defaultUbuntuVersion = "24.04"
5+
6+
defaultInstanceName = "capn-builder"
7+
defaultInstanceType = "container"
8+
defaultInstanceProfiles = []string{"default"}
9+
10+
defaultPullExtraImages = []string{
11+
"docker.io/flannel/flannel-cni-plugin:v1.6.0-flannel1",
12+
"docker.io/flannel/flannel:v0.26.3",
13+
}
14+
)

cmd/image-builder/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package main
2+
3+
import (
4+
"os"
5+
)
6+
7+
func main() {
8+
if err := rootCmd.Execute(); err != nil {
9+
os.Exit(1)
10+
}
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"os"
9+
10+
"sigs.k8s.io/controller-runtime/pkg/log"
11+
12+
"github.com/lxc/cluster-api-provider-incus/internal/static"
13+
)
14+
15+
type stageCleanupInstance struct{}
16+
17+
func (*stageCleanupInstance) name() string { return "cleanup-instance" }
18+
19+
// cat cleanup-instance.sh | incus exec capn-builder -- bash -s
20+
func (*stageCleanupInstance) run(ctx context.Context) error {
21+
log.FromContext(ctx).V(1).Info("Running cleanup-instance.sh script")
22+
23+
var stdout, stderr io.Writer
24+
if log.FromContext(ctx).V(4).Enabled() {
25+
stdout = os.Stdout
26+
stderr = os.Stderr
27+
}
28+
29+
stdin := bytes.NewBufferString(static.CleanupInstanceScript())
30+
if err := client.RunCommand(ctx, cfg.instanceName, []string{"bash", "-s"}, stdin, stdout, stderr); err != nil {
31+
return fmt.Errorf("failed to run cleanup-instance.sh script: %w", err)
32+
}
33+
34+
return nil
35+
}

0 commit comments

Comments
 (0)