Skip to content

Commit 1c64e02

Browse files
committed
Use ephemeral S3 buckets for E2E tests
Use S3 buckets created during the lifecycle of a test instead of a static one and provide the capability to make them read-only public. Signed-off-by: Arnaud Meukam <[email protected]>
1 parent a860afb commit 1c64e02

File tree

3 files changed

+189
-3
lines changed

3 files changed

+189
-3
lines changed

tests/e2e/kubetest2-kops/aws/s3.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package aws
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
"regexp"
24+
"strings"
25+
"time"
26+
27+
"github.com/aws/aws-sdk-go-v2/aws"
28+
awsconfig "github.com/aws/aws-sdk-go-v2/config"
29+
"github.com/aws/aws-sdk-go-v2/service/s3"
30+
"github.com/aws/aws-sdk-go-v2/service/s3/types"
31+
"github.com/aws/aws-sdk-go-v2/service/sts"
32+
"k8s.io/klog/v2"
33+
)
34+
35+
// It contains S3Client, an Amazon S3 service client that is used to perform bucket
36+
// and object actions.
37+
type awsClient struct {
38+
S3Client *s3.Client
39+
}
40+
41+
func NewAWSClient(ctx context.Context) (*awsClient, error) {
42+
cfg, err := awsconfig.LoadDefaultConfig(ctx)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to load AWS config: %w", err)
45+
}
46+
return &awsClient{
47+
S3Client: s3.NewFromConfig(cfg),
48+
}, nil
49+
}
50+
51+
// AWSBucketName constructs an unique bucket name using the AWS account ID on region us-east-2
52+
func AWSBucketName(ctx context.Context) (string, error) {
53+
config, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(string(types.BucketLocationConstraintUsEast2)))
54+
if err != nil {
55+
return "", fmt.Errorf("failed to load AWS config: %w", err)
56+
}
57+
58+
stsSvc := sts.NewFromConfig(config)
59+
callerIdentity, err := stsSvc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
60+
if err != nil {
61+
return "", fmt.Errorf("building AWS STS presigned request: %w", err)
62+
}
63+
64+
// Add timestamp suffix
65+
timestamp := time.Now().Format("01022006")
66+
bucket := fmt.Sprintf("k8s-infra-kops-%s", *callerIdentity.Account)
67+
bucket = fmt.Sprintf("%s-%s", bucket, timestamp)
68+
69+
bucket = strings.ToLower(bucket)
70+
bucket = regexp.MustCompile("[^a-z0-9-]").ReplaceAllString(bucket, "") // Only allow lowercase letters, numbers, and hyphens
71+
72+
if len(bucket) > 63 {
73+
bucket = bucket[:63] // Max length is 63
74+
}
75+
76+
return bucket, nil
77+
}
78+
79+
func (client awsClient) EnsureS3Bucket(ctx context.Context, bucketName string, publicRead bool) error {
80+
_, err := client.S3Client.CreateBucket(ctx, &s3.CreateBucketInput{
81+
Bucket: aws.String(bucketName),
82+
CreateBucketConfiguration: &types.CreateBucketConfiguration{
83+
LocationConstraint: types.BucketLocationConstraintUsEast2,
84+
},
85+
})
86+
87+
var exists *types.BucketAlreadyExists
88+
if err != nil {
89+
if errors.As(err, &exists) {
90+
klog.Infof("Bucket %s already exists.\n", bucketName)
91+
err = exists
92+
}
93+
} else {
94+
err := s3.NewBucketExistsWaiter(client.S3Client).Wait(
95+
ctx, &s3.HeadBucketInput{
96+
Bucket: aws.String(bucketName),
97+
},
98+
time.Minute)
99+
if err != nil {
100+
klog.Infof("Failed attempt to wait for bucket %s to exist.", bucketName)
101+
}
102+
}
103+
104+
klog.Infof("Bucket %s created successfully", bucketName)
105+
106+
if err == nil && publicRead {
107+
err = client.setPublicReadPolicy(ctx, bucketName)
108+
if err != nil {
109+
klog.Errorf("Failed to set public read policy on bucket %s: %v", bucketName, err)
110+
return err
111+
}
112+
klog.Infof("Public read policy set on bucket %s", bucketName)
113+
}
114+
115+
return err
116+
}
117+
118+
func (client awsClient) DeleteS3Bucket(ctx context.Context, bucketName string) error {
119+
_, err := client.S3Client.DeleteBucket(ctx, &s3.DeleteBucketInput{
120+
Bucket: aws.String(bucketName),
121+
})
122+
if err != nil {
123+
var noBucket *types.NoSuchBucket
124+
if errors.As(err, &noBucket) {
125+
klog.Infof("Bucket %s does not exits", bucketName)
126+
err = noBucket
127+
} else {
128+
klog.Infof("Couldn't delete bucket %s. Reason: %v", bucketName, err)
129+
}
130+
} else {
131+
err = s3.NewBucketNotExistsWaiter(client.S3Client).Wait(
132+
ctx, &s3.HeadBucketInput{
133+
Bucket: aws.String(bucketName),
134+
},
135+
time.Minute)
136+
if err != nil {
137+
klog.Infof("Failed attempt to wait for bucket %s to be deleted", bucketName)
138+
} else {
139+
klog.Infof("Bucket %s deleted", bucketName)
140+
}
141+
}
142+
return err
143+
}
144+
145+
func (client awsClient) setPublicReadPolicy(ctx context.Context, bucketName string) error {
146+
policy := fmt.Sprintf(`{
147+
"Version": "2012-10-17",
148+
"Statement": [
149+
{
150+
"Sid": "PublicReadGetObject",
151+
"Effect": "Allow",
152+
"Principal": "*",
153+
"Action": "s3:GetObject",
154+
"Resource": "arn:aws:s3:::%s/*"
155+
}
156+
]
157+
}`, bucketName)
158+
159+
_, err := client.S3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
160+
Bucket: aws.String(bucketName),
161+
Policy: aws.String(policy),
162+
})
163+
if err != nil {
164+
return fmt.Errorf("failed to put bucket policy for %s: %w", bucketName, err)
165+
}
166+
167+
return err
168+
}

