github.com/defang-io/defang/src@v0.0.0-20240505002154-bdf411911834/pkg/clouds/aws/ecs/cfn/setup.go (about) 1 package cfn 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go-v2/service/cloudformation" 11 cfnTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 12 "github.com/aws/smithy-go" 13 "github.com/aws/smithy-go/ptr" 14 common "github.com/defang-io/defang/src/pkg/clouds/aws" 15 "github.com/defang-io/defang/src/pkg/clouds/aws/ecs" 16 "github.com/defang-io/defang/src/pkg/clouds/aws/ecs/cfn/outputs" 17 "github.com/defang-io/defang/src/pkg/clouds/aws/region" 18 "github.com/defang-io/defang/src/pkg/types" 19 ) 20 21 type AwsEcs struct { 22 ecs.AwsEcs 23 stackName string 24 } 25 26 // var _ types.Driver = (*AwsEcs)(nil) 27 28 const stackTimeout = time.Minute * 3 29 30 func OptionVPCAndSubnetID(ctx context.Context, vpcID, subnetID string) func(types.Driver) error { 31 return func(d types.Driver) error { 32 if ecs, ok := d.(*AwsEcs); ok { 33 return ecs.PopulateVPCandSubnetID(ctx, vpcID, subnetID) 34 } 35 return errors.New("only AwsEcs driver supports VPC ID and Subnet ID option") 36 } 37 } 38 39 func New(stack string, region region.Region) *AwsEcs { 40 if stack == "" { 41 panic("stack must be set") 42 } 43 return &AwsEcs{ 44 stackName: stack, 45 AwsEcs: ecs.AwsEcs{ 46 Aws: common.Aws{Region: region}, 47 Spot: true, 48 }, 49 } 50 } 51 52 func (a *AwsEcs) newClient(ctx context.Context) (*cloudformation.Client, error) { 53 cfg, err := a.LoadConfig(ctx) 54 if err != nil { 55 return nil, err 56 } 57 58 return cloudformation.NewFromConfig(cfg), nil 59 } 60 61 // update1s is a functional option for cloudformation.StackUpdateCompleteWaiter that sets the MinDelay to 1 62 func update1s(o *cloudformation.StackUpdateCompleteWaiterOptions) { 63 o.MinDelay = 1 64 } 65 66 func (a *AwsEcs) updateStackAndWait(ctx context.Context, templateBody string) error { 67 cfn, err := a.newClient(ctx) 68 if err != nil { 69 return err 70 } 71 72 uso, err := cfn.UpdateStack(ctx, &cloudformation.UpdateStackInput{ 73 StackName: ptr.String(a.stackName), 74 TemplateBody: ptr.String(templateBody), 75 Capabilities: []cfnTypes.Capability{cfnTypes.CapabilityCapabilityNamedIam}, 76 }) 77 if err != nil { 78 // Go SDK doesn't have --no-fail-on-empty-changeset; ignore ValidationError: No updates are to be performed. 79 var apiError smithy.APIError 80 if ok := errors.As(err, &apiError); ok && apiError.ErrorCode() == "ValidationError" && apiError.ErrorMessage() == "No updates are to be performed." { 81 return a.FillOutputs(ctx) 82 } 83 // TODO: handle UPDATE_COMPLETE_CLEANUP_IN_PROGRESS 84 return err // might call createStackAndWait depending on the error 85 } 86 87 fmt.Println("Waiting for stack", a.stackName, "to be updated...") // TODO: verbose only 88 o, err := cloudformation.NewStackUpdateCompleteWaiter(cfn, update1s).WaitForOutput(ctx, &cloudformation.DescribeStacksInput{ 89 StackName: uso.StackId, 90 }, stackTimeout) 91 if err != nil { 92 return err 93 } 94 return a.fillWithOutputs(o) 95 } 96 97 // create1s is a functional option for cloudformation.StackCreateCompleteWaiter that sets the MinDelay to 1 98 func create1s(o *cloudformation.StackCreateCompleteWaiterOptions) { 99 o.MinDelay = 1 100 } 101 102 func (a *AwsEcs) createStackAndWait(ctx context.Context, templateBody string) error { 103 cfn, err := a.newClient(ctx) 104 if err != nil { 105 return err 106 } 107 108 _, err = cfn.CreateStack(ctx, &cloudformation.CreateStackInput{ 109 StackName: ptr.String(a.stackName), 110 TemplateBody: ptr.String(templateBody), 111 Capabilities: []cfnTypes.Capability{cfnTypes.CapabilityCapabilityNamedIam}, 112 OnFailure: cfnTypes.OnFailureDelete, 113 }) 114 if err != nil { 115 // Ignore AlreadyExistsException; return all other errors 116 var alreadyExists *cfnTypes.AlreadyExistsException 117 if !errors.As(err, &alreadyExists) { 118 return err 119 } 120 } 121 122 fmt.Println("Waiting for stack", a.stackName, "to be created...") // TODO: verbose only 123 dso, err := cloudformation.NewStackCreateCompleteWaiter(cfn, create1s).WaitForOutput(ctx, &cloudformation.DescribeStacksInput{ 124 StackName: ptr.String(a.stackName), 125 }, stackTimeout) 126 if err != nil { 127 return err 128 } 129 return a.fillWithOutputs(dso) 130 } 131 132 func (a *AwsEcs) SetUp(ctx context.Context, containers []types.Container) error { 133 template, err := createTemplate(a.stackName, containers, TemplateOverrides{VpcID: a.VpcID}, a.Spot).YAML() 134 if err != nil { 135 return err 136 } 137 138 // Upsert 139 if err := a.updateStackAndWait(ctx, string(template)); err != nil { 140 // Check if the stack doesn't exist; if so, create it, otherwise return the error 141 var apiError smithy.APIError 142 if ok := errors.As(err, &apiError); !ok || (apiError.ErrorCode() != "ValidationError") || !strings.HasSuffix(apiError.ErrorMessage(), "does not exist") { 143 return err 144 } 145 return a.createStackAndWait(ctx, string(template)) 146 } 147 return nil 148 } 149 150 func (a *AwsEcs) FillOutputs(ctx context.Context) error { 151 // println("Filling outputs for stack", stackId) 152 cfn, err := a.newClient(ctx) 153 if err != nil { 154 return err 155 } 156 157 // FIXME: this always returns the latest outputs, not the ones from the recent update 158 dso, err := cfn.DescribeStacks(ctx, &cloudformation.DescribeStacksInput{ 159 StackName: &a.stackName, 160 }) 161 if err != nil { 162 return err 163 } 164 return a.fillWithOutputs(dso) 165 } 166 167 func (a *AwsEcs) fillWithOutputs(dso *cloudformation.DescribeStacksOutput) error { 168 for _, stack := range dso.Stacks { 169 for _, output := range stack.Outputs { 170 switch *output.OutputKey { 171 case outputs.SubnetID: 172 if a.SubNetID == "" { 173 a.SubNetID = *output.OutputValue 174 } 175 case outputs.TaskDefArn: 176 if a.TaskDefARN == "" { 177 a.TaskDefARN = *output.OutputValue 178 } 179 case outputs.ClusterName: 180 a.ClusterName = *output.OutputValue 181 case outputs.LogGroupARN: 182 a.LogGroupARN = *output.OutputValue 183 case outputs.SecurityGroupID: 184 a.SecurityGroupID = *output.OutputValue 185 case outputs.BucketName: 186 a.BucketName = *output.OutputValue 187 // default:; TODO: should do this but only for stack the driver created 188 // return fmt.Errorf("unknown output key %q", *output.OutputKey) 189 } 190 } 191 } 192 193 return nil 194 } 195 196 func (a *AwsEcs) Run(ctx context.Context, env map[string]string, cmd ...string) (ecs.TaskArn, error) { 197 if err := a.FillOutputs(ctx); err != nil { 198 return nil, err 199 } 200 201 return a.AwsEcs.Run(ctx, env, cmd...) 202 } 203 204 func (a *AwsEcs) Tail(ctx context.Context, taskArn ecs.TaskArn) error { 205 if err := a.FillOutputs(ctx); err != nil { 206 return err 207 } 208 return a.AwsEcs.Tail(ctx, taskArn) 209 } 210 211 func (a *AwsEcs) Stop(ctx context.Context, taskArn ecs.TaskArn) error { 212 if err := a.FillOutputs(ctx); err != nil { 213 return err 214 } 215 return a.AwsEcs.Stop(ctx, taskArn) 216 } 217 218 func (a *AwsEcs) GetInfo(ctx context.Context, taskArn ecs.TaskArn) (*types.TaskInfo, error) { 219 if err := a.FillOutputs(ctx); err != nil { 220 return nil, err 221 } 222 return a.AwsEcs.Info(ctx, taskArn) 223 } 224 225 func (a *AwsEcs) TearDown(ctx context.Context) error { 226 cfn, err := a.newClient(ctx) 227 if err != nil { 228 return err 229 } 230 231 _, err = cfn.DeleteStack(ctx, &cloudformation.DeleteStackInput{ 232 StackName: ptr.String(a.stackName), 233 // RetainResources: []string{"Bucket"}, only when the stack is in the DELETE_FAILED state 234 }) 235 if err != nil { 236 return err 237 } 238 239 fmt.Println("Waiting for stack", a.stackName, "to be deleted...") // TODO: verbose only 240 return cloudformation.NewStackDeleteCompleteWaiter(cfn, delete1s).Wait(ctx, &cloudformation.DescribeStacksInput{ 241 StackName: ptr.String(a.stackName), 242 }, stackTimeout) 243 } 244 245 // delete1s is a functional option for cloudformation.StackDeleteCompleteWaiter that sets the MinDelay to 1 246 func delete1s(o *cloudformation.StackDeleteCompleteWaiterOptions) { 247 o.MinDelay = 1 248 }