Repository: aws/amazon-ecs-cli-v2
Pull request #245 (2019-10-29 01:47:04)

Modified existing code in order to add pipeline CFN
<!-- Provide summary of changes -->
Refactored cloudformation.go and updated the pipeline data model to prepare for the work on dynamic Archer pipeline CFN generation.

<!-- Issue number, if available. E.g. "Fixes #31", "Addresses #42, 77" -->
Part of #223.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/internal/pkg/archer/manifest.go b/internal/pkg/archer/manifest.go
index 64b7de17..52596d7d 100644
--- a/internal/pkg/archer/manifest.go
+++ b/internal/pkg/archer/manifest.go
@@ -6,5 +6,4 @@ package archer
 // Manifest is the interface for serializing a manifest object to a YAML document or CloudFormation template.
 type Manifest interface {
 	Marshal() ([]byte, error)
-	CFNTemplate() (string, error)
 }
diff --git a/internal/pkg/deploy/cloudformation/pipeline.go b/internal/pkg/deploy/cloudformation/pipeline.go
new file mode 100644
index 00000000..fee64394
--- /dev/null
+++ b/internal/pkg/deploy/cloudformation/pipeline.go
@@ -0,0 +1,43 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package cloudformation
+
+import (
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/cloudformation"
+
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+)
+
+type pipelineStackConfig struct {
+	*deploy.CreatePipelineInput
+}
+
+func newPipelineStackConfig(in *deploy.CreatePipelineInput) *pipelineStackConfig {
+	return &pipelineStackConfig{
+		CreatePipelineInput: in,
+	}
+}
+
+func (p *pipelineStackConfig) StackName() string {
+	return p.ProjectName + "-" + p.Name
+}
+
+func (p *pipelineStackConfig) Template() (string, error) {
+	// TODO: Render the template
+	return "", nil
+}
+
+func (p *pipelineStackConfig) Parameters() []*cloudformation.Parameter {
+	return nil
+}
+
+func (p *pipelineStackConfig) Tags() []*cloudformation.Tag {
+	return []*cloudformation.Tag{
+		{
+			Key:   aws.String(projectTagKey),
+			Value: aws.String(p.ProjectName),
+		},
+	}
+}
diff --git a/internal/pkg/deploy/deploy.go b/internal/pkg/deploy/deploy.go
index 8dd5af20..f51ae92b 100644
--- a/internal/pkg/deploy/deploy.go
+++ b/internal/pkg/deploy/deploy.go
@@ -12,3 +12,41 @@ type CreateEnvironmentInput struct {
 	PublicLoadBalancer       bool   // Whether or not this environment should contain a shared public load balancer between applications.
 	ToolsAccountPrincipalARN string // The Principal ARN of the tools account.
 }
+
+// CreatePipelineInput represents the fields required to deploy a pipeline.
+type CreatePipelineInput struct {
+	// Name of the project this pipeline belongs to
+	ProjectName string
+	// Name of the pipeline
+	Name   string
+	Source *Source
+	Stages []PipelineStage
+}
+
+// Source defines the source of the artifacts to be built and deployed.
+type Source struct {
+	ProviderName string
+	Properties   map[string]interface{}
+}
+
+// PipelineStage represents configuration for each deployment stage
+// of a workspace. A stage consists of the Archer Environment the pipeline
+// is deloying to and the containerized applications that will be deployed.
+type PipelineStage struct {
+	*AssociatedEnvironment
+	LocalApplications []string
+}
+
+// AssociatedEnvironment defines the necessary information a pipline stage
+// needs for an Archer Environment.
+type AssociatedEnvironment struct {
+	// Name of the environment, must be unique within a project.
+	// This is also the name of the pipeline stage.
+	Name string
+	// T region this environment is stored in.
+	Region string
+	// AccountId of the account this environment is stored in.
+	AccountId string
+	// Whether or not this environment is a production environment.
+	Prod bool
+}
\ No newline at end of file
diff --git a/internal/pkg/manifest/pipeline.go b/internal/pkg/manifest/pipeline.go
index ff14cf06..5808ce65 100644
--- a/internal/pkg/manifest/pipeline.go
+++ b/internal/pkg/manifest/pipeline.go
@@ -12,7 +12,6 @@ import (
 	"github.com/fatih/structs"
 	"gopkg.in/yaml.v3"
 
-	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/archer"
 	"github.com/aws/amazon-ecs-cli-v2/templates"
 )
 
