// Copyright 2015-2017 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 config import ( "fmt" "io/ioutil" "os" "path/filepath" "github.com/pkg/errors" "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) const ( clusterConfigFileName = "config" profileConfigFileName = "credentials" configFileMode = os.FileMode(0600) // Owner=read/write, Other=None ) // ReadWriter interface has methods to read and write ecs-cli config to and from the config file. type ReadWriter interface { SaveProfile(string, *Profile) error SaveCluster(string, *Cluster) error SetDefaultProfile(string) error SetDefaultCluster(string) error Get(string, string) (*LocalConfig, error) } // YAMLReadWriter implements the ReadWriter interfaces. It can be used to save and load // ecs-cli config. Sample ecs-cli config: // cluster: test // aws_profile: // region: us-west-2 // aws_access_key_id: // aws_secret_access_key: // compose-project-name-prefix: ecscompose- // compose-service-name-prefix: // cfn-stack-name-prefix: ecs-cli- type YAMLReadWriter struct { destination *Destination } // NewReadWriter creates a new Parser object. func NewReadWriter() (*YAMLReadWriter, error) { dest, err := NewDefaultDestination() if err != nil { return nil, err } return &YAMLReadWriter{destination: dest}, nil } func readINI(dest *Destination, localConfig *LocalConfig) error { // Only read if the file exists; ini library is not good about throwing file not exist errors if _, err := os.Stat(ConfigFilePath(dest)); err == nil { iniReadWriter, err := NewINIReadWriter(dest) if err != nil { return err } return iniReadWriter.GetConfig(localConfig) } return nil } // ReadClusterFile reads the cluster config file and returns a cluster config object func ReadClusterFile(path string) (*ClusterConfig, error) { dat, err := ioutil.ReadFile(path) if err != nil { return nil, errors.Wrap(err, "Failed to read config file: "+path) } config := ClusterConfig{Clusters: make(map[string]Cluster)} if err = yaml.Unmarshal(dat, &config); err != nil { return nil, errors.Wrap(err, "Failed to parse yaml file: "+path) } return &config, nil } // ReadCredFile reads the cluster config file and returns a profile config object func ReadCredFile(path string) (*ProfileConfig, error) { dat, err := ioutil.ReadFile(path) if err != nil { return nil, errors.Wrap(err, "Failed to read config file: "+path) } config := ProfileConfig{Profiles: make(map[string]Profile)} if err = yaml.Unmarshal(dat, &config); err != nil { return nil, errors.Wrap(err, "Failed to parse yaml file: "+path) } return &config, nil } // readClusterConfig does all the work to read and parse the yaml cluster config func readClusterConfig(path string, clusterConfigKey string, localConfig *LocalConfig) error { // read cluster file config, err := ReadClusterFile(path) if err != nil { return err } // get the correct cluster chosenCluster := clusterConfigKey if clusterConfigKey == "" { chosenCluster = config.Default } cluster, ok := config.Clusters[chosenCluster] if !ok { return fmt.Errorf("Cluster Configuration %s could not be found. Configure clusters using 'ecs-cli configure'.", chosenCluster) } // Get the info out of the cluster localConfig.Region = cluster.Region localConfig.Cluster = cluster.Cluster localConfig.ComposeServiceNamePrefix = cluster.ComposeServiceNamePrefix localConfig.CFNStackName = cluster.CFNStackName localConfig.DefaultLaunchType = cluster.DefaultLaunchType // Fields must be explicitly set as empty because the iniReadWriter will set them to default localConfig.ComposeProjectNamePrefix = "" localConfig.CFNStackNamePrefix = "" localConfig.Version = yamlConfigVersion return nil } // readProfileConfig does all the work to read and parse the yaml cluster config func readProfileConfig(path string, profileConfigKey string, localConfig *LocalConfig) error { // read profile file config, err := ReadCredFile(path) if err != nil { return err } // get the correct profile chosenProfile := profileConfigKey if profileConfigKey == "" { chosenProfile = config.Default } profile, ok := config.Profiles[chosenProfile] if !ok { return fmt.Errorf("ECS Profile %s could not be found. Configure profiles using 'ecs-cli configure profile'.", chosenProfile) } // Get the info out of the cluster localConfig.AWSSecretKey = profile.AWSSecretKey localConfig.AWSAccessKey = profile.AWSAccessKey localConfig.AWSSessionToken = profile.AWSSessionToken localConfig.Version = yamlConfigVersion return nil } // Get gets the ecs-cli config object from the config file(s). // This function either reads the old single configuration file // Or if the new files are present, it reads from them instead func (rdwr *YAMLReadWriter) Get(clusterConfig string, profileConfig string) (*LocalConfig, error) { localConfig := &LocalConfig{Version: yamlConfigVersion} profilePath := credentialsFilePath(rdwr.destination) configPath := ConfigFilePath(rdwr.destination) // try to readINI first; it is either successful or it // set localConfig to be its default value (all fields empty strings) readINI(rdwr.destination, localConfig) // Try to read the config as YAML // nothing will happen if it fails errYAML := readClusterConfig(configPath, clusterConfig, localConfig) if _, err := os.Stat(profilePath); err == nil { // credentials file exists- so that means we are using the new style configs err = readProfileConfig(profilePath, profileConfig, localConfig) if err != nil { return nil, err } } // Check if there was a format error on the config file // this happens when both ini and yaml readers fail to read anything // but the file does exist (the files are allowed to not exist). if _, err := os.Stat(configPath); err == nil && localConfig.Cluster == "" && localConfig.Region == "" { return nil, errors.Wrapf(errYAML, "Error parsing %s", configPath) } // if no configs exist, we return an empty object return localConfig, nil } func (rdwr *YAMLReadWriter) saveConfig(path string, config interface{}) error { data, err := yaml.Marshal(config) if err != nil { return errors.Wrap(err, "Error saving config file") } destMode := rdwr.destination.Mode if err = os.MkdirAll(rdwr.destination.Path, *destMode); err != nil { return err } // If config file exists, set permissions first, because we may be writing creds. if _, err = os.Stat(path); err == nil { if err = os.Chmod(path, configFileMode); err != nil { logrus.Errorf("Unable to chmod %s to mode %s", path, configFileMode) return err } } if err = ioutil.WriteFile(path, data, configFileMode.Perm()); err != nil { logrus.Errorf("Unable to write configuration to %s", path) return err } return nil } // SaveProfile saves a single credential configuration func (rdwr *YAMLReadWriter) SaveProfile(configName string, profile *Profile) error { path := credentialsFilePath(rdwr.destination) config := &ProfileConfig{Profiles: make(map[string]Profile), Version: configVersion} if _, err := os.Stat(path); err == nil { // an existing config file is there config, err = ReadCredFile(path) if err != nil { return err } } config.Profiles[configName] = *profile if len(config.Profiles) == 1 { config.Default = configName } // save the modified config return rdwr.saveConfig(path, config) } // SaveCluster save a single cluster configuration func (rdwr *YAMLReadWriter) SaveCluster(configName string, cluster *Cluster) error { path := ConfigFilePath(rdwr.destination) // if no err on read- then existing yaml config config, err := ReadClusterFile(path) if err != nil { // err on read: this means that no yaml file currently exists config = &ClusterConfig{Clusters: make(map[string]Cluster), Version: configVersion} } config.Clusters[configName] = *cluster if len(config.Clusters) == 1 { config.Default = configName } // save the modified config return rdwr.saveConfig(path, config) } // SetDefaultProfile updates which set of credentials is defined as default func (rdwr *YAMLReadWriter) SetDefaultProfile(configName string) error { path := credentialsFilePath(rdwr.destination) config, err := ReadCredFile(path) if err != nil { return err } if _, ok := config.Profiles[configName]; !ok { return fmt.Errorf("%s must be defined as a profile before it can be set as default. ", configName) } config.Default = configName // save the modified config return rdwr.saveConfig(path, config) } // SetDefaultCluster updates which cluster configuration is default func (rdwr *YAMLReadWriter) SetDefaultCluster(configName string) error { path := ConfigFilePath(rdwr.destination) config, err := ReadClusterFile(path) if err != nil { return err } if _, ok := config.Clusters[configName]; !ok { return fmt.Errorf("%s must be defined as a profile before it can be set as default. ", configName) } config.Default = configName // save the modified config return rdwr.saveConfig(path, config) } func credentialsFilePath(dest *Destination) string { return filepath.Join(dest.Path, profileConfigFileName) } func ConfigFilePath(dest *Destination) string { return filepath.Join(dest.Path, clusterConfigFileName) }