Skip to content

Commit

Permalink
kubetest2-kops: automatically create buckets
Browse files Browse the repository at this point in the history
When working with boskos, this means we don't need to have a shared
bucket with dynamic permissions.
  • Loading branch information
justinsb committed Nov 11, 2024
1 parent 0d70757 commit f7612c8
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 27 deletions.
1 change: 1 addition & 0 deletions tests/e2e/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ replace k8s.io/kops => ../../.
replace k8s.io/client-go => k8s.io/client-go v0.31.0

require (
github.com/aws/aws-sdk-go v1.44.283
github.com/blang/semver/v4 v4.0.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/octago/sflags v0.2.0
Expand Down
126 changes: 126 additions & 0 deletions tests/e2e/kubetest2-kops/aws/s3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aws

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/sts"
"k8s.io/klog/v2"
)

// We need to pick some region to query the AWS APIs through, even if we are not running on AWS.
const defaultRegion = "us-east-2"

type awsClient struct {
sts *sts.STS
s3 *s3.S3
}

func newAWSClient(ctx context.Context, creds *credentials.Credentials) (*awsClient, error) {
awsConfig := aws.NewConfig().WithRegion(defaultRegion).WithUseDualStack(true)
awsConfig = awsConfig.WithCredentialsChainVerboseErrors(true)
if creds != nil {
awsConfig = awsConfig.WithCredentials(creds)
}

awsSession, err := session.NewSessionWithOptions(session.Options{
Config: *awsConfig,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, fmt.Errorf("error starting new AWS session: %v", err)
}

return &awsClient{
sts: sts.New(awsSession, awsConfig),
s3: s3.New(awsSession, awsConfig),
}, nil
}

// AWSBucketName constructs a bucket name that is unique to the AWS account.
func AWSBucketName(ctx context.Context, creds *credentials.Credentials) (string, error) {
client, err := newAWSClient(ctx, creds)
if err != nil {
return "", err
}

callerIdentity, err := client.sts.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
return "", fmt.Errorf("error getting AWS caller identity from STS: %w", err)
}
bucket := "kops-test-" + aws.StringValue(callerIdentity.Account)
return bucket, nil
}

// EnsureAWSBucket creates a bucket if it does not exist in the account.
// If a different account has already created the bucket, that is treated as an error to prevent "preimage" attacks.
func EnsureAWSBucket(ctx context.Context, creds *credentials.Credentials, bucketName string) error {
// These don't need to be in the same region, so we pick a region arbitrarily
location := "us-east-2"

client, err := newAWSClient(ctx, creds)
if err != nil {
return err
}

// Note that this lists only our buckets, so we know that someone else hasn't created the bucket
buckets, err := client.s3.ListBucketsWithContext(ctx, &s3.ListBucketsInput{})
if err != nil {
return fmt.Errorf("error listing buckets: %w", err)
}

var existingBucket *s3.Bucket
for _, bucket := range buckets.Buckets {
if aws.StringValue(bucket.Name) == bucketName {
existingBucket = bucket
}
}

if existingBucket == nil {
klog.Infof("creating S3 bucket s3://%s", bucketName)
if _, err := client.s3.CreateBucketWithContext(ctx, &s3.CreateBucketInput{
Bucket: &bucketName,
CreateBucketConfiguration: &s3.CreateBucketConfiguration{
LocationConstraint: &location,
},
}); err != nil {
return fmt.Errorf("error creating bucket s3://%v: %w", bucketName, err)
}
}

return nil
}

// DeleteAWSBucket deletes an AWS bucket.
func DeleteAWSBucket(ctx context.Context, creds *credentials.Credentials, bucketName string) error {
client, err := newAWSClient(ctx, creds)
if err != nil {
return err
}

klog.Infof("deleting S3 bucket s3://%s", bucketName)
if _, err := client.s3.DeleteBucketWithContext(ctx, &s3.DeleteBucketInput{Bucket: &bucketName}); err != nil {
return fmt.Errorf("error deleting bucket: %w", err)
}
return nil
}
95 changes: 73 additions & 22 deletions tests/e2e/kubetest2-kops/deployer/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
"path/filepath"
"strings"

