Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cicd): allow CodeCommit as pipeline source #1808

Merged
merged 25 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dfdfdbd
chore: tiny refactors + add conditional to cfn template
huanjani Dec 31, 2020
27bffa0
build: refactor conversion of info b/w manifest and deploy (owner/rep…
huanjani Jan 4, 2021
d49df8e
fix: move gh secret condition to method from template; minor refactoring
huanjani Jan 4, 2021
766458a
build: get to functional state for cc; need to make more flexible
huanjani Jan 5, 2021
0da9c86
Merge remote-tracking branch 'archer/mainline' into pipeline-update
huanjani Jan 5, 2021
1ccacc5
build: enable both GH and CC as sources
huanjani Jan 5, 2021
4b5f15a
build: specify cc allowed actions, add unit tests
huanjani Jan 6, 2021
3bcddf0
fix: cap 'URL'
huanjani Jan 6, 2021
1bb4c60
chore: update docs
huanjani Jan 6, 2021
2228cfe
fix: move vars closer to their usage
huanjani Jan 7, 2021
bebc056
chore: add regex-passing examples
huanjani Jan 7, 2021
89de541
fix: remove replicated code
huanjani Jan 7, 2021
10957e7
fix: tweak wording in docs
huanjani Jan 7, 2021
1992889
chore: split source into two types, use go templates instead of cfn c…
huanjani Jan 7, 2021
c80e798
fix: change go template consecutive 'if's to 'if-else'
huanjani Jan 8, 2021
529671d
chore: use switch to assign source type
huanjani Jan 8, 2021
5816806
Merge remote-tracking branch 'archer/mainline' into pipeline-update
huanjani Jan 11, 2021
7a4aff9
address feedback
huanjani Jan 12, 2021
a2f80a4
fix: stringify more safely; catch session err
huanjani Jan 12, 2021
905d37d
fix: address feedback
huanjani Jan 13, 2021
28f5da1
fix: remove TODO
huanjani Jan 13, 2021
d7c4600
Update internal/pkg/deploy/pipeline.go
huanjani Jan 13, 2021
c875b98
fix: update ecs-cli-v2 to copilot
huanjani Jan 13, 2021
07eec32
chore: use local variables
huanjani Jan 14, 2021
040d5dc
Merge branch 'mainline' into pipeline-update
mergify[bot] Jan 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions internal/pkg/cli/pipeline_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const (
fmtGHPipelineName = "pipeline-%s-%s-%s"
fmtCCPipelineName = "pipeline-%s-%s"
fmtGHRepoURL = "https://%s/%s/%s"
fmtCCRepoURL = "https://%s.console.%s/codesuite/codecommit/repositories/%s"
fmtCCRepoURL = "https://%s.console.%s/codesuite/codecommit/repositories/%s/browse"
)

var (
Expand All @@ -80,6 +80,7 @@ type initPipelineOpts struct {
secretsmanager secretsManager
parser template.Parser
runner runner
sessProvider sessionProvider
cfnClient appResourcesGetter
store store
prompt prompter
Expand Down Expand Up @@ -133,6 +134,7 @@ func newInitPipelineOpts(vars initPipelineVars) (*initPipelineOpts, error) {
workspace: ws,
secretsmanager: secretsmanager,
parser: template.New(),
sessProvider: p,
cfnClient: cloudformation.New(defaultSession),
store: ssmStore,
prompt: prompter,
Expand All @@ -147,10 +149,8 @@ func (o *initPipelineOpts) Validate() error {
if o.appName == "" {
return errNoAppInWorkspace
}
if o.appName != "" {
if _, err := o.store.GetApplication(o.appName); err != nil {
return err
}
if _, err := o.store.GetApplication(o.appName); err != nil {
return err
}

if o.repoURL != "" {
Expand Down Expand Up @@ -292,12 +292,16 @@ func (o *initPipelineOpts) askCodeCommitRepoDetails() error {
o.repoName = repoDetails.name
o.ccRegion = repoDetails.region

// If any one of the chosen environments is in a region besides that of the CodeCommit repo, pipeline init errors out.
for _, env := range o.envConfigs {
if env.Region != o.ccRegion {
return fmt.Errorf("repository %s is in %s, but environment %s is in %s; they must be in the same region", o.repoName, o.ccRegion, env.Name, env.Region)
}
// If the CodeCommit region is different than that of the app, pipeline init errors out. TODO: compare account from app config against CC acct?
sess, err := o.sessProvider.Default()
if err != nil {
return fmt.Errorf("retrieve session: %w", err)
}
region := *sess.Config.Region
if o.ccRegion != region {
return fmt.Errorf("repository %s is in %s, but app %s is in %s; they must be in the same region", o.repoName, o.ccRegion, o.appName, region)
}

if o.repoBranch == "" {
o.repoBranch = defaultCCBranch
}
Expand Down Expand Up @@ -561,16 +565,16 @@ func (o *initPipelineOpts) pipelineName() (string, error) {
func (o *initPipelineOpts) pipelineProvider() (manifest.Provider, error) {
if o.provider == ghProviderName {
config := &manifest.GitHubProperties{
OwnerAndRepository: fmt.Sprintf(fmtGHRepoURL, githubURL, o.githubOwner, o.repoName),
RepositoryURL: fmt.Sprintf(fmtGHRepoURL, githubURL, o.githubOwner, o.repoName),
Branch: o.repoBranch,
GithubSecretIdKeyName: o.secret,
}
return manifest.NewProvider(config)
}
if o.provider == ccProviderName {
config := &manifest.CodeCommitProperties{
Repository: fmt.Sprintf(fmtCCRepoURL, o.ccRegion, awsURL, o.repoName),
Branch: o.repoBranch,
RepositoryURL: fmt.Sprintf(fmtCCRepoURL, o.ccRegion, awsURL, o.repoName),
Branch: o.repoBranch,
}
return manifest.NewProvider(config)
}
Expand Down
67 changes: 47 additions & 20 deletions internal/pkg/cli/pipeline_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"

"github.com/aws/copilot-cli/internal/pkg/aws/secretsmanager"
"github.com/aws/copilot-cli/internal/pkg/cli/mocks"
"github.com/aws/copilot-cli/internal/pkg/config"
Expand Down Expand Up @@ -159,11 +162,12 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
inGitHubAccessToken string
inGitBranch string

mockPrompt func(m *mocks.Mockprompter)
mockRunner func(m *mocks.Mockrunner)
mockSelector func(m *mocks.MockpipelineSelector)
mockStore func(m *mocks.Mockstore)
buffer bytes.Buffer
mockPrompt func(m *mocks.Mockprompter)
mockRunner func(m *mocks.Mockrunner)
mockSessProvider func(m *mocks.MocksessionProvider)
mockSelector func(m *mocks.MockpipelineSelector)
mockStore func(m *mocks.Mockstore)
buffer bytes.Buffer

expectedEnvironments []string
expectedRepoURL string
Expand Down Expand Up @@ -201,6 +205,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(githubAnotherURL, nil).Times(1)
m.EXPECT().GetSecret(gomock.Eq("Please enter your GitHub Personal Access Token for your repository bhaOS:"), gomock.Any()).Return(githubToken, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedRepoURL: githubAnotherURL,
expectedGitHubOwner: githubAnotherOwner,
Expand All @@ -211,11 +216,9 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
expectedError: nil,
},
"no flags, success case for CodeCommit": {
inEnvironments: []string{},
inRepoURL: "",
inGitHubAccessToken: "",
inGitBranch: "",
buffer: *bytes.NewBufferString("archer\[email protected]:goodGoose/bhaOS (fetch)\narcher\thttps://github.com/badGoose/chaOS (push)\narcher\thttps://git-codecommit.us-west-2.amazonaws.com/v1/repos/repo-man (fetch)\narcher\tssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/repo-woman (push)\narcher\tcodecommit::us-west-2://repo-man (fetch)\n"),
inEnvironments: []string{},
inRepoURL: "",
buffer: *bytes.NewBufferString("archer\[email protected]:goodGoose/bhaOS (fetch)\narcher\thttps://github.com/badGoose/chaOS (push)\narcher\thttps://git-codecommit.us-west-2.amazonaws.com/v1/repos/repo-man (fetch)\narcher\tssh://git-codecommit.us-west-2.amazonaws.com/v1/repos/repo-woman (push)\narcher\tcodecommit::us-west-2://repo-man (fetch)\n"),

mockSelector: func(m *mocks.MockpipelineSelector) {
m.EXPECT().Environments(pipelineSelectEnvPrompt, gomock.Any(), "my-app", gomock.Any()).Return([]string{"test", "prod"}, nil)
Expand All @@ -236,6 +239,13 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(codecommitSSHURL, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {
m.EXPECT().Default().Return(&session.Session{
Config: &aws.Config{
Region: aws.String("us-west-2"),
},
}, nil)
},

expectedRepoURL: codecommitSSHURL,
expectedRepoName: codecommitAnotherRepoName,
Expand All @@ -250,9 +260,10 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockSelector: func(m *mocks.MockpipelineSelector) {
m.EXPECT().Environments(pipelineSelectEnvPrompt, gomock.Any(), "my-app", gomock.Any()).Return(nil, errors.New("some error"))
},
mockStore: func(m *mocks.Mockstore) {},
mockRunner: func(m *mocks.Mockrunner) {},
mockPrompt: func(m *mocks.Mockprompter) {},
mockStore: func(m *mocks.Mockstore) {},
mockRunner: func(m *mocks.Mockrunner) {},
mockPrompt: func(m *mocks.Mockprompter) {},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedEnvironments: []string{},
expectedError: fmt.Errorf("select environments: some error"),
Expand Down Expand Up @@ -282,6 +293,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return("", errors.New("some error")).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedGitHubOwner: "",
expectedRepoName: "",
Expand Down Expand Up @@ -313,6 +325,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return("https://bitbub.com/badGoose/chaOS", nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedError: fmt.Errorf("Copilot currently accepts only URLs to GitHub and CodeCommit repository sources"),
},
Expand Down Expand Up @@ -342,6 +355,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), []string{githubReallyBadURL}).Return(githubReallyBadURL, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedGitHubOwner: "",
expectedRepoName: "",
Expand Down Expand Up @@ -377,6 +391,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(githubURL, nil).Times(1)
m.EXPECT().GetSecret(gomock.Eq("Please enter your GitHub Personal Access Token for your repository chaOS:"), gomock.Any()).Return("", errors.New("some error")).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedGitHubOwner: githubOwner,
expectedRepoName: githubRepoName,
Expand Down Expand Up @@ -409,6 +424,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(codecommitBadURL, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedRepoName: "",
expectedEnvironments: []string{"test", "prod"},
Expand Down Expand Up @@ -436,6 +452,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(codecommitBadRegion, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {},

expectedRepoURL: codecommitHTTPSURL,
expectedRepoName: codecommitRepoName,
Expand All @@ -444,7 +461,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
expectedEnvironments: []string{"test", "prod"},
expectedError: fmt.Errorf("unable to parse the AWS region from %s", codecommitBadRegion),
},
"returns error if repo region is not an environment's region": {
"returns error if repo region is not app's region": {
buffer: *bytes.NewBufferString(""),

mockSelector: func(m *mocks.MockpipelineSelector) {
Expand All @@ -466,10 +483,17 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
mockPrompt: func(m *mocks.Mockprompter) {
m.EXPECT().SelectOne(pipelineSelectURLPrompt, gomock.Any(), gomock.Any()).Return(codecommitFedURL, nil).Times(1)
},
mockSessProvider: func(m *mocks.MocksessionProvider) {
m.EXPECT().Default().Return(&session.Session{
Config: &aws.Config{
Region: aws.String("us-east-1"),
},
}, nil)
},

expectedRepoName: "",
expectedEnvironments: []string{},
expectedError: fmt.Errorf("repository repo-man is in us-west-2, but environment prod is in us-east-1; they must be in the same region"),
expectedError: fmt.Errorf("repository repo-man is in us-west-2, but app my-app is in us-east-1; they must be in the same region"),
},
}

Expand All @@ -481,6 +505,7 @@ func TestInitPipelineOpts_Ask(t *testing.T) {

mockPrompt := mocks.NewMockprompter(ctrl)
mockRunner := mocks.NewMockrunner(ctrl)
mocksSessProvider := mocks.NewMocksessionProvider(ctrl)
mockSelector := mocks.NewMockpipelineSelector(ctrl)
mockStore := mocks.NewMockstore(ctrl)

Expand All @@ -491,15 +516,17 @@ func TestInitPipelineOpts_Ask(t *testing.T) {
repoURL: tc.inRepoURL,
githubAccessToken: tc.inGitHubAccessToken,
},
prompt: mockPrompt,
runner: mockRunner,
buffer: tc.buffer,
sel: mockSelector,
store: mockStore,
prompt: mockPrompt,
runner: mockRunner,
sessProvider: mocksSessProvider,
buffer: tc.buffer,
sel: mockSelector,
store: mockStore,
}

tc.mockPrompt(mockPrompt)
tc.mockRunner(mockRunner)
tc.mockSessProvider(mocksSessProvider)
tc.mockSelector(mockSelector)
tc.mockStore(mockStore)

Expand Down
24 changes: 20 additions & 4 deletions internal/pkg/cli/pipeline_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const (

type updatePipelineVars struct {
appName string
pipelineName string
skipConfirmation bool
}

Expand All @@ -56,6 +55,8 @@ type updatePipelineOpts struct {
region string
envStore environmentStore
ws wsPipelineReader

pipelineName string
}

func newUpdatePipelineOpts(vars updatePipelineVars) (*updatePipelineOpts, error) {
Expand Down Expand Up @@ -211,9 +212,24 @@ func (o *updatePipelineOpts) Execute() error {
return fmt.Errorf("unmarshal pipeline manifest: %w", err)
}
o.pipelineName = pipeline.Name
source := &deploy.Source{
ProviderName: pipeline.Source.ProviderName,
Properties: pipeline.Source.Properties,

var source interface{}
switch pipeline.Source.ProviderName {
case manifest.GithubProviderName:
source = &deploy.GitHubSource{
ProviderName: manifest.GithubProviderName,
Branch: (pipeline.Source.Properties["branch"]).(string),
RepositoryURL: (pipeline.Source.Properties["repository"]).(string),
PersonalAccessTokenSecretID: (pipeline.Source.Properties["access_token_secret"]).(string),
}
case manifest.CodeCommitProviderName:
source = &deploy.CodeCommitSource{
ProviderName: manifest.CodeCommitProviderName,
Branch: (pipeline.Source.Properties["branch"]).(string),
RepositoryURL: (pipeline.Source.Properties["repository"]).(string),
}
default:
return fmt.Errorf("invalid repo source provider: %s", pipeline.Source.ProviderName)
}

// convert environments to deployment stages
Expand Down
30 changes: 28 additions & 2 deletions internal/pkg/cli/pipeline_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,31 @@ stages:
},
expectedError: fmt.Errorf("unmarshal pipeline manifest: pipeline.yml contains invalid schema version: 0"),
},
"returns an error if provider is not a supported type": {
inApp: &app,
inAppName: appName,
inRegion: region,
callMocks: func(m updatePipelineMocks) {
content := `
name: pipepiper
version: 1

source:
provider: NotGitHub
properties:
repository: aws/somethingCool
access_token_secret: "github-token-badgoose-backend"
branch: main
`
gomock.InOrder(
m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateResourcesStart, appName)).Times(1),
m.deployer.EXPECT().AddPipelineResourcesToApp(&app, region).Return(nil),
m.prog.EXPECT().Stop(log.Ssuccessf(fmtPipelineUpdateResourcesComplete, appName)).Times(1),
m.ws.EXPECT().ReadPipelineManifest().Return([]byte(content), nil),
)
},
expectedError: fmt.Errorf("invalid repo source provider: NotGitHub"),
},
"returns an error if unable to convert environments to deployment stage": {
inApp: &app,
inRegion: region,
Expand Down Expand Up @@ -601,8 +626,7 @@ stages:

opts := &updatePipelineOpts{
updatePipelineVars: updatePipelineVars{
pipelineName: tc.inPipelineName,
appName: tc.inAppName,
appName: tc.inAppName,
},
pipelineDeployer: mockPipelineDeployer,
ws: mockWorkspace,
Expand All @@ -611,6 +635,8 @@ stages:
envStore: mockEnvStore,
prog: mockProgress,
prompt: mockPrompt,

pipelineName: tc.inPipelineName,
}

// WHEN
Expand Down
11 changes: 4 additions & 7 deletions internal/pkg/deploy/cloudformation/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,10 @@ func TestCloudFormation_UpdatePipeline(t *testing.T) {
in := &deploy.CreatePipelineInput{
AppName: "kudos",
Name: "cicd",
Source: &deploy.Source{
ProviderName: "GitHub",
Properties: map[string]interface{}{
"repository": "aws/somethingCool",
"access_token_secret": "github-token-badgoose-backend",
"branch": "main",
},
Source: &deploy.GitHubSource{
RepositoryURL: "aws/somethingCool",
PersonalAccessTokenSecretID: "github-token-badgoose-backend",
Branch: "main",
},
Stages: nil,
ArtifactBuckets: nil,
Expand Down
11 changes: 4 additions & 7 deletions internal/pkg/deploy/cloudformation/stack/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,10 @@ func mockCreatePipelineInput() *deploy.CreatePipelineInput {
return &deploy.CreatePipelineInput{
AppName: projectName,
Name: pipelineName,
Source: &deploy.Source{
ProviderName: "GitHub",
Properties: map[string]interface{}{
"repository": "hencrice/amazon-ecs-cli-v2",
"branch": defaultBranch,
"access_token_secret": "testGitHubSecret",
},
Source: &deploy.GitHubSource{
RepositoryURL: "hencrice/amazon-ecs-cli-v2",
Branch: defaultBranch,
PersonalAccessTokenSecretID: "testGitHubSecret",
},
Stages: []deploy.PipelineStage{
{
Expand Down
Loading