Skip to content

Commit f46050b

Browse files
Merge pull request #8889 from ironcladlou/aws-auth-refactor
NO-JIRA: Fall back to AWS SDK default credential chain when no explicit credentials provided
2 parents d174956 + 5951a54 commit f46050b

10 files changed

Lines changed: 386 additions & 40 deletions

File tree

cmd/cluster/aws/destroy.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,18 @@ func DestroyCluster(ctx context.Context, o *core.DestroyOptions) error {
157157
// ValidateCredentialInfo validates if the credentials secret name is empty, the aws-creds or sts-creds mutually exclusive and are not empty; validates if
158158
// the credentials secret is not empty, that it can be retrieved.
159159
func ValidateCredentialInfo(opts awsutil.AWSCredentialsOptions, credentialSecretName, namespace, kubeconfigPath string) error {
160+
return validateCredentialInfo(opts, credentialSecretName, namespace, kubeconfigPath, opts.Validate)
161+
}
162+
163+
// ValidateProductCredentialInfo is like ValidateCredentialInfo but requires explicit --sts-creds and --role-arn
164+
// flags rather than allowing SDK default chain fallback.
165+
func ValidateProductCredentialInfo(opts awsutil.AWSCredentialsOptions, credentialSecretName, namespace, kubeconfigPath string) error {
166+
return validateCredentialInfo(opts, credentialSecretName, namespace, kubeconfigPath, opts.ValidateProduct)
167+
}
168+
169+
func validateCredentialInfo(opts awsutil.AWSCredentialsOptions, credentialSecretName, namespace, kubeconfigPath string, validate func() error) error {
160170
if len(credentialSecretName) == 0 {
161-
if err := opts.Validate(); err != nil {
171+
if err := validate(); err != nil {
162172
return err
163173
}
164174
return nil

cmd/cluster/aws/destroy_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func Test_ValidateCredentialInfo(t *testing.T) {
1414
inputOptions *core.DestroyOptions
1515
expectError bool
1616
}{
17-
"when CredentialSecretName is blank and aws-creds is also blank": {
17+
"when CredentialSecretName is blank and aws-creds is also blank it should fall back to SDK default chain": {
1818
inputOptions: &core.DestroyOptions{
1919
CredentialSecretName: "",
2020
AWSPlatform: core.AWSPlatformDestroyOptions{
@@ -23,7 +23,7 @@ func Test_ValidateCredentialInfo(t *testing.T) {
2323
},
2424
},
2525
},
26-
expectError: true,
26+
expectError: false,
2727
},
2828
"when CredentialSecretName is blank and aws-creds is not blank": {
2929
inputOptions: &core.DestroyOptions{

cmd/infra/aws/create_cli_role.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ func NewCreateCLIRoleCommand() *cobra.Command {
175175
cmd.Flags().StringVar(&opts.RoleName, "name", opts.RoleName, "Role name")
176176
cmd.Flags().StringToStringVarP(&opts.AdditionalTags, "additional-tags", "t", opts.AdditionalTags, "Additional tags to apply to the role created (e.g. 'key1=value1,key2=value2')")
177177

178-
_ = cmd.MarkFlagRequired("aws-creds")
179-
180178
logger := log.Log
181179
cmd.RunE = func(cmd *cobra.Command, args []string) error {
182180
if err := opts.Run(cmd.Context(), logger); err != nil {

cmd/infra/aws/create_operator_roles.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,12 @@ func (o *CreateOperatorRolesOptions) Run(ctx context.Context, logger logr.Logger
126126
return err
127127
}
128128

129-
var awsSession *aws.Config
130-
if o.AWSCredentialsOpts.AWSCredentialsFile != "" || o.AWSCredentialsOpts.STSCredentialsFile != "" {
131-
if err := o.AWSCredentialsOpts.Validate(); err != nil {
132-
return err
133-
}
134-
var err error
135-
awsSession, err = o.AWSCredentialsOpts.GetSession(ctx, "cli-create-operator-roles", nil, o.Region)
136-
if err != nil {
137-
return err
138-
}
139-
} else {
140-
awsSession = awsutil.NewSession(ctx, "cli-create-operator-roles", "", "", "", o.Region)
129+
if err := o.AWSCredentialsOpts.Validate(); err != nil {
130+
return err
131+
}
132+
awsSession, err := o.AWSCredentialsOpts.GetSession(ctx, "cli-create-operator-roles", nil, o.Region)
133+
if err != nil {
134+
return err
141135
}
142136
awsConfig := awsutil.NewConfig()
143137
iamClient := iam.NewFromConfig(*awsSession, func(o *iam.Options) {

cmd/infra/aws/destroy.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,17 @@ func (o *DelegatedAWSCredentialOptions) Validate() error {
101101
}
102102
}
103103

104-
// ensure that only one type of credential has been passed
105-
globalCredentialsPresent := o.AWSCredentialsOpts.AWSCredentialsFile != "" || o.AWSCredentialsOpts.STSCredentialsFile != ""
106-
if globalCredentialsPresent {
107-
if !anyComponentCredentialsPresent {
108-
return o.AWSCredentialsOpts.Validate()
109-
} else {
104+
if anyComponentCredentialsPresent {
105+
if o.AWSCredentialsOpts.AWSCredentialsFile != "" || o.AWSCredentialsOpts.STSCredentialsFile != "" || o.AWSCredentialsOpts.RoleArn != "" {
110106
return fmt.Errorf("cannot set any --aws-creds.component flags at the same time as other credentials")
111107
}
112-
} else {
113108
if !allComponentCredentialsPresent {
114-
return fmt.Errorf("either --aws-creds, --sts-creds, or all --aws-creds.component flags must be set")
109+
return fmt.Errorf("all --aws-creds.component flags must be set when using per-component credentials")
115110
}
111+
return nil
116112
}
117-
return nil
113+
114+
return o.AWSCredentialsOpts.Validate()
118115
}
119116

120117
func NewDestroyCommand() *cobra.Command {
@@ -199,7 +196,13 @@ func (o *DestroyInfraOptions) DestroyInfra(ctx context.Context) error {
199196
var clusterRoute53Client, vpcOwnerRoute53Client, listRoute53Client, recordsRoute53Client awsapi.ROUTE53API
200197
var s3Client awsapi.S3API
201198
var ramClient *ram.Client
202-
if o.AWSCredentialsOpts.AWSCredentialsOpts.AWSCredentialsFile != "" || o.AWSCredentialsOpts.AWSCredentialsOpts.STSCredentialsFile != "" {
199+
useDelegatedCredentials := o.AWSEbsCsiDriverControllerCredentialsFile != "" ||
200+
o.CloudControllerCredentialsFile != "" ||
201+
o.CloudNetworkConfigControllerCredentialsFile != "" ||
202+
o.ControlPlaneOperatorCredentialsFile != "" ||
203+
o.NodePoolCredentialsFile != "" ||
204+
o.OpenshiftImageRegistryCredentialsFile != ""
205+
if !useDelegatedCredentials {
203206
awsSession, err := o.AWSCredentialsOpts.AWSCredentialsOpts.GetSession(ctx, "cli-destroy-infra", o.CredentialsSecretData, o.Region)
204207
if err != nil {
205208
return err
@@ -265,6 +268,7 @@ func (o *DestroyInfraOptions) DestroyInfra(ctx context.Context) error {
265268
return fmt.Errorf("failed to create delegating client: %w", err)
266269
}
267270
ec2Client = delegatingClent.EC2Client
271+
vpcOwnerEC2Client = ec2Client
268272
elbClient = delegatingClent.ELBClient
269273
elbv2Client = delegatingClent.ELBV2Client
270274
listRoute53Client = delegatingClent.ROUTE53Client

cmd/infra/aws/destroy_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"testing"
99

10+
awsutil "github.com/openshift/hypershift/cmd/infra/aws/util"
1011
"github.com/openshift/hypershift/support/awsapi"
1112

1213
"github.com/aws/aws-sdk-go-v2/aws"
@@ -21,6 +22,107 @@ import (
2122
"go.uber.org/mock/gomock"
2223
)
2324

25+
func TestDelegatedAWSCredentialOptionsValidate(t *testing.T) {
26+
allComponentCreds := DelegatedAWSCredentialOptions{
27+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{},
28+
AWSEbsCsiDriverControllerCredentialsFile: "/tmp/ebs.ini",
29+
CloudControllerCredentialsFile: "/tmp/cloud.ini",
30+
CloudNetworkConfigControllerCredentialsFile: "/tmp/net.ini",
31+
ControlPlaneOperatorCredentialsFile: "/tmp/cpo.ini",
32+
NodePoolCredentialsFile: "/tmp/np.ini",
33+
OpenshiftImageRegistryCredentialsFile: "/tmp/registry.ini",
34+
}
35+
36+
testCases := []struct {
37+
name string
38+
opts DelegatedAWSCredentialOptions
39+
expectError bool
40+
errorContains string
41+
}{
42+
{
43+
name: "When all component credentials provided, it should succeed",
44+
opts: allComponentCreds,
45+
expectError: false,
46+
},
47+
{
48+
name: "When component credentials mixed with aws-creds, it should fail",
49+
opts: DelegatedAWSCredentialOptions{
50+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{
51+
AWSCredentialsFile: "/tmp/global.ini",
52+
},
53+
AWSEbsCsiDriverControllerCredentialsFile: "/tmp/ebs.ini",
54+
CloudControllerCredentialsFile: "/tmp/cloud.ini",
55+
CloudNetworkConfigControllerCredentialsFile: "/tmp/net.ini",
56+
ControlPlaneOperatorCredentialsFile: "/tmp/cpo.ini",
57+
NodePoolCredentialsFile: "/tmp/np.ini",
58+
OpenshiftImageRegistryCredentialsFile: "/tmp/registry.ini",
59+
},
60+
expectError: true,
61+
errorContains: "cannot set any --aws-creds.component flags at the same time as other credentials",
62+
},
63+
{
64+
name: "When component credentials mixed with role-arn, it should fail",
65+
opts: DelegatedAWSCredentialOptions{
66+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{
67+
RoleArn: "arn:aws:iam::123456789012:role/test-role",
68+
},
69+
AWSEbsCsiDriverControllerCredentialsFile: "/tmp/ebs.ini",
70+
CloudControllerCredentialsFile: "/tmp/cloud.ini",
71+
CloudNetworkConfigControllerCredentialsFile: "/tmp/net.ini",
72+
ControlPlaneOperatorCredentialsFile: "/tmp/cpo.ini",
73+
NodePoolCredentialsFile: "/tmp/np.ini",
74+
OpenshiftImageRegistryCredentialsFile: "/tmp/registry.ini",
75+
},
76+
expectError: true,
77+
errorContains: "cannot set any --aws-creds.component flags at the same time as other credentials",
78+
},
79+
{
80+
name: "When partial component credentials provided, it should fail",
81+
opts: DelegatedAWSCredentialOptions{
82+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{},
83+
AWSEbsCsiDriverControllerCredentialsFile: "/tmp/ebs.ini",
84+
CloudControllerCredentialsFile: "/tmp/cloud.ini",
85+
CloudNetworkConfigControllerCredentialsFile: "/tmp/net.ini",
86+
},
87+
expectError: true,
88+
errorContains: "all --aws-creds.component flags must be set when using per-component credentials",
89+
},
90+
{
91+
name: "When no component credentials provided, it should fall back to AWSCredentialsOpts.Validate",
92+
opts: DelegatedAWSCredentialOptions{
93+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{},
94+
},
95+
expectError: false,
96+
},
97+
{
98+
name: "When no component credentials and invalid AWSCredentialsOpts, it should propagate validation error",
99+
opts: DelegatedAWSCredentialOptions{
100+
AWSCredentialsOpts: &awsutil.AWSCredentialsOptions{
101+
STSCredentialsFile: "/tmp/creds.json",
102+
},
103+
},
104+
expectError: true,
105+
errorContains: "'role-arn' is required when 'sts-creds' is provided",
106+
},
107+
}
108+
109+
for _, tc := range testCases {
110+
t.Run(tc.name, func(t *testing.T) {
111+
err := tc.opts.Validate()
112+
if tc.expectError {
113+
if err == nil {
114+
t.Fatal("Expected error but got nil")
115+
}
116+
if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) {
117+
t.Errorf("Expected error containing %q, got %q", tc.errorContains, err.Error())
118+
}
119+
} else if err != nil {
120+
t.Errorf("Unexpected error: %v", err)
121+
}
122+
})
123+
}
124+
}
125+
24126
func TestEmptyBucket(t *testing.T) {
25127
tests := []struct {
26128
name string

cmd/infra/aws/util/util.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package util
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"net"
87
"os"
98
"time"
109

1110
"github.com/openshift/hypershift/cmd/util"
11+
supportawsutil "github.com/openshift/hypershift/support/awsutil"
1212

1313
"github.com/aws/aws-sdk-go-v2/aws"
1414
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
@@ -20,6 +20,8 @@ import (
2020
"github.com/spf13/pflag"
2121
)
2222

23+
var assumeRoleFn = supportawsutil.AssumeRole
24+
2325
type AWSCredentialsOptions struct {
2426
AWSCredentialsFile string
2527

@@ -32,24 +34,31 @@ func (opts *AWSCredentialsOptions) Validate() error {
3234
if opts.STSCredentialsFile != "" || opts.RoleArn != "" {
3335
return fmt.Errorf("only one of 'aws-creds' or 'role-arn' and 'sts-creds' can be provided")
3436
}
35-
3637
return nil
3738
}
3839

39-
if err := util.ValidateRequiredOption("sts-creds", opts.STSCredentialsFile); err != nil {
40-
return err
40+
if opts.STSCredentialsFile != "" && opts.RoleArn == "" {
41+
return fmt.Errorf("'role-arn' is required when 'sts-creds' is provided")
4142
}
42-
if err := util.ValidateRequiredOption("role-arn", opts.RoleArn); err != nil {
43+
44+
return nil
45+
}
46+
47+
func (opts *AWSCredentialsOptions) ValidateProduct() error {
48+
if err := opts.Validate(); err != nil {
4349
return err
4450
}
51+
if opts.STSCredentialsFile == "" || opts.RoleArn == "" {
52+
return fmt.Errorf("'sts-creds' and 'role-arn' are required")
53+
}
4554
return nil
4655
}
4756

4857
func (opts *AWSCredentialsOptions) BindFlags(flags *pflag.FlagSet) {
4958
opts.BindProductFlags(flags)
5059

5160
flags.StringVar(&opts.AWSCredentialsFile, "aws-creds", opts.AWSCredentialsFile, "Path to an AWS credentials file")
52-
_ = flags.MarkDeprecated("aws-creds", "please use '--sts-creds' with '--role-arn' instead")
61+
_ = flags.MarkDeprecated("aws-creds", "the AWS SDK default credential chain is used when no explicit credentials are provided; use '--role-arn' to assume a specific role")
5362
}
5463

5564
func (opts *AWSCredentialsOptions) BindVPCOwnerFlags(flags *pflag.FlagSet) {
@@ -86,7 +95,30 @@ func (opts *AWSCredentialsOptions) GetSession(ctx context.Context, agent string,
8695
return NewSTSSession(ctx, agent, opts.RoleArn, region, &v2Creds)
8796
}
8897

89-
return nil, errors.New("could not create AWS session, no credentials were given")
98+
// Fall back to the SDK default credential chain (env vars, AWS_PROFILE, instance profile, etc.)
99+
cfg := NewSession(ctx, agent, "", "", "", region)
100+
if opts.RoleArn != "" {
101+
assumedCreds, err := assumeRoleFn(ctx, *cfg, agent, opts.RoleArn)
102+
if err != nil {
103+
return nil, err
104+
}
105+
var loadOptions []func(*config.LoadOptions) error
106+
loadOptions = append(loadOptions, config.WithCredentialsProvider(
107+
credentials.StaticCredentialsProvider{Value: *assumedCreds},
108+
))
109+
if region != "" {
110+
loadOptions = append(loadOptions, config.WithRegion(region))
111+
}
112+
loadOptions = append(loadOptions, config.WithAPIOptions([]func(*middleware.Stack) error{
113+
awsmiddleware.AddUserAgentKeyValue("openshift.io hypershift", agent),
114+
}))
115+
finalCfg, err := config.LoadDefaultConfig(ctx, loadOptions...)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to load config after assuming role: %w", err)
118+
}
119+
return &finalCfg, nil
120+
}
121+
return cfg, nil
90122
}
91123

92124
func NewSession(ctx context.Context, agent, credentialsFile, credKey, credSecretKey, region string) *aws.Config {

0 commit comments

Comments
 (0)