@@ -3,6 +3,7 @@ package aws
33import (
44 "context"
55 b64 "encoding/base64"
6+ "errors"
67 "fmt"
78 "strings"
89 "sync"
@@ -22,11 +23,25 @@ import (
2223 "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
2324)
2425
26+ var (
27+ ErrInstanceTypeNotFound = errors .New ("instance type not found" )
28+ ErrAMINotFound = errors .New ("AMI not found" )
29+ ErrSubnetsNotSet = errors .New ("aws-subnets must be set" )
30+ ErrArchMismatch = errors .New ("instance type architecture not supported by AMI" )
31+ ErrTypeNotInRegion = errors .New ("instance type not offered in region" )
32+ ErrNoDeployCandidates = errors .New ("no deploy candidates resolved" )
33+ )
34+
35+ // deployCandidate is one resolved instance-type/region pair the provider will
36+ // try, in order, when deploying an agent. An empty region lets AWS decide.
37+ type deployCandidate struct {
38+ instanceType ec2_types.InstanceTypeInfo
39+ region string
40+ }
41+
2542type provider struct {
2643 name string
2744 config * config.Config
28- instanceType string
29- amiID string
3045 tags []string
3146 region string
3247 subnets []string
@@ -37,17 +52,18 @@ type provider struct {
3752 lock sync.Mutex
3853 subnetRR int
3954 sshKeyName string
55+ // resolved config
56+ deployCandidates []deployCandidate
57+ image ec2_types.Image
4058}
4159
4260func New (ctx context.Context , c * cli.Command , config * config.Config ) (types.Provider , error ) {
4361 if len (c .StringSlice ("aws-subnets" )) == 0 {
44- return nil , fmt . Errorf ( "aws-subnets must be set" )
62+ return nil , ErrSubnetsNotSet
4563 }
4664 p := & provider {
4765 name : "aws" ,
4866 config : config ,
49- instanceType : c .String ("aws-instance-type" ),
50- amiID : c .String ("aws-ami-id" ),
5167 tags : c .StringSlice ("aws-tags" ),
5268 region : c .String ("aws-region" ),
5369 subnets : c .StringSlice ("aws-subnets" ),
@@ -62,9 +78,38 @@ func New(ctx context.Context, c *cli.Command, config *config.Config) (types.Prov
6278 }
6379 p .client = ec2 .NewFromConfig (cfg )
6480
81+ // AMI must be resolved first: its architecture constrains which instance
82+ // types are valid deploy candidates.
83+ if err := p .resolveImage (ctx , c .String ("aws-ami-id" )); err != nil {
84+ return nil , err
85+ }
86+ if err := p .resolveDeployCandidates (ctx , c .StringSlice ("aws-instance-type" )); err != nil {
87+ return nil , err
88+ }
89+
90+ p .printResolvedConfig ()
91+
6592 return p , nil
6693}
6794
95+ func (p * provider ) printResolvedConfig () {
96+ log .Info ().
97+ Str ("ami" , aws .ToString (p .image .ImageId )).
98+ Str ("ami_arch" , string (p .image .Architecture )).
99+ Msg ("resolved AMI" )
100+ for _ , c := range p .deployCandidates {
101+ region := c .region
102+ if region == "" {
103+ region = "<aws-decides>"
104+ }
105+ log .Info ().
106+ Str ("type" , string (c .instanceType .InstanceType )).
107+ Str ("region" , region ).
108+ Bool ("current_gen" , aws .ToBool (c .instanceType .CurrentGeneration )).
109+ Msg ("deploy candidate" )
110+ }
111+ }
112+
68113func (p * provider ) DeployAgent (ctx context.Context , agent * woodpecker.Agent ) error {
69114 userData , err := cloudinit .RenderUserDataTemplate (p .config , agent , cloudinit.RenderOption {})
70115 if err != nil {
@@ -103,8 +148,7 @@ func (p *provider) DeployAgent(ctx context.Context, agent *woodpecker.Agent) err
103148 IamInstanceProfile : & ec2_types.IamInstanceProfileSpecification {
104149 Arn : aws .String (p .iamInstanceProfileArn ),
105150 },
106- ImageId : aws .String (p .amiID ),
107- InstanceType : ec2_types .InstanceType (p .instanceType ),
151+ ImageId : p .image .ImageId ,
108152 MetadataOptions : & ec2_types.InstanceMetadataOptionsRequest {
109153 HttpEndpoint : ec2_types .InstanceMetadataEndpointStateEnabled ,
110154 HttpPutResponseHopLimit : aws .Int32 (1 ),
@@ -142,8 +186,43 @@ func (p *provider) DeployAgent(ctx context.Context, agent *woodpecker.Agent) err
142186 }
143187
144188 runInstancesInput .UserData = aws .String (b64 .StdEncoding .EncodeToString ([]byte (userData )))
145- result , err := p .client .RunInstances (ctx , & runInstancesInput )
146- if err != nil {
189+
190+ var result * ec2.RunInstancesOutput
191+ for i , c := range p .deployCandidates {
192+ runInstancesInput .InstanceType = c .instanceType .InstanceType
193+
194+ // An empty region keeps the client's default region.
195+ var optFns []func (* ec2.Options )
196+ if c .region != "" {
197+ region := c .region
198+ optFns = append (optFns , func (o * ec2.Options ) { o .Region = region })
199+ }
200+
201+ log .Info ().
202+ Str ("type" , string (c .instanceType .InstanceType )).
203+ Str ("region" , c .region ).
204+ Msg ("create agent" )
205+
206+ result , err = p .client .RunInstances (ctx , & runInstancesInput , optFns ... )
207+ if err == nil {
208+ break
209+ }
210+
211+ // Continue to next fallback entry only if capacity is unavailable.
212+ if ! isInsufficientCapacity (err ) {
213+ return fmt .Errorf ("%s: RunInstances: %w" , p .name , err )
214+ }
215+
216+ // Only log and continue if there are more candidates left.
217+ if i < len (p .deployCandidates )- 1 {
218+ log .Warn ().Msgf (
219+ "create agent failed: type = %s region = %s: %s" ,
220+ c .instanceType .InstanceType , c .region , err ,
221+ )
222+ continue
223+ }
224+
225+ // Last candidate failed.
147226 return fmt .Errorf ("%s: RunInstances: %w" , p .name , err )
148227 }
149228
@@ -169,27 +248,6 @@ func (p *provider) DeployAgent(ctx context.Context, agent *woodpecker.Agent) err
169248 return fmt .Errorf ("instance did not resolve in agent list: %s" , * result .Instances [0 ].InstanceId )
170249}
171250
172- func (p * provider ) getAgent (ctx context.Context , agent * woodpecker.Agent ) (* ec2_types.Instance , error ) {
173- instances , err := p .client .DescribeInstances (ctx , & ec2.DescribeInstancesInput {
174- Filters : []ec2_types.Filter {
175- {
176- Name : aws .String ("tag:Name" ),
177- Values : []string {agent .Name },
178- },
179- },
180- })
181- if err != nil {
182- return nil , err
183- }
184- if len (instances .Reservations ) != 1 {
185- return nil , fmt .Errorf ("expected 1 reservation with tag:Name=%s, got %d" , agent .Name , len (instances .Reservations ))
186- }
187- if len (instances .Reservations [0 ].Instances ) != 1 {
188- return nil , fmt .Errorf ("expected 1 instance with tag:Name=%s, got %d" , agent .Name , len (instances .Reservations [0 ].Instances ))
189- }
190- return & instances .Reservations [0 ].Instances [0 ], nil
191- }
192-
193251func (p * provider ) RemoveAgent (ctx context.Context , agent * woodpecker.Agent ) error {
194252 instance , err := p .getAgent (ctx , agent )
195253 if err != nil {
0 commit comments