Skip to content

Commit d5dc82c

Browse files
authored
Build arm64 tiny buildpack builder image (#2617)
Enable arm64 support for tiny builder (Java,Go). This commit actually enables only Java since some additinal work has to be done for Go because upstream paketo buildpack do not support Go fully yet. Signed-off-by: Matej Vašek <mvasek@redhat.com>
1 parent d05857a commit d5dc82c

File tree

1 file changed

+176
-41
lines changed

1 file changed

+176
-41
lines changed

hack/update-builder.go

Lines changed: 176 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"os/signal"
1515
"path/filepath"
16+
"regexp"
1617
"slices"
1718
"strings"
1819
"syscall"
@@ -31,7 +32,11 @@ import (
3132
"github.com/docker/docker/pkg/jsonmessage"
3233
"github.com/google/go-containerregistry/pkg/authn"
3334
"github.com/google/go-containerregistry/pkg/name"
35+
"github.com/google/go-containerregistry/pkg/v1/empty"
36+
"github.com/google/go-containerregistry/pkg/v1/mutate"
37+
"github.com/google/go-containerregistry/pkg/v1/partial"
3438
"github.com/google/go-containerregistry/pkg/v1/remote"
39+
"github.com/google/go-containerregistry/pkg/v1/types"
3540
"github.com/google/go-github/v49/github"
3641
"github.com/paketo-buildpacks/libpak/carton"
3742
"github.com/pelletier/go-toml"
@@ -52,7 +57,7 @@ func main() {
5257
var hadError bool
5358
for _, variant := range []string{"tiny", "base", "full"} {
5459
fmt.Println("::group::" + variant)
55-
err := buildBuilderImage(ctx, variant)
60+
err := buildBuilderImageMultiArch(ctx, variant)
5661
if err != nil {
5762
_, _ = fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
5863
hadError = true
@@ -64,10 +69,10 @@ func main() {
6469
}
6570
}
6671

67-
func buildBuilderImage(ctx context.Context, variant string) error {
72+
func buildBuilderImage(ctx context.Context, variant, arch string) (string, error) {
6873
buildDir, err := os.MkdirTemp("", "")
6974
if err != nil {
70-
return fmt.Errorf("cannot create temporary build directory: %w", err)
75+
return "", fmt.Errorf("cannot create temporary build directory: %w", err)
7176
}
7277
defer func(path string) {
7378
_ = os.RemoveAll(path)
@@ -77,68 +82,73 @@ func buildBuilderImage(ctx context.Context, variant string) error {
7782
listOpts := &github.ListOptions{Page: 0, PerPage: 1}
7883
releases, ghResp, err := ghClient.Repositories.ListReleases(ctx, "paketo-buildpacks", "builder-jammy-"+variant, listOpts)
7984
if err != nil {
80-
return fmt.Errorf("cannot get upstream builder release: %w", err)
85+
return "", fmt.Errorf("cannot get upstream builder release: %w", err)
8186
}
8287
defer func(Body io.ReadCloser) {
8388
_ = Body.Close()
8489
}(ghResp.Body)
8590

8691
if len(releases) <= 0 {
87-
return fmt.Errorf("cannot get latest release")
92+
return "", fmt.Errorf("cannot get latest release")
8893
}
8994

9095
release := releases[0]
9196

9297
if release.Name == nil {
93-
return fmt.Errorf("the name of the release is not defined")
98+
return "", fmt.Errorf("the name of the release is not defined")
9499
}
95100
if release.TarballURL == nil {
96-
return fmt.Errorf("the tarball url of the release is not defined")
101+
return "", fmt.Errorf("the tarball url of the release is not defined")
97102
}
98-
99103
newBuilderImage := "ghcr.io/knative/builder-jammy-" + variant
100-
newBuilderImageTagged := newBuilderImage + ":" + *release.Name
101-
newBuilderImageLatest := newBuilderImage + ":latest"
104+
newBuilderImageTagged := newBuilderImage + ":" + *release.Name + "-" + arch
102105
dockerUser := "gh-action"
103106
dockerPassword := os.Getenv("GITHUB_TOKEN")
104107

105108
ref, err := name.ParseReference(newBuilderImageTagged)
106109
if err != nil {
107-
return fmt.Errorf("cannot parse reference to builder target: %w", err)
110+
return "", fmt.Errorf("cannot parse reference to builder target: %w", err)
108111
}
109-
_, err = remote.Head(ref, remote.WithAuth(auth{dockerUser, dockerPassword}))
112+
desc, err := remote.Head(ref, remote.WithAuth(auth{dockerUser, dockerPassword}))
110113
if err == nil {
111114
fmt.Fprintln(os.Stderr, "The image has been already built.")
112-
return nil
115+
return newBuilderImage + "@" + desc.Digest.String(), nil
113116
}
114117

115118
builderTomlPath := filepath.Join(buildDir, "builder.toml")
116119
err = downloadBuilderToml(ctx, *release.TarballURL, builderTomlPath)
117120
if err != nil {
118-
return fmt.Errorf("cannot download builder toml: %w", err)
121+
return "", fmt.Errorf("cannot download builder toml: %w", err)
119122
}
120123

121124
builderConfig, _, err := builder.ReadConfig(builderTomlPath)
122125
if err != nil {
123-
return fmt.Errorf("cannot parse builder.toml: %w", err)
126+
return "", fmt.Errorf("cannot parse builder.toml: %w", err)
124127
}
125128

126-
err = updateJavaBuildpacks(ctx, &builderConfig)
129+
err = updateJavaBuildpacks(ctx, &builderConfig, arch)
127130
if err != nil {
128-
return fmt.Errorf("cannot patch java buildpacks: %w", err)
131+
return "", fmt.Errorf("cannot patch java buildpacks: %w", err)
129132
}
130133
addGoAndRustBuildpacks(&builderConfig)
131134

132135
packClient, err := pack.NewClient()
133136
if err != nil {
134-
return fmt.Errorf("cannot create pack client: %w", err)
137+
return "", fmt.Errorf("cannot create pack client: %w", err)
135138
}
139+
136140
createBuilderOpts := pack.CreateBuilderOptions{
137141
RelativeBaseDir: buildDir,
138-
BuilderName: newBuilderImageTagged,
139-
Config: builderConfig,
140-
Publish: false,
141-
PullPolicy: bpimage.PullIfNotPresent,
142+
Targets: []dist.Target{
143+
{
144+
OS: "linux",
145+
Arch: arch,
146+
},
147+
},
148+
BuilderName: newBuilderImageTagged,
149+
Config: builderConfig,
150+
Publish: false,
151+
PullPolicy: bpimage.PullAlways,
142152
Labels: map[string]string{
143153
"org.opencontainers.image.description": "Paketo Jammy builder enriched with Rust and Func-Go buildpacks.",
144154
"org.opencontainers.image.source": "https://github.com/knative/func",
@@ -150,16 +160,12 @@ func buildBuilderImage(ctx context.Context, variant string) error {
150160

151161
err = packClient.CreateBuilder(ctx, createBuilderOpts)
152162
if err != nil {
153-
return fmt.Errorf("canont create builder: %w", err)
163+
return "", fmt.Errorf("canont create builder: %w", err)
154164
}
155165

156166
dockerClient, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
157167
if err != nil {
158-
return fmt.Errorf("cannot create docker client")
159-
}
160-
err = dockerClient.ImageTag(ctx, newBuilderImageTagged, newBuilderImageLatest)
161-
if err != nil {
162-
return fmt.Errorf("cannot tag latest: %w", err)
168+
return "", fmt.Errorf("cannot create docker client")
163169
}
164170

165171
authConfig := registry.AuthConfig{
@@ -168,38 +174,161 @@ func buildBuilderImage(ctx context.Context, variant string) error {
168174
}
169175
bs, err := json.Marshal(&authConfig)
170176
if err != nil {
171-
return fmt.Errorf("cannot marshal credentials: %w", err)
177+
return "", fmt.Errorf("cannot marshal credentials: %w", err)
172178
}
173179
imagePushOptions := image.PushOptions{
174180
All: false,
175181
RegistryAuth: base64.StdEncoding.EncodeToString(bs),
176182
}
177183

178-
pushImage := func(image string) error {
184+
pushImage := func(image string) (string, error) {
179185
rc, err := dockerClient.ImagePush(ctx, image, imagePushOptions)
180186
if err != nil {
181-
return fmt.Errorf("cannot initialize image push: %w", err)
187+
return "", fmt.Errorf("cannot initialize image push: %w", err)
182188
}
183189
defer func(rc io.ReadCloser) {
184190
_ = rc.Close()
185191
}(rc)
192+
193+
pr, pw := io.Pipe()
194+
digestCh := make(chan string)
195+
go func() {
196+
var (
197+
jm jsonmessage.JSONMessage
198+
dec = json.NewDecoder(pr)
199+
err error
200+
)
201+
for {
202+
err = dec.Decode(&jm)
203+
if err != nil {
204+
if errors.Is(err, io.EOF) {
205+
break
206+
}
207+
panic(err)
208+
}
209+
if jm.Error != nil {
210+
continue
211+
}
212+
213+
re := regexp.MustCompile(`\sdigest: (?P<hash>sha256:[a-zA-Z0-9]+)\s`)
214+
matches := re.FindStringSubmatch(jm.Status)
215+
if len(matches) == 2 {
216+
digestCh <- matches[1]
217+
}
218+
}
219+
}()
220+
r := io.TeeReader(rc, pw)
221+
186222
fd := os.Stdout.Fd()
187223
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
188-
err = jsonmessage.DisplayJSONMessagesStream(rc, os.Stderr, fd, isTerminal, nil)
224+
err = jsonmessage.DisplayJSONMessagesStream(r, os.Stderr, fd, isTerminal, nil)
225+
_ = pw.Close()
226+
if err != nil {
227+
return "", err
228+
}
229+
230+
return <-digestCh, nil
231+
}
232+
233+
var d string
234+
d, err = pushImage(newBuilderImageTagged)
235+
if err != nil {
236+
return "", fmt.Errorf("cannot push the image: %w", err)
237+
}
238+
239+
return newBuilderImage + "@" + d, nil
240+
}
241+
242+
// Builds builder for each arch and creates manifest list
243+
func buildBuilderImageMultiArch(ctx context.Context, variant string) error {
244+
ghClient := newGHClient(ctx)
245+
listOpts := &github.ListOptions{Page: 0, PerPage: 1}
246+
releases, ghResp, err := ghClient.Repositories.ListReleases(ctx, "paketo-buildpacks", "builder-jammy-"+variant, listOpts)
247+
if err != nil {
248+
return fmt.Errorf("cannot get upstream builder release: %w", err)
249+
}
250+
defer func(Body io.ReadCloser) {
251+
_ = Body.Close()
252+
}(ghResp.Body)
253+
254+
if len(releases) <= 0 {
255+
return fmt.Errorf("cannot get latest release")
256+
}
257+
258+
release := releases[0]
259+
260+
if release.Name == nil {
261+
return fmt.Errorf("the name of the release is not defined")
262+
}
263+
if release.TarballURL == nil {
264+
return fmt.Errorf("the tarball url of the release is not defined")
265+
}
266+
267+
remoteOpts := []remote.Option{
268+
remote.WithAuth(authn.FromConfig(authn.AuthConfig{
269+
Username: "gh-action",
270+
Password: os.Getenv("GITHUB_TOKEN"),
271+
})),
272+
}
273+
274+
idx := mutate.IndexMediaType(empty.Index, types.DockerManifestList)
275+
for _, arch := range []string{"arm64", "amd64"} {
276+
if arch == "arm64" && variant != "tiny" {
277+
_, _ = fmt.Fprintf(os.Stderr, "skipping arm64 build for variant: %q\n", variant)
278+
continue
279+
}
280+
281+
var imgName string
282+
283+
imgName, err = buildBuilderImage(ctx, variant, arch)
189284
if err != nil {
190285
return err
191286
}
192-
return nil
287+
288+
imgRef, err := name.ParseReference(imgName)
289+
if err != nil {
290+
return fmt.Errorf("cannot parse image ref: %w", err)
291+
}
292+
img, err := remote.Image(imgRef, remoteOpts...)
293+
if err != nil {
294+
return fmt.Errorf("cannot get the image: %w", err)
295+
}
296+
297+
cf, err := img.ConfigFile()
298+
if err != nil {
299+
return fmt.Errorf("cannot get config file for the image: %w", err)
300+
}
301+
302+
newDesc, err := partial.Descriptor(img)
303+
if err != nil {
304+
return fmt.Errorf("cannot get partial descriptor for the image: %w", err)
305+
}
306+
newDesc.Platform = cf.Platform()
307+
308+
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
309+
Add: img,
310+
Descriptor: *newDesc,
311+
})
193312
}
194313

195-
err = pushImage(newBuilderImageTagged)
314+
idxRef, err := name.ParseReference("ghcr.io/knative/builder-jammy-" + variant + ":" + *release.Name)
196315
if err != nil {
197-
return fmt.Errorf("cannot push the image: %w", err)
316+
return fmt.Errorf("cannot parse image index ref: %w", err)
198317
}
199318

200-
err = pushImage(newBuilderImageLatest)
319+
err = remote.WriteIndex(idxRef, idx, remoteOpts...)
201320
if err != nil {
202-
return fmt.Errorf("cannot push the image: %w", err)
321+
return fmt.Errorf("cannot write image index: %w", err)
322+
}
323+
324+
idxRef, err = name.ParseReference("ghcr.io/knative/builder-jammy-" + variant + ":latest")
325+
if err != nil {
326+
return fmt.Errorf("cannot parse image index ref: %w", err)
327+
}
328+
329+
err = remote.WriteIndex(idxRef, idx, remoteOpts...)
330+
if err != nil {
331+
return fmt.Errorf("cannot write image index: %w", err)
203332
}
204333

205334
return nil
@@ -212,7 +341,7 @@ type buildpack struct {
212341
patchFunc func(packageDesc *buildpackage.Config, bpDesc *dist.BuildpackDescriptor)
213342
}
214343

215-
func buildBuildpackImage(ctx context.Context, bp buildpack) error {
344+
func buildBuildpackImage(ctx context.Context, bp buildpack, arch string) error {
216345
ghClient := newGHClient(ctx)
217346

218347
var (
@@ -326,10 +455,16 @@ func buildBuildpackImage(ctx context.Context, bp buildpack) error {
326455
Format: pack.FormatImage,
327456
Config: cfg,
328457
Publish: false,
329-
PullPolicy: bpimage.PullIfNotPresent,
458+
PullPolicy: bpimage.PullAlways,
330459
Registry: "",
331460
Flatten: false,
332461
FlattenExclude: nil,
462+
Targets: []dist.Target{
463+
{
464+
OS: "linux",
465+
Arch: arch,
466+
},
467+
},
333468
}
334469
packClient, err := pack.NewClient()
335470
if err != nil {
@@ -473,7 +608,7 @@ func addGoAndRustBuildpacks(config *builder.Config) {
473608
}
474609

475610
// updated java and java-native-image buildpack to include quarkus buildpack
476-
func updateJavaBuildpacks(ctx context.Context, builderConfig *builder.Config) error {
611+
func updateJavaBuildpacks(ctx context.Context, builderConfig *builder.Config, arch string) error {
477612
var err error
478613

479614
for _, entry := range builderConfig.Order {
@@ -485,7 +620,7 @@ func updateJavaBuildpacks(ctx context.Context, builderConfig *builder.Config) er
485620
version: entry.Group[0].Version,
486621
image: img,
487622
patchFunc: addQuarkusBuildpack,
488-
})
623+
}, arch)
489624
// TODO we might want to push these images to registry
490625
// but it's not absolutely necessary since they are included in builder
491626
if err != nil {

0 commit comments

Comments
 (0)