diff --git a/internal/pkg/aws/cloudformation/changeset.go b/internal/pkg/aws/cloudformation/changeset.go index 6b7bd7fbe90..3bda3c45c04 100644 --- a/internal/pkg/aws/cloudformation/changeset.go +++ b/internal/pkg/aws/cloudformation/changeset.go @@ -90,11 +90,10 @@ func (cs *changeSet) String() string { // create creates a ChangeSet, waits until it's created, and returns the ChangeSet ID on success. func (cs *changeSet) create(conf *stackConfig) error { - out, err := cs.client.CreateChangeSet(&cloudformation.CreateChangeSetInput{ + input := &cloudformation.CreateChangeSetInput{ ChangeSetName: aws.String(cs.name), StackName: aws.String(cs.stackName), ChangeSetType: aws.String(cs.csType.String()), - TemplateBody: aws.String(conf.Template), Parameters: conf.Parameters, Tags: conf.Tags, RoleARN: conf.RoleARN, @@ -104,7 +103,15 @@ func (cs *changeSet) create(conf *stackConfig) error { cloudformation.CapabilityCapabilityNamedIam, cloudformation.CapabilityCapabilityAutoExpand, }), - }) + } + if conf.TemplateBody != "" { + input.TemplateBody = aws.String(conf.TemplateBody) + } + if conf.TemplateURL != "" { + input.TemplateURL = aws.String(conf.TemplateURL) + } + + out, err := cs.client.CreateChangeSet(input) if err != nil { return fmt.Errorf("create %s: %w", cs, err) } @@ -114,7 +121,6 @@ func (cs *changeSet) create(conf *stackConfig) error { if err != nil { return fmt.Errorf("wait for creation of %s: %w", cs, err) } - // Since the ChangeSet creation succeeded, use the full ARN instead of the name. // Using the full ID is essential in case the ChangeSet execution status is obsolete. // If we call DescribeChangeSet using the ChangeSet name and Stack name on an obsolete changeset, the results is empty. diff --git a/internal/pkg/aws/cloudformation/cloudformation_test.go b/internal/pkg/aws/cloudformation/cloudformation_test.go index 0d76d9eb533..afa06c75039 100644 --- a/internal/pkg/aws/cloudformation/cloudformation_test.go +++ b/internal/pkg/aws/cloudformation/cloudformation_test.go @@ -88,6 +88,14 @@ func TestCloudFormation_Create(t *testing.T) { return m }, }, + "creates the stack with templateURL": { + createMock: func(ctrl *gomock.Controller) client { + m := mocks.NewMockclient(ctrl) + m.EXPECT().DescribeStacks(gomock.Any()).Return(nil, errDoesNotExist) + addCreateDeployCalls(m) + return m + }, + }, "creates the stack after cleaning the previously failed execution": { createMock: func(ctrl *gomock.Controller) client { m := mocks.NewMockclient(ctrl) @@ -1184,7 +1192,7 @@ func addDeployCalls(m *mocks.Mockclient, changeSetType string) { ChangeSetName: aws.String(mockChangeSetName), StackName: aws.String(mockStack.Name), ChangeSetType: aws.String(changeSetType), - TemplateBody: aws.String(mockStack.Template), + TemplateBody: aws.String(mockStack.TemplateBody), Parameters: nil, Tags: nil, RoleARN: nil, diff --git a/internal/pkg/aws/cloudformation/stack.go b/internal/pkg/aws/cloudformation/stack.go index 026d4d69d4c..1c1d7fd8097 100644 --- a/internal/pkg/aws/cloudformation/stack.go +++ b/internal/pkg/aws/cloudformation/stack.go @@ -15,10 +15,11 @@ type Stack struct { } type stackConfig struct { - Template string - Parameters []*cloudformation.Parameter - Tags []*cloudformation.Tag - RoleARN *string + TemplateBody string + TemplateURL string + Parameters []*cloudformation.Parameter + Tags []*cloudformation.Tag + RoleARN *string } // StackOption allows you to initialize a Stack with additional properties. @@ -29,7 +30,21 @@ func NewStack(name, template string, opts ...StackOption) *Stack { s := &Stack{ Name: name, stackConfig: &stackConfig{ - Template: template, + TemplateBody: template, + }, + } + for _, opt := range opts { + opt(s) + } + return s +} + +// NewStackWithURL creates a stack with a URL to the template. +func NewStackWithURL(name, templateURL string, opts ...StackOption) *Stack { + s := &Stack{ + Name: name, + stackConfig: &stackConfig{ + TemplateURL: templateURL, }, } for _, opt := range opts { diff --git a/internal/pkg/aws/cloudformation/stack_test.go b/internal/pkg/aws/cloudformation/stack_test.go index d80676dc2c3..ec25d5cf22a 100644 --- a/internal/pkg/aws/cloudformation/stack_test.go +++ b/internal/pkg/aws/cloudformation/stack_test.go @@ -24,7 +24,36 @@ func TestNewStack(t *testing.T) { // THEN require.Equal(t, "hello", s.Name) - require.Equal(t, "world", s.Template) + require.Equal(t, "world", s.TemplateBody) + require.Equal(t, []*cloudformation.Parameter{ + { + ParameterKey: aws.String("Port"), + ParameterValue: aws.String("80"), + }, + }, s.Parameters) + require.Equal(t, []*cloudformation.Tag{ + { + Key: aws.String("copilot-application"), + Value: aws.String("phonetool"), + }, + }, s.Tags) + require.Equal(t, aws.String("arn"), s.RoleARN) +} + +func TestNewStackWithURL(t *testing.T) { + // WHEN + s := NewStackWithURL("hello", "worldlyURL", + WithParameters(map[string]string{ + "Port": "80", + }), + WithTags(map[string]string{ + "copilot-application": "phonetool", + }), + WithRoleARN("arn")) + + // THEN + require.Equal(t, "hello", s.Name) + require.Equal(t, "worldlyURL", s.TemplateURL) require.Equal(t, []*cloudformation.Parameter{ { ParameterKey: aws.String("Port"), diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index a447a573223..ea890b7cb29 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -334,8 +334,8 @@ type imageRemover interface { } type pipelineDeployer interface { - CreatePipeline(env *deploy.CreatePipelineInput) error - UpdatePipeline(env *deploy.CreatePipelineInput) error + CreatePipeline(env *deploy.CreatePipelineInput, bucketName string) error + UpdatePipeline(env *deploy.CreatePipelineInput, bucketName string) error PipelineExists(env *deploy.CreatePipelineInput) (bool, error) DeletePipeline(pipelineName string) error AddPipelineResourcesToApp(app *config.Application, region string) error diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index c2bbd719660..295493d1190 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -3302,17 +3302,17 @@ func (mr *MockpipelineDeployerMockRecorder) AddPipelineResourcesToApp(app, regio } // CreatePipeline mocks base method. -func (m *MockpipelineDeployer) CreatePipeline(env *deploy.CreatePipelineInput) error { +func (m *MockpipelineDeployer) CreatePipeline(env *deploy.CreatePipelineInput, bucketName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePipeline", env) + ret := m.ctrl.Call(m, "CreatePipeline", env, bucketName) ret0, _ := ret[0].(error) return ret0 } // CreatePipeline indicates an expected call of CreatePipeline. -func (mr *MockpipelineDeployerMockRecorder) CreatePipeline(env interface{}) *gomock.Call { +func (mr *MockpipelineDeployerMockRecorder) CreatePipeline(env, bucketName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockpipelineDeployer)(nil).CreatePipeline), env) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*MockpipelineDeployer)(nil).CreatePipeline), env, bucketName) } // DeletePipeline mocks base method. @@ -3375,17 +3375,17 @@ func (mr *MockpipelineDeployerMockRecorder) PipelineExists(env interface{}) *gom } // UpdatePipeline mocks base method. -func (m *MockpipelineDeployer) UpdatePipeline(env *deploy.CreatePipelineInput) error { +func (m *MockpipelineDeployer) UpdatePipeline(env *deploy.CreatePipelineInput, bucketName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePipeline", env) + ret := m.ctrl.Call(m, "UpdatePipeline", env, bucketName) ret0, _ := ret[0].(error) return ret0 } // UpdatePipeline indicates an expected call of UpdatePipeline. -func (mr *MockpipelineDeployerMockRecorder) UpdatePipeline(env interface{}) *gomock.Call { +func (mr *MockpipelineDeployerMockRecorder) UpdatePipeline(env, bucketName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePipeline", reflect.TypeOf((*MockpipelineDeployer)(nil).UpdatePipeline), env) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePipeline", reflect.TypeOf((*MockpipelineDeployer)(nil).UpdatePipeline), env, bucketName) } // MockappDeployer is a mock of appDeployer interface. @@ -3798,17 +3798,17 @@ func (mr *MockdeployerMockRecorder) AddServiceToApp(app, svcName interface{}) *g } // CreatePipeline mocks base method. -func (m *Mockdeployer) CreatePipeline(env *deploy.CreatePipelineInput) error { +func (m *Mockdeployer) CreatePipeline(env *deploy.CreatePipelineInput, bucketName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePipeline", env) + ret := m.ctrl.Call(m, "CreatePipeline", env, bucketName) ret0, _ := ret[0].(error) return ret0 } // CreatePipeline indicates an expected call of CreatePipeline. -func (mr *MockdeployerMockRecorder) CreatePipeline(env interface{}) *gomock.Call { +func (mr *MockdeployerMockRecorder) CreatePipeline(env, bucketName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*Mockdeployer)(nil).CreatePipeline), env) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePipeline", reflect.TypeOf((*Mockdeployer)(nil).CreatePipeline), env, bucketName) } // DelegateDNSPermissions mocks base method. @@ -4000,17 +4000,17 @@ func (mr *MockdeployerMockRecorder) UpdateEnvironmentTemplate(appName, envName, } // UpdatePipeline mocks base method. -func (m *Mockdeployer) UpdatePipeline(env *deploy.CreatePipelineInput) error { +func (m *Mockdeployer) UpdatePipeline(env *deploy.CreatePipelineInput, bucketName string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePipeline", env) + ret := m.ctrl.Call(m, "UpdatePipeline", env, bucketName) ret0, _ := ret[0].(error) return ret0 } // UpdatePipeline indicates an expected call of UpdatePipeline. -func (mr *MockdeployerMockRecorder) UpdatePipeline(env interface{}) *gomock.Call { +func (mr *MockdeployerMockRecorder) UpdatePipeline(env, bucketName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePipeline", reflect.TypeOf((*Mockdeployer)(nil).UpdatePipeline), env) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePipeline", reflect.TypeOf((*Mockdeployer)(nil).UpdatePipeline), env, bucketName) } // MockdomainHostedZoneGetter is a mock of domainHostedZoneGetter interface. diff --git a/internal/pkg/cli/pipeline_update.go b/internal/pkg/cli/pipeline_update.go index 1a1bd02f586..afb93ffc367 100644 --- a/internal/pkg/cli/pipeline_update.go +++ b/internal/pkg/cli/pipeline_update.go @@ -103,6 +103,76 @@ func (o *updatePipelineOpts) Validate() error { return nil } +// Execute creates a new pipeline or updates the current pipeline if it already exists. +func (o *updatePipelineOpts) Execute() error { + // bootstrap pipeline resources + o.prog.Start(fmt.Sprintf(fmtPipelineUpdateResourcesStart, color.HighlightUserInput(o.appName))) + err := o.pipelineDeployer.AddPipelineResourcesToApp(o.app, o.region) + if err != nil { + o.prog.Stop(log.Serrorf(fmtPipelineUpdateResourcesFailed, color.HighlightUserInput(o.appName))) + return fmt.Errorf("add pipeline resources to application %s in %s: %w", o.appName, o.region, err) + } + o.prog.Stop(log.Ssuccessf(fmtPipelineUpdateResourcesComplete, color.HighlightUserInput(o.appName))) + + // read pipeline manifest + data, err := o.ws.ReadPipelineManifest() + if err != nil { + return fmt.Errorf("read pipeline manifest: %w", err) + } + pipeline, err := manifest.UnmarshalPipeline(data) + if err != nil { + return fmt.Errorf("unmarshal pipeline manifest: %w", err) + } + if len(pipeline.Name) > 100 { + return fmt.Errorf(`pipeline name '%s' must be shorter than 100 characters`, pipeline.Name) + } + o.pipelineName = pipeline.Name + + // If the source has an existing connection, get the correlating ConnectionARN . + connection, ok := pipeline.Source.Properties["connection_name"] + if ok { + arn, err := o.codestar.GetConnectionARN((connection).(string)) + if err != nil { + return fmt.Errorf("get connection ARN: %w", err) + } + pipeline.Source.Properties["connection_arn"] = arn + } + + source, bool, err := deploy.PipelineSourceFromManifest(pipeline.Source) + if err != nil { + return fmt.Errorf("read source from manifest: %w", err) + } + o.shouldPromptUpdateConnection = bool + + // convert environments to deployment stages + stages, err := o.convertStages(pipeline.Stages) + if err != nil { + return fmt.Errorf("convert environments to deployment stage: %w", err) + } + + // get cross-regional resources + artifactBuckets, err := o.getArtifactBuckets() + if err != nil { + return fmt.Errorf("get cross-regional resources: %w", err) + } + + deployPipelineInput := &deploy.CreatePipelineInput{ + AppName: o.appName, + Name: pipeline.Name, + Source: source, + Build: deploy.PipelineBuildFromManifest(pipeline.Build), + Stages: stages, + ArtifactBuckets: artifactBuckets, + AdditionalTags: o.app.Tags, + } + + if err := o.deployPipeline(deployPipelineInput); err != nil { + return err + } + + return nil +} + func (o *updatePipelineOpts) convertStages(manifestStages []manifest.PipelineStage) ([]deploy.PipelineStage, error) { var stages []deploy.PipelineStage workloads, err := o.ws.WorkloadNames() @@ -150,6 +220,14 @@ func (o *updatePipelineOpts) getArtifactBuckets() ([]deploy.ArtifactBucket, erro return buckets, nil } +func (o *updatePipelineOpts) getBucketName() (string, error) { + resources, err := o.pipelineDeployer.GetAppResourcesByRegion(o.app, o.region) + if err != nil { + return "", fmt.Errorf("get app resources: %w", err) + } + return resources.S3Bucket, nil +} + func (o *updatePipelineOpts) shouldUpdate() (bool, error) { if o.skipConfirmation { return true, nil @@ -167,6 +245,12 @@ func (o *updatePipelineOpts) deployPipeline(in *deploy.CreatePipelineInput) erro if err != nil { return fmt.Errorf("check if pipeline exists: %w", err) } + + // Find the bucket to push the pipeline template to. + bucketName, err := o.getBucketName() + if err != nil { + return fmt.Errorf("get bucket name: %w", err) + } if !exist { o.prog.Start(fmt.Sprintf(fmtPipelineUpdateStart, color.HighlightUserInput(o.pipelineName))) @@ -186,8 +270,7 @@ func (o *updatePipelineOpts) deployPipeline(in *deploy.CreatePipelineInput) erro log.Infof("%s Go to %s to update the status of connection %s from PENDING to AVAILABLE.", color.Emphasize("ACTION REQUIRED!"), color.HighlightResource(connectionsURL), color.HighlightUserInput(connectionName)) log.Infoln() } - - if err := o.pipelineDeployer.CreatePipeline(in); err != nil { + if err := o.pipelineDeployer.CreatePipeline(in, bucketName); err != nil { var alreadyExists *cloudformation.ErrStackAlreadyExists if !errors.As(err, &alreadyExists) { o.prog.Stop(log.Serrorf(fmtPipelineUpdateFailed, color.HighlightUserInput(o.pipelineName))) @@ -207,7 +290,7 @@ func (o *updatePipelineOpts) deployPipeline(in *deploy.CreatePipelineInput) erro return nil } o.prog.Start(fmt.Sprintf(fmtPipelineUpdateProposalStart, color.HighlightUserInput(o.pipelineName))) - if err := o.pipelineDeployer.UpdatePipeline(in); err != nil { + if err := o.pipelineDeployer.UpdatePipeline(in, bucketName); err != nil { o.prog.Stop(log.Serrorf(fmtPipelineUpdateProposalFailed, color.HighlightUserInput(o.pipelineName))) return fmt.Errorf("update pipeline: %w", err) } @@ -215,76 +298,6 @@ func (o *updatePipelineOpts) deployPipeline(in *deploy.CreatePipelineInput) erro return nil } -// Execute create a new pipeline or update the current pipeline if it already exists. -func (o *updatePipelineOpts) Execute() error { - // bootstrap pipeline resources - o.prog.Start(fmt.Sprintf(fmtPipelineUpdateResourcesStart, color.HighlightUserInput(o.appName))) - err := o.pipelineDeployer.AddPipelineResourcesToApp(o.app, o.region) - if err != nil { - o.prog.Stop(log.Serrorf(fmtPipelineUpdateResourcesFailed, color.HighlightUserInput(o.appName))) - return fmt.Errorf("add pipeline resources to application %s in %s: %w", o.appName, o.region, err) - } - o.prog.Stop(log.Ssuccessf(fmtPipelineUpdateResourcesComplete, color.HighlightUserInput(o.appName))) - - // read pipeline manifest - data, err := o.ws.ReadPipelineManifest() - if err != nil { - return fmt.Errorf("read pipeline manifest: %w", err) - } - pipeline, err := manifest.UnmarshalPipeline(data) - if err != nil { - return fmt.Errorf("unmarshal pipeline manifest: %w", err) - } - if len(pipeline.Name) > 100 { - return fmt.Errorf(`pipeline name '%s' must be shorter than 100 characters`, pipeline.Name) - } - o.pipelineName = pipeline.Name - - // If the source has an existing connection, get the correlating ConnectionARN . - connection, ok := pipeline.Source.Properties["connection_name"] - if ok { - arn, err := o.codestar.GetConnectionARN((connection).(string)) - if err != nil { - return fmt.Errorf("get connection ARN: %w", err) - } - pipeline.Source.Properties["connection_arn"] = arn - } - - source, bool, err := deploy.PipelineSourceFromManifest(pipeline.Source) - if err != nil { - return fmt.Errorf("read source from manifest: %w", err) - } - o.shouldPromptUpdateConnection = bool - - // convert environments to deployment stages - stages, err := o.convertStages(pipeline.Stages) - if err != nil { - return fmt.Errorf("convert environments to deployment stage: %w", err) - } - - // get cross-regional resources - artifactBuckets, err := o.getArtifactBuckets() - if err != nil { - return fmt.Errorf("get cross-regional resources: %w", err) - } - - deployPipelineInput := &deploy.CreatePipelineInput{ - AppName: o.appName, - Name: pipeline.Name, - Source: source, - Build: deploy.PipelineBuildFromManifest(pipeline.Build), - Stages: stages, - ArtifactBuckets: artifactBuckets, - AdditionalTags: o.app.Tags, - } - - if err := o.deployPipeline(deployPipelineInput); err != nil { - return err - } - - return nil -} - // RecommendedActions returns follow-up actions the user can take after successfully executing the command. func (o *updatePipelineOpts) RecommendedActions() []string { return []string{ diff --git a/internal/pkg/cli/pipeline_update_test.go b/internal/pkg/cli/pipeline_update_test.go index 0f7335ee960..8098bd5a228 100644 --- a/internal/pkg/cli/pipeline_update_test.go +++ b/internal/pkg/cli/pipeline_update_test.go @@ -277,6 +277,10 @@ stages: }, } + mockResource := &stack.AppRegionalResources{ + S3Bucket: "someOtherBucket", + } + mockEnv := &config.Environment{ Name: "test", App: appName, @@ -316,8 +320,9 @@ stages: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(false, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateStart, pipelineName)).Times(1), - m.deployer.EXPECT().CreatePipeline(gomock.Any()).Return(nil), + m.deployer.EXPECT().CreatePipeline(gomock.Any(), gomock.Any()).Return(nil), m.prog.EXPECT().Stop(log.Ssuccessf(fmtPipelineUpdateComplete, pipelineName)).Times(1), ) }, @@ -345,9 +350,10 @@ stages: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(true, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prompt.EXPECT().Confirm(fmt.Sprintf(fmtPipelineUpdateExistPrompt, pipelineName), "").Return(true, nil), m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateProposalStart, pipelineName)).Times(1), - m.deployer.EXPECT().UpdatePipeline(gomock.Any()).Return(nil), + m.deployer.EXPECT().UpdatePipeline(gomock.Any(), gomock.Any()).Return(nil), m.prog.EXPECT().Stop(log.Ssuccessf(fmtPipelineUpdateProposalComplete, pipelineName)).Times(1), ) }, @@ -375,6 +381,7 @@ stages: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(true, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prompt.EXPECT().Confirm(fmt.Sprintf(fmtPipelineUpdateExistPrompt, pipelineName), "").Return(false, nil), ) }, @@ -402,6 +409,7 @@ stages: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(true, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prompt.EXPECT().Confirm(fmt.Sprintf(fmtPipelineUpdateExistPrompt, pipelineName), "").Return(false, errors.New("some error")), ) }, @@ -581,8 +589,9 @@ source: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(false, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateStart, pipelineName)).Times(1), - m.deployer.EXPECT().CreatePipeline(gomock.Any()).Return(errors.New("some error")), + m.deployer.EXPECT().CreatePipeline(gomock.Any(), gomock.Any()).Return(errors.New("some error")), m.prog.EXPECT().Stop(log.Serrorf(fmtPipelineUpdateFailed, pipelineName)).Times(1), ) }, @@ -610,9 +619,10 @@ source: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(true, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prompt.EXPECT().Confirm(fmt.Sprintf(fmtPipelineUpdateExistPrompt, pipelineName), "").Return(true, nil), m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateProposalStart, pipelineName)).Times(1), - m.deployer.EXPECT().UpdatePipeline(gomock.Any()).Return(errors.New("some error")), + m.deployer.EXPECT().UpdatePipeline(gomock.Any(), gomock.Any()).Return(errors.New("some error")), m.prog.EXPECT().Stop(log.Serrorf(fmtPipelineUpdateProposalFailed, pipelineName)).Times(1), ) }, @@ -666,9 +676,10 @@ stages: // deployPipeline m.deployer.EXPECT().PipelineExists(gomock.Any()).Return(true, nil), + m.deployer.EXPECT().GetAppResourcesByRegion(&app, region).Return(mockResource, nil), m.prompt.EXPECT().Confirm(fmt.Sprintf(fmtPipelineUpdateExistPrompt, pipelineName), "").Return(true, nil), m.prog.EXPECT().Start(fmt.Sprintf(fmtPipelineUpdateProposalStart, pipelineName)).Times(1), - m.deployer.EXPECT().UpdatePipeline(gomock.Any()).Return(nil), + m.deployer.EXPECT().UpdatePipeline(gomock.Any(), gomock.Any()).Return(nil), m.prog.EXPECT().Stop(log.Ssuccessf(fmtPipelineUpdateProposalComplete, pipelineName)).Times(1), ) }, diff --git a/internal/pkg/deploy/cloudformation/cc_pipeline_integration_test.go b/internal/pkg/deploy/cloudformation/cc_pipeline_integration_test.go index cdc5bf208b0..ec26472b4ef 100644 --- a/internal/pkg/deploy/cloudformation/cc_pipeline_integration_test.go +++ b/internal/pkg/deploy/cloudformation/cc_pipeline_integration_test.go @@ -56,7 +56,8 @@ func TestCCPipelineCreation(t *testing.T) { envDeployer := cloudformation.New(envSess) s3Client := s3.New(envSess) uploader := template.New() - var bucketName string + var envBucketName string + var appBucketName string environmentToDeploy := deploy.CreateEnvironmentInput{ Name: randStringBytes(10), @@ -87,7 +88,10 @@ func TestCCPipelineCreation(t *testing.T) { require.NoError(t, err) require.Equal(t, len(stackInstances.Summaries), 2) - err = s3Client.EmptyBucket(bucketName) + err = s3Client.EmptyBucket(envBucketName) + require.NoError(t, err) + + err = s3Client.EmptyBucket(appBucketName) require.NoError(t, err) _, err = appCfClient.DeleteStackInstances(&awsCF.DeleteStackInstancesInput{ @@ -166,9 +170,9 @@ func TestCCPipelineCreation(t *testing.T) { regionalResource, err := appDeployer.GetAppResourcesByRegion(&app, envRegion.ID()) require.NoError(t, err) - bucketName = regionalResource.S3Bucket + envBucketName = regionalResource.S3Bucket urls, err := uploader.UploadEnvironmentCustomResources(s3.CompressAndUploadFunc(func(key string, objects ...s3.NamedBinary) (string, error) { - return s3Client.ZipAndUpload(bucketName, key, objects...) + return s3Client.ZipAndUpload(envBucketName, key, objects...) })) require.NoError(t, err) environmentToDeploy.CustomResourcesURLs = urls @@ -219,7 +223,10 @@ func TestCCPipelineCreation(t *testing.T) { }, ArtifactBuckets: artifactBuckets, } - require.NoError(t, appDeployer.CreatePipeline(pipelineInput)) + appRegionResources, err := appDeployer.GetAppResourcesByRegion(&app, *appSess.Config.Region) + require.NoError(t, err) + appBucketName = appRegionResources.S3Bucket + require.NoError(t, appDeployer.CreatePipeline(pipelineInput, appBucketName)) // Ensure that the new stack exists assertStackExists(t, appCfClient, pipelineStackName) diff --git a/internal/pkg/deploy/cloudformation/cloudformation.go b/internal/pkg/deploy/cloudformation/cloudformation.go index 79dfe3068b3..92ebc54af2f 100644 --- a/internal/pkg/deploy/cloudformation/cloudformation.go +++ b/internal/pkg/deploy/cloudformation/cloudformation.go @@ -8,9 +8,12 @@ import ( "context" "errors" "fmt" + "io" "strings" "time" + "github.com/aws/copilot-cli/internal/pkg/aws/s3" + "github.com/aws/copilot-cli/internal/pkg/aws/codepipeline" "github.com/aws/copilot-cli/internal/pkg/aws/codestar" @@ -83,6 +86,10 @@ type codePipelineClient interface { RetryStageExecution(pipelineName, stageName string) error } +type s3Client interface { + PutArtifact(bucket, fileName string, data io.Reader) (string, error) +} + type stackSetClient interface { Create(name, template string, opts ...stackset.CreateOrUpdateOption) error CreateInstancesAndWait(name string, accounts, regions []string) error @@ -102,6 +109,7 @@ type CloudFormation struct { regionalClient func(region string) cfnClient appStackSet stackSetClient box packd.Box + s3Client s3Client } // New returns a configured CloudFormation client. @@ -118,6 +126,7 @@ func New(sess *session.Session) CloudFormation { }, appStackSet: stackset.New(sess), box: templates.Box(), + s3Client: s3.New(sess), } return client } @@ -384,6 +393,17 @@ func toStack(config StackConfiguration) (*cloudformation.Stack, error) { return stack, nil } +func toStackFromS3(config StackConfiguration, s3url string) (*cloudformation.Stack, error) { + stack := cloudformation.NewStackWithURL(config.StackName(), s3url) + var err error + stack.Parameters, err = config.Parameters() + if err != nil { + return nil, err + } + stack.Tags = config.Tags() + return stack, nil +} + func toMap(tags []*sdkcloudformation.Tag) map[string]string { m := make(map[string]string) for _, t := range tags { diff --git a/internal/pkg/deploy/cloudformation/env_test.go b/internal/pkg/deploy/cloudformation/env_test.go index d0337139f12..15b888875e5 100644 --- a/internal/pkg/deploy/cloudformation/env_test.go +++ b/internal/pkg/deploy/cloudformation/env_test.go @@ -289,7 +289,7 @@ func TestCloudFormation_UpdateEnvironmentTemplate(t *testing.T) { require.Equal(t, "phonetool-test", s.Name) require.Equal(t, params, s.Parameters) require.Equal(t, tags, s.Tags) - require.Equal(t, "hello", s.Template) + require.Equal(t, "hello", s.TemplateBody) require.Equal(t, aws.String("arn"), s.RoleARN) }) return m diff --git a/internal/pkg/deploy/cloudformation/ghV1_pipeline_integration_test.go b/internal/pkg/deploy/cloudformation/ghV1_pipeline_integration_test.go index 327202c0815..46e07ffc65b 100644 --- a/internal/pkg/deploy/cloudformation/ghV1_pipeline_integration_test.go +++ b/internal/pkg/deploy/cloudformation/ghV1_pipeline_integration_test.go @@ -71,7 +71,8 @@ func TestGHv1PipelineCreation(t *testing.T) { envDeployer := cloudformation.New(envSess) s3Client := s3.New(envSess) uploader := template.New() - var bucketName string + var envBucketName string + var appBucketName string environmentToDeploy := deploy.CreateEnvironmentInput{ Name: randStringBytes(10), @@ -102,7 +103,9 @@ func TestGHv1PipelineCreation(t *testing.T) { require.NoError(t, err) require.Equal(t, len(stackInstances.Summaries), 2) - err = s3Client.EmptyBucket(bucketName) + err = s3Client.EmptyBucket(envBucketName) + require.NoError(t, err) + err = s3Client.EmptyBucket(appBucketName) require.NoError(t, err) _, err = appCfClient.DeleteStackInstances(&awsCF.DeleteStackInstancesInput{ @@ -183,9 +186,9 @@ func TestGHv1PipelineCreation(t *testing.T) { regionalResource, err := appDeployer.GetAppResourcesByRegion(&app, envRegion.ID()) require.NoError(t, err) - bucketName = regionalResource.S3Bucket + envBucketName = regionalResource.S3Bucket urls, err := uploader.UploadEnvironmentCustomResources(s3.CompressAndUploadFunc(func(key string, objects ...s3.NamedBinary) (string, error) { - return s3Client.ZipAndUpload(bucketName, key, objects...) + return s3Client.ZipAndUpload(envBucketName, key, objects...) })) require.NoError(t, err) environmentToDeploy.CustomResourcesURLs = urls @@ -237,7 +240,10 @@ func TestGHv1PipelineCreation(t *testing.T) { }, ArtifactBuckets: artifactBuckets, } - require.NoError(t, appDeployer.CreatePipeline(pipelineInput)) + appRegionResources, err := appDeployer.GetAppResourcesByRegion(&app, *appSess.Config.Region) + require.NoError(t, err) + appBucketName = appRegionResources.S3Bucket + require.NoError(t, appDeployer.CreatePipeline(pipelineInput, appBucketName)) // Ensure that the new stack exists assertStackExists(t, appCfClient, pipelineStackName) diff --git a/internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go b/internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go index 7149429b5e6..ed30de740cd 100644 --- a/internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go +++ b/internal/pkg/deploy/cloudformation/mocks/mock_cloudformation.go @@ -6,6 +6,7 @@ package mocks import ( context "context" + io "io" reflect "reflect" cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" @@ -494,6 +495,44 @@ func (mr *MockcodePipelineClientMockRecorder) RetryStageExecution(pipelineName, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryStageExecution", reflect.TypeOf((*MockcodePipelineClient)(nil).RetryStageExecution), pipelineName, stageName) } +// Mocks3Client is a mock of s3Client interface. +type Mocks3Client struct { + ctrl *gomock.Controller + recorder *Mocks3ClientMockRecorder +} + +// Mocks3ClientMockRecorder is the mock recorder for Mocks3Client. +type Mocks3ClientMockRecorder struct { + mock *Mocks3Client +} + +// NewMocks3Client creates a new mock instance. +func NewMocks3Client(ctrl *gomock.Controller) *Mocks3Client { + mock := &Mocks3Client{ctrl: ctrl} + mock.recorder = &Mocks3ClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mocks3Client) EXPECT() *Mocks3ClientMockRecorder { + return m.recorder +} + +// PutArtifact mocks base method. +func (m *Mocks3Client) PutArtifact(bucket, fileName string, data io.Reader) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutArtifact", bucket, fileName, data) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutArtifact indicates an expected call of PutArtifact. +func (mr *Mocks3ClientMockRecorder) PutArtifact(bucket, fileName, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutArtifact", reflect.TypeOf((*Mocks3Client)(nil).PutArtifact), bucket, fileName, data) +} + // MockstackSetClient is a mock of stackSetClient interface. type MockstackSetClient struct { ctrl *gomock.Controller diff --git a/internal/pkg/deploy/cloudformation/pipeline.go b/internal/pkg/deploy/cloudformation/pipeline.go index 353d520805c..d12e9c5fe01 100644 --- a/internal/pkg/deploy/cloudformation/pipeline.go +++ b/internal/pkg/deploy/cloudformation/pipeline.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "strings" "time" "github.com/aws/copilot-cli/internal/pkg/aws/cloudformation" @@ -17,8 +18,9 @@ import ( ) const ( - sourceStage = "Source" - connectionARNKey = "PipelineConnectionARN" + sourceStage = "Source" + connectionARNKey = "PipelineConnectionARN" + fmtPipelineCfnTemplateName = "%s.pipeline.stack.yml" ) // PipelineExists checks if the pipeline with the provided config exists. @@ -36,8 +38,12 @@ func (cf CloudFormation) PipelineExists(in *deploy.CreatePipelineInput) (bool, e } // CreatePipeline sets up a new CodePipeline for deploying services. -func (cf CloudFormation) CreatePipeline(in *deploy.CreatePipelineInput) error { - s, err := toStack(stack.NewPipelineStackConfig(in)) +func (cf CloudFormation) CreatePipeline(in *deploy.CreatePipelineInput, bucketName string) error { + templateURL, err := cf.pushTemplateToS3Bucket(bucketName, stack.NewPipelineStackConfig(in)) + if err != nil { + return err + } + s, err := toStackFromS3(stack.NewPipelineStackConfig(in), templateURL) if err != nil { return err } @@ -67,8 +73,12 @@ func (cf CloudFormation) CreatePipeline(in *deploy.CreatePipelineInput) error { } // UpdatePipeline updates an existing CodePipeline for deploying services. -func (cf CloudFormation) UpdatePipeline(in *deploy.CreatePipelineInput) error { - s, err := toStack(stack.NewPipelineStackConfig(in)) +func (cf CloudFormation) UpdatePipeline(in *deploy.CreatePipelineInput, bucketName string) error { + templateURL, err := cf.pushTemplateToS3Bucket(bucketName, stack.NewPipelineStackConfig(in)) + if err != nil { + return err + } + s, err := toStackFromS3(stack.NewPipelineStackConfig(in), templateURL) if err != nil { return err } @@ -86,3 +96,16 @@ func (cf CloudFormation) UpdatePipeline(in *deploy.CreatePipelineInput) error { func (cf CloudFormation) DeletePipeline(stackName string) error { return cf.cfnClient.DeleteAndWait(stackName) } + +func (cf CloudFormation) pushTemplateToS3Bucket(bucket string, config StackConfiguration) (string, error) { + template, err := config.Template() + if err != nil { + return "", fmt.Errorf("generate template: %w", err) + } + reader := strings.NewReader(template) + url, err := cf.s3Client.PutArtifact(bucket, fmt.Sprintf(fmtPipelineCfnTemplateName, config.StackName()), reader) + if err != nil { + return "", fmt.Errorf("upload pipeline template to S3 bucket %s: %w", bucket, err) + } + return url, nil +} diff --git a/internal/pkg/deploy/cloudformation/pipeline_test.go b/internal/pkg/deploy/cloudformation/pipeline_test.go index 3938ff37890..dd331c1ab1d 100644 --- a/internal/pkg/deploy/cloudformation/pipeline_test.go +++ b/internal/pkg/deploy/cloudformation/pipeline_test.go @@ -87,10 +87,14 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { Stages: nil, ArtifactBuckets: nil, } + mockS3BucketName := "BitterBucket" + mockURL := "templateURL" + testCases := map[string]struct { createCfnMock func(ctrl *gomock.Controller) cfnClient createCsMock func(ctrl *gomock.Controller) codeStarClient createCpMock func(ctrl *gomock.Controller) codePipelineClient + createS3Mock func(ctrl *gomock.Controller) s3Client wantedErr error }{ "exits successfully with base case (no connection)": { @@ -100,6 +104,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { m.EXPECT().Outputs(gomock.Any()).Return(nil, nil) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Times(0) @@ -122,6 +131,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { }, nil) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Return(nil) @@ -141,6 +155,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { m.EXPECT().Outputs(gomock.Any()).Times(0) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Times(0) @@ -153,6 +172,30 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { }, wantedErr: fmt.Errorf("some error"), }, + "returns error if fails to upload template to S3 bucket": { + createCfnMock: func(ctrl *gomock.Controller) cfnClient { + m := mocks.NewMockcfnClient(ctrl) + m.EXPECT().CreateAndWait(gomock.Any()).Times(0) + m.EXPECT().Outputs(gomock.Any()).Times(0) + return m + }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return("", errors.New("some error")) + return m + }, + createCsMock: func(ctrl *gomock.Controller) codeStarClient { + m := mocks.NewMockcodeStarClient(ctrl) + m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Times(0) + return m + }, + createCpMock: func(ctrl *gomock.Controller) codePipelineClient { + m := mocks.NewMockcodePipelineClient(ctrl) + m.EXPECT().RetryStageExecution(gomock.Any(), gomock.Any()).Times(0) + return m + }, + wantedErr: fmt.Errorf("upload pipeline template to S3 bucket %s: some error", "BitterBucket"), + }, "returns err if retrieving outputs fails": { createCfnMock: func(ctrl *gomock.Controller) cfnClient { m := mocks.NewMockcfnClient(ctrl) @@ -160,6 +203,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { m.EXPECT().Outputs(gomock.Any()).Return(nil, errors.New("some error")) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Times(0) @@ -181,6 +229,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { }, nil) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Return(errors.New("some error")) @@ -202,6 +255,11 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { }, nil) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, createCsMock: func(ctrl *gomock.Controller) codeStarClient { m := mocks.NewMockcodeStarClient(ctrl) m.EXPECT().WaitUntilConnectionStatusAvailable(gomock.Any(), "mockConnectionARN").Return(nil) @@ -225,13 +283,18 @@ func TestCloudFormation_CreatePipeline(t *testing.T) { cfnClient: tc.createCfnMock(ctrl), codeStarClient: tc.createCsMock(ctrl), cpClient: tc.createCpMock(ctrl), + s3Client: tc.createS3Mock(ctrl), } // WHEN - err := c.CreatePipeline(in) + err := c.CreatePipeline(in, mockS3BucketName) // THEN - require.Equal(t, tc.wantedErr, err) + if tc.wantedErr != nil { + require.EqualError(t, err, tc.wantedErr.Error()) + } else { + require.NoError(t, err) + } }) } } @@ -250,9 +313,12 @@ func TestCloudFormation_UpdatePipeline(t *testing.T) { Stages: nil, ArtifactBuckets: nil, } + mockS3BucketName := "BitterBucket" + mockURL := "templateURL" testCases := map[string]struct { - createMock func(ctrl *gomock.Controller) cfnClient - wantedErr error + createMock func(ctrl *gomock.Controller) cfnClient + createS3Mock func(ctrl *gomock.Controller) s3Client + wantedErr error }{ "exits successfully if there are no updates": { createMock: func(ctrl *gomock.Controller) cfnClient { @@ -260,6 +326,25 @@ func TestCloudFormation_UpdatePipeline(t *testing.T) { m.EXPECT().UpdateAndWait(gomock.Any()).Return(&cloudformation.ErrChangeSetEmpty{}) return m }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return(mockURL, nil) + return m + }, + wantedErr: nil, + }, + "returns an error if can't push template to S3 bucket": { + createMock: func(ctrl *gomock.Controller) cfnClient { + m := mocks.NewMockcfnClient(ctrl) + //m.EXPECT().UpdateAndWait(gomock.Any()).Return(&cloudformation.ErrChangeSetEmpty{}) + return m + }, + createS3Mock: func(ctrl *gomock.Controller) s3Client { + m := mocks.NewMocks3Client(ctrl) + m.EXPECT().PutArtifact(mockS3BucketName, gomock.Any(), gomock.Any()).Return("", errors.New("some error")) + return m + }, + wantedErr: fmt.Errorf("upload pipeline template to S3 bucket %s: some error", "BitterBucket"), }, } @@ -270,13 +355,18 @@ func TestCloudFormation_UpdatePipeline(t *testing.T) { defer ctrl.Finish() c := CloudFormation{ cfnClient: tc.createMock(ctrl), + s3Client: tc.createS3Mock(ctrl), } // WHEN - err := c.UpdatePipeline(in) + err := c.UpdatePipeline(in, mockS3BucketName) // THEN - require.Equal(t, tc.wantedErr, err) + if tc.wantedErr != nil { + require.EqualError(t, err, tc.wantedErr.Error()) + } else { + require.NoError(t, err) + } }) } } diff --git a/internal/pkg/exec/mocks/mock_exec.go b/internal/pkg/exec/mocks/mock_exec.go index b5254f6e747..12c44db20b1 100644 --- a/internal/pkg/exec/mocks/mock_exec.go +++ b/internal/pkg/exec/mocks/mock_exec.go @@ -5,12 +5,51 @@ package mocks import ( + http "net/http" reflect "reflect" command "github.com/aws/copilot-cli/internal/pkg/term/command" gomock "github.com/golang/mock/gomock" ) +// MockhttpClient is a mock of httpClient interface. +type MockhttpClient struct { + ctrl *gomock.Controller + recorder *MockhttpClientMockRecorder +} + +// MockhttpClientMockRecorder is the mock recorder for MockhttpClient. +type MockhttpClientMockRecorder struct { + mock *MockhttpClient +} + +// NewMockhttpClient creates a new mock instance. +func NewMockhttpClient(ctrl *gomock.Controller) *MockhttpClient { + mock := &MockhttpClient{ctrl: ctrl} + mock.recorder = &MockhttpClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockhttpClient) EXPECT() *MockhttpClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockhttpClient) Get(url string) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", url) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockhttpClientMockRecorder) Get(url interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockhttpClient)(nil).Get), url) +} + // Mockrunner is a mock of runner interface. type Mockrunner struct { ctrl *gomock.Controller