From 17105bfc50c7adb0e2626fe1e04ef0989d8a2cf3 Mon Sep 17 00:00:00 2001 From: Kohei Tokunaga Date: Wed, 1 Feb 2023 10:27:43 +0900 Subject: [PATCH] monitor: resolve paths arguments in client Signed-off-by: Kohei Tokunaga --- commands/build.go | 156 +++++++++++++++++++++++++ commands/build_test.go | 251 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 commands/build_test.go diff --git a/commands/build.go b/commands/build.go index 90ce152f..616d810a 100644 --- a/commands/build.go +++ b/commands/build.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "path/filepath" "runtime" "strconv" "strings" @@ -28,6 +29,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" dockeropts "github.com/docker/cli/opts" + "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/docker/docker/pkg/ioutils" "github.com/moby/buildkit/client" "github.com/moby/buildkit/exporter/containerimage/exptypes" @@ -514,6 +516,13 @@ func launchControllerAndRunBuild(dockerCli command.Cli, options buildOptions) er } // Start build + // NOTE: buildx server has the current working directory different from the client + // so we need to resolve paths to abosolute ones in the client. + optsP, err := resolvePaths(&opts) + if err != nil { + return err + } + opts = *optsP ref, resp, err := c.Build(ctx, opts, pr, os.Stdout, os.Stderr, progress) if err != nil { return errors.Wrapf(err, "failed to build") // TODO: allow invoke even on error @@ -649,3 +658,150 @@ func dockerUlimitToControllerUlimit(u *dockeropts.UlimitOpt) *controllerapi.Ulim } return &controllerapi.UlimitOpt{Values: values} } + +// resolvePaths resolves all paths contained in controllerapi.BuildOptions +// and replaces them to absolute paths. +func resolvePaths(options *controllerapi.BuildOptions) (_ *controllerapi.BuildOptions, err error) { + if options.ContextPath != "" && options.ContextPath != "-" { + options.ContextPath, err = filepath.Abs(options.ContextPath) + if err != nil { + return nil, err + } + } + if options.DockerfileName != "" && options.DockerfileName != "-" { + options.DockerfileName, err = filepath.Abs(options.DockerfileName) + if err != nil { + return nil, err + } + } + var contexts map[string]string + for k, v := range options.NamedContexts { + p := v + if !urlutil.IsGitURL(p) && !urlutil.IsURL(p) && !strings.HasPrefix(p, "docker-image://") && !strings.HasPrefix(p, "oci-layout://") { + // named context can specify non-path value + // https://github.com/docker/buildx/blob/v0.10.3/docs/reference/buildx_build.md#-additional-build-contexts---build-context + p, err = filepath.Abs(p) + if err != nil { + return nil, err + } + } + if contexts == nil { + contexts = make(map[string]string) + } + contexts[k] = p + } + options.NamedContexts = contexts + + var cacheFrom []*controllerapi.CacheOptionsEntry + for _, co := range options.CacheFrom { + switch co.Type { + case "local": + var attrs map[string]string + for k, v := range co.Attrs { + if attrs == nil { + attrs = make(map[string]string) + } + switch k { + case "src": + p := v + if p != "" { + p, err = filepath.Abs(p) + if err != nil { + return nil, err + } + } + attrs[k] = p + default: + attrs[k] = v + } + } + co.Attrs = attrs + cacheFrom = append(cacheFrom, co) + default: + cacheFrom = append(cacheFrom, co) + } + } + options.CacheFrom = cacheFrom + + var cacheTo []*controllerapi.CacheOptionsEntry + for _, co := range options.CacheTo { + switch co.Type { + case "local": + var attrs map[string]string + for k, v := range co.Attrs { + if attrs == nil { + attrs = make(map[string]string) + } + switch k { + case "dest": + p := v + if p != "" { + p, err = filepath.Abs(p) + if err != nil { + return nil, err + } + } + attrs[k] = p + default: + attrs[k] = v + } + } + co.Attrs = attrs + cacheTo = append(cacheTo, co) + default: + cacheTo = append(cacheTo, co) + } + } + options.CacheTo = cacheTo + var exports []*controllerapi.ExportEntry + for _, e := range options.Exports { + if e.Destination != "" && e.Destination != "-" { + e.Destination, err = filepath.Abs(e.Destination) + if err != nil { + return nil, err + } + } + exports = append(exports, e) + } + options.Exports = exports + + var secrets []*controllerapi.Secret + for _, s := range options.Secrets { + if s.FilePath != "" { + s.FilePath, err = filepath.Abs(s.FilePath) + if err != nil { + return nil, err + } + } + secrets = append(secrets, s) + } + options.Secrets = secrets + + var ssh []*controllerapi.SSH + for _, s := range options.SSH { + var ps []string + for _, pt := range s.Paths { + p := pt + if p != "" { + p, err = filepath.Abs(p) + if err != nil { + return nil, err + } + } + ps = append(ps, p) + + } + s.Paths = ps + ssh = append(ssh, s) + } + options.SSH = ssh + + if options.Opts != nil && options.Opts.MetadataFile != "" { + options.Opts.MetadataFile, err = filepath.Abs(options.Opts.MetadataFile) + if err != nil { + return nil, err + } + } + + return options, nil +} diff --git a/commands/build_test.go b/commands/build_test.go new file mode 100644 index 00000000..1a7f335e --- /dev/null +++ b/commands/build_test.go @@ -0,0 +1,251 @@ +package commands + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + controllerapi "github.com/docker/buildx/controller/pb" + "github.com/stretchr/testify/require" +) + +func TestResolvePaths(t *testing.T) { + tmpwd, err := os.MkdirTemp("", "testresolvepaths") + require.NoError(t, err) + defer os.Remove(tmpwd) + require.NoError(t, os.Chdir(tmpwd)) + tests := []struct { + name string + options controllerapi.BuildOptions + want controllerapi.BuildOptions + }{ + { + name: "contextpath", + options: controllerapi.BuildOptions{ContextPath: "test"}, + want: controllerapi.BuildOptions{ContextPath: filepath.Join(tmpwd, "test")}, + }, + { + name: "contextpath-cwd", + options: controllerapi.BuildOptions{ContextPath: "."}, + want: controllerapi.BuildOptions{ContextPath: tmpwd}, + }, + { + name: "contextpath-dash", + options: controllerapi.BuildOptions{ContextPath: "-"}, + want: controllerapi.BuildOptions{ContextPath: "-"}, + }, + { + name: "dockerfilename", + options: controllerapi.BuildOptions{DockerfileName: "test"}, + want: controllerapi.BuildOptions{DockerfileName: filepath.Join(tmpwd, "test")}, + }, + { + name: "dockerfilename-dash", + options: controllerapi.BuildOptions{DockerfileName: "-"}, + want: controllerapi.BuildOptions{DockerfileName: "-"}, + }, + { + name: "contexts", + options: controllerapi.BuildOptions{NamedContexts: map[string]string{"a": "test1", "b": "test2", + "alpine": "docker-image://alpine@sha256:0123456789", "project": "https://github.com/myuser/project.git"}}, + want: controllerapi.BuildOptions{NamedContexts: map[string]string{"a": filepath.Join(tmpwd, "test1"), "b": filepath.Join(tmpwd, "test2"), + "alpine": "docker-image://alpine@sha256:0123456789", "project": "https://github.com/myuser/project.git"}}, + }, + { + name: "cache-from", + options: controllerapi.BuildOptions{ + CacheFrom: []*controllerapi.CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{"src": "test"}, + }, + { + Type: "registry", + Attrs: map[string]string{"ref": "user/app"}, + }, + }, + }, + want: controllerapi.BuildOptions{ + CacheFrom: []*controllerapi.CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{"src": filepath.Join(tmpwd, "test")}, + }, + { + Type: "registry", + Attrs: map[string]string{"ref": "user/app"}, + }, + }, + }, + }, + { + name: "cache-to", + options: controllerapi.BuildOptions{ + CacheTo: []*controllerapi.CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{"dest": "test"}, + }, + { + Type: "registry", + Attrs: map[string]string{"ref": "user/app"}, + }, + }, + }, + want: controllerapi.BuildOptions{ + CacheTo: []*controllerapi.CacheOptionsEntry{ + { + Type: "local", + Attrs: map[string]string{"dest": filepath.Join(tmpwd, "test")}, + }, + { + Type: "registry", + Attrs: map[string]string{"ref": "user/app"}, + }, + }, + }, + }, + { + name: "exports", + options: controllerapi.BuildOptions{ + Exports: []*controllerapi.ExportEntry{ + { + Type: "local", + Destination: "-", + }, + { + Type: "local", + Destination: "test1", + }, + { + Type: "tar", + Destination: "test3", + }, + { + Type: "oci", + Destination: "-", + }, + { + Type: "docker", + Destination: "test4", + }, + { + Type: "image", + Attrs: map[string]string{"push": "true"}, + }, + }, + }, + want: controllerapi.BuildOptions{ + Exports: []*controllerapi.ExportEntry{ + { + Type: "local", + Destination: "-", + }, + { + Type: "local", + Destination: filepath.Join(tmpwd, "test1"), + }, + { + Type: "tar", + Destination: filepath.Join(tmpwd, "test3"), + }, + { + Type: "oci", + Destination: "-", + }, + { + Type: "docker", + Destination: filepath.Join(tmpwd, "test4"), + }, + { + Type: "image", + Attrs: map[string]string{"push": "true"}, + }, + }, + }, + }, + { + name: "secrets", + options: controllerapi.BuildOptions{ + Secrets: []*controllerapi.Secret{ + { + FilePath: "test1", + }, + { + ID: "val", + Env: "a", + }, + { + ID: "test", + FilePath: "test3", + }, + }, + }, + want: controllerapi.BuildOptions{ + Secrets: []*controllerapi.Secret{ + { + FilePath: filepath.Join(tmpwd, "test1"), + }, + { + ID: "val", + Env: "a", + }, + { + ID: "test", + FilePath: filepath.Join(tmpwd, "test3"), + }, + }, + }, + }, + { + name: "ssh", + options: controllerapi.BuildOptions{ + SSH: []*controllerapi.SSH{ + { + ID: "default", + Paths: []string{"test1", "test2"}, + }, + { + ID: "a", + Paths: []string{"test3"}, + }, + }, + }, + want: controllerapi.BuildOptions{ + SSH: []*controllerapi.SSH{ + { + ID: "default", + Paths: []string{filepath.Join(tmpwd, "test1"), filepath.Join(tmpwd, "test2")}, + }, + { + ID: "a", + Paths: []string{filepath.Join(tmpwd, "test3")}, + }, + }, + }, + }, + { + name: "metadatafile", + options: controllerapi.BuildOptions{ + Opts: &controllerapi.CommonOptions{ + MetadataFile: "test1", + }, + }, + want: controllerapi.BuildOptions{ + Opts: &controllerapi.CommonOptions{ + MetadataFile: filepath.Join(tmpwd, "test1"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolvePaths(&tt.options) + require.NoError(t, err) + if !reflect.DeepEqual(tt.want, *got) { + t.Fatalf("expected %#v, got %#v", tt.want, *got) + } + }) + } +}