// Copyright 2015-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file 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 localproject provides functionality to retrieve a task definition, convert it to a Docker Compose config, and write it to a file. package localproject import ( "bufio" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "strings" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/local/converter" ecsclient "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/ecs" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config" "github.com/aws/aws-sdk-go/aws" arnParser "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ecs" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) const ( // LocalTaskDefType indicates if the task definition is read from a local file. LocalTaskDefType = "local" // RemoteTaskDefType indicates if the task definition is retrieved from ECS via ARN or name. RemoteTaskDefType = "remote" ) const ( // LocalOutDefaultFileName represents the default name for the output Docker Compose file. LocalOutDefaultFileName = "docker-compose.ecs-local.yml" // LocalOutFileMode represents the file can be read/write by its owner. LocalOutFileMode = os.FileMode(0600) // Owner=read/write, Other=none // LocalInFileName represents the default local file name for task definition JSON. LocalInFileName = "task-definition.json" ) // Indexes of Compose files const ( baseComposeIndex = iota overrideComposeIndex ) // LocalProject holds data needed to convert an ECS Task Definition to Docker Compose files. type LocalProject struct { context *cli.Context taskDefinition *ecs.TaskDefinition composeBytes [][]byte inputMetadata *converter.LocalCreateMetadata } // New instantiates a new Local Project. func New(context *cli.Context) *LocalProject { return &LocalProject{ context: context, composeBytes: [][]byte{ nil, nil, }, } } // TaskDefinition returns the ECS task definition to be converted. func (p *LocalProject) TaskDefinition() *ecs.TaskDefinition { return p.taskDefinition } // LocalOutFileName returns name of compose file output by local.Create func (p *LocalProject) LocalOutFileName() string { if customName := p.context.String(flags.Output); customName != "" { return customName } return LocalOutDefaultFileName } // LocalOutFileFullPath returns the absolute path of the local task definition file. func (p *LocalProject) LocalOutFileFullPath() (string, error) { return filepath.Abs(p.LocalOutFileName()) } // OverrideFileName returns the name of the override Compose file. func (p *LocalProject) OverrideFileName() string { baseName := p.LocalOutFileName() baseExt := filepath.Ext(baseName) return baseName[:len(baseName)-len(baseExt)] + ".override.yml" } // InputMetadata returns the metadata on the task definition used to create the docker compose file. func (p *LocalProject) InputMetadata() *converter.LocalCreateMetadata { return p.inputMetadata } // ReadTaskDefinition reads an ECS Task Definition either from a local file // or from retrieving one from ECS and stores it on the local project. func (p *LocalProject) ReadTaskDefinition() error { remote := p.context.String(flags.TaskDefinitionRemote) filename := p.context.String(flags.TaskDefinitionFile) if remote != "" && filename != "" { return fmt.Errorf("cannot specify both --%s and --%s flags", flags.TaskDefinitionRemote, flags.TaskDefinitionFile) } var taskDefinition *ecs.TaskDefinition var err error if remote != "" { taskDefinition, err = p.readTaskDefinitionFromRemote(remote) if err != nil { return err } } else if filename != "" { filename, err = filepath.Abs(filename) if err != nil { return err } taskDefinition, err = p.readTaskDefinitionFromFile(filename) if err != nil { return err } } else if defaultInputExists() { filename, err = filepath.Abs(LocalInFileName) if err != nil { return err } taskDefinition, err = p.readTaskDefinitionFromFile(filename) if err != nil { return err } } if taskDefinition == nil { return fmt.Errorf("could not detect valid task definition (either set one of --%s or --%s flags, or define a %s file)", flags.TaskDefinitionFile, flags.TaskDefinitionRemote, LocalInFileName) } p.taskDefinition = taskDefinition return nil } var defaultInputExists = func() bool { if _, err := os.Stat(LocalInFileName); err == nil { return true } return false } func (p *LocalProject) readTaskDefinitionFromFile(filename string) (*ecs.TaskDefinition, error) { p.inputMetadata = &converter.LocalCreateMetadata{ InputType: LocalTaskDefType, Value: filename, UseRole: p.context.Bool(flags.UseRole), } return readTaskDefFromLocal(filename) } var readTaskDefFromLocal = func(filename string) (*ecs.TaskDefinition, error) { bytes, err := ioutil.ReadFile(filename) if err != nil { return nil, fmt.Errorf("Error reading task definition from %s: %s", filename, err.Error()) } taskDefinition := ecs.TaskDefinition{} err = json.Unmarshal(bytes, &taskDefinition) if err != nil { return nil, fmt.Errorf("Error parsing task definition JSON: %s", err.Error()) } logrus.Infof("Reading task definition from %s\n", filename) return &taskDefinition, nil } var newCommandConfig = func(context *cli.Context, rdwr config.ReadWriter, region string) (*config.CommandConfig, error) { if region != "" { return config.NewCommandConfigWithRegion(context, rdwr, region) } return config.NewCommandConfig(context, rdwr) } func (p *LocalProject) readTaskDefinitionFromRemote(remote string) (*ecs.TaskDefinition, error) { p.inputMetadata = &converter.LocalCreateMetadata{ InputType: RemoteTaskDefType, Value: remote, UseRole: p.context.Bool(flags.UseRole), } return readTaskDefFromRemote(remote, p) } var readTaskDefFromRemote = func(remote string, p *LocalProject) (*ecs.TaskDefinition, error) { rdwr, err := config.NewReadWriter() if err != nil { return nil, err } region := "" if parsedArn, err := arnParser.Parse(remote); err == nil { region = parsedArn.Region } commandConfig, err := newCommandConfig(p.context, rdwr, region) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, "region": region, // Useful for debugging region resolution across partitions, e.g. }).Error("Unable to create an instance of CommandConfig given the cli context") return nil, err } ecsClient := ecsclient.NewECSClient(commandConfig) taskDefinition, err := ecsClient.DescribeTaskDefinition(remote) if err != nil { logrus.WithFields(logrus.Fields{ "taskDefinition": remote, }).Error("Unable to Describe TaskDefinition") return nil, err } logrus.Infof("Reading task definition from %s:%v\n", aws.StringValue(taskDefinition.Family), aws.Int64Value(taskDefinition.Revision)) return taskDefinition, nil } // Convert translates an ECS Task Definition into a Compose V3 schema and // stores the data on the project func (p *LocalProject) Convert() error { if err := p.convertBaseCompose(); err != nil { return err } if err := p.convertOverrideCompose(); err != nil { return err } return nil } func (p *LocalProject) convertBaseCompose() error { conf, err := converter.ConvertToComposeConfig(p.taskDefinition, p.inputMetadata) if err != nil { return err } data, err := converter.MarshalComposeConfig(*conf, p.LocalOutFileName()) if err != nil { return err } p.composeBytes[baseComposeIndex] = data return nil } func (p *LocalProject) convertOverrideCompose() error { conf, err := converter.ConvertToComposeOverride(p.taskDefinition) if err != nil { return err } data, err := converter.MarshalComposeConfig(*conf, p.OverrideFileName()) if err != nil { return err } p.composeBytes[overrideComposeIndex] = data return nil } // Write writes the compose data to a local compose file. The output filename // is stored on the project func (p *LocalProject) Write() error { // write compose file if err := p.writeFile(p.LocalOutFileName(), p.composeBytes[baseComposeIndex], false); err != nil { return err } // write override file if err := p.writeFile(p.OverrideFileName(), p.composeBytes[overrideComposeIndex], true); err != nil { return err } return nil } // writeFile writes the content to a filename. If the file already exists, user is // prompted to overwrite unless force flag specified. func (p *LocalProject) writeFile(filename string, content []byte, skipOverwrite bool) error { forceWrite := p.context.Bool(flags.ForceFlag) // File does not exist or force flag specified; create and write file if !fileExists(filename) || forceWrite { return write(filename, content) } // File exists if skipOverwrite { logrus.Infof("%s already exists, skipping write.", filename) return nil } return overwriteFile(filename, content) } var fileExists = func(filename string) bool { _, err := os.Stat(filename) return err == nil } var overwriteFile = func(filename string, content []byte) error { fmt.Printf("%s file already exists. Do you want to write over this file? [y/N]\n", filename) reader := bufio.NewReader(os.Stdin) stdin, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("failed reading stdin: %s", err.Error()) } input := strings.ToLower(strings.TrimSpace(stdin)) if input != "yes" && input != "y" { return fmt.Errorf("aborted writing file. To retry, rename or move %s", filename) } // Overwrite local compose file return write(filename, content) } var write = func(filename string, content []byte) error { err := ioutil.WriteFile(filename, content, LocalOutFileMode) if err == nil { logrus.Infof("Successfully wrote %s", filename) } return err }