diff --git a/Gopkg.lock b/Gopkg.lock index 59c2f9e8..56075af1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -5,7 +5,7 @@ branch = "master" name = "github.com/KablamoOSS/go-cli-printer" packages = ["."] - revision = "a4ee444a59b3c0bf9092ce50993a5d3b0c583ad7" + revision = "e261ebc9ccb87b2531c0603c7b8d4144e226081a" [[projects]] name = "github.com/KablamoOSS/yaml" diff --git a/config/errors.go b/config/errors.go index 5007046e..ede20601 100644 --- a/config/errors.go +++ b/config/errors.go @@ -4,8 +4,7 @@ package config var ErrorHelpInfo string func init() { - ErrorHelpInfo = ` - If this may be an issue with Kombustion, or happens repeatedly - please file a bug report https://github.com/KablamoOSS/kombustion/issues/new?template=bug_report.md -` + ErrorHelpInfo = `-- +If this may be an issue with Kombustion, or happens repeatedly please file a bug report: +https://github.com/KablamoOSS/kombustion/issues/new?template=bug_report.md` } diff --git a/internal/cloudformation/parameters.go b/internal/cloudformation/parameters.go new file mode 100644 index 00000000..b131c373 --- /dev/null +++ b/internal/cloudformation/parameters.go @@ -0,0 +1,93 @@ +package cloudformation + +import ( + "strings" + + "github.com/KablamoOSS/kombustion/internal/manifest" + "github.com/aws/aws-sdk-go/aws" + awsCF "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/urfave/cli" +) + +// GetParamMap retrives the --param if any, for the map of +// Parameters in the template +func GetParamMap(c *cli.Context) map[string]string { + paramMap := make(map[string]string) + params := c.StringSlice("param") + for _, param := range params { + parts := strings.Split(param, "=") + if len(parts) > 1 { + paramMap[parts[0]] = strings.Join(parts[1:], "=") + } + } + return paramMap +} + +// ResolveParameters for the template +func ResolveParameters( + c *cli.Context, + cfYaml YamlCloudformation, + manifestFile *manifest.Manifest, +) []*awsCF.Parameter { + results := []*awsCF.Parameter{} + + env := resolveEnvironmentParameters(manifestFile, c.String("environment")) + + // override envFile values with optional --param values + params := GetParamMap(c) + for key, value := range params { + env[key] = value + } + + // convert to aws Parameter list + for paramKey := range cfYaml.Parameters { + for key, value := range env { + if paramKey == key { + // Filter to params in the stack + results = append(results, &awsCF.Parameter{ + ParameterKey: aws.String(key), + ParameterValue: aws.String(value), + }) + } + } + } + + return results +} + +// ResolveParametersS3 for an S3 based template +func ResolveParametersS3( + c *cli.Context, + manifestFile *manifest.Manifest, +) []*awsCF.Parameter { + + results := []*awsCF.Parameter{} + + params := make(map[string]string) + + env := resolveEnvironmentParameters(manifestFile, c.String("environment")) + for key, value := range params { + env[key] = value + } + + // convert to aws Parameter list + for key, value := range params { + // Filter to params in the stack + results = append(results, &awsCF.Parameter{ + ParameterKey: aws.String(key), + ParameterValue: aws.String(value), + }) + } + + return results +} + +func resolveEnvironmentParameters(manifestFile *manifest.Manifest, environment string) (parameters map[string]string) { + if manifestFile.Environments[environment].Parameters != nil { + envParams := manifestFile.Environments[environment].Parameters + if envParams != nil { + parameters = envParams + } + } + return +} diff --git a/internal/cloudformation/parameters_test.go b/internal/cloudformation/parameters_test.go new file mode 100644 index 00000000..1bfc1e5c --- /dev/null +++ b/internal/cloudformation/parameters_test.go @@ -0,0 +1,190 @@ +package cloudformation + +import ( + "flag" + "fmt" + "testing" + + "github.com/KablamoOSS/kombustion/internal/manifest" + "github.com/KablamoOSS/kombustion/types" + awsCF "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +type mockCloudFormationClient struct { + cloudformationiface.CloudFormationAPI +} + +func TestResolveEnvironmentParameters(t *testing.T) { + tests := []struct { + name string + environment string + manifest manifest.Manifest + output map[string]string + }{ + { + name: "Returns map of env vars", + environment: "development", + manifest: manifest.Manifest{ + Name: "TestManifestWithEnvironment", + Plugins: nil, + Architectures: []string(nil), + HideDefaultExports: false, + Environments: map[string]manifest.Environment{ + "development": { + AccountIDs: nil, + Parameters: map[string]string{ + "parameterOneName": "parameterOneValue", + "parameterTwoName": "8654238642489624862", + "parameterThreeName": "3so87tg4y98n7y34ts3t4sh st34y79p4y3t7 8s", + "parameterFourName": "hhh:://asdfasdf.sadfasdf:3452345@f][a;v-][0[-", + }, + }, + }, + }, + output: map[string]string{ + "parameterOneName": "parameterOneValue", + "parameterTwoName": "8654238642489624862", + "parameterThreeName": "3so87tg4y98n7y34ts3t4sh st34y79p4y3t7 8s", + "parameterFourName": "hhh:://asdfasdf.sadfasdf:3452345@f][a;v-][0[-", + }, + }, + { + name: "Returns emtpy map", + environment: "production", + manifest: manifest.Manifest{ + Name: "TestManifestWithEnvironment", + Plugins: nil, + Architectures: []string(nil), + HideDefaultExports: false, + Environments: map[string]manifest.Environment{ + "development": { + AccountIDs: nil, + Parameters: map[string]string{ + "parameterOneName": "parameterOneValue", + "parameterTwoName": "8654238642489624862", + "parameterThreeName": "3so87tg4y98n7y34ts3t4sh st34y79p4y3t7 8s", + "parameterFourName": "hhh:://asdfasdf.sadfasdf:3452345@f][a;v-][0[-", + }, + }, + }, + }, + output: nil, + }, + } + + for i, test := range tests { + assert := assert.New(t) + testOutput := resolveEnvironmentParameters(&test.manifest, test.environment) + assert.Equal(testOutput, test.output, fmt.Sprintf("Test %d: %s", i, test.name)) + } +} + +func TestResolveParameters(t *testing.T) { + type input struct { + ctx *cli.Context + cfYaml YamlCloudformation + cfClient *awsCF.CloudFormation + manifestFile *manifest.Manifest + } + + tests := []struct { + name string + input input + output []*awsCF.Parameter + }{ + { + name: "Dev", + input: input{ + ctx: func() *cli.Context { + set := flag.NewFlagSet("test", 0) + set.String("environment", "development", "development") + context := cli.NewContext(nil, set, nil) + return context + }(), + cfYaml: YamlCloudformation{ + AWSTemplateFormatVersion: "version", + Description: "Test Template", + Parameters: types.TemplateObject{ + "parameterOneName": "parameterOneValue", + "parameterTwoName": "8654238642489624862", + "parameterThreeName": "3so87tg4y98n7y34ts3t4sh st34y79p4y3t7 8s", + "parameterFourName": "hhh:://asdfasdf.sadfasdf:3452345@f][a;v-][0[-", + }, + Mappings: types.TemplateObject{}, + Conditions: types.TemplateObject{}, + Transform: types.TemplateObject{}, + Resources: types.TemplateObject{}, + Outputs: types.TemplateObject{}, + }, + manifestFile: &manifest.Manifest{ + Name: "TestManifestWithEnvironment", + Plugins: nil, + Architectures: []string(nil), + HideDefaultExports: false, + Environments: map[string]manifest.Environment{ + "development": { + AccountIDs: nil, + Parameters: map[string]string{ + "parameterOneName": "parameterOneValue", + "parameterTwoName": "8654238642489624862", + "parameterThreeName": "3so87tg4y98n7y34ts3t4sh st34y79p4y3t7 8s", + "parameterFourName": "hhh:://asdfasdf.sadfasdf:3452345@f][a;v-][0[-", + }, + }, + }, + }, + }, + output: []*awsCF.Parameter{}, + }, + } + + for i, test := range tests { + assert := assert.New(t) + testOutput := ResolveParameters( + test.input.ctx, + test.input.cfYaml, + test.input.manifestFile, + ) + assert.Equal(testOutput, test.output, fmt.Sprintf("Test %d: %s", i, test.name)) + } +} + +// func ResolveParameters(t *testing.T) { +// tests := []struct { +// name string +// input struct { +// ctx *cli.Context +// cfYaml YamlCloudformation +// cfClient *awsCF.CloudFormation +// manifestFile *manifest.Manifest +// } +// output []*awsCF.Parameter +// throws bool +// }{ +// { +// name: "Dev", +// environment: "development", +// output: map[string]string{}, +// throws: false, +// }, +// } + +// for i, test := range tests { +// assert := assert.New(t) +// testOutput, err := ResolveParameters( +// test.input.ctx, +// test.input.cfYaml, +// test.input.cfClient, +// test.input.manifestFile, +// ) +// if test.throws { +// assert.NotNil(err) +// } else { +// assert.Nil(err) +// assert.Equal(testOutput, test.output, fmt.Sprintf("Test %d: %s", i, test.name)) +// } +// } +// } diff --git a/internal/cloudformation/tasks/upsert.go b/internal/cloudformation/tasks/upsert.go index 5411ce19..d15605de 100644 --- a/internal/cloudformation/tasks/upsert.go +++ b/internal/cloudformation/tasks/upsert.go @@ -11,14 +11,14 @@ import ( awsCF "github.com/aws/aws-sdk-go/service/cloudformation" ) -// Upsert a stack +// UpsertStack - func UpsertStack( templateBody []byte, parameters []*awsCF.Parameter, capabilities []*string, - stackName, profile, region string, + stackName string, + cf *awsCF.CloudFormation, ) { - cf := GetCloudformationClient(profile, region) var err error var status *awsCF.DescribeStacksOutput @@ -69,14 +69,14 @@ func UpsertStack( } } -// Upsert a stack +// UpsertStackViaS3 - func UpsertStackViaS3( templateURL string, parameters []*awsCF.Parameter, capabilities []*string, - stackName, profile, region string, + stackName string, + cf *awsCF.CloudFormation, ) { - cf := GetCloudformationClient(profile, region) var err error var status *awsCF.DescribeStacksOutput diff --git a/internal/manifest/init.go b/internal/manifest/init.go index be50a1ac..971c6ecf 100644 --- a/internal/manifest/init.go +++ b/internal/manifest/init.go @@ -1,9 +1,11 @@ package manifest import ( - "log" + "fmt" "strings" + "github.com/KablamoOSS/go-cli-printer" + "gopkg.in/AlecAivazis/survey.v1" ) @@ -12,9 +14,13 @@ func InitaliseNewManifest() error { // TODO: Check if there is a manifest file and exit // Load the manifest file from this directory - _, err := FindAndLoadManifest() - if err == nil { - log.Fatal("Sorry we can't create a new kombustion.yaml, one already exists.") + manifestSearch := FindAndLoadManifest() + if &manifestSearch != nil { + printer.Fatal( + fmt.Errorf("Sorry we can't create a new kombustion.yaml, one already exists."), + "If you want to re-initialise your kombustion.yaml file, first remove it.", + "https://kombustion.io/manifest", + ) } // Survey the user for required info @@ -28,7 +34,7 @@ func InitaliseNewManifest() error { Environments: environments, } - err = WriteManifestToDisk(manifest) + err = WriteManifestToDisk(&manifest) if err != nil { return err } @@ -86,7 +92,7 @@ func surveyForName() (string, error) { return strings.Replace(surveyAnswers.Name, " ", "", -1), nil } -// surveyForEnvironments - prompt the user to find out what environments this +// surveyForEnvironments prompts the user to find out what environments this // project uses func surveyForEnvironments() (manifestEnvironments map[string]Environment, err error) { // Survey for which environments are used in this project @@ -103,15 +109,31 @@ func surveyForEnvironments() (manifestEnvironments map[string]Environment, err e if err != nil { return manifestEnvironments, err } - - // TODO: prompt to ask for whitelisting account ids - for _, env := range environments { + accountId, err := surveyForAccountId(env) + if err != nil { + return manifestEnvironments, err + } manifestEnvironments[env] = Environment{ - AccountIDs: []string{""}, - Parameters: map[string]string{"": ""}, + AccountIDs: []string{accountId}, + Parameters: map[string]string{"ENVIRONMENT": env}, } } return manifestEnvironments, err } + +// surveyForAccountId prompts the user to find out what accounts each environment uses +func surveyForAccountId(environment string) (accountId string, err error) { + prompt := &survey.Input{ + Message: fmt.Sprintf("What is the Account ID for %s:", environment), + Help: "This is a whitelist of accounts, these stacks and parameters can be deployed to. This can prevent unintentional deployment.", + } + // Prompts the user + err = survey.AskOne(prompt, &accountId, nil) + if err != nil { + return accountId, err + } + + return accountId, err +} diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 9bfab72e..b375bc71 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -6,17 +6,44 @@ import ( "log" "os" "path/filepath" + "sync" + + "github.com/KablamoOSS/go-cli-printer" yaml "github.com/KablamoOSS/yaml" ) +// Once loaded, we keep the manifest in memory +var ( + loadedManifest *Manifest + once sync.Once +) + // FindAndLoadManifest - Search the current directory for a manifest file, and load it -func FindAndLoadManifest() (Manifest, error) { - path, err := filepath.Abs(filepath.Dir(os.Args[0])) - if err != nil { - log.Fatal(err) +func FindAndLoadManifest() *Manifest { + if loadedManifest == nil { + once.Do(func() { + path, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err != nil { + log.Fatal(err) + printer.Fatal( + err, + "If you want to re-initialise your kombustion.yaml file, first remove it.", + "https://kombustion.io/manifest", + ) + } + manifest, err := findAndLoadManifest(path) + if err != nil { + printer.Fatal( + err, + "If you want to re-initialise your kombustion.yaml file, first remove it.", + "https://kombustion.io/manifest", + ) + } + loadedManifest = &manifest + }) } - return findAndLoadManifest(path) + return loadedManifest } // findAndLoadManifest - Search the given directory for a manifest file, and load it diff --git a/internal/manifest/write.go b/internal/manifest/write.go index 838ec8cd..b47d3be6 100644 --- a/internal/manifest/write.go +++ b/internal/manifest/write.go @@ -7,7 +7,7 @@ import ( ) // WriteManifestToDisk - Write the final manifest to disk -func WriteManifestToDisk(manifest Manifest) error { +func WriteManifestToDisk(manifest *Manifest) error { // Mashall the the struct into yaml manifestString, err := yaml.Marshal(&manifest) diff --git a/internal/plugins/add.go b/internal/plugins/add.go index 25628317..49f51fa4 100644 --- a/internal/plugins/add.go +++ b/internal/plugins/add.go @@ -16,7 +16,7 @@ import ( // AddPluginsToManifest - Add all new plugin to the manifest // update it if it's already there // then write the manifest to disk -func AddPluginsToManifest(manifest manifestType.Manifest, pluginLocations []string) (manifestType.Manifest, error) { +func AddPluginsToManifest(manifest *manifestType.Manifest, pluginLocations []string) (*manifestType.Manifest, error) { printer.Progress("Kombusting") lockFile, err := lock.FindAndLoadLock() @@ -53,7 +53,7 @@ func AddPluginsToManifest(manifest manifestType.Manifest, pluginLocations []stri } // updatePluginInManifest - Write a new manifest to disk -func updatePluginInManifest(manifest manifestType.Manifest) error { +func updatePluginInManifest(manifest *manifestType.Manifest) error { printer.Progress("Updating manifest") err := manifestType.WriteManifestToDisk(manifest) if err != nil { @@ -64,7 +64,7 @@ func updatePluginInManifest(manifest manifestType.Manifest) error { // constructGithubPlugin - Create a plugin based on a github url func constructGithubPlugin( - manifest manifestType.Manifest, pluginURI string, + manifest *manifestType.Manifest, pluginURI string, ) ( plugin manifestType.Plugin, pluginLock lock.Plugin, diff --git a/internal/plugins/load.go b/internal/plugins/load.go index 166619d7..e295a1e2 100644 --- a/internal/plugins/load.go +++ b/internal/plugins/load.go @@ -26,7 +26,7 @@ func LoadPlugins() (resources, outputs, mappings map[string]kombustionTypes.Pars log.Fatal(err) } - manifestFile, err := manifest.FindAndLoadManifest() + manifestFile := manifest.FindAndLoadManifest() if err != nil { log.Fatal(err) } diff --git a/internal/plugins/lock/update.go b/internal/plugins/lock/update.go index 1c72a5b8..da23b578 100644 --- a/internal/plugins/lock/update.go +++ b/internal/plugins/lock/update.go @@ -5,7 +5,7 @@ import ( ) // UpdateLock - update and write out a new lock file -func UpdateLock(manifest manifestType.Manifest, newLockFile Lock) error { +func UpdateLock(manifest *manifestType.Manifest, newLockFile Lock) error { // First load the lock file lockFile, err := FindAndLoadLock() if err != nil { @@ -18,7 +18,7 @@ func UpdateLock(manifest manifestType.Manifest, newLockFile Lock) error { return err } -func updateLock(manifest manifestType.Manifest, lockFile Lock, newLockFile Lock) error { +func updateLock(manifest *manifestType.Manifest, lockFile Lock, newLockFile Lock) error { // TODO: reconcile the manifest with the lock file err := WriteLockToDisk(newLockFile) diff --git a/internal/tasks/add.go b/internal/tasks/add.go index 099de515..78be4c97 100644 --- a/internal/tasks/add.go +++ b/internal/tasks/add.go @@ -14,16 +14,13 @@ import ( func AddPluginToManifest(c *cli.Context) error { printer.Step("Adding plugins") // Try and load the manifest - manifest, err := manifest.FindAndLoadManifest() - if err != nil { - log.Fatal("No kombustion.yaml manifest. Create one with: kombustion init") - } + manifest := manifest.FindAndLoadManifest() // Get the plugin to add pluginNames := c.Args() // Add them - manifest, err = plugins.AddPluginsToManifest(manifest, pluginNames) + manifest, err := plugins.AddPluginsToManifest(manifest, pluginNames) if err != nil { log.Fatal(err) } diff --git a/internal/tasks/flags.go b/internal/tasks/flags.go index 12f8efa5..b8335e70 100644 --- a/internal/tasks/flags.go +++ b/internal/tasks/flags.go @@ -22,10 +22,11 @@ var CloudFormationStackFlags = []cli.Flag{ }, cli.StringFlag{ Name: "stack-name", - Usage: "stack name to deploy (defaults to filename)", + Usage: "stack name to deploy (defaults to ProjectName-Filename--Environment)", }, cli.StringFlag{ - Name: "env", + Name: "environment, e", Usage: "environment config to use from ./kombustion.yaml", + // Help: "If you omit this, it will be derived based on the account the role is assumed from, provided that account is listed under an environment.", }, } diff --git a/internal/tasks/generate.go b/internal/tasks/generate.go index e41795f0..04d0a339 100644 --- a/internal/tasks/generate.go +++ b/internal/tasks/generate.go @@ -24,7 +24,7 @@ func init() { // Generate a template and save it to disk, without upserting it func Generate(c *cli.Context) { - paramMap := getParamMap(c) + paramMap := cloudformation.GetParamMap(c) tasks.GenerateTemplate(cloudformation.GenerateParams{ Filename: c.Args().Get(0), diff --git a/internal/tasks/upsert.go b/internal/tasks/upsert.go index a1cd76ba..bf2217c1 100644 --- a/internal/tasks/upsert.go +++ b/internal/tasks/upsert.go @@ -1,8 +1,10 @@ package tasks import ( + printer "github.com/KablamoOSS/go-cli-printer" "github.com/KablamoOSS/kombustion/internal/cloudformation" "github.com/KablamoOSS/kombustion/internal/cloudformation/tasks" + "github.com/KablamoOSS/kombustion/internal/manifest" "github.com/aws/aws-sdk-go/aws" awsCF "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/urfave/cli" @@ -34,7 +36,15 @@ func init() { // Upsert a stack func Upsert(c *cli.Context) { - paramMap := getParamMap(c) + printer.Step("Upserting stack") + manifest := manifest.FindAndLoadManifest() + + cfClient := tasks.GetCloudformationClient( + c.GlobalString("profile"), + c.String("region"), + ) + + paramMap := cloudformation.GetParamMap(c) // Template generation parameters generateParams := cloudformation.GenerateParams{ @@ -55,7 +65,7 @@ func Upsert(c *cli.Context) { stackName = c.String("stack-name") } if len(c.String("url")) > 0 { - parameters = resolveParametersS3(c) + parameters = cloudformation.ResolveParametersS3(c, manifest) templateURL := c.String("url") @@ -64,21 +74,19 @@ func Upsert(c *cli.Context) { parameters, capabilities, stackName, - c.GlobalString("profile"), - c.String("region"), + cfClient, ) } else { templateBody, cfYaml := tasks.GenerateYamlTemplate(generateParams) - parameters = resolveParameters(c, cfYaml) + parameters = cloudformation.ResolveParameters(c, cfYaml, manifest) tasks.UpsertStack( templateBody, parameters, capabilities, stackName, - c.GlobalString("profile"), - c.String("region"), + cfClient, ) } } diff --git a/internal/tasks/util.go b/internal/tasks/util.go deleted file mode 100644 index cd44c455..00000000 --- a/internal/tasks/util.go +++ /dev/null @@ -1,78 +0,0 @@ -package tasks - -import ( - "strings" - - "github.com/KablamoOSS/kombustion/internal/cloudformation" - "github.com/KablamoOSS/kombustion/types" - "github.com/aws/aws-sdk-go/aws" - awsCF "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/urfave/cli" -) - -func getParamMap(c *cli.Context) map[string]string { - paramMap := make(map[string]string) - params := c.StringSlice("param") - for _, param := range params { - parts := strings.Split(param, "=") - if len(parts) > 1 { - paramMap[parts[0]] = strings.Join(parts[1:], "=") - } - } - return paramMap -} - -func resolveParameters(c *cli.Context, cfYaml cloudformation.YamlCloudformation) []*awsCF.Parameter { - results := []*awsCF.Parameter{} - - // Get params from the envFile - env := cloudformation.ResolveEnvironment(c.String("env-file"), c.String("env")) - - // override envFile values with optional --param values - params := getParamMap(c) - for k, v := range params { - env[k] = v - } - - // convert to aws Parameter list - for paramK := range cfYaml.Parameters { - for k, v := range env { - if paramK == k { - if s, ok := v.(string); ok { - // Filter to params in the stack - results = append(results, &awsCF.Parameter{ - ParameterKey: aws.String(k), - ParameterValue: aws.String(s), - }) - } - } - } - } - - return results -} - -func resolveParametersS3(c *cli.Context) []*awsCF.Parameter { - results := []*awsCF.Parameter{} - - var params types.TemplateObject - - // override envFile values with optional --param values - paramMap := getParamMap(c) - for k, v := range paramMap { - params[k] = v - } - - // convert to aws Parameter list - for k, v := range params { - if s, ok := v.(string); ok { - // Filter to params in the stack - results = append(results, &awsCF.Parameter{ - ParameterKey: aws.String(k), - ParameterValue: aws.String(s), - }) - } - } - - return results -} diff --git a/main.go b/main.go index 6369ef5c..1f58358d 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ import ( printer "github.com/KablamoOSS/go-cli-printer" "github.com/KablamoOSS/kombustion/internal/tasks" log "github.com/sirupsen/logrus" + "github.com/ttacon/chalk" "github.com/urfave/cli" ) @@ -74,16 +75,16 @@ func main() { version = "BUILT_FROM_SOURCE" } - kombustionLogo := ` + kombustionLogo := chalk.Dim.TextStyle(` __ __ __ _ / /_____ __ _ / / __ _____ / /_(_)__ ___ / '_/ _ \/ ' \/ _ \/ // (_-