Skip to content

Commit 54c2851

Browse files
emosbaughsgalsaleh
authored andcommitted
feat: build third party images with chainguard
1 parent 357a2c2 commit 54c2851

File tree

13 files changed

+528
-10
lines changed

13 files changed

+528
-10
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Update image deps
2+
3+
on:
4+
schedule:
5+
- cron: '0 4 * * *'
6+
workflow_dispatch:
7+
inputs:
8+
k0s-version:
9+
description: 'K0s version for discovering image versions'
10+
required: false
11+
push:
12+
branches:
13+
- sgalsaleh/sc-108755/use-chainguard-images-for-embedded-cluster
14+
15+
jobs:
16+
update-k0s-images:
17+
runs-on: ubuntu-20.04
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Compile buildtools
23+
run: |
24+
make buildtools
25+
26+
- name: Update k0s images
27+
env:
28+
REGISTRY_SERVER: docker.io
29+
REGISTRY_USER: ${{ secrets.DOCKERHUB_USER }}
30+
REGISTRY_PASS: ${{ secrets.DOCKERHUB_PASSWORD }}
31+
run: output/bin/buildtools update images k0s
32+
33+
- name: Create Pull Request # creates a PR if there are differences
34+
uses: peter-evans/create-pull-request@v6
35+
id: cpr
36+
with:
37+
token: ${{ secrets.AUTOMATED_PR_GH_PAT }}
38+
commit-message: 'Update image versions'
39+
title: 'Automated image updates'
40+
branch: automation/image-dependencies
41+
delete-branch: true
42+
labels: |
43+
automated-pr
44+
images
45+
type::security
46+
draft: false
47+
# base: "main"
48+
base: "sgalsaleh/sc-108755/use-chainguard-images-for-embedded-cluster"
49+
body: "Automated changes by the [image-deps-updater](https://github.com/replicatedhq/embedded-cluster/blob/main/.github/workflows/image-deps-updater.yaml) GitHub action"
50+
51+
- name: Check outputs
52+
if: ${{ steps.cpr.outputs.pull-request-number }}
53+
run: |
54+
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
55+
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ VERSION ?= $(shell git describe --tags --dirty)
22
UNAME := $(shell uname)
33
ARCH := $(shell uname -m)
44
APP_NAME = embedded-cluster
5+
COREDNS_IMAGE =
6+
COREDNS_VERSION =
7+
CALICO_NODE_IMAGE =
8+
CALICO_NODE_VERSION =
9+
METRICS_SERVER_IMAGE =
10+
METRICS_SERVER_VERSION =
511
ADMIN_CONSOLE_CHART_REPO_OVERRIDE =
612
ADMIN_CONSOLE_CHART_VERSION = 1.112.1-build.1
713
ADMIN_CONSOLE_IMAGE_OVERRIDE =
@@ -38,6 +44,12 @@ LD_FLAGS = -X github.com/replicatedhq/embedded-cluster/pkg/defaults.K0sVersion=$
3844
-X github.com/replicatedhq/embedded-cluster/pkg/defaults.TroubleshootVersion=$(TROUBLESHOOT_VERSION) \
3945
-X github.com/replicatedhq/embedded-cluster/pkg/defaults.KubectlVersion=$(KUBECTL_VERSION) \
4046
-X github.com/replicatedhq/embedded-cluster/pkg/defaults.LocalArtifactMirrorImage=$(LOCAL_ARTIFACT_MIRROR_IMAGE_LOCATION) \
47+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.CoreDNSImage=$(COREDNS_IMAGE) \
48+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.CoreDNSVersion=$(COREDNS_VERSION) \
49+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.CalicoNodeImage=$(CALICO_NODE_IMAGE) \
50+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.CalicoNodeVersion=$(CALICO_NODE_VERSION) \
51+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.MetricsServerImage=$(METRICS_SERVER_IMAGE) \
52+
-X github.com/replicatedhq/embedded-cluster/pkg/config/images.MetricsServerVersion=$(METRICS_SERVER_VERSION) \
4153
-X github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole.ChartRepoOverride=$(ADMIN_CONSOLE_CHART_REPO_OVERRIDE) \
4254
-X github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole.Version=$(ADMIN_CONSOLE_CHART_VERSION) \
4355
-X github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole.ImageOverride=$(ADMIN_CONSOLE_IMAGE_OVERRIDE) \

