From 656902196da2e847bdfa30999883702f22a7e118 Mon Sep 17 00:00:00 2001 From: hang lv Date: Fri, 12 May 2023 09:11:34 +0800 Subject: [PATCH] feat: build image by platform (#1582) * feat: build image by platform Signed-off-by: hang lv * feat: support sequential multi-platform image building Signed-off-by: hang lv * feat: set goos/goarch as default target platform Signed-off-by: hang lv * fix: lint Signed-off-by: hang lv * fix: pull correct base image Signed-off-by: hang lv --------- Signed-off-by: hang lv --- go.mod | 2 +- pkg/app/build.go | 34 +++++++++++++++++++++++------ pkg/app/build/build.go | 2 ++ pkg/app/up.go | 6 +++++ pkg/builder/build.go | 27 +++++++++++++++++++++-- pkg/builder/types.go | 3 +++ pkg/builder/util.go | 14 ++++++------ pkg/editor/vscode/util.go | 4 ++-- pkg/lang/ir/graph.go | 4 +++- pkg/lang/ir/util.go | 6 ++++- pkg/lang/ir/v0/compile.go | 12 +++++++--- pkg/lang/ir/v0/types.go | 4 ++++ pkg/lang/ir/v1/compile.go | 12 +++++++--- pkg/lang/ir/v1/editor.go | 5 +---- pkg/lang/ir/v1/system.go | 2 +- pkg/lang/ir/v1/types.go | 4 ++++ pkg/types/envd.go | 2 +- pkg/util/runtimeutil/runtimeutil.go | 25 +++++++++++++++++++++ 18 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 pkg/util/runtimeutil/runtimeutil.go diff --git a/go.mod b/go.mod index fa9c779d6..685d55d4f 100644 --- a/go.mod +++ b/go.mod @@ -180,7 +180,7 @@ require ( go.opentelemetry.io/otel/sdk v1.14.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.9.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect diff --git a/pkg/app/build.go b/pkg/app/build.go index 8457d0426..1db90d5bd 100644 --- a/pkg/app/build.go +++ b/pkg/app/build.go @@ -15,6 +15,7 @@ package app import ( + "strings" "time" "github.com/sirupsen/logrus" @@ -23,6 +24,7 @@ import ( buildutil "github.com/tensorchord/envd/pkg/app/build" "github.com/tensorchord/envd/pkg/app/telemetry" sshconfig "github.com/tensorchord/envd/pkg/ssh/config" + "github.com/tensorchord/envd/pkg/util/runtimeutil" ) var CommandBuild = &cli.Command{ @@ -89,6 +91,12 @@ To build and push the image to a registry: Usage: "Import the cache (e.g. type=registry,ref=)", Aliases: []string{"ic"}, }, + &cli.StringFlag{ + Name: "platform", + Usage: `Specify the target platforms for the build output (for example, windows/amd64 or linux/amd64,darwin/arm64). + Build images with same tags could cause image overwriting, platform suffixes will be added to differentiate the images.`, + DefaultText: runtimeutil.GetRuntimePlatform(), + }, }, Action: build, } @@ -106,12 +114,24 @@ func build(clicontext *cli.Context) error { logger := logrus.WithField("builder-options", opt) logger.Debug("starting build command") - builder, err := buildutil.GetBuilder(clicontext, opt) - if err != nil { - return err - } - if err = buildutil.InterpretEnvdDef(builder); err != nil { - return err + platforms := strings.Split(opt.Platform, ",") + for _, platform := range platforms { + o := opt + o.Platform = platform + if len(platforms) > 1 { + // Transform the platform suffix to comply with the tag naming rule. + o.Tag += "-" + strings.Replace(platform, "/", "-", 1) + } + builder, err := buildutil.GetBuilder(clicontext, o) + if err != nil { + return err + } + if err = buildutil.InterpretEnvdDef(builder); err != nil { + return err + } + if err := buildutil.BuildImage(clicontext, builder); err != nil { + return err + } } - return buildutil.BuildImage(clicontext, builder) + return nil } diff --git a/pkg/app/build/build.go b/pkg/app/build/build.go index a9d3c206a..6d0423501 100644 --- a/pkg/app/build/build.go +++ b/pkg/app/build/build.go @@ -135,6 +135,7 @@ func ParseBuildOpt(clicontext *cli.Context) (builder.Options, error) { exportCache := clicontext.String("export-cache") importCache := clicontext.String("import-cache") useProxy := clicontext.Bool("use-proxy") + platform := clicontext.String("platform") opt := builder.Options{ ManifestFilePath: manifest, @@ -148,6 +149,7 @@ func ParseBuildOpt(clicontext *cli.Context) (builder.Options, error) { ExportCache: exportCache, ImportCache: importCache, UseHTTPProxy: useProxy, + Platform: platform, } debug := clicontext.Bool("debug") diff --git a/pkg/app/up.go b/pkg/app/up.go index f7ac8ca0e..27043e109 100644 --- a/pkg/app/up.go +++ b/pkg/app/up.go @@ -29,6 +29,7 @@ import ( "github.com/tensorchord/envd/pkg/home" sshconfig "github.com/tensorchord/envd/pkg/ssh/config" "github.com/tensorchord/envd/pkg/types" + "github.com/tensorchord/envd/pkg/util/runtimeutil" ) var CommandUp = &cli.Command{ @@ -141,6 +142,11 @@ var CommandUp = &cli.Command{ Usage: "Import the cache (e.g. type=registry,ref=)", Aliases: []string{"ic"}, }, + &cli.StringFlag{ + Name: "platform", + Usage: "Specify the target platform for the build output, (for example, windows/amd64, linux/amd64, or darwin/arm64)", + DefaultText: runtimeutil.GetRuntimePlatform(), + }, }, Action: up, diff --git a/pkg/builder/build.go b/pkg/builder/build.go index 04f9d7d57..4c67a25d2 100644 --- a/pkg/builder/build.go +++ b/pkg/builder/build.go @@ -19,6 +19,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/cockroachdb/errors" "github.com/docker/cli/cli/config" @@ -26,6 +27,7 @@ import ( "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/session" "github.com/moby/buildkit/session/auth/authprovider" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" @@ -157,7 +159,11 @@ func (b generalBuilder) Interpret() error { func (b generalBuilder) Compile(ctx context.Context) (*llb.Definition, error) { envName := filepath.Base(b.BuildContextDir) - def, err := b.graph.Compile(ctx, envName, b.PubKeyPath, b.Options.ProgressMode) + platform, err := parsePlatform(b.Platform) + if err != nil { + return nil, err + } + def, err := b.graph.Compile(ctx, envName, b.PubKeyPath, platform, b.Options.ProgressMode) if err != nil { return nil, errors.Wrap(err, "failed to compile build.envd") } @@ -191,8 +197,9 @@ func (b generalBuilder) imageConfig(ctx context.Context) (string, error) { env := b.graph.GetEnviron() user := b.graph.GetUser() + platform := b.graph.GetPlatform() - data, err := ImageConfigStr(labels, ports, ep, env, user) + data, err := ImageConfigStr(labels, ports, ep, env, user, platform) if err != nil { return "", errors.Wrap(err, "failed to get image config") } @@ -335,3 +342,19 @@ func constructSolveOpt(ce []client.CacheOptionsEntry, entry client.ExportEntry, } return opt } + +func parsePlatform(platform string) (*ocispecs.Platform, error) { + os, arch, variant := "linux", "amd64", "" + if platform == "" { + return &ocispecs.Platform{Architecture: arch, OS: os, Variant: variant}, nil + } + arr := strings.Split(platform, "/") + if len(arr) < 2 { + return nil, errors.New("invalid platform format, expected `os/arch[/variant]`") + } + os, arch = arr[0], arr[1] + if len(arr) >= 3 { + variant = arr[2] + } + return &ocispecs.Platform{Architecture: arch, OS: os, Variant: variant}, nil +} diff --git a/pkg/builder/types.go b/pkg/builder/types.go index ba6c899de..9127c8a4f 100644 --- a/pkg/builder/types.go +++ b/pkg/builder/types.go @@ -49,6 +49,9 @@ type Options struct { ImportCache string // UseHTTPProxy uses HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the build process. UseHTTPProxy bool + // Specify the target platform for the build output. + // e.g. platform=linux/arm64,linux/amd64 + Platform string } type generalBuilder struct { diff --git a/pkg/builder/util.go b/pkg/builder/util.go index 367c9cfd0..9b35368f1 100644 --- a/pkg/builder/util.go +++ b/pkg/builder/util.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/console" "github.com/moby/buildkit/client" gatewayclient "github.com/moby/buildkit/frontend/gateway/client" - v1 "github.com/opencontainers/image-spec/specs-go/v1" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" ) @@ -35,9 +35,9 @@ const ( ) func ImageConfigStr(labels map[string]string, ports map[string]struct{}, - entrypoint []string, env []string, user string) (string, error) { - img := v1.Image{ - Config: v1.ImageConfig{ + entrypoint []string, env []string, user string, platform *ocispecs.Platform) (string, error) { + img := ocispecs.Image{ + Config: ocispecs.ImageConfig{ Labels: labels, User: user, WorkingDir: "/", @@ -45,10 +45,10 @@ func ImageConfigStr(labels map[string]string, ports map[string]struct{}, ExposedPorts: ports, Entrypoint: entrypoint, }, - Architecture: "amd64", + Architecture: platform.Architecture, // Refer to https://github.com/tensorchord/envd/issues/269#issuecomment-1152944914 - OS: "linux", - RootFS: v1.RootFS{ + OS: platform.OS, + RootFS: ocispecs.RootFS{ Type: "layers", }, } diff --git a/pkg/editor/vscode/util.go b/pkg/editor/vscode/util.go index e88139b3e..867f7dc55 100644 --- a/pkg/editor/vscode/util.go +++ b/pkg/editor/vscode/util.go @@ -22,7 +22,7 @@ import ( "strings" "github.com/cockroachdb/errors" - v1 "github.com/opencontainers/image-spec/specs-go/v1" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" ) @@ -38,7 +38,7 @@ const ( PLATFORM_ALPINE_X64 = "alpine-x64" ) -func ConvertLLBPlatform(platform *v1.Platform) (string, error) { +func ConvertLLBPlatform(platform *ocispecs.Platform) (string, error) { // Convert opencontainers style platform to VSCode extension style platform. switch platform.OS { case "windows": diff --git a/pkg/lang/ir/graph.go b/pkg/lang/ir/graph.go index 6600eebd7..958f8451b 100644 --- a/pkg/lang/ir/graph.go +++ b/pkg/lang/ir/graph.go @@ -18,12 +18,13 @@ import ( "context" "github.com/moby/buildkit/client/llb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/tensorchord/envd/pkg/progress/compileui" ) type Graph interface { - Compile(ctx context.Context, envName string, pub string, progressMode string) (*llb.Definition, error) + Compile(ctx context.Context, envName string, pub string, platform *ocispecs.Platform, progressMode string) (*llb.Definition, error) graphDebugger graphVisitor @@ -56,4 +57,5 @@ type graphVisitor interface { GetHTTP() []HTTPInfo GetRuntimeCommands() map[string]string GetUser() string + GetPlatform() *ocispecs.Platform } diff --git a/pkg/lang/ir/util.go b/pkg/lang/ir/util.go index 99aecf99f..744f7f31b 100644 --- a/pkg/lang/ir/util.go +++ b/pkg/lang/ir/util.go @@ -26,12 +26,16 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -func FetchImageConfig(ctx context.Context, imageName string) (config v1.ImageConfig, err error) { +func FetchImageConfig(ctx context.Context, imageName string, platform *v1.Platform) (config v1.ImageConfig, err error) { ref, err := docker.ParseReference(fmt.Sprintf("//%s", imageName)) if err != nil { return config, errors.Wrap(err, "failed to parse image reference") } sys := types.SystemContext{} + if platform != nil { + sys.ArchitectureChoice = platform.Architecture + sys.OSChoice = platform.OS + } src, err := ref.NewImageSource(ctx, &sys) if err != nil { return config, errors.Wrap(err, "failed to get image source from ref") diff --git a/pkg/lang/ir/v0/compile.go b/pkg/lang/ir/v0/compile.go index fd0359301..706552b4f 100644 --- a/pkg/lang/ir/v0/compile.go +++ b/pkg/lang/ir/v0/compile.go @@ -24,6 +24,7 @@ import ( "github.com/cockroachdb/errors" "github.com/moby/buildkit/client/llb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "github.com/spf13/viper" servertypes "github.com/tensorchord/envd-server/api/types" @@ -65,6 +66,7 @@ func NewGraph() ir.Graph { Shell: shellBASH, CondaConfig: conda, RuntimeGraph: runtimeGraph, + Platform: &ocispecs.Platform{}, } } @@ -106,7 +108,11 @@ func (g generalGraph) GetUser() string { return "envd" } -func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, progressMode string) (*llb.Definition, error) { +func (g generalGraph) GetPlatform() *ocispecs.Platform { + return g.Platform +} + +func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, platform *ocispecs.Platform, progressMode string) (*llb.Definition, error) { w, err := compileui.New(ctx, os.Stdout, progressMode) if err != nil { return nil, errors.Wrap(err, "failed to create compileui") @@ -114,6 +120,7 @@ func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, g.Writer = w g.EnvironmentName = envName g.PublicKeyPath = pub + g.Platform = platform uid, gid, err := getUIDGID() if err != nil { @@ -123,8 +130,7 @@ func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, if err != nil { return nil, errors.Wrap(err, "failed to compile the graph") } - // TODO(gaocegege): Support multi platform. - def, err := state.Marshal(ctx, llb.LinuxAmd64) + def, err := state.Marshal(ctx, llb.Platform(*g.Platform)) if err != nil { return nil, errors.Wrap(err, "failed to marshal the llb definition") } diff --git a/pkg/lang/ir/v0/types.go b/pkg/lang/ir/v0/types.go index 51edcc188..96f0ac545 100644 --- a/pkg/lang/ir/v0/types.go +++ b/pkg/lang/ir/v0/types.go @@ -15,6 +15,8 @@ package v0 import ( + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/tensorchord/envd/pkg/editor/vscode" "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/progress/compileui" @@ -77,6 +79,8 @@ type generalGraph struct { EnvironmentName string ir.RuntimeGraph + + Platform *ocispecs.Platform } const ( diff --git a/pkg/lang/ir/v1/compile.go b/pkg/lang/ir/v1/compile.go index b7d02f3ec..176f84140 100644 --- a/pkg/lang/ir/v1/compile.go +++ b/pkg/lang/ir/v1/compile.go @@ -24,6 +24,7 @@ import ( "github.com/cockroachdb/errors" "github.com/moby/buildkit/client/llb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" servertypes "github.com/tensorchord/envd-server/api/types" @@ -57,6 +58,7 @@ func NewGraph() ir.Graph { UserDirectories: []string{}, Shell: shellBASH, RuntimeGraph: runtimeGraph, + Platform: &ocispecs.Platform{}, } } @@ -102,7 +104,11 @@ func (g generalGraph) GetRuntimeCommands() map[string]string { return g.RuntimeCommands } -func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, progressMode string) (*llb.Definition, error) { +func (g generalGraph) GetPlatform() *ocispecs.Platform { + return g.Platform +} + +func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, platform *ocispecs.Platform, progressMode string) (*llb.Definition, error) { w, err := compileui.New(ctx, os.Stdout, progressMode) if err != nil { return nil, errors.Wrap(err, "failed to create compileui") @@ -110,6 +116,7 @@ func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, g.Writer = w g.EnvironmentName = envName g.PublicKeyPath = pub + g.Platform = platform uid, gid, err := g.getUIDGID() if err != nil { @@ -119,8 +126,7 @@ func (g *generalGraph) Compile(ctx context.Context, envName string, pub string, if err != nil { return nil, errors.Wrap(err, "failed to compile the graph") } - // TODO(gaocegege): Support multi platform. - def, err := state.Marshal(ctx, llb.LinuxAmd64) + def, err := state.Marshal(ctx, llb.Platform(*g.Platform)) if err != nil { return nil, errors.Wrap(err, "failed to marshal the llb definition") } diff --git a/pkg/lang/ir/v1/editor.go b/pkg/lang/ir/v1/editor.go index 02ed8732b..c081fd90d 100644 --- a/pkg/lang/ir/v1/editor.go +++ b/pkg/lang/ir/v1/editor.go @@ -20,7 +20,6 @@ import ( "github.com/cockroachdb/errors" "github.com/moby/buildkit/client/llb" - v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/tensorchord/envd/pkg/config" "github.com/tensorchord/envd/pkg/editor/vscode" @@ -31,9 +30,7 @@ import ( ) func (g generalGraph) compileVSCode(root llb.State) (llb.State, error) { - // TODO(n063h): support multiple platforms - p := &v1.Platform{Architecture: "amd64", OS: "linux"} - platform, err := vscode.ConvertLLBPlatform(p) + platform, err := vscode.ConvertLLBPlatform(g.Platform) if err != nil { return llb.State{}, errors.Wrap(err, "failed to convert llb platform") } diff --git a/pkg/lang/ir/v1/system.go b/pkg/lang/ir/v1/system.go index 480c587ab..8b4205421 100644 --- a/pkg/lang/ir/v1/system.go +++ b/pkg/lang/ir/v1/system.go @@ -354,7 +354,7 @@ func (g *generalGraph) compileBaseImage() (llb.State, error) { if !g.Dev { // fetching the image config may take some time - config, err := ir.FetchImageConfig(context.Background(), g.Image) + config, err := ir.FetchImageConfig(context.Background(), g.Image, g.Platform) if err != nil { return llb.State{}, err } diff --git a/pkg/lang/ir/v1/types.go b/pkg/lang/ir/v1/types.go index 1c34077a7..cc869d6a3 100644 --- a/pkg/lang/ir/v1/types.go +++ b/pkg/lang/ir/v1/types.go @@ -15,6 +15,8 @@ package v1 import ( + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/tensorchord/envd/pkg/editor/vscode" "github.com/tensorchord/envd/pkg/lang/ir" "github.com/tensorchord/envd/pkg/progress/compileui" @@ -78,6 +80,8 @@ type generalGraph struct { EnvironmentName string ir.RuntimeGraph + + Platform *ocispecs.Platform } const ( diff --git a/pkg/types/envd.go b/pkg/types/envd.go index 560b396f6..567c8aa79 100644 --- a/pkg/types/envd.go +++ b/pkg/types/envd.go @@ -36,7 +36,7 @@ const ( PythonBaseImage = "ubuntu:20.04" EnvdStarshipImage = "tensorchord/starship:v0.0.1" // supervisor - HorustImage = "tensorchord/horust:v0.2.1" + HorustImage = "tensorchord/horust:v0.2.3" HorustServiceDir = "/etc/horust/services" HorustLogDir = "/var/log/horust" // env diff --git a/pkg/util/runtimeutil/runtimeutil.go b/pkg/util/runtimeutil/runtimeutil.go new file mode 100644 index 000000000..5e5ccfca2 --- /dev/null +++ b/pkg/util/runtimeutil/runtimeutil.go @@ -0,0 +1,25 @@ +// Copyright 2023 The envd Authors +// Copyright 2023 mateors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtimeutil + +import ( + "fmt" + "runtime" +) + +func GetRuntimePlatform() string { + return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) +}