Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 26 additions & 1 deletion cmd/copilot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package main

import (
"context"
"errors"
"os"
"os/signal"
"syscall"

"github.com/aproint/copilot-cli/cmd/copilot/template"
"github.com/aproint/copilot-cli/internal/pkg/cli"
Expand All @@ -24,14 +27,19 @@ type exitCodeError interface {
ExitCode() int
}

const sigtermExitCode = 128 + int(syscall.SIGTERM)

func init() {
color.DisableColorBasedOnEnvVar()
cobra.EnableCommandSorting = false // Maintain the order in which we add commands.
}

func main() {
ctx, stop := rootContext()
defer stop()

cmd := buildRootCmd()
if err := cmd.Execute(); err != nil {
if err := cmd.ExecuteContext(ctx); err != nil {
var ac actionRecommender
var exitCodeErr exitCodeError

Expand All @@ -47,6 +55,23 @@ func main() {
}
}

func rootContext() (context.Context, func()) {
ctx, cancel := context.WithCancel(context.Background())
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
stop := func() {
signal.Stop(sigCh)
cancel()
}
go func() {
<-sigCh
cancel()
signal.Stop(sigCh)
os.Exit(sigtermExitCode)
}()
return ctx, stop
}

func buildRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "copilot",
Expand Down
8 changes: 7 additions & 1 deletion internal/pkg/aws/cloudformation/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,13 @@ func (c *CloudFormation) DeleteAndWaitWithRoleARN(stackName, roleARN string) err
// Describe returns a description of an existing stack.
// If the stack does not exist, returns ErrStackNotFound.
func (c *CloudFormation) Describe(name string) (*StackDescription, error) {
out, err := c.client.DescribeStacks(context.Background(), &cloudformation.DescribeStacksInput{
return c.DescribeWithContext(context.Background(), name)
}

// DescribeWithContext returns a description of an existing stack using ctx.
// If the stack does not exist, returns ErrStackNotFound.
func (c *CloudFormation) DescribeWithContext(ctx context.Context, name string) (*StackDescription, error) {
out, err := c.client.DescribeStacks(ctx, &cloudformation.DescribeStacksInput{
StackName: awsv2.String(name),
})
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Double struct {
DeleteAndWaitFn func(stackName string) error
DeleteAndWaitWithRoleARNFn func(stackName, roleARN string) error
DescribeFn func(name string) (*cfn.StackDescription, error)
DescribeWithContextFn func(ctx context.Context, name string) (*cfn.StackDescription, error)
ExistsFn func(name string) (bool, error)
MetadataFn func(opt cfn.MetadataOpts) (string, error)
TemplateBodyFn func(name string) (string, error)
Expand Down Expand Up @@ -91,6 +92,14 @@ func (d *Double) Describe(name string) (*cfn.StackDescription, error) {
return d.DescribeFn(name)
}

// DescribeWithContext calls the stubbed function.
func (d *Double) DescribeWithContext(ctx context.Context, name string) (*cfn.StackDescription, error) {
if d.DescribeWithContextFn != nil {
return d.DescribeWithContextFn(ctx, name)
}
return d.DescribeFn(name)
}

// Exists calls the stubbed function.
func (d *Double) Exists(name string) (bool, error) {
return d.ExistsFn(name)
Expand Down
4 changes: 2 additions & 2 deletions internal/pkg/aws/identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type Caller struct {
}

// Get returns the Caller associated with the Client's session.
func (s STS) Get() (Caller, error) {
out, err := s.client.GetCallerIdentity(context.Background(), &sts.GetCallerIdentityInput{})
func (s STS) Get(ctx context.Context) (Caller, error) {
out, err := s.client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
if err != nil {
return Caller{}, fmt.Errorf("get caller identity: %w", err)
}
Expand Down
12 changes: 7 additions & 5 deletions internal/pkg/aws/identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package identity

import (
"context"
"errors"
"fmt"
"testing"
Expand All @@ -16,6 +17,7 @@ import (
)

func TestIdentity_Get(t *testing.T) {
ctx := context.WithValue(context.Background(), "test-key", "test-value")
mockError := errors.New("error")
mockBadARN := "mockArn"
mockARN := "arn:aws:iam::1111:role/phonetool-test-CFNExecutionRole"
Expand All @@ -31,13 +33,13 @@ func TestIdentity_Get(t *testing.T) {
}{
"should return wrapped error given error from STS GetCallerIdentity": {
callMock: func(m *mocks.Mockapi) {
m.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(nil, mockError)
m.EXPECT().GetCallerIdentity(ctx, gomock.Any()).Return(nil, mockError)
},
wantErr: fmt.Errorf("get caller identity: %w", mockError),
},
"should return wrapped error if cannot parse the account arn": {
callMock: func(m *mocks.Mockapi) {
m.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(&sts.GetCallerIdentityOutput{
m.EXPECT().GetCallerIdentity(ctx, gomock.Any()).Return(&sts.GetCallerIdentityOutput{
Account: awsv2.String(mockAccount),
Arn: awsv2.String(mockBadARN),
UserId: awsv2.String(mockUserID),
Expand All @@ -47,7 +49,7 @@ func TestIdentity_Get(t *testing.T) {
},
"should return Identity": {
callMock: func(m *mocks.Mockapi) {
m.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(&sts.GetCallerIdentityOutput{
m.EXPECT().GetCallerIdentity(ctx, gomock.Any()).Return(&sts.GetCallerIdentityOutput{
Account: awsv2.String(mockAccount),
Arn: awsv2.String(mockARN),
UserId: awsv2.String(mockUserID),
Expand All @@ -61,7 +63,7 @@ func TestIdentity_Get(t *testing.T) {
},
"should return Identity in non standard partition": {
callMock: func(m *mocks.Mockapi) {
m.EXPECT().GetCallerIdentity(gomock.Any(), gomock.Any()).Return(&sts.GetCallerIdentityOutput{
m.EXPECT().GetCallerIdentity(ctx, gomock.Any()).Return(&sts.GetCallerIdentityOutput{
Account: awsv2.String(mockAccount),
Arn: awsv2.String(mockChinaARN),
UserId: awsv2.String(mockUserID),
Expand All @@ -88,7 +90,7 @@ func TestIdentity_Get(t *testing.T) {

tc.callMock(mockClient)

gotIdentity, gotErr := sts.Get()
gotIdentity, gotErr := sts.Get(ctx)

if tc.wantErr != nil {
require.EqualError(t, gotErr, tc.wantErr.Error())
Expand Down
68 changes: 34 additions & 34 deletions internal/pkg/cli/app_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ func (o *deleteAppOpts) Validate() error {
}

// Ask prompts the user for any required flags that they didn't provide.
func (o *deleteAppOpts) Ask() error {
if err := o.validateOrAskAppName(); err != nil {
func (o *deleteAppOpts) Ask(ctx context.Context) error {
if err := o.validateOrAskAppName(ctx); err != nil {
return err
}
if o.skipConfirmation {
Expand All @@ -183,57 +183,57 @@ func (o *deleteAppOpts) Ask() error {
// Execute deletes the application.
// It removes the pipelines, all the services from each environment, the environments, the pipeline S3 buckets,
// the application, removes the variables from the config store, and deletes the local workspace.
func (o *deleteAppOpts) Execute() error {
if err := o.deletePipelines(); err != nil {
func (o *deleteAppOpts) Execute(ctx context.Context) error {
if err := o.deletePipelines(ctx); err != nil {
return err
}

if err := o.deleteSvcs(); err != nil {
if err := o.deleteSvcs(ctx); err != nil {
return err
}

if err := o.deleteJobs(); err != nil {
if err := o.deleteJobs(ctx); err != nil {
return err
}

if err := o.deleteEnvs(); err != nil {
if err := o.deleteEnvs(ctx); err != nil {
return err
}

if err := o.emptyS3Bucket(); err != nil {
if err := o.emptyS3Bucket(ctx); err != nil {
return err
}

if err := o.deleteAppResources(); err != nil {
if err := o.deleteAppResources(ctx); err != nil {
return err
}

if err := o.deleteAppConfigs(); err != nil {
if err := o.deleteAppConfigs(ctx); err != nil {
return err
}

if err := o.deleteWs(); err != nil {
if err := o.deleteWs(ctx); err != nil {
return err
}

return nil
}

func (o *deleteAppOpts) validateOrAskAppName() error {
func (o *deleteAppOpts) validateOrAskAppName(ctx context.Context) error {
if o.name != "" {
_, err := o.store.GetApplication(o.name)
_, err := o.store.GetApplication(ctx, o.name)
return err
}
name, err := o.sel.Application(appDeleteNamePrompt, "")
name, err := o.sel.Application(ctx, appDeleteNamePrompt, "")
if err != nil {
return fmt.Errorf("select application name: %w", err)
}
o.name = name
return nil
}

func (o *deleteAppOpts) deleteSvcs() error {
svcs, err := o.store.ListServices(o.name)
func (o *deleteAppOpts) deleteSvcs(ctx context.Context) error {
svcs, err := o.store.ListServices(ctx, o.name)
if err != nil {
return fmt.Errorf("list services for application %s: %w", o.name, err)
}
Expand All @@ -243,15 +243,15 @@ func (o *deleteAppOpts) deleteSvcs() error {
if err != nil {
return err
}
if err := cmd.Execute(); err != nil {
if err := cmd.Execute(ctx); err != nil {
return fmt.Errorf("execute svc delete: %w", err)
}
}
return nil
}

func (o *deleteAppOpts) deleteJobs() error {
jobs, err := o.store.ListJobs(o.name)
func (o *deleteAppOpts) deleteJobs(ctx context.Context) error {
jobs, err := o.store.ListJobs(ctx, o.name)
if err != nil {
return fmt.Errorf("list jobs for application %s: %w", o.name, err)
}
Expand All @@ -261,15 +261,15 @@ func (o *deleteAppOpts) deleteJobs() error {
if err != nil {
return err
}
if err := cmd.Execute(); err != nil {
if err := cmd.Execute(ctx); err != nil {
return fmt.Errorf("execute job delete: %w", err)
}
}
return nil
}

func (o *deleteAppOpts) deleteEnvs() error {
envs, err := o.store.ListEnvironments(o.name)
func (o *deleteAppOpts) deleteEnvs(ctx context.Context) error {
envs, err := o.store.ListEnvironments(ctx, o.name)
if err != nil {
return fmt.Errorf("list environments for application %s: %w", o.name, err)
}
Expand All @@ -285,7 +285,7 @@ func (o *deleteAppOpts) deleteEnvs() error {
if err != nil {
return err
}
if err := taskCmd.Execute(); err != nil {
if err := taskCmd.Execute(ctx); err != nil {
return fmt.Errorf("execute task delete")
}
}
Expand All @@ -294,18 +294,18 @@ func (o *deleteAppOpts) deleteEnvs() error {
if err != nil {
return err
}
if err := cmd.Ask(); err != nil {
if err := cmd.Ask(ctx); err != nil {
return fmt.Errorf("ask env delete: %w", err)
}
if err := cmd.Execute(); err != nil {
if err := cmd.Execute(ctx); err != nil {
return fmt.Errorf("execute env delete: %w", err)
}
}
return nil
}

func (o *deleteAppOpts) emptyS3Bucket() error {
app, err := o.store.GetApplication(o.name)
func (o *deleteAppOpts) emptyS3Bucket(ctx context.Context) error {
app, err := o.store.GetApplication(ctx, o.name)
if err != nil {
return fmt.Errorf("get application %s: %w", o.name, err)
}
Expand Down Expand Up @@ -335,7 +335,7 @@ func (o *deleteAppOpts) emptyS3Bucket() error {
return nil
}

func (o *deleteAppOpts) deletePipelines() error {
func (o *deleteAppOpts) deletePipelines(ctx context.Context) error {
pipelines, err := o.pipelineLister.ListDeployedPipelines(o.name)
if err != nil {
return fmt.Errorf("list pipelines for application %s: %w", o.name, err)
Expand All @@ -346,31 +346,31 @@ func (o *deleteAppOpts) deletePipelines() error {
if err != nil {
return err
}
if err := cmd.Execute(); err != nil {
if err := cmd.Execute(ctx); err != nil {
return fmt.Errorf("execute pipeline delete: %w", err)
}
}
return nil
}

func (o *deleteAppOpts) deleteAppResources() error {
func (o *deleteAppOpts) deleteAppResources(ctx context.Context) error {
if err := o.cfn.DeleteApp(o.name); err != nil {
return fmt.Errorf("delete app resources: %w", err)
}
return nil
}

func (o *deleteAppOpts) deleteAppConfigs() error {
func (o *deleteAppOpts) deleteAppConfigs(ctx context.Context) error {
o.spinner.Start(deleteAppConfigStartMsg)
if err := o.store.DeleteApplication(o.name); err != nil {
if err := o.store.DeleteApplication(ctx, o.name); err != nil {
o.spinner.Stop(log.Serrorln("Error deleting application configuration."))
return fmt.Errorf("delete application %s configuration: %w", o.name, err)
}
o.spinner.Stop(log.Ssuccess(deleteAppConfigStopMsg))
return nil
}

func (o *deleteAppOpts) deleteWs() error {
func (o *deleteAppOpts) deleteWs(ctx context.Context) error {
ws, err := o.existingWorkSpace()
if err != nil {
return nil
Expand Down Expand Up @@ -403,7 +403,7 @@ func buildAppDeleteCommand() *cobra.Command {
if err != nil {
return err
}
return run(opts)
return run(cmd.Context(), opts)
}),
}

Expand Down
Loading