@@ -25,24 +24,24 @@ type Provider interface {
 }
 
 type githubProvider struct {
-	properties *GithubProperties
+	properties *GitHubProperties
 }
 
 func (p *githubProvider) Name() string {
-	return "github"
+	return "GitHub"
 }
 
 func (p *githubProvider) String() string {
-	return "github"
+	return "GitHub"
 }
 
 func (p *githubProvider) Properties() map[string]interface{} {
 	return structs.Map(p.properties)
 }
 
-// GithubProperties contain information for configuring a Github
+// GitHubProperties contain information for configuring a Github
 // source provider.
-type GithubProperties struct {
+type GitHubProperties struct {
 	// use tag from https://godoc.org/github.com/fatih/structs#example-Map--Tags
 	// to specify the name of the field in the output properties
 	Repository string `structs:"repository" yaml:"repository"`
@@ -53,7 +52,7 @@ type GithubProperties struct {
 // the provided provider-specific configurations
 func NewProvider(configs interface{}) (Provider, error) {
 	switch props := configs.(type) {
-	case *GithubProperties:
+	case *GitHubProperties:
 		return &githubProvider{
 			properties: props,
 		}, nil
@@ -71,9 +70,11 @@ const (
 	Ver1 PipelineSchemaMajorVersion = iota + 1
 )
 
-// pipelineManifest contains information that defines the relationship
+// PipelineManifest contains information that defines the relationship
 // and deployment ordering of your environments.
-type pipelineManifest struct {
+type PipelineManifest struct {
+	// Name of the pipeline
+	Name    string                     `yaml:"name"`
 	Version PipelineSchemaMajorVersion `yaml:"version"`
 	Source  *Source                    `yaml:"source"`
 	Stages  []PipelineStage            `yaml:"stages"`
@@ -85,56 +86,38 @@ type Source struct {
 	Properties   map[string]interface{} `yaml:"properties"`
 }
 
-// PipelineStage represents configuration for each deployment stage
-// of a workspace. A stage consists of the Archer environment the pipeline
-// is deloying to and the containerized applications that will be deployed.
+// PipelineStage represents a stage in the pipeline manifest
 type PipelineStage struct {
-	*AssociatedEnvironment `yaml:",inline"`
-	Applications           []*archer.Application `yaml:"-"`
-}
-
-// AssociatedEnvironment defines the necessary information a pipline stage
-// needs for an Archer environment.
-type AssociatedEnvironment struct {
-	Project   string `yaml:"-"`    // Name of the project this environment belongs to.
-	Name      string `yaml:"name"` // Name of the environment, must be unique within a project.
-	Region    string `yaml:"-"`    // Name of the region this environment is stored in.
-	AccountID string `yaml:"-"`    // Account ID of the account this environment is stored in.
-	Prod      bool   `yaml:"-"`    // Whether or not this environment is a production environment.
+	Name string `yaml:"name`
 }
 
 // CreatePipeline returns a pipeline manifest object.
-func CreatePipeline(provider Provider, stages ...PipelineStage) (archer.Manifest, error) {
+func CreatePipeline(pipelineName string, provider Provider, stageNames ...string) (*PipelineManifest, error) {
 	// TODO: #221 Do more validations
-	var defaultStages []PipelineStage
-	if len(stages) == 0 {
-		defaultStages = []PipelineStage{
-			{
-				AssociatedEnvironment: &AssociatedEnvironment{
-					Name: "test",
-				},
-			},
-			{
-				AssociatedEnvironment: &AssociatedEnvironment{
-					Name: "prod",
-				},
-			},
-		}
+	if len(stageNames) == 0 {
+		return nil, fmt.Errorf("a pipeline %s can not be created without a deployment stage",
+			pipelineName)
+	}
+
+	stages := make([]PipelineStage, 0, len(stageNames))
+	for _, name := range stageNames {
+		stages = append(stages, PipelineStage{Name: name})
 	}
 
-	return &pipelineManifest{
+	return &PipelineManifest{
+		Name:    pipelineName,
 		Version: Ver1,
 		Source: &Source{
 			ProviderName: provider.Name(),
 			Properties:   provider.Properties(),
 		},
-		Stages: append(defaultStages, stages...),
+		Stages: stages,
 	}, nil
 }
 
 // Marshal serializes the pipeline manifest object into byte array that
 // represents the pipeline.yml document.
-func (m *pipelineManifest) Marshal() ([]byte, error) {
+func (m *PipelineManifest) Marshal() ([]byte, error) {
 	box := templates.Box()
 	content, err := box.FindString("cicd/pipeline.yml")
 	if err != nil {
@@ -154,8 +137,8 @@ func (m *pipelineManifest) Marshal() ([]byte, error) {
 // UnmarshalPipeline deserializes the YAML input stream into a pipeline
 // manifest object. It returns an error if any issue occurs during
 // deserialization or the YAML input contains invalid fields.
-func UnmarshalPipeline(in []byte) (archer.Manifest, error) {
-	pm := pipelineManifest{}
+func UnmarshalPipeline(in []byte) (*PipelineManifest, error) {
+	pm := PipelineManifest{}
 	err := yaml.Unmarshal(in, &pm)
 	if err != nil {
 		return nil, err
@@ -175,7 +158,7 @@ func UnmarshalPipeline(in []byte) (archer.Manifest, error) {
 	return nil, errors.New("unexpected error occurs while unmarshalling pipeline.yml")
 }
 
-func validateVersion(pm *pipelineManifest) (PipelineSchemaMajorVersion, error) {
+func validateVersion(pm *PipelineManifest) (PipelineSchemaMajorVersion, error) {
 	switch pm.Version {
 	case Ver1:
 		return Ver1, nil
@@ -186,9 +169,3 @@ func validateVersion(pm *pipelineManifest) (PipelineSchemaMajorVersion, error) {
 			}
 	}
 }
-
-// CFNTemplate serializes the manifest object into a CloudFormation template.
-func (m *pipelineManifest) CFNTemplate() (string, error) {
-	// TODO: #223 Generate CFN template for the archer pipeline
-	return "", nil
-}
diff --git a/templates/cicd/pipeline.yml b/templates/cicd/pipeline.yml
index 1f195b33..bfd5db4c 100644
--- a/templates/cicd/pipeline.yml
+++ b/templates/cicd/pipeline.yml
@@ -1,5 +1,8 @@
 # This YAML file defines the relationship and deployment ordering of your environments.
 
+# The name of the pipeline
+name: {{.Name}}
+
 # The version of the schema used in this template
 version: {{.Version}}
 


Test patch:
diff --git a/internal/pkg/deploy/cloudformation/pipeline_test.go b/internal/pkg/deploy/cloudformation/pipeline_test.go
new file mode 100644
index 00000000..4c9e9412
--- /dev/null
+++ b/internal/pkg/deploy/cloudformation/pipeline_test.go
@@ -0,0 +1,84 @@
+// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package cloudformation
+
+import (
+	"testing"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/cloudformation"
+	"github.com/stretchr/testify/require"
+
+	"github.com/aws/amazon-ecs-cli-v2/internal/pkg/deploy"
+)
+
+const (
+	projectName = "chickenProject"
+	pipelineName = "wingspipeline"
+)
+
+func TestPipelineParameters(t *testing.T) {
+	pipeline := newPipelineStackConfig(
+		mockDeployPipelineInput(),
+	)
+
+	require.Nil(t, pipeline.Parameters(), "pipeline cloudformation template should not expose any parameters")
+}
+
+func TestPipelineTags(t *testing.T) {
+	pipeline := newPipelineStackConfig(
+		mockDeployPipelineInput(),
+	)
+
+	expectedTags := []*cloudformation.Tag{
+		{
+			Key:   aws.String(projectTagKey),
+			Value: aws.String(projectName),
+		},
+	}
+	require.ElementsMatch(t, expectedTags, pipeline.Tags())
+}
+
+func TestPipelineStackName(t *testing.T) {
+	pipeline := newPipelineStackConfig(
+		mockDeployPipelineInput(),
+	)
+
+	require.Equal(t, projectName+"-"+pipelineName,
+		pipeline.StackName(), "unexpected StackName")
+}
+
+func mockAssociatedEnv(envName, region string, isProd bool) *deploy.AssociatedEnvironment {
+	return &deploy.AssociatedEnvironment{
+		Name: envName,
+		Region: region,
+		AccountId: "012345678910",
+		Prod: isProd,
+	}
+}
+
+func mockDeployPipelineInput() *deploy.CreatePipelineInput {	
+	return &deploy.CreatePipelineInput{
+		ProjectName: projectName,
+		Name:            pipelineName         ,
+		Source: &deploy.Source{
+			ProviderName: "GitHub",
+			Properties: map[string]interface{}{
+				"repository": "aws/somethingCool",
+				"branch":     "master",
+			},
+
+		},
+		Stages: []deploy.PipelineStage{
+			{
+				AssociatedEnvironment: mockAssociatedEnv("test", "us-west-2", false),
+				LocalApplications: []string{"frontend", "backend"},
+			},
+			{
+				AssociatedEnvironment: mockAssociatedEnv("prod", "us-east-1", true),
+				LocalApplications: []string{"frontend", "backend"},
+			},
+		},
+	}
+}
\ No newline at end of file
diff --git a/internal/pkg/manifest/pipeline_test.go b/internal/pkg/manifest/pipeline_test.go
index 8dfb69f8..b2fd861c 100644
--- a/internal/pkg/manifest/pipeline_test.go
+++ b/internal/pkg/manifest/pipeline_test.go
@@ -5,6 +5,7 @@ package manifest
 
 import (
 	"errors"
+	"fmt"
 	"strings"
 	"testing"
 
@@ -16,8 +17,8 @@ func TestNewProvider(t *testing.T) {
 		providerConfig interface{}
 		expectedErr    error
 	}{
-		"successfully create Github provider": {
-			providerConfig: &GithubProperties{
+		"successfully create GitHub provider": {
+			providerConfig: &GitHubProperties{
 				Repository: "aws/amazon-ecs-cli-v2",
 				Branch:     "master",
 			},
@@ -38,96 +39,68 @@ func TestNewProvider(t *testing.T) {
 }
 
 func TestCreatePipeline(t *testing.T) {
+	const pipelineName = "pipepiper"
+
 	testCases := map[string]struct {
 		beforeEach     func() error
 		provider       Provider
 		expectedErr    error
-		inputStages    []PipelineStage
+		inputStages    []string
 		expectedStages []PipelineStage
 	}{
-		"happy case with default stages": {
+		"errors out when no stage provided": {
 			provider: func() Provider {
-				p, err := NewProvider(&GithubProperties{
+				p, err := NewProvider(&GitHubProperties{
 					Repository: "aws/amazon-ecs-cli-v2",
 					Branch:     "master",
 				})
 				require.NoError(t, err, "failed to create provider")
 				return p
 			}(),
-			expectedStages: []PipelineStage{
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "test",
-					},
-				},
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "prod",
-					},
-				},
-			},
+			expectedErr: fmt.Errorf("a pipeline %s can not be created without a deployment stage",
+				pipelineName),
 		},
 		"happy case with non-default stages": {
 			provider: func() Provider {
-				p, err := NewProvider(&GithubProperties{
+				p, err := NewProvider(&GitHubProperties{
 					Repository: "aws/amazon-ecs-cli-v2",
 					Branch:     "master",
 				})
 				require.NoError(t, err, "failed to create provider")
 				return p
 			}(),
-			inputStages: []PipelineStage{
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "chicken",
-					},
-				},
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "wings",
-					},
-				},
-			},
-			expectedStages: []PipelineStage{
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "chicken",
-					},
-				},
-				{
-					AssociatedEnvironment: &AssociatedEnvironment{
-						Name: "wings",
-					},
-				},
-			},
+			inputStages:    []string{"chicken", "wings"},
+			expectedStages: []PipelineStage{{"chicken"}, {"wings"}},
 		},
 	}
 
 	for name, tc := range testCases {
 		t.Run(name, func(t *testing.T) {
-			m, err := CreatePipeline(tc.provider, tc.inputStages...)
+			m, err := CreatePipeline(pipelineName, tc.provider, tc.inputStages...)
 
 			if tc.expectedErr != nil {
 				require.EqualError(t, err, tc.expectedErr.Error())
 			} else {
-				p, ok := m.(*pipelineManifest)
-				require.True(t, ok)
-				require.Equal(t, tc.expectedStages, p.Stages, "the stages are different from the expected")
+				require.Equal(t, tc.expectedStages, m.Stages, "the stages are different from the expected")
 			}
 		})
 	}
 }
 
 func TestPipelineManifestMarshal(t *testing.T) {
+	const pipelineName = "pipepiper"
 	wantedContent := `# This YAML file defines the relationship and deployment ordering of your environments.
 
+# The name of the pipeline
+name: pipepiper
+
 # The version of the schema used in this template
 version: 1
 
 # This section defines the source artifacts.
 source:
   # The name of the provider that is used to store the source artifacts.
-  provider: github
+  provider: GitHub
   # Additional properties that further specifies the exact location
   # the artifacts should be sourced from.
   properties:
@@ -139,19 +112,19 @@ source:
 stages:
     - 
       # The name of the environment to deploy to.
-      name: test
+      name: chicken
     - 
       # The name of the environment to deploy to.
-      name: prod
+      name: wings
 `
 	// reset the global map before each test case is run
-	provider, err := NewProvider(&GithubProperties{
+	provider, err := NewProvider(&GitHubProperties{
 		Repository: "aws/amazon-ecs-cli-v2",
 		Branch:     "master",
 	})
 	require.NoError(t, err)
 
-	m, err := CreatePipeline(provider)
+	m, err := CreatePipeline(pipelineName, provider, "chicken", "wings")
 	require.NoError(t, err)
 
 	b, err := m.Marshal()
@@ -162,15 +135,16 @@ stages:
 func TestUnmarshalPipeline(t *testing.T) {
 	testCases := map[string]struct {
 		inContent        string
-		expectedManifest *pipelineManifest
+		expectedManifest *PipelineManifest
 		expectedErr      error
 	}{
 		"invalid pipeline schema version": {
 			inContent: `
+name: pipepiper
 version: -1
 
 source:
-  provider: github
+  provider: GitHub
   properties:
     repository: aws/somethingCool
     branch: master
@@ -178,7 +152,7 @@ source:
 stages:
     - 
       name: test
-    - 
+    -
       name: prod
 `,
 			expectedErr: &ErrInvalidPipelineManifestVersion{
@@ -187,14 +161,15 @@ stages:
 		},
 		"invalid pipeline.yml": {
 			inContent:   `corrupted yaml`,
-			expectedErr: errors.New("yaml: unmarshal errors:\n  line 1: cannot unmarshal !!str `corrupt...` into manifest.pipelineManifest"),
+			expectedErr: errors.New("yaml: unmarshal errors:\n  line 1: cannot unmarshal !!str `corrupt...` into manifest.PipelineManifest"),
 		},
 		"valid pipeline.yml": {
 			inContent: `
+name: pipepiper
 version: 1
 
 source:
-  provider: github
+  provider: GitHub
   properties:
     repository: aws/somethingCool
     branch: master
@@ -205,27 +180,19 @@ stages:
     - 
       name: wings
 `,
-			expectedManifest: &pipelineManifest{
+			expectedManifest: &PipelineManifest{
+				Name:    "pipepiper",
 				Version: Ver1,
 				Source: &Source{
-					ProviderName: "github",
+					ProviderName: "GitHub",
 					Properties: map[string]interface{}{
 						"repository": "aws/somethingCool",
 						"branch":     "master",
 					},
 				},
 				Stages: []PipelineStage{
-					{
-						AssociatedEnvironment: &AssociatedEnvironment{
-							Name: "chicken",
-						},
-					},
-					{
-						AssociatedEnvironment: &AssociatedEnvironment{
-							Name: "wings",
-						},
-					},
-				},
+					PipelineStage{"chicken"},
+					PipelineStage{"wings"}},
 			},
 		},
 	}
@@ -237,9 +204,7 @@ stages:
 			if tc.expectedErr != nil {
 				require.EqualError(t, err, tc.expectedErr.Error())
 			} else {
-				actualManifest, ok := m.(*pipelineManifest)
-				require.True(t, ok)
-				require.Equal(t, actualManifest, tc.expectedManifest)
+				require.Equal(t, tc.expectedManifest, m)
 			}
 		})
 	}