diff --git a/aci/backend.go b/aci/backend.go index 80cf9370..40ec0f42 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -396,10 +396,6 @@ func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) err return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition) } -func (cs *aciComposeService) Emulate(context.Context, *cli.ProjectOptions) error { - return errdefs.ErrNotImplemented -} - func (cs *aciComposeService) Down(ctx context.Context, project string) error { logrus.Debugf("Down on project with name %q\n", project) diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index b9323944..eee3bc16 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -60,7 +60,7 @@ func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) { } // Command returns the compose command with its child commands -func Command(contextType string) *cobra.Command { +func Command() *cobra.Command { command := &cobra.Command{ Short: "Docker Compose", Use: "compose", @@ -70,7 +70,7 @@ func Command(contextType string) *cobra.Command { } command.AddCommand( - upCommand(contextType), + upCommand(), downCommand(), psCommand(), logsCommand(), diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 71968049..f4f70da2 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -24,17 +24,15 @@ import ( "github.com/spf13/cobra" "github.com/docker/compose-cli/client" - "github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/progress" ) -func upCommand(contextType string) *cobra.Command { +func upCommand() *cobra.Command { opts := composeOptions{} - var simulation bool upCmd := &cobra.Command{ Use: "up", RunE: func(cmd *cobra.Command, args []string) error { - return runUp(cmd.Context(), opts, simulation) + return runUp(cmd.Context(), opts) }, } upCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name") @@ -42,14 +40,10 @@ func upCommand(contextType string) *cobra.Command { upCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") upCmd.Flags().BoolP("detach", "d", true, " Detached mode: Run containers in the background") - if contextType == store.EcsContextType { - upCmd.Flags().BoolVar(&simulation, "simulate", false, " Simulation mode: run compose app with ECS local container endpoints") - } - return upCmd } -func runUp(ctx context.Context, opts composeOptions, simulation bool) error { +func runUp(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { return err @@ -65,9 +59,6 @@ func runUp(ctx context.Context, opts composeOptions, simulation bool) error { return err } - if simulation { - return c.ComposeService().Emulate(ctx, options) - } return c.ComposeService().Up(ctx, project) }) } diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index b40287f5..979b881c 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -38,17 +38,22 @@ $ docker context create ecs CONTEXT [flags] } func createEcsCommand() *cobra.Command { + var localSimulation bool var opts ecs.ContextParams cmd := &cobra.Command{ Use: "ecs CONTEXT [flags]", Short: "Create a context for Amazon ECS", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if localSimulation { + return runCreateLocalSimulation(cmd.Context(), args[0], opts) + } return runCreateEcs(cmd.Context(), args[0], opts) }, } addDescriptionFlag(cmd, &opts.Description) + cmd.Flags().BoolVar(&localSimulation, "local-simulation", false, "Create context for ECS local simulation endpoints") cmd.Flags().StringVar(&opts.Profile, "profile", "", "Profile") cmd.Flags().StringVar(&opts.Region, "region", "", "Region") cmd.Flags().StringVar(&opts.AwsID, "key-id", "", "AWS Access Key ID") @@ -56,6 +61,21 @@ func createEcsCommand() *cobra.Command { return cmd } +func runCreateLocalSimulation(ctx context.Context, contextName string, opts ecs.ContextParams) error { + if contextExists(ctx, contextName) { + return errors.Wrapf(errdefs.ErrAlreadyExists, "context %q", contextName) + } + cs, err := client.GetCloudService(ctx, store.EcsLocalSimulationContextType) + if err != nil { + return errors.Wrap(err, "cannot connect to ECS backend") + } + data, description, err := cs.CreateContextData(ctx, opts) + if err != nil { + return err + } + return createDockerContext(ctx, contextName, store.EcsLocalSimulationContextType, description, data) +} + func runCreateEcs(ctx context.Context, contextName string, opts ecs.ContextParams) error { if contextExists(ctx, contextName) { return errors.Wrapf(errdefs.ErrAlreadyExists, "context %q", contextName) @@ -71,7 +91,7 @@ func runCreateEcs(ctx context.Context, contextName string, opts ecs.ContextParam func getEcsContextData(ctx context.Context, opts ecs.ContextParams) (interface{}, string, error) { cs, err := client.GetCloudService(ctx, store.EcsContextType) if err != nil { - return nil, "", errors.Wrap(err, "cannot connect to AWS backend") + return nil, "", errors.Wrap(err, "cannot connect to ECS backend") } return cs.CreateContextData(ctx, opts) } diff --git a/cli/main.go b/cli/main.go index 6fd31d4d..06e23ce7 100644 --- a/cli/main.go +++ b/cli/main.go @@ -27,6 +27,8 @@ import ( "syscall" "time" + "github.com/docker/compose-cli/cli/cmd/compose" + "github.com/docker/compose-cli/cli/cmd/logout" "github.com/docker/compose-cli/errdefs" @@ -38,12 +40,12 @@ import ( // Backend registrations _ "github.com/docker/compose-cli/aci" _ "github.com/docker/compose-cli/ecs" + _ "github.com/docker/compose-cli/ecs/local" _ "github.com/docker/compose-cli/example" _ "github.com/docker/compose-cli/local" "github.com/docker/compose-cli/metrics" "github.com/docker/compose-cli/cli/cmd" - "github.com/docker/compose-cli/cli/cmd/compose" contextcmd "github.com/docker/compose-cli/cli/cmd/context" "github.com/docker/compose-cli/cli/cmd/login" "github.com/docker/compose-cli/cli/cmd/run" @@ -126,6 +128,7 @@ func main() { cmd.VersionCommand(version), cmd.StopCommand(), cmd.SecretCommand(), + compose.Command(), // Place holders cmd.EcsCommand(), @@ -183,8 +186,6 @@ func main() { $ docker context create %s `, cc.Type(), store.EcsContextType)) } - root.AddCommand(compose.Command(ctype)) - metrics.Track(ctype, os.Args[1:], root.PersistentFlags()) ctx = apicontext.WithCurrentContext(ctx, currentContext) diff --git a/client/compose.go b/client/compose.go index 4b4a7871..bd554e23 100644 --- a/client/compose.go +++ b/client/compose.go @@ -34,11 +34,6 @@ func (c *composeService) Up(context.Context, *types.Project) error { return errdefs.ErrNotImplemented } -// Emulate executes the equivalent to a `compose up` in platform emulation mode -func (c *composeService) Emulate(context.Context, *cli.ProjectOptions) error { - return errdefs.ErrNotImplemented -} - // Down executes the equivalent to a `compose down` func (c *composeService) Down(context.Context, string) error { return errdefs.ErrNotImplemented diff --git a/compose/api.go b/compose/api.go index 84b3143d..bb0b13c6 100644 --- a/compose/api.go +++ b/compose/api.go @@ -35,8 +35,6 @@ type Service interface { Ps(ctx context.Context, projectName string) ([]ServiceStatus, error) // Convert translate compose model into backend's native format Convert(ctx context.Context, project *types.Project) ([]byte, error) - // Emulate executes the equivalent to a `compose up` in platform emulation mode - Emulate(ctx context.Context, options *cli.ProjectOptions) error } // PortPublisher hold status about published port diff --git a/context/store/store.go b/context/store/store.go index 9bbc9aa8..2fad618e 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -44,6 +44,11 @@ const ( // EcsContextType is the endpoint key in the context endpoints for an ECS // backend EcsContextType = "ecs" + + // EcsLocalSimulationContextType is the endpoint key in the context endpoints for an ECS backend + // running local simulation endpoints + EcsLocalSimulationContextType = "ecs-local" + // AciContextType is the endpoint key in the context endpoints for an ACI // backend AciContextType = "aci" diff --git a/ecs/backend.go b/ecs/backend.go index 5bd391ff..f5ab5e49 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -22,8 +22,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" - "github.com/docker/compose-cli/secrets" - "github.com/docker/compose-cli/backend" "github.com/docker/compose-cli/compose" "github.com/docker/compose-cli/containers" @@ -31,6 +29,7 @@ import ( "github.com/docker/compose-cli/context/cloud" "github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/errdefs" + "github.com/docker/compose-cli/secrets" ) const backendType = store.EcsContextType diff --git a/ecs/local/backend.go b/ecs/local/backend.go new file mode 100644 index 00000000..31633005 --- /dev/null +++ b/ecs/local/backend.go @@ -0,0 +1,67 @@ +/* + Copyright 2020 Docker, Inc. + + 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 local + +import ( + "context" + + "github.com/docker/compose-cli/compose" + "github.com/docker/compose-cli/containers" + "github.com/docker/compose-cli/secrets" + "github.com/docker/docker/client" + + "github.com/docker/compose-cli/backend" + "github.com/docker/compose-cli/context/cloud" + "github.com/docker/compose-cli/context/store" +) + +const backendType = store.EcsLocalSimulationContextType + +func init() { + backend.Register(backendType, backendType, service, getCloudService) +} + +type ecsLocalSimulation struct { + moby *client.Client +} + +func service(ctx context.Context) (backend.Service, error) { + apiClient, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, err + } + + return &ecsLocalSimulation{ + moby: apiClient, + }, nil +} + +func getCloudService() (cloud.Service, error) { + return ecsLocalSimulation{}, nil +} + +func (e ecsLocalSimulation) ContainerService() containers.Service { + return nil +} + +func (e ecsLocalSimulation) SecretsService() secrets.Service { + return nil +} + +func (e ecsLocalSimulation) ComposeService() compose.Service { + return e +} diff --git a/ecs/emulate.go b/ecs/local/compose.go similarity index 77% rename from ecs/emulate.go rename to ecs/local/compose.go index 6e11648c..2d908f0e 100644 --- a/ecs/emulate.go +++ b/ecs/local/compose.go @@ -14,31 +14,59 @@ limitations under the License. */ -package ecs +package local import ( "bufio" "bytes" "context" "fmt" + "io" "os" "os/exec" "path/filepath" "strings" + "github.com/docker/compose-cli/compose" + "github.com/docker/compose-cli/errdefs" + "github.com/aws/aws-sdk-go/aws" - "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" "github.com/pkg/errors" "github.com/sanathkr/go-yaml" "golang.org/x/mod/semver" ) -func (c *ecsAPIService) Emulate(ctx context.Context, options *cli.ProjectOptions) error { - project, err := cli.ProjectFromOptions(options) +func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project) error { + cmd := exec.Command("docker-compose", "version", "--short") + b := bytes.Buffer{} + b.WriteString("v") + cmd.Stdout = bufio.NewWriter(&b) + err := cmd.Run() + if err != nil { + return errors.Wrap(err, "ECS simulation mode require Docker-compose 1.27") + } + version := semver.MajorMinor(strings.TrimSpace(b.String())) + if version == "" { + return fmt.Errorf("can't parse docker-compose version: %s", b.String()) + } + if semver.Compare(version, "v1.27") < 0 { + return fmt.Errorf("ECS simulation mode require Docker-compose 1.27, found %s", version) + } + + converted, err := e.Convert(ctx, project) if err != nil { return err } + + cmd = exec.Command("docker-compose", "--context", "default", "--project-directory", project.WorkingDir, "--project-name", project.Name, "-f", "-", "up") + cmd.Stdin = strings.NewReader(string(converted)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project) ([]byte, error) { project.Networks["credentials_network"] = types.NetworkConfig{ Driver: "bridge", Ipam: types.IPAMConfig{ @@ -54,7 +82,7 @@ func (c *ecsAPIService) Emulate(ctx context.Context, options *cli.ProjectOptions // On Windows, this directory can be found at "%UserProfile%\.aws" home, err := os.UserHomeDir() if err != nil { - return err + return nil, err } for i, service := range project.Services { @@ -62,7 +90,6 @@ func (c *ecsAPIService) Emulate(ctx context.Context, options *cli.ProjectOptions Ipv4Address: fmt.Sprintf("169.254.170.%d", i+3), } service.DependsOn = append(service.DependsOn, "ecs-local-endpoints") - service.Environment["AWS_DEFAULT_REGION"] = aws.String(c.ctx.Region) service.Environment["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"] = aws.String("/creds") service.Environment["ECS_CONTAINER_METADATA_URI"] = aws.String("http://169.254.170.2/v3") project.Services[i] = service @@ -102,30 +129,17 @@ func (c *ecsAPIService) Emulate(ctx context.Context, options *cli.ProjectOptions "secrets": project.Secrets, "configs": project.Configs, } - marshal, err := yaml.Marshal(config) - if err != nil { - return err - } - - cmd := exec.Command("docker-compose", "version", "--short") - b := bytes.Buffer{} - b.WriteString("v") - cmd.Stdout = bufio.NewWriter(&b) - err = cmd.Run() - if err != nil { - return errors.Wrap(err, "ECS simulation mode require Docker-compose 1.27") - } - version := semver.MajorMinor(strings.TrimSpace(b.String())) - if version == "" { - return fmt.Errorf("can't parse docker-compose version: %s", b.String()) - } - if semver.Compare(version, "v1.27") < 0 { - return fmt.Errorf("ECS simulation mode require Docker-compose 1.27, found %s", version) - } - - cmd = exec.Command("docker-compose", "--context", "default", "--project-directory", project.WorkingDir, "--project-name", project.Name, "-f", "-", "up") - cmd.Stdin = strings.NewReader(string(marshal)) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + return yaml.Marshal(config) +} + +func (e ecsLocalSimulation) Down(ctx context.Context, projectName string) error { + return errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose down") +} + +func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, w io.Writer) error { + return errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose logs") +} + +func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) { + return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps") } diff --git a/ecs/local/context.go b/ecs/local/context.go new file mode 100644 index 00000000..174d99e3 --- /dev/null +++ b/ecs/local/context.go @@ -0,0 +1,40 @@ +/* + Copyright 2020 Docker, Inc. + + 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 local + +import ( + "context" + + "github.com/docker/compose-cli/context/cloud" + "github.com/docker/compose-cli/ecs" + "github.com/docker/compose-cli/errdefs" +) + +var _ cloud.Service = ecsLocalSimulation{} + +func (e ecsLocalSimulation) Login(ctx context.Context, params interface{}) error { + return errdefs.ErrNotImplemented +} + +func (e ecsLocalSimulation) Logout(ctx context.Context) error { + return errdefs.ErrNotImplemented +} + +func (e ecsLocalSimulation) CreateContextData(ctx context.Context, params interface{}) (contextData interface{}, description string, err error) { + opts := params.(ecs.ContextParams) + return struct{}{}, opts.Description, nil +} diff --git a/example/backend.go b/example/backend.go index 79428e52..83f9e879 100644 --- a/example/backend.go +++ b/example/backend.go @@ -132,10 +132,6 @@ func (cs *composeService) Down(ctx context.Context, project string) error { return nil } -func (cs *composeService) Emulate(context.Context, *cli.ProjectOptions) error { - return errdefs.ErrNotImplemented -} - func (cs *composeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { return nil, errdefs.ErrNotImplemented }