"github.com/aws/aws-sdk-go/aws/credentials"
"k8s.io/klog/v2"
"k8s.io/kops/tests/e2e/kubetest2-kops/aws"
"k8s.io/kops/tests/e2e/kubetest2-kops/gce"
"k8s.io/kops/tests/e2e/pkg/target"
"k8s.io/kops/tests/e2e/pkg/util"
Expand Down Expand Up @@ -70,7 +72,7 @@ func (d *deployer) initialize(ctx context.Context) error {
if err != nil {
return fmt.Errorf("init failed to get resource %q from boskos: %w", d.BoskosResourceType, err)
}
klog.V(1).Infof("Got AWS account %s from boskos", resource.Name)
klog.Infof("got AWS account %q from boskos", resource.Name)

accessKeyIDObj, ok := resource.UserData.Load("access-key-id")
if !ok {
Expand All @@ -80,10 +82,8 @@ func (d *deployer) initialize(ctx context.Context) error {
if !ok {
return fmt.Errorf("secret-access-key not found in boskos resource %q", resource.Name)
}
d.awsStaticCredentials = &awsStaticCredentials{
AccessKeyID: accessKeyIDObj.(string),
SecretAccessKey: secretAccessKeyObj.(string),
}
d.awsCredentials = credentials.NewStaticCredentials(accessKeyIDObj.(string), secretAccessKeyObj.(string), "")
d.createStateStoreBucket = true
}

if d.SSHPrivateKeyPath == "" || d.SSHPublicKeyPath == "" {
Expand Down Expand Up @@ -154,6 +154,10 @@ func (d *deployer) initialize(ctx context.Context) error {
}
}

if err := d.initStateStore(ctx); err != nil {
return err
}

if d.SSHUser == "" {
d.SSHUser = os.Getenv("KUBE_SSH_USER")
}
Expand Down Expand Up @@ -216,7 +220,7 @@ func (d *deployer) env() []string {
vars = append(vars, []string{
fmt.Sprintf("PATH=%v", os.Getenv("PATH")),
fmt.Sprintf("HOME=%v", os.Getenv("HOME")),
fmt.Sprintf("KOPS_STATE_STORE=%v", d.stateStore()),
fmt.Sprintf("KOPS_STATE_STORE=%v", d.stateStore),
fmt.Sprintf("KOPS_FEATURE_FLAGS=%v", d.featureFlags()),
"KOPS_RUN_TOO_NEW_VERSION=1",
}...)
Expand Down Expand Up @@ -244,9 +248,23 @@ func (d *deployer) env() []string {
// https://github.com/kubernetes/kubernetes/blob/a750d8054a6cb3167f495829ce3e77ab0ccca48e/test/e2e/framework/ssh/ssh.go#L59-L62
vars = append(vars, fmt.Sprintf("KUBE_SSH_KEY_PATH=%v", d.SSHPrivateKeyPath))

if d.awsStaticCredentials != nil {
vars = append(vars, fmt.Sprintf("AWS_ACCESS_KEY_ID=%v", d.awsStaticCredentials.AccessKeyID))
vars = append(vars, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%v", d.awsStaticCredentials.SecretAccessKey))
if d.awsCredentials != nil {
credentials, err := d.awsCredentials.Get()
if err != nil {
klog.Fatalf("error getting aws credentials: %v", err)
}
if credentials.AccessKeyID != "" {
klog.Infof("setting AWS_ACCESS_KEY_ID")
vars = append(vars, fmt.Sprintf("AWS_ACCESS_KEY_ID=%v", credentials.AccessKeyID))
} else {
klog.Warningf("AWS credentials configured but AWS_ACCESS_KEY_ID was empty")
}
if credentials.SecretAccessKey != "" {
klog.Infof("setting AWS_SECRET_ACCESS_KEY")
vars = append(vars, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%v", credentials.SecretAccessKey))
} else {
klog.Warningf("AWS credentials configured but AWS_SECRET_ACCESS_KEY was empty")
}
}
} else if d.CloudProvider == "digitalocean" {
// Pass through some env vars if set
Expand Down Expand Up @@ -347,22 +365,55 @@ func defaultClusterName(cloudProvider string) (string, error) {
return fmt.Sprintf("%v.%v", jobName, suffix), nil
}

// stateStore returns the kops state store to use
// defaulting to values used in prow jobs
func (d *deployer) stateStore() string {
// initStateStore initializes the kops state store to use
// defaulting to values used in prow jobs,
// but creating a bucket if we are using a dynamic bucket.
func (d *deployer) initStateStore(ctx context.Context) error {
ss := os.Getenv("KOPS_STATE_STORE")
if ss == "" {
switch d.CloudProvider {
case "aws":
ss = "s3://k8s-kops-prow"
case "gce":
d.createBucket = true
ss = "gs://" + gce.GCSBucketName(d.GCPProject, "state")
case "digitalocean":
ss = "do://e2e-kops-space"

switch d.CloudProvider {
case "aws":
if d.createStateStoreBucket {
bucketName, err := aws.AWSBucketName(ctx, d.awsCredentials)
if err != nil {
return fmt.Errorf("error building aws bucket name: %w", err)
}

if err := aws.EnsureAWSBucket(ctx, d.awsCredentials, bucketName); err != nil {
return err
}

ss = "s3://" + bucketName
} else {
if ss == "" {
ss = "s3://k8s-kops-prow"
}
}
case "gce":
if d.createStateStoreBucket {
ss = "gs://" + gce.GCSBucketName(d.GCPProject)
if err := gce.EnsureGCSBucket(ss, d.GCPProject); err != nil {
return err
}
}
d.createBucket = true
ss = "gs://" + gce.GCSBucketName(d.GCPProject, "state")

case "digitalocean":
ss = "do://e2e-kops-space"

default:
if d.createStateStoreBucket {
return fmt.Errorf("bucket creation not implemented for cloud %q", d.CloudProvider)
}
}
return ss

if ss == "" {
return fmt.Errorf("cannot determine KOPS_STATE_STORE")
}

d.stateStore = ss
return nil
}

// discoveryStore returns the VFS path to use for public OIDC documents
Expand Down
18 changes: 16 additions & 2 deletions tests/e2e/kubetest2-kops/deployer/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,15 @@ type deployer struct {
KubernetesFeatureGates string `flag:"kubernetes-feature-gates" desc:"Feature Gates to enable on Kubernetes components"`
createBucket bool `flag:"-"`

// ControlPlaneCount specifies the number of VMs in the control-plane.
ControlPlaneCount int `flag:"control-plane-count" desc:"Number of control-plane instances"`
// // ControlPlaneCount specifies the number of VMs in the control-plane.
// ControlPlaneCount int `flag:"control-plane-count" desc:"Number of control-plane instances"`
// ClusterName string `flag:"cluster-name" desc:"The FQDN to use for the cluster name"`
// ControlPlaneSize int `flag:"control-plane-size" desc:"Number of control plane instances"`
// CloudProvider string `flag:"cloud-provider" desc:"Which cloud provider to use"`
// GCPProject string `flag:"gcp-project" desc:"Which GCP Project to use when --cloud-provider=gce"`
// Env []string `flag:"env" desc:"Additional env vars to set for kops commands in NAME=VALUE format"`
// CreateArgs string `flag:"create-args" desc:"Extra space-separated arguments passed to 'kops create cluster'"`
// KopsBinaryPath string `flag:"kops-binary-path" desc:"The path to kops executable used for testing"`

ControlPlaneIGOverrides []string `flag:"control-plane-instance-group-overrides" desc:"overrides for the control plane instance groups"`
NodeIGOverrides []string `flag:"node-instance-group-overrides" desc:"overrides for the node instance groups"`
Expand Down Expand Up @@ -104,6 +111,13 @@ type deployer struct {

// boskos boskosHelper

// // awsCredentials holds credentials for AWS loaded from boskos
// awsCredentials *credentials.Credentials

// // stateStore holds the kops state-store URL
// stateStore string

// createStateStoreBucket bool `flag:"-"`
// // awsStaticCredentials holds credentials for AWS loaded from boskos
// awsStaticCredentials *awsStaticCredentials
}
Expand Down
23 changes: 20 additions & 3 deletions tests/e2e/kubetest2-kops/deployer/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ package deployer

import (
"context"
"fmt"
"strings"

"k8s.io/klog/v2"
"k8s.io/kops/tests/e2e/kubetest2-kops/aws"
"k8s.io/kops/tests/e2e/kubetest2-kops/gce"
"k8s.io/kops/tests/e2e/pkg/kops"
"sigs.k8s.io/kubetest2/pkg/exec"
Expand Down Expand Up @@ -73,9 +75,24 @@ func (d *deployer) Down() error {
return err
}

if d.CloudProvider == "gce" && d.createBucket {
gce.DeleteGCSBucket(d.stateStore(), d.GCPProject)
gce.DeleteGCSBucket(d.stagingStore(), d.GCPProject)
if d.createStateStoreBucket {
switch d.CloudProvider {
case "gce":
gce.DeleteGCSBucket(d.stateStore(), d.GCPProject)
gce.DeleteGCSBucket(d.stagingStore(), d.GCPProject)
// gce.DeleteGCSBucket(d.stateStore, d.GCPProject)
case "aws":
bucketName, err := aws.AWSBucketName(ctx, d.awsCredentials)
if err != nil {
return fmt.Errorf("error building aws bucket name: %w", err)
}

if err := aws.DeleteAWSBucket(ctx, d.awsCredentials, bucketName); err != nil {
klog.Warningf("error deleting AWS bucket: %w", err)
}
default:
return fmt.Errorf("bucket cleanup not implemented for cloud %q", d.CloudProvider)
}
}

if err := d.boskos.Cleanup(ctx); err != nil {
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/kubetest2-kops/deployer/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func (d *deployer) templateValues(zones []string, publicIP string) (map[string]i
"publicIP": publicIP,
"stateStore": d.stateStore(),
"discoveryStore": d.discoveryStore(),
"stateStore": d.stateStore,
"zones": zones,
"sshPublicKey": string(publicKey),
}, nil
Expand Down

0 comments on commit f7612c8

Please sign in to comment.