tests/e2e/kubetest2-kops/deployer/common.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ limitations under the License.
1717
package deployer
1818

1919
import (
20+
"context"
2021
"errors"
2122
"fmt"
2223
"os"
2324
"path/filepath"
2425
"strings"
2526

2627
"k8s.io/klog/v2"
28+
"k8s.io/kops/tests/e2e/kubetest2-kops/aws"
2729
"k8s.io/kops/tests/e2e/kubetest2-kops/gce"
2830
"k8s.io/kops/tests/e2e/pkg/target"
2931
"k8s.io/kops/tests/e2e/pkg/util"
@@ -320,7 +322,20 @@ func (d *deployer) stateStore() string {
320322
if ss == "" {
321323
switch d.CloudProvider {
322324
case "aws":
323-
ss = "s3://k8s-kops-prow"
325+
ctx := context.TODO()
326+
bucketName, err := aws.AWSBucketName(ctx)
327+
if err != nil {
328+
klog.Fatalf("Failed to generate bucket name: %v", err)
329+
}
330+
awsClient, err := aws.NewAWSClient(ctx)
331+
if err != nil {
332+
klog.Fatalf("failed to load client config: %v", err)
333+
}
334+
if err := awsClient.EnsureS3Bucket(ctx, bucketName, d.PublicReadOnlyBucket); err != nil {
335+
klog.Fatalf("Failed to ensure S3 bucket exists: %v", err)
336+
return ""
337+
}
338+
ss = "s3://" + bucketName
324339
case "gce":
325340
d.createBucket = true
326341
ss = "gs://" + gce.GCSBucketName(d.GCPProject, "state")

tests/e2e/kubetest2-kops/deployer/deployer.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type deployer struct {
5858
KopsBinaryPath string `flag:"kops-binary-path" desc:"The path to kops executable used for testing"`
5959
KubernetesFeatureGates string `flag:"kubernetes-feature-gates" desc:"Feature Gates to enable on Kubernetes components"`
6060
createBucket bool `flag:"-"`
61+
PublicReadOnlyBucket bool `flag:"-"`
6162

6263
// ControlPlaneCount specifies the number of VMs in the control-plane.
6364
ControlPlaneCount int `flag:"control-plane-count" desc:"Number of control-plane instances"`
@@ -106,8 +107,10 @@ type deployer struct {
106107
var _ types.NewDeployer = New
107108

108109
// assert that deployer implements types.Deployer
109-
var _ types.Deployer = &deployer{}
110-
var _ types.DeployerWithPostTester = &deployer{}
110+
var (
111+
_ types.Deployer = &deployer{}
112+
_ types.DeployerWithPostTester = &deployer{}
113+
)
111114

112115
func (d *deployer) Provider() string {
113116
return Name

0 commit comments

Comments
 (0)