cmd/buildtools/k0s.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"sort"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/sirupsen/logrus"
15+
"github.com/urfave/cli/v2"
16+
)
17+
18+
const (
19+
apkIndexURL = "https://packages.wolfi.dev/os/x86_64/APKINDEX.tar.gz"
20+
)
21+
22+
var k0sComponents = []struct {
23+
name string
24+
makefileVar string
25+
}{
26+
{
27+
name: "coredns",
28+
makefileVar: "COREDNS_VERSION",
29+
},
30+
{
31+
name: "calico-node",
32+
makefileVar: "CALICO_NODE_VERSION",
33+
},
34+
{
35+
name: "metrics-server",
36+
makefileVar: "METRICS_SERVER_VERSION",
37+
},
38+
}
39+
40+
var updateK0sImagesCommand = &cli.Command{
41+
Name: "k0s",
42+
Usage: "Updates the k0s images",
43+
UsageText: environmentUsageText,
44+
Flags: []cli.Flag{
45+
&cli.StringFlag{
46+
Name: "k0s-version",
47+
Usage: "The version of k0s to use to determine image versions",
48+
},
49+
},
50+
Action: func(c *cli.Context) error {
51+
logrus.Infof("updating k0s images")
52+
53+
tmpdir, err := os.MkdirTemp(os.TempDir(), "k0s-images-*")
54+
if err != nil {
55+
return fmt.Errorf("failed to create temporary directory: %w", err)
56+
}
57+
58+
if err := DownloadFile(apkIndexURL, filepath.Join(tmpdir, "APKINDEX.tar.gz")); err != nil {
59+
return fmt.Errorf("failed to download APKINDEX.tar.gz: %w", err)
60+
}
61+
62+
if err := ExtractTarGz(filepath.Join(tmpdir, "APKINDEX.tar.gz"), tmpdir); err != nil {
63+
return fmt.Errorf("failed to extract APKINDEX.tar.gz: %w", err)
64+
}
65+
66+
k0sVersion := c.String("k0s-version")
67+
if k0sVersion != "" {
68+
if err := runCommand("make", "pkg/goods/bins/k0s", fmt.Sprintf("K0S_VERSION=%s", k0sVersion), "K0S_BINARY_SOURCE_OVERRIDE="); err != nil {
69+
return fmt.Errorf("failed to make k0s: %w", err)
70+
}
71+
} else {
72+
if err := runCommand("make", "pkg/goods/bins/k0s"); err != nil {
73+
return fmt.Errorf("failed to make k0s: %w", err)
74+
}
75+
}
76+
77+
if err := runCommand("make", "bin/apko"); err != nil {
78+
return fmt.Errorf("failed to make bin/apko: %w", err)
79+
}
80+
81+
for _, component := range k0sComponents {
82+
version, err := getPackageVersion(tmpdir, component.name)
83+
if err != nil {
84+
return fmt.Errorf("failed to get package version for %s: %w", component.name, err)
85+
}
86+
87+
apkoConfig, err := generateApkoConfig(component.name, version, tmpdir)
88+
if err != nil {
89+
return fmt.Errorf("failed to generate apko config for %s: %w", component.name, err)
90+
}
91+
92+
if err := runCommand(
93+
"make",
94+
"apko-build-and-publish",
95+
fmt.Sprintf("REGISTRY=%s", os.Getenv("REGISTRY_SERVER")),
96+
fmt.Sprintf("USERNAME=%s", os.Getenv("REGISTRY_USER")),
97+
fmt.Sprintf("PASSWORD=%s", os.Getenv("REGISTRY_PASS")),
98+
fmt.Sprintf("IMAGE=replicated/ec-%s:%s", component.name, version),
99+
fmt.Sprintf("APKO_CONFIG=%s", apkoConfig),
100+
fmt.Sprintf("VERSION=%s", version),
101+
); err != nil {
102+
return fmt.Errorf("failed to build and publish apko for %s: %w", component.name, err)
103+
}
104+
105+
digest, err := getDigestFromBuildFile()
106+
if err != nil {
107+
return fmt.Errorf("failed to get digest from build file: %w", err)
108+
}
109+
110+
if err := SetMakefileVariable(component.makefileVar, fmt.Sprintf("%s@%s", version, digest)); err != nil {
111+
return fmt.Errorf("failed to set %s version: %w", component.name, err)
112+
}
113+
}
114+
115+
return nil
116+
},
117+
}
118+
119+
func getPackageVersion(tmpdir, name string) (string, error) {
120+
output, err := exec.Command("pkg/goods/bins/k0s", "airgap", "list-images", "--all").Output()
121+
if err != nil {
122+
return "", fmt.Errorf("failed to list k0s images: %w", err)
123+
}
124+
125+
// example output:
126+
// quay.io/k0sproject/calico-node:v3.26.1-1
127+
// quay.io/k0sproject/coredns:1.11.3
128+
// quay.io/k0sproject/apiserver-network-proxy-agent:v0.1.4
129+
130+
pinnedVersion := ""
131+
scanner := bufio.NewScanner(bytes.NewReader(output))
132+
for scanner.Scan() {
133+
line := scanner.Text()
134+
if !strings.Contains(line, "/"+name+":") {
135+
continue
136+
}
137+
parts := strings.Split(line, ":")
138+
if len(parts) != 2 {
139+
return "", fmt.Errorf("incorrect number of parts in image line: %s", line)
140+
}
141+
pinnedVersion = strings.TrimPrefix(parts[1], "v")
142+
pinnedVersion = strings.Split(pinnedVersion, "-")[0]
143+
break
144+
}
145+
146+
if pinnedVersion == "" {
147+
return "", fmt.Errorf("failed to find pinned version for %s", name)
148+
}
149+
150+
apkIndex, err := os.ReadFile(filepath.Join(tmpdir, "APKINDEX"))
151+
if err != nil {
152+
return "", fmt.Errorf("failed to read APKINDEX: %w", err)
153+
}
154+
155+
// example APKINDEX content:
156+
// P:calico-node
157+
// V:3.26.1-r1
158+
// ...
159+
//
160+
// P:calico-node
161+
// V:3.26.1-r10
162+
// ...
163+
//
164+
// P:calico-node
165+
// V:3.26.1-r9
166+
// ...
167+
168+
var revisions []int
169+
scanner = bufio.NewScanner(bytes.NewReader(apkIndex))
170+
for scanner.Scan() {
171+
line := scanner.Text()
172+
// filter by package name
173+
if !strings.HasPrefix(line, "P:"+name) {
174+
continue
175+
}
176+
scanner.Scan()
177+
line = scanner.Text()
178+
// filter by package version
179+
if !strings.Contains(line, "V:"+pinnedVersion) {
180+
continue
181+
}
182+
// find the latest revision for the package version
183+
parts := strings.Split(line, "-r")
184+
if len(parts) != 2 {
185+
return "", fmt.Errorf("incorrect number of parts in APKINDEX line: %s", line)
186+
}
187+
revision, err := strconv.Atoi(parts[1])
188+
if err != nil {
189+
return "", fmt.Errorf("failed to parse revision: %w", err)
190+
}
191+
revisions = append(revisions, revision)
192+
}
193+
194+
if len(revisions) == 0 {
195+
return "", fmt.Errorf("failed to find package version for %s", name)
196+
}
197+
198+
sort.Slice(revisions, func(i, j int) bool {
199+
return revisions[i] > revisions[j]
200+
})
201+
202+
return fmt.Sprintf("%s-r%d", pinnedVersion, revisions[0]), nil
203+
}
204+
205+
func generateApkoConfig(name, version, destdir string) (string, error) {
206+
inputFile := filepath.Join("deploy", "images", name, "apko.tmpl.yaml")
207+
outputFile := filepath.Join(destdir, "apko.yaml")
208+
209+
contents, err := os.ReadFile(inputFile)
210+
if err != nil {
211+
return "", fmt.Errorf("failed to read input file: %w", err)
212+
}
213+
214+
updated := strings.ReplaceAll(string(contents), "__VERSION__", version)
215+
216+
if err := os.WriteFile(outputFile, []byte(updated), 0644); err != nil {
217+
return "", fmt.Errorf("failed to write output file: %w", err)
218+
}
219+
return outputFile, nil
220+
}
221+
222+
func getDigestFromBuildFile() (string, error) {
223+
contents, err := os.ReadFile("build/digest")
224+
if err != nil {
225+
return "", fmt.Errorf("failed to read build file: %w", err)
226+
}
227+
parts := strings.Split(string(contents), "@")
228+
if len(parts) != 2 {
229+
return "", fmt.Errorf("incorrect number of parts in build file")
230+
}
231+
return strings.TrimSpace(parts[1]), nil
232+
}
233+
234+
func runCommand(name string, args ...string) error {
235+
cmd := exec.Command(name, args...)
236+
cmd.Stdout = os.Stdout
237+
cmd.Stderr = os.Stderr
238+
return cmd.Run()
239+
}

