diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc0716a..5655394d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Change Log +## [v1.4.1](https://github.com/stelligent/mu/tree/v1.4.1) (2018-01-23) +[Full Changelog](https://github.com/stelligent/mu/compare/v1.3.2...v1.4.1) + +**Implemented enhancements:** + +- Add support for pipeline failure notifications [\#230](https://github.com/stelligent/mu/issues/230) + +**Closed issues:** + +- is it possible to infer the repo path from the local checkout? [\#268](https://github.com/stelligent/mu/issues/268) +- Use codepipeline ecs deploy target, rather than cloudformation? [\#266](https://github.com/stelligent/mu/issues/266) +- prompt for github token near the beginning of "mu pipeline up" [\#263](https://github.com/stelligent/mu/issues/263) +- support discovering subnet values and vpc id by targeting another mu service by name [\#253](https://github.com/stelligent/mu/issues/253) + +**Merged pull requests:** + +- add support for centos7 for env provider [\#262](https://github.com/stelligent/mu/pull/262) ([cplee](https://github.com/cplee)) + +## [v1.3.2](https://github.com/stelligent/mu/tree/v1.3.2) (2018-01-11) +[Full Changelog](https://github.com/stelligent/mu/compare/v1.3.1...v1.3.2) + +**Fixed bugs:** + +- Unable to update Service with target tracking capabilities if they weren't already there [\#259](https://github.com/stelligent/mu/issues/259) +- Issue 259 Added application-autoscaling:DescribeScheduledActions perm… [\#260](https://github.com/stelligent/mu/pull/260) ([akuma12](https://github.com/akuma12)) + +**Merged pull requests:** + +- v1.3.2 [\#261](https://github.com/stelligent/mu/pull/261) ([cplee](https://github.com/cplee)) + +## [v0.2.6](https://github.com/stelligent/mu/tree/v0.2.6) (2018-01-10) +[Full Changelog](https://github.com/stelligent/mu/compare/v0.2.5...v0.2.6) + +**Implemented enhancements:** + +- Add support for CodeBuild's caching feature [\#229](https://github.com/stelligent/mu/issues/229) + +**Closed issues:** + +- Add support for artifact caching in CodeBuild [\#242](https://github.com/stelligent/mu/issues/242) + ## [v1.3.1](https://github.com/stelligent/mu/tree/v1.3.1) (2018-01-08) [Full Changelog](https://github.com/stelligent/mu/compare/v1.2.3...v1.3.1) @@ -16,6 +57,9 @@ **Merged pull requests:** - use blue/green deploy as default [\#254](https://github.com/stelligent/mu/pull/254) ([brentley](https://github.com/brentley)) +- v1.3.1 [\#249](https://github.com/stelligent/mu/pull/249) ([cplee](https://github.com/cplee)) +- Issue 128 [\#247](https://github.com/stelligent/mu/pull/247) ([cplee](https://github.com/cplee)) +- Add support for docker links [\#245](https://github.com/stelligent/mu/pull/245) ([nilsga](https://github.com/nilsga)) ## [v1.2.3](https://github.com/stelligent/mu/tree/v1.2.3) (2017-12-11) [Full Changelog](https://github.com/stelligent/mu/compare/v1.2.2...v1.2.3) diff --git a/VERSION b/VERSION index 1892b926..347f5833 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.2 +1.4.1 diff --git a/cli/environments.go b/cli/environments.go index 512c6a50..e1342128 100644 --- a/cli/environments.go +++ b/cli/environments.go @@ -2,12 +2,13 @@ package cli import ( "errors" - "github.com/stelligent/mu/common" - "github.com/stelligent/mu/workflows" - "github.com/urfave/cli" "os" "strings" "time" + + "github.com/stelligent/mu/common" + "github.com/stelligent/mu/workflows" + "github.com/urfave/cli" ) func newEnvironmentsCommand(ctx *common.Context) *cli.Command { diff --git a/cli/environments_test.go b/cli/environments_test.go index a3845a62..14eaf99f 100644 --- a/cli/environments_test.go +++ b/cli/environments_test.go @@ -2,10 +2,11 @@ package cli import ( "bytes" + "testing" + "github.com/stelligent/mu/common" "github.com/stretchr/testify/assert" "github.com/urfave/cli" - "testing" ) func TestNewEnvironmentsCommand(t *testing.T) { @@ -44,10 +45,12 @@ func TestNewEnvironmentsUpsertCommand(t *testing.T) { assertion.NotNil(err) assertion.Equal(FailExitCode, lastExitCode) + lastExitCode = 0 + args = []string{UpsertCmd, TestEnv} err = runCommand(command, args) - assertion.NotNil(err) - assertion.Equal(FailExitCode, lastExitCode) + assertion.Nil(err) + assertion.Equal(0, lastExitCode) } func TestNewEnvironmentsListCommand(t *testing.T) { diff --git a/common/extension.go b/common/extension.go index c6936f3a..000f09bb 100644 --- a/common/extension.go +++ b/common/extension.go @@ -306,12 +306,12 @@ func newTemplateArchiveExtension(u *url.URL, artifactManager ArtifactManager) (E // log info about the new extension if name, ok := extManifest["name"]; ok { if version, ok := extManifest["version"]; ok { - log.Noticef("Loaded extension %s (version=%v)", name, version) + log.Warningf("Loaded extension %s (version=%v)", name, version) } else { - log.Noticef("Loaded extension %s", name) + log.Warningf("Loaded extension %s", name) } } else { - log.Noticef("Loaded extension %s", u) + log.Warningf("Loaded extension %s", u) } return ext, nil diff --git a/common/subscription.go b/common/subscription.go new file mode 100644 index 00000000..8cb8039e --- /dev/null +++ b/common/subscription.go @@ -0,0 +1,17 @@ +package common + +// SubscriptionCreator for creating subscriptions +type SubscriptionCreator interface { + CreateSubscription(topic string, protocol string, endpoint string) error +} + +// SubscriptionGetter for creating subscriptions +type SubscriptionGetter interface { + GetSubscription(topic string, protocol string, endpoint string) (interface{}, error) +} + +// SubscriptionManager composite of all subscription capabilities +type SubscriptionManager interface { + SubscriptionCreator + SubscriptionGetter +} diff --git a/common/types.go b/common/types.go index b6008136..60aeb3da 100644 --- a/common/types.go +++ b/common/types.go @@ -1,6 +1,7 @@ package common import ( + "fmt" "io" "time" ) @@ -21,6 +22,7 @@ type Context struct { DockerOut io.Writer TaskManager TaskManager ArtifactManager ArtifactManager + SubscriptionManager SubscriptionManager RolesetManager RolesetManager ExtensionsManager ExtensionsManager } @@ -188,7 +190,8 @@ type Pipeline struct { Pipeline string `yaml:"pipeline,omitempty"` Build string `yaml:"build,omitempty"` } `yaml:"roles,omitempty"` - Bucket string `yaml:"bucket,omitempty"` + Bucket string `yaml:"bucket,omitempty"` + Notify []string `yaml:"notify,omitempty"` } // Stack summary @@ -374,3 +377,21 @@ var CPUMemorySupport = []CPUMemory{ {CPU: 4096, Memory: []int{8 * GB, 9 * GB, 10 * GB, 11 * GB, 12 * GB, 13 * GB, 14 * GB, 15 * GB, 16 * GB, 17 * GB, 18 * GB, 19 * GB, 20 * GB, 21 * GB, 22 * GB, 23 * GB, 24 * GB, 25 * GB, 26 * GB, 27 * GB, 28 * GB, 29 * GB, 30 * GB}}, } + +// Warning that implements `error` but safe to ignore +type Warning struct { + Message string +} + +// Error the contract for error +func (w Warning) Error() string { + return w.Message +} + +// Warningf create a warning +func Warningf(format string, args ...interface{}) Warning { + w := Warning{ + Message: fmt.Sprintf(format, args), + } + return w +} diff --git a/provider/aws/cloudformation.go b/provider/aws/cloudformation.go index 994fddf5..303e5409 100644 --- a/provider/aws/cloudformation.go +++ b/provider/aws/cloudformation.go @@ -29,6 +29,8 @@ type cloudformationStackManager struct { cfnAPI cloudformationiface.CloudFormationAPI ec2API ec2iface.EC2API extensionsManager common.ExtensionsManager + statusSpinner *spinner.Spinner + spinnerRefCnt int } // NewStackManager creates a new StackManager backed by cloudformation @@ -42,12 +44,19 @@ func newStackManager(sess *session.Session, extensionsManager common.ExtensionsM log.Debug("Connecting to EC2 service") ec2API := ec2.New(sess) + // initialize Spinner + var statusSpinner *spinner.Spinner + if terminal.IsTerminal(int(os.Stdout.Fd())) { + statusSpinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond) + } + return &cloudformationStackManager{ dryrunPath: dryrunPath, skipVersionCheck: skipVersionCheck, cfnAPI: cfnAPI, ec2API: ec2API, extensionsManager: extensionsManager, + statusSpinner: statusSpinner, }, nil } @@ -226,7 +235,14 @@ func (cfnMgr *cloudformationStackManager) UpsertStack(stackName string, template if err != nil { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "ValidationError" && awsErr.Message() == "No updates are to be performed." { + pauseSpinner := cfnMgr.spinnerRefCnt > 0 + if pauseSpinner { + cfnMgr.stopSpinner() + } log.Infof(" No changes for stack '%s'", stackName) + if pauseSpinner { + cfnMgr.startSpinner() + } return nil } } @@ -237,6 +253,21 @@ func (cfnMgr *cloudformationStackManager) UpsertStack(stackName string, template return nil } +func (cfnMgr *cloudformationStackManager) startSpinner() { + if cfnMgr.statusSpinner != nil { + cfnMgr.statusSpinner.Start() + cfnMgr.spinnerRefCnt++ + } +} +func (cfnMgr *cloudformationStackManager) stopSpinner() { + if cfnMgr.statusSpinner != nil { + cfnMgr.spinnerRefCnt-- + //if cfnMgr.spinnerRefCnt == 0 { + cfnMgr.statusSpinner.Stop() + //} + } +} + // AwaitFinalStatus waits for the stack to arrive in a final status // returns: final status, or empty string if stack doesn't exist func (cfnMgr *cloudformationStackManager) AwaitFinalStatus(stackName string) *common.Stack { @@ -246,25 +277,15 @@ func (cfnMgr *cloudformationStackManager) AwaitFinalStatus(stackName string) *co StackName: aws.String(stackName), } - // initialize Spinner - var statusSpinner *spinner.Spinner - if terminal.IsTerminal(int(os.Stdout.Fd())) { - statusSpinner = spinner.New(spinner.CharSets[9], 100*time.Millisecond) - } - - if statusSpinner != nil { - statusSpinner.Start() - defer statusSpinner.Stop() - } + cfnMgr.startSpinner() + defer cfnMgr.stopSpinner() var priorEventTime *time.Time for { resp, err := cfnAPI.DescribeStacks(params) - if statusSpinner != nil { - statusSpinner.Stop() - } + //cfnMgr.stopSpinner() if err != nil || resp == nil || len(resp.Stacks) != 1 { log.Debugf(" Stack doesn't exist ... stack=%s", stackName) @@ -296,20 +317,29 @@ func (cfnMgr *cloudformationStackManager) AwaitFinalStatus(stackName string) *co StackName: aws.String(stackName), } eventResp, err := cfnAPI.DescribeStackEvents(eventParams) - if err == nil && eventResp != nil { - numEvents := len(eventResp.StackEvents) - for i := numEvents - 1; i >= 0; i-- { + numEvents := len(eventResp.StackEvents) + if err == nil && eventResp != nil && numEvents > 0 { + firstEventIndex := 0 + for i := 0; i < numEvents; i++ { + e := eventResp.StackEvents[i] + firstEventIndex = i + if aws.StringValue(e.ResourceType) == "AWS::CloudFormation::Stack" && strings.HasSuffix(aws.StringValue(e.ResourceStatus), "_COMPLETE") { + break + } + } + for i := firstEventIndex; i >= 0; i-- { e := eventResp.StackEvents[i] if priorEventTime == nil || priorEventTime.Before(aws.TimeValue(e.Timestamp)) { - status := aws.StringValue(e.ResourceStatus) - eventMesg := fmt.Sprintf(" %s (%s) %s %s", aws.StringValue(e.LogicalResourceId), + eventMesg := fmt.Sprintf(" %s: %s (%s) %s %s", + stackName, + aws.StringValue(e.LogicalResourceId), aws.StringValue(e.ResourceType), status, aws.StringValue(e.ResourceStatusReason)) if strings.HasSuffix(status, "_IN_PROGRESS") { - if statusSpinner != nil { - statusSpinner.Suffix = eventMesg + if cfnMgr.statusSpinner != nil { + cfnMgr.statusSpinner.Suffix = eventMesg log.Debug(eventMesg) } else { log.Info(eventMesg) @@ -327,9 +357,7 @@ func (cfnMgr *cloudformationStackManager) AwaitFinalStatus(stackName string) *co } log.Debugf(" Not in final status (%s)...sleeping for 5 seconds", *resp.Stacks[0].StackStatus) - if statusSpinner != nil { - statusSpinner.Start() - } + cfnMgr.startSpinner() time.Sleep(time.Second * 5) } } diff --git a/provider/aws/init.go b/provider/aws/init.go index 0d003733..c76a674a 100644 --- a/provider/aws/init.go +++ b/provider/aws/init.go @@ -108,6 +108,12 @@ func InitializeContext(ctx *common.Context, profile string, assumeRole string, r return err } + // initialize SubscriptionManager + ctx.SubscriptionManager, err = newSnsManager(sess) + if err != nil { + return err + } + // initialize the RolesetManager ctx.RolesetManager, err = newRolesetManager(ctx) if err != nil { diff --git a/provider/aws/sns.go b/provider/aws/sns.go new file mode 100644 index 00000000..c1913ef9 --- /dev/null +++ b/provider/aws/sns.go @@ -0,0 +1,56 @@ +package aws + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/service/sns/snsiface" + "github.com/stelligent/mu/common" +) + +type snsManager struct { + snsAPI snsiface.SNSAPI +} + +func newSnsManager(sess *session.Session) (common.SubscriptionManager, error) { + log.Debug("Connecting to SNS service") + snsAPI := sns.New(sess) + + return &snsManager{ + snsAPI: snsAPI, + }, nil +} + +// CreateSubscription +func (snsMgr *snsManager) CreateSubscription(topic string, protocol string, endpoint string) error { + snsAPI := snsMgr.snsAPI + + _, err := snsAPI.Subscribe(&sns.SubscribeInput{ + TopicArn: aws.String(topic), + Endpoint: aws.String(endpoint), + Protocol: aws.String(protocol), + }) + return err +} + +// GetSubscription +func (snsMgr *snsManager) GetSubscription(topic string, protocol string, endpoint string) (interface{}, error) { + snsAPI := snsMgr.snsAPI + + out, err := snsAPI.ListSubscriptionsByTopic(&sns.ListSubscriptionsByTopicInput{ + TopicArn: aws.String(topic), + }) + + for _, sub := range out.Subscriptions { + if aws.StringValue(sub.Protocol) == protocol && aws.StringValue(sub.Endpoint) == endpoint { + return sub, nil + } + } + + if err != nil { + return nil, err + } + + return nil, fmt.Errorf("unable to find subscription") +} diff --git a/templates/assets/env-ec2.yml b/templates/assets/env-ec2.yml index a2907e7e..c9597449 100644 --- a/templates/assets/env-ec2.yml +++ b/templates/assets/env-ec2.yml @@ -47,6 +47,14 @@ Parameters: Description: ECS AMI to launch Type: String Default: '' + ImageOsType: + Description: OS Type for ECS AMI + Type: String + Default: 'amazon' + AllowedValues: + - 'amazon' + - 'centos7' + - 'windows' InstanceSubnetIds: Type: String Description: Name of the value to import for the ecs subnet ids @@ -130,6 +138,8 @@ Outputs: Value: !Ref SshAllow ImageId: Value: !Ref ImageId + ImageOsType: + Value: !Ref ImageOsType HttpProxy: Value: !Ref HttpProxy ConsulServerAutoScalingGroup: diff --git a/templates/assets/pipeline-iam.yml b/templates/assets/pipeline-iam.yml index e86fbe34..7748c5ff 100644 --- a/templates/assets/pipeline-iam.yml +++ b/templates/assets/pipeline-iam.yml @@ -1,6 +1,6 @@ --- AWSTemplateFormatVersion: '2010-09-09' -Description: MU IAM roles for pipeline +Description: MU IAM roles and keys for pipeline Parameters: Namespace: Type: String @@ -263,6 +263,11 @@ Resources: Resource: - Fn::Sub: arn:aws:s3:::${PipelineBucket} Effect: Allow + - Action: + - sns:Publish + Effect: Allow + Resource: + - Fn::Sub: arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${Namespace}-pipeline-${ServiceName}-* - Action: - iam:PassRole Resource: diff --git a/templates/assets/pipeline.yml b/templates/assets/pipeline.yml index 9515eae6..6e08881f 100644 --- a/templates/assets/pipeline.yml +++ b/templates/assets/pipeline.yml @@ -271,8 +271,8 @@ Resources: commands: - curl -sL ${MuDownloadBaseurl}/v${MuDownloadVersion}/${MuDownloadFile} -o /usr/bin/mu - chmod +rx /usr/bin/mu - - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} --disable-iam env up ${AcptEnv} || echo "Skipping update of environment" - - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} --disable-iam db up ${AcptEnv} || echo "Skipping update of database" + - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} --disable-iam env up ${AcptEnv} + - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} --disable-iam db up ${AcptEnv} - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} --disable-iam svc deploy ${AcptEnv} - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} env show ${AcptEnv} -f json > env.json - mu -c ${MuFile} --assume-role ${MuAcptRoleArn} env show ${AcptEnv} -f shell > mu-env.sh @@ -330,8 +330,8 @@ Resources: commands: - curl -sL ${MuDownloadBaseurl}/v${MuDownloadVersion}/${MuDownloadFile} -o /usr/bin/mu - chmod +rx /usr/bin/mu - - mu -c ${MuFile} --assume-role ${MuProdRoleArn} --disable-iam env up ${ProdEnv} || echo "Skipping update of environment" - - mu -c ${MuFile} --assume-role ${MuProdRoleArn} --disable-iam db up ${ProdEnv} || echo "Skipping update of database" + - mu -c ${MuFile} --assume-role ${MuProdRoleArn} --disable-iam env up ${ProdEnv} + - mu -c ${MuFile} --assume-role ${MuProdRoleArn} --disable-iam db up ${ProdEnv} - mu -c ${MuFile} --assume-role ${MuProdRoleArn} --disable-iam svc deploy ${ProdEnv} - mu -c ${MuFile} --assume-role ${MuProdRoleArn} env show ${ProdEnv} -f json > env.json - mu -c ${MuFile} --assume-role ${MuProdRoleArn} env show ${ProdEnv} -f shell > mu-env.sh @@ -480,6 +480,7 @@ Resources: Provider: Manual Configuration: CustomData: Approve deployment to production + NotificationArn: !Ref PipelineNotificationTopic RunOrder: 10 - Name: Deploy ActionTypeId: @@ -514,6 +515,74 @@ Resources: Id: !Ref CodePipelineKeyArn Type: KMS Location: !Ref PipelineBucket + PipelineNotificationTopic: + Type: AWS::SNS::Topic + Properties: + TopicName: !Sub ${Namespace}-pipeline-${ServiceName}-notification + PipelineNotificationTopicPolicy: + Type: AWS::SNS::TopicPolicy + Properties: + Topics: + - !Ref PipelineNotificationTopic + PolicyDocument: + Version: "2012-10-17" + Id: "__default_policy_ID" + Statement: + - Sid: AWSEvents + Effect: Allow + Principal: + Service: events.amazonaws.com + Action: sns:Publish + Resource: !Ref PipelineNotificationTopic + + PipelineSucceededEventRule: + Type: AWS::Events::Rule + Properties: + Description: !Sub Pipeline Succeeded Event Rule for service ${ServiceName} + EventPattern: + source: + - aws.codepipeline + detail-type: + - CodePipeline Pipeline Execution State Change + detail: + state: + - SUCCEEDED + pipeline: + - !Sub ${Namespace}-${ServiceName} + State: "ENABLED" + Targets: + - Arn: !Ref PipelineNotificationTopic + Id: "SucceededTopic" + InputTransformer: + InputTemplate: + Fn::Sub: > + "Pipeline has succeeded. Details available at https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/" + InputPathsMap: + pipeline: "$.detail.pipeline" + PipelineFailedEventRule: + Type: AWS::Events::Rule + Properties: + Description: !Sub Pipeline Failed Event Rule for service ${ServiceName} + EventPattern: + source: + - aws.codepipeline + detail-type: + - CodePipeline Pipeline Execution State Change + detail: + state: + - FAILED + pipeline: + - !Sub ${Namespace}-${ServiceName} + State: "ENABLED" + Targets: + - Arn: !Ref PipelineNotificationTopic + Id: "FailedTopic" + InputTransformer: + InputTemplate: + Fn::Sub: > + "Pipeline has failed. Details available at https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/" + InputPathsMap: + pipeline: "$.detail.pipeline" Outputs: CodePipelineUrl: Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline} @@ -521,3 +590,6 @@ Outputs: PipelineName: Value: !Sub ${Pipeline} Description: Pipeline Name + PipelineNotificationTopicArn: + Value: !Ref PipelineNotificationTopic + Description: SNS Topic for pipeline notifications diff --git a/templates/assets/service-ec2.yml b/templates/assets/service-ec2.yml index c8d63450..76f42626 100644 --- a/templates/assets/service-ec2.yml +++ b/templates/assets/service-ec2.yml @@ -93,6 +93,14 @@ Parameters: Description: EC2 AMI to launch Type: String Default: '' + ImageOsType: + Description: OS Type for ECS AMI + Type: String + Default: 'amazon' + AllowedValues: + - 'amazon' + - 'centos7' + - 'windows' InstanceSubnetIds: Type: String Description: Name of the value to import for the ecs subnet ids @@ -155,6 +163,11 @@ Parameters: Description: Target CPU Utilization for Tracking Policy on ASG Default: '75' Conditions: + HasProxy: + "Fn::Not": + - "Fn::Equals": + - !Ref HttpProxy + - '' HasPathPattern: "Fn::Not": - "Fn::Equals": @@ -262,28 +275,25 @@ Resources: Type: AWS::AutoScaling::LaunchConfiguration Metadata: AWS::CloudFormation::Init: - config: - sources: - "/opt/consul/bin": !Ref ConsulUrl + configSets: + amazon: + - commonLinux + - amazonLinux + centos7: + - commonLinux + - centos7Linux + amazonLinux: packages: yum: awslogs: [] aws-cli: [] - ruby: [] files: - "/etc/environment": + "/etc/awslogs/awscli.conf": content: !Sub | - # created via mu - {{with .Environment}} - {{range $key, $val := .}} - {{$key}}={{$val}} - {{end}} - {{end}} - "/tmp/codedeploy-install": - source: !Sub https://aws-codedeploy-${AWS::Region}.s3.amazonaws.com/latest/install - mode: '000755' - owner: root - group: root + [plugins] + cwlogs = cwlogs + [default] + region = ${AWS::Region} "/etc/init/consul.conf": content: !Sub | description "Consul Client process" @@ -297,6 +307,71 @@ Resources: mode: '000755' owner: root group: root + + services: + sysvinit: + codedeploy-agent: + enabled: 'true' + ensureRunning: 'true' + awslogs: + enabled: 'true' + ensureRunning: 'true' + files: + - "/etc/awslogs/awslogs.conf" + - "/etc/awslogs/etc/proxy.conf" + cfn-hup: + enabled: 'true' + ensureRunning: 'true' + files: + - "/etc/cfn/cfn-hup.conf" + - "/etc/cfn/hooks.d/cfn-auto-reloader.conf" + commands: + start-consul: + command: "start consul" + centos7Linux: + files: + "/etc/systemd/system/consul.service": + content: + Fn::Sub: | + [Unit] + Description=Consul Agent + + [Service] + Type=simple + ExecStart=/opt/consul/bin/consul agent -config-dir /opt/consul/config + Restart=always + User=root + Group=root + LimitNOFILE=10240 + LimitFSIZE=infinity + + [Install] + WantedBy=multi-user.target + group: root + mode: "000755" + owner: root + commands: + awscli-install: + command: pip install --upgrade awscli + awslogs-install: + command: curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O && python ./awslogs-agent-setup.py --region ${AWS::Region} --non-interactive --configfile=/etc/awslogs/awslogs.conf + start-consul: + command: "systemctl start consul.service && systemctl enable consul.service" + commonLinux: + sources: + "/opt/consul/bin": !Ref ConsulUrl + packages: + yum: + ruby: [] + files: + "/etc/environment": + content: !Sub | + # created via mu + {{with .Environment}} + {{range $key, $val := .}} + {{$key}}={{$val}} + {{end}} + {{end}} "/opt/consul/config/service.json": content: !Sub | { @@ -320,6 +395,11 @@ Resources: ] } } + "/tmp/codedeploy-install": + source: !Sub https://aws-codedeploy-${AWS::Region}.s3.amazonaws.com/latest/install + mode: '000755' + owner: root + group: root "/etc/codedeploy-agent/conf/codedeployagent.yml": content: !Sub | --- @@ -344,12 +424,20 @@ Resources: [cfn-auto-reloader-hook] triggers=post.update path=Resources.ServiceInstances.Metadata.AWS::CloudFormation::Init - action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource ServiceInstances --region ${AWS::Region} + action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource ServiceInstances --configsets ${ImageOsType} --region ${AWS::Region} runas=root "/etc/awslogs/etc/proxy.conf": content: !Sub | HTTP_PROXY=http://${HttpProxy}/ HTTPS_PROXY=http://${HttpProxy}/ + "/etc/profile.d/proxy.sh": + content: !Sub | + http_proxy=http://${HttpProxy}/ + https_proxy=http://${HttpProxy}/ + no_proxy=169.254.169.254 + mode: '000755' + owner: root + group: root "/etc/awslogs/awscli.conf": content: !Sub | [plugins] @@ -401,27 +489,12 @@ Resources: log_group_name = ${AWS::StackName} commands: codedeploy-install: - command: "./codedeploy-install auto" + command: + Fn::If: + - HasProxy + - Fn::Sub: ./codedeploy-install auto --proxy http://${HttpProxy} + - ./codedeploy-install auto cwd: "/tmp" - start-consul: - command: "start consul" - services: - sysvinit: - codedeploy-agent: - enabled: 'true' - ensureRunning: 'true' - awslogs: - enabled: 'true' - ensureRunning: 'true' - files: - - "/etc/awslogs/awslogs.conf" - - "/etc/awslogs/etc/proxy.conf" - cfn-hup: - enabled: 'true' - ensureRunning: 'true' - files: - - "/etc/cfn/cfn-hup.conf" - - "/etc/cfn/hooks.d/cfn-auto-reloader.conf" Properties: ImageId: !Ref ImageId SecurityGroups: @@ -458,7 +531,7 @@ Resources: yum -y update yum install -y aws-cfn-bootstrap - /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource ServiceInstances --region ${AWS::Region} $CFN_PROXY_ARGS + /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource ServiceInstances --configsets ${ImageOsType} --region ${AWS::Region} $CFN_PROXY_ARGS /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ServiceAutoScalingGroup --region ${AWS::Region} $CFN_PROXY_ARGS DeployGroup: Type: AWS::CodeDeploy::DeploymentGroup diff --git a/templates/assets/service-iam.yml b/templates/assets/service-iam.yml index b4d9038e..d3d32167 100644 --- a/templates/assets/service-iam.yml +++ b/templates/assets/service-iam.yml @@ -1,6 +1,6 @@ --- AWSTemplateFormatVersion: '2010-09-09' -Description: MU IAM roles for a service +Description: MU IAM roles and keys for a service Parameters: Namespace: Type: String diff --git a/workflows/environment_upsert.go b/workflows/environment_upsert.go index d188a91a..1459315c 100644 --- a/workflows/environment_upsert.go +++ b/workflows/environment_upsert.go @@ -2,9 +2,10 @@ package workflows import ( "fmt" - "github.com/stelligent/mu/common" "strconv" "strings" + + "github.com/stelligent/mu/common" ) var ecsImagePattern = "amzn-ami-*-amazon-ecs-optimized" @@ -43,7 +44,7 @@ func (workflow *environmentWorkflow) environmentFinder(config *common.Config, en return nil } } - return fmt.Errorf("Unable to find environment named '%s' in configuration", environmentName) + return common.Warningf("Unable to find environment named '%s' in configuration", environmentName) } } diff --git a/workflows/executor.go b/workflows/executor.go index 5d1368f9..cbc80358 100644 --- a/workflows/executor.go +++ b/workflows/executor.go @@ -2,6 +2,8 @@ package workflows import ( "errors" + + "github.com/stelligent/mu/common" ) // Executor define contract for the steps of a workflow @@ -15,8 +17,15 @@ func newPipelineExecutor(executors ...Executor) Executor { for _, executor := range executors { err := executor() if err != nil { - log.Errorf("%v", err) - return errors.New("") + switch err.(type) { + case common.Warning: + log.Warning(err.Error()) + return nil + default: + log.Errorf("%v", err) + log.Debugf("%+v", err) + return errors.New("") + } } } return nil diff --git a/workflows/pipeline_common.go b/workflows/pipeline_common.go index c25b409c..a03e2e13 100644 --- a/workflows/pipeline_common.go +++ b/workflows/pipeline_common.go @@ -13,6 +13,7 @@ type pipelineWorkflow struct { codeBranch string repoName string codeDeployBucket string + notificationArn string } func colorizeActionStatus(actionStatus string) string { diff --git a/workflows/pipeline_upsert.go b/workflows/pipeline_upsert.go index 4504802f..cd104576 100644 --- a/workflows/pipeline_upsert.go +++ b/workflows/pipeline_upsert.go @@ -28,10 +28,12 @@ func NewPipelineUpserter(ctx *common.Context, tokenProvider func(bool) string) E return newPipelineExecutor( workflow.serviceFinder("", ctx), + workflow.pipelineToken(ctx.Config.Namespace, tokenProvider, ctx.StackManager, stackParams), workflow.pipelineBucket(ctx.Config.Namespace, stackParams, ctx.StackManager, ctx.StackManager), workflow.codedeployBucket(ctx.Config.Namespace, &ctx.Config.Service, ctx.StackManager, ctx.StackManager), workflow.pipelineRolesetUpserter(ctx.RolesetManager, ctx.RolesetManager, stackParams), - workflow.pipelineUpserter(ctx.Config.Namespace, tokenProvider, ctx.StackManager, ctx.StackManager, stackParams)) + workflow.pipelineUpserter(ctx.Config.Namespace, ctx.StackManager, ctx.StackManager, stackParams), + workflow.pipelineNotifyUpserter(ctx.Config.Namespace, &ctx.Config.Service.Pipeline, ctx.SubscriptionManager)) } @@ -113,6 +115,18 @@ func (workflow *pipelineWorkflow) pipelineBucket(namespace string, params map[st } } +// Fetch token if needed +func (workflow *pipelineWorkflow) pipelineToken(namespace string, tokenProvider func(bool) string, stackWaiter common.StackWaiter, params map[string]string) Executor { + return func() error { + pipelineStackName := common.CreateStackName(namespace, common.StackTypePipeline, workflow.serviceName) + pipelineStack := stackWaiter.AwaitFinalStatus(pipelineStackName) + if workflow.pipelineConfig.Source.Provider == "GitHub" { + params["GitHubToken"] = tokenProvider(pipelineStack == nil) + } + return nil + } +} + func (workflow *pipelineWorkflow) pipelineRolesetUpserter(rolesetUpserter common.RolesetUpserter, rolesetGetter common.RolesetGetter, params map[string]string) Executor { return func() error { err := rolesetUpserter.UpsertCommonRoleset() @@ -120,21 +134,16 @@ func (workflow *pipelineWorkflow) pipelineRolesetUpserter(rolesetUpserter common return err } + rolesetCount := 0 + errChan := make(chan error) + if !workflow.pipelineConfig.Acceptance.Disabled { envName := workflow.pipelineConfig.Acceptance.Environment if envName == "" { envName = "acceptance" } - err := rolesetUpserter.UpsertEnvironmentRoleset(envName) - if err != nil { - return err - } - - err = rolesetUpserter.UpsertServiceRoleset(envName, workflow.serviceName, workflow.codeDeployBucket) - if err != nil { - return err - } - + go updateEnvRoleset(rolesetUpserter, envName, workflow.serviceName, workflow.codeDeployBucket, errChan) + rolesetCount++ } if !workflow.pipelineConfig.Production.Disabled { @@ -142,12 +151,12 @@ func (workflow *pipelineWorkflow) pipelineRolesetUpserter(rolesetUpserter common if envName == "" { envName = "production" } - err := rolesetUpserter.UpsertEnvironmentRoleset(envName) - if err != nil { - return err - } + go updateEnvRoleset(rolesetUpserter, envName, workflow.serviceName, workflow.codeDeployBucket, errChan) + rolesetCount++ + } - err = rolesetUpserter.UpsertServiceRoleset(envName, workflow.serviceName, workflow.codeDeployBucket) + for i := 0; i < rolesetCount; i++ { + err := <-errChan if err != nil { return err } @@ -173,10 +182,21 @@ func (workflow *pipelineWorkflow) pipelineRolesetUpserter(rolesetUpserter common } } -func (workflow *pipelineWorkflow) pipelineUpserter(namespace string, tokenProvider func(bool) string, stackUpserter common.StackUpserter, stackWaiter common.StackWaiter, params map[string]string) Executor { +func updateEnvRoleset(rolesetUpserter common.RolesetUpserter, envName string, serviceName string, codeDeployBucket string, errChan chan error) { + err := rolesetUpserter.UpsertEnvironmentRoleset(envName) + if err != nil { + errChan <- err + return + } + + err = rolesetUpserter.UpsertServiceRoleset(envName, serviceName, codeDeployBucket) + errChan <- err + return +} + +func (workflow *pipelineWorkflow) pipelineUpserter(namespace string, stackUpserter common.StackUpserter, stackWaiter common.StackWaiter, params map[string]string) Executor { return func() error { pipelineStackName := common.CreateStackName(namespace, common.StackTypePipeline, workflow.serviceName) - pipelineStack := stackWaiter.AwaitFinalStatus(pipelineStackName) log.Noticef("Upserting Pipeline for service '%s' ...", workflow.serviceName) pipelineParams := params @@ -197,10 +217,6 @@ func (workflow *pipelineWorkflow) pipelineUpserter(namespace string, tokenProvid pipelineParams["SourceObjectKey"] = strings.Join(repoParts[1:], "/") } - if workflow.pipelineConfig.Source.Provider == "GitHub" { - pipelineParams["GitHubToken"] = tokenProvider(pipelineStack == nil) - } - if workflow.pipelineConfig.Build.Type != "" { pipelineParams["BuildType"] = workflow.pipelineConfig.Build.Type } @@ -281,6 +297,28 @@ func (workflow *pipelineWorkflow) pipelineUpserter(namespace string, tokenProvid return fmt.Errorf("Ended in failed status %s %s", stack.Status, stack.StatusReason) } + workflow.notificationArn = stack.Outputs["PipelineNotificationTopicArn"] + + return nil + } +} + +func (workflow *pipelineWorkflow) pipelineNotifyUpserter(namespace string, pipeline *common.Pipeline, subManager common.SubscriptionManager) Executor { + return func() error { + if len(workflow.notificationArn) > 0 && len(pipeline.Notify) > 0 { + log.Noticef("Updating pipeline notifications for service '%s' ...", workflow.serviceName) + for _, notify := range pipeline.Notify { + sub, _ := subManager.GetSubscription(workflow.notificationArn, "email", notify) + if sub == nil { + log.Infof(" Subscribing '%s' to '%s'", notify, workflow.notificationArn) + err := subManager.CreateSubscription(workflow.notificationArn, "email", notify) + if err != nil { + return err + } + } + + } + } return nil } } diff --git a/workflows/pipeline_upsert_test.go b/workflows/pipeline_upsert_test.go index b55bef65..7655d920 100644 --- a/workflows/pipeline_upsert_test.go +++ b/workflows/pipeline_upsert_test.go @@ -87,7 +87,9 @@ func TestPipelineUpserter(t *testing.T) { } params := make(map[string]string) - err := workflow.pipelineUpserter("mu", tokenProvider, stackManager, stackManager, params)() + err := workflow.pipelineToken("mu", tokenProvider, stackManager, params)() + assert.Nil(err) + err = workflow.pipelineUpserter("mu", stackManager, stackManager, params)() assert.Nil(err) stackManager.AssertExpectations(t) diff --git a/workflows/service_deploy.go b/workflows/service_deploy.go index 74027441..0eefe695 100644 --- a/workflows/service_deploy.go +++ b/workflows/service_deploy.go @@ -180,6 +180,7 @@ func (workflow *serviceWorkflow) serviceApplyEc2Params(params map[string]string, "SshAllow", "InstanceType", "ImageId", + "ImageOsType", "KeyName", "HttpProxy", "ConsulServerAutoScalingGroup",