From ff73827a6fbb3b4b991c98ddc2eaf7707e8c262d Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Wed, 30 Mar 2022 11:47:34 +0200 Subject: [PATCH] Add support of ssh authentications defined in compose file or via cli flags Signed-off-by: Guillaume Lours --- cmd/compose/build.go | 42 +++++++++++++++++++----- docs/reference/compose_build.md | 1 + docs/reference/docker_compose_build.yaml | 10 ++++++ go.mod | 2 +- go.sum | 4 +-- pkg/api/api.go | 2 ++ pkg/compose/build.go | 20 +++++++++++ pkg/e2e/compose_build_test.go | 15 +++++++++ pkg/e2e/framework.go | 15 +++++---- 9 files changed, 93 insertions(+), 18 deletions(-) diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 4a4fe9933..5dccc3452 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" buildx "github.com/docker/buildx/util/progress" "github.com/docker/compose/v2/pkg/utils" @@ -40,6 +41,28 @@ type buildOptions struct { args []string noCache bool memory string + ssh string +} + +func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) { + var SSHKeys []types.SSHKey + var err error + if opts.ssh != "" { + SSHKeys, err = loader.ParseShortSSHSyntax(opts.ssh) + if err != nil { + return api.BuildOptions{}, err + } + } + + return api.BuildOptions{ + Pull: opts.pull, + Progress: opts.progress, + Args: types.NewMappingWithEquals(opts.args), + NoCache: opts.noCache, + Quiet: opts.quiet, + Services: services, + SSHs: SSHKeys, + }, nil } var printerModes = []string{ @@ -73,7 +96,10 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command { } return nil }), - RunE: Adapt(func(ctx context.Context, args []string) error { + RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("ssh") && opts.ssh == "" { + opts.ssh = "default" + } return runBuild(ctx, backend, opts, args) }), ValidArgsFunction: serviceCompletion(p), @@ -82,6 +108,7 @@ func buildCommand(p *projectOptions, backend api.Service) *cobra.Command { cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.") cmd.Flags().StringVar(&opts.progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", "))) cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.") + cmd.Flags().StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using you default SSH Agent)") cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED") cmd.Flags().MarkHidden("parallel") //nolint:errcheck cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED") @@ -103,12 +130,9 @@ func runBuild(ctx context.Context, backend api.Service, opts buildOptions, servi return err } - return backend.Build(ctx, project, api.BuildOptions{ - Pull: opts.pull, - Progress: opts.progress, - Args: types.NewMappingWithEquals(opts.args), - NoCache: opts.noCache, - Quiet: opts.quiet, - Services: services, - }) + apiBuildOptions, err := opts.toAPIBuildOptions(services) + if err != nil { + return err + } + return backend.Build(ctx, project, apiBuildOptions) } diff --git a/docs/reference/compose_build.md b/docs/reference/compose_build.md index dbb3a4692..68d01f187 100644 --- a/docs/reference/compose_build.md +++ b/docs/reference/compose_build.md @@ -12,6 +12,7 @@ Build or rebuild services | `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) | | `--pull` | | | Always attempt to pull a newer version of the image. | | `-q`, `--quiet` | | | Don't print anything to STDOUT | +| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using you default SSH Agent) | diff --git a/docs/reference/docker_compose_build.yaml b/docs/reference/docker_compose_build.yaml index 8e14e303f..8fdca2e4d 100644 --- a/docs/reference/docker_compose_build.yaml +++ b/docs/reference/docker_compose_build.yaml @@ -117,6 +117,16 @@ options: experimentalcli: false kubernetes: false swarm: false +- option: ssh + value_type: string + description: | + Set SSH authentications used when building service images. (use 'default' for using you default SSH Agent) + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false deprecated: false experimental: false experimentalcli: false diff --git a/go.mod b/go.mod index 3dabf2171..d07ad0535 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.2 github.com/buger/goterm v1.0.4 github.com/cnabio/cnab-to-oci v0.3.1-beta1 - github.com/compose-spec/compose-go v1.2.1 + github.com/compose-spec/compose-go v1.2.2 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.6.1 github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e diff --git a/go.sum b/go.sum index 57fad434f..69a38dcdf 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/compose-spec/compose-go v1.0.8/go.mod h1:REnCbBugoIdHB7S1sfkN/aJ7AJpNApGNjNiVjA9L8x4= -github.com/compose-spec/compose-go v1.2.1 h1:8+DAP7Mt/Ohl5y6YbZdilLMvIhMxvuSZcNZyywjQmJE= -github.com/compose-spec/compose-go v1.2.1/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8= +github.com/compose-spec/compose-go v1.2.2 h1:y1dwl3KUTBnWPVur6EZno9zUIum6Q87/F5keljnGQB4= +github.com/compose-spec/compose-go v1.2.2/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8= github.com/compose-spec/godotenv v1.1.1/go.mod h1:zF/3BOa18Z24tts5qnO/E9YURQanJTBUf7nlcCTNsyc= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= diff --git a/pkg/api/api.go b/pkg/api/api.go index fdc42fee6..3abe00626 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -91,6 +91,8 @@ type BuildOptions struct { Quiet bool // Services passed in the command line to be built Services []string + // Ssh authentications passed in the command line + SSHs []types.SSHKey } // CreateOptions group options of the Create API diff --git a/pkg/compose/build.go b/pkg/compose/build.go index b88fe3310..1c6a76667 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -31,6 +31,7 @@ import ( bclient "github.com/moby/buildkit/client" "github.com/moby/buildkit/session" "github.com/moby/buildkit/session/auth/authprovider" + "github.com/moby/buildkit/session/sshforward/sshprovider" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/docker/compose/v2/pkg/api" @@ -81,6 +82,14 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti }) } + if len(options.SSHs) > 0 || len(service.Build.SSH) > 0 { + sshAgentProvider, err := sshAgentProvider(append(service.Build.SSH, options.SSHs...)) + if err != nil { + return err + } + buildOptions.Session = append(buildOptions.Session, sshAgentProvider) + } + opts[imageName] = buildOptions } } @@ -296,3 +305,14 @@ func dockerFilePath(context string, dockerfile string) string { } return filepath.Join(context, dockerfile) } + +func sshAgentProvider(sshKeys types.SSHConfig) (session.Attachable, error) { + sshConfig := make([]sshprovider.AgentConfig, 0, len(sshKeys)) + for _, sshKey := range sshKeys { + sshConfig = append(sshConfig, sshprovider.AgentConfig{ + ID: sshKey.ID, + Paths: []string{sshKey.Path}, + }) + } + return sshprovider.NewSSHAgentProvider(sshConfig) +} diff --git a/pkg/e2e/compose_build_test.go b/pkg/e2e/compose_build_test.go index 719653956..e94beacfb 100644 --- a/pkg/e2e/compose_build_test.go +++ b/pkg/e2e/compose_build_test.go @@ -18,6 +18,7 @@ package e2e import ( "net/http" + "os" "strings" "testing" "time" @@ -79,6 +80,20 @@ func TestLocalComposeBuild(t *testing.T) { res.Assert(t, icmd.Expected{Out: `"RESULT": "SUCCESS"`}) }) + t.Run("build failed with ssh default value", func(t *testing.T) { + //unset SSH_AUTH_SOCK to be sure we don't have a default value for the SSH Agent + defaultSSHAUTHSOCK := os.Getenv("SSH_AUTH_SOCK") + os.Unsetenv("SSH_AUTH_SOCK") //nolint:errcheck + defer os.Setenv("SSH_AUTH_SOCK", defaultSSHAUTHSOCK) //nolint:errcheck + + res := c.RunDockerComposeCmdNoCheck("--project-directory", "fixtures/build-test", "build", "--ssh", "") + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "invalid empty ssh agent socket: make sure SSH_AUTH_SOCK is set", + }) + + }) + t.Run("build as part of up", func(t *testing.T) { c.RunDockerOrExitError("rmi", "build-test_nginx") c.RunDockerOrExitError("rmi", "custom-nginx") diff --git a/pkg/e2e/framework.go b/pkg/e2e/framework.go index 7346514c7..d093fda39 100644 --- a/pkg/e2e/framework.go +++ b/pkg/e2e/framework.go @@ -204,17 +204,20 @@ func (c *E2eCLI) RunDockerCmd(args ...string) *icmd.Result { // RunDockerComposeCmd runs a docker compose command, expects no error and returns a result func (c *E2eCLI) RunDockerComposeCmd(args ...string) *icmd.Result { + res := c.RunDockerComposeCmdNoCheck(args...) + res.Assert(c.test, icmd.Success) + return res +} + +// RunDockerComposeCmdNoCheck runs a docker compose command, don't presume of any expectation and returns a result +func (c *E2eCLI) RunDockerComposeCmdNoCheck(args ...string) *icmd.Result { if composeStandaloneMode { composeBinary, err := findExecutable(DockerComposeExecutableName, []string{"../../bin", "../../../bin"}) assert.NilError(c.test, err) - res := icmd.RunCmd(c.NewCmd(composeBinary, args...)) - res.Assert(c.test, icmd.Success) - return res + return icmd.RunCmd(c.NewCmd(composeBinary, args...)) } args = append([]string{"compose"}, args...) - res := icmd.RunCmd(c.NewCmd(DockerExecutableName, args...)) - res.Assert(c.test, icmd.Success) - return res + return icmd.RunCmd(c.NewCmd(DockerExecutableName, args...)) } // StdoutContains returns a predicate on command result expecting a string in stdout