cmd/buildtools/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
const environmentUsageText = `
1414
This script uses the following environment variables:
15-
- REGISTRY_SERVER: the registry server to push the chart to (used for authentication, e.g. index.docker.io)
15+
- REGISTRY_SERVER: the registry server to push the chart/image to (only used for authentication in the case of charts, e.g. index.docker.io)
1616
- REGISTRY_USER: the username to authenticate with.
1717
- REGISTRY_PASS: the password to authenticate with.
1818
- DESTINATION: the destination repository to push the chart to (e.g. oci://ttl.sh/embedded-cluster-charts)
@@ -29,7 +29,7 @@ func main() {
2929
Name: "buildtools",
3030
Usage: "Provide a set of tools for building embedded cluster binarires",
3131
Commands: []*cli.Command{
32-
addonCommand,
32+
updateCommand,
3333
},
3434
}
3535
if err := app.RunContext(ctx, os.Args); err != nil {

cmd/buildtools/addon.go renamed to cmd/buildtools/update.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,36 @@ import (
44
"github.com/urfave/cli/v2"
55
)
66

7-
var addonCommand = &cli.Command{
7+
var updateCommand = &cli.Command{
88
Name: "update",
9-
Usage: "Manage the embedded cluster addons",
10-
Flags: []cli.Flag{
11-
&cli.BoolFlag{
12-
Name: "force",
13-
Usage: "Pushes the addon chart even if no new version was found",
14-
},
15-
},
9+
Usage: "Manage the embedded cluster components",
1610
Subcommands: []*cli.Command{
1711
updateAddonCommand,
12+
updateImagesCommand,
1813
},
1914
}
2015

2116
var updateAddonCommand = &cli.Command{
2217
Name: "addon",
2318
Usage: "Update an embedded cluster addon by copying the chart to the Replicated registry and setting the version in the Makefile",
19+
Flags: []cli.Flag{
20+
&cli.BoolFlag{
21+
Name: "force",
22+
Usage: "Pushes the addon chart even if no new version was found",
23+
},
24+
},
2425
Subcommands: []*cli.Command{
2526
updateOpenEBSAddonCommand,
2627
updateSeaweedFSAddonCommand,
2728
updateRegistryAddonCommand,
2829
updateVeleroAddonCommand,
2930
},
3031
}
32+
33+
var updateImagesCommand = &cli.Command{
34+
Name: "images",
35+
Usage: "Update embedded cluster images",
36+
Subcommands: []*cli.Command{
37+
updateK0sImagesCommand,
38+
},
39+
}

0 commit comments

Comments
 (0)