From b83c81efe92583f93a614a1a32f09fc9b0e0809a Mon Sep 17 00:00:00 2001 From: Brent Barbachem Date: Tue, 30 Jan 2024 15:05:26 -0500 Subject: [PATCH] CORS-3212: GCP use the apis to create the bucket and presigned url for ignition. ** Add creation of Bucket, BucketObject, and Signed URL. ** Add ability to update/set the contents of the Bucket Object after the signed url has been added to the bootstrap machine. This allows us to create the storage object then update its contents after the ignition data can be edited. --- pkg/infrastructure/gcp/bootstrap.go | 153 ++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 pkg/infrastructure/gcp/bootstrap.go diff --git a/pkg/infrastructure/gcp/bootstrap.go b/pkg/infrastructure/gcp/bootstrap.go new file mode 100644 index 00000000000..57a4037fa22 --- /dev/null +++ b/pkg/infrastructure/gcp/bootstrap.go @@ -0,0 +1,153 @@ +package gcp + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "cloud.google.com/go/storage" + + "github.com/openshift/installer/pkg/asset/installconfig" + gcpic "github.com/openshift/installer/pkg/asset/installconfig/gcp" +) + +const ( + BootstrapIgnitionBucket = "bootstrap.ign" +) + +// GetBootstrapStorageName gets the name of the storage bucket for the bootstrap process. +func GetBootstrapStorageName(clusterID string) string { + return fmt.Sprintf("%s-bootstrap-ignition", clusterID) +} + +// NewClient creates a new Google storage client. +func NewClient(ctx context.Context) (*storage.Client, error) { + client, err := storage.NewClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create client: %w", err) + } + + return client, nil +} + +// CreateBucketHandle will create the bucket handle that can be used as a reference for other storage resources. +func CreateBucketHandle(ctx context.Context, bucketName string) (*storage.BucketHandle, error) { + ctx, cancel := context.WithTimeout(ctx, time.Second*60) + defer cancel() + + client, err := NewClient(ctx) + if err != nil { + return nil, err + } + return client.Bucket(bucketName), nil +} + +// CreateStorage creates the gcp bucket/storage. The storage bucket does Not include the bucket object. The +// bucket object is created as a separate process/function, so that the two are not tied together, and +// the data stored inside the object can be set at a later time. +func CreateStorage(ctx context.Context, ic *installconfig.InstallConfig, clusterID, bucketName string) error { + bucketHandle, err := CreateBucketHandle(ctx, bucketName) + if err != nil { + return fmt.Errorf("failed to create bucket handle: %w", err) + } + + labels := map[string]string{} + labels[fmt.Sprintf("kubernetes-io-cluster-%s", clusterID)] = "owned" + for _, label := range ic.Config.GCP.UserLabels { + labels[label.Key] = label.Value + } + + bucketAttrs := storage.BucketAttrs{ + UniformBucketLevelAccess: storage.UniformBucketLevelAccess{ + Enabled: true, + }, + Location: ic.Config.GCP.Region, + Labels: labels, + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*60) + defer cancel() + + if err := bucketHandle.Create(ctx, ic.Config.GCP.ProjectID, &bucketAttrs); err != nil { + return fmt.Errorf("failed to create bucket: %w", err) + } + return nil +} + +// CreateSignedURL creates a signed url and correlates the signed url with a storage bucket. +func CreateSignedURL(handle *storage.BucketHandle, objectName string) (string, error) { + opts := storage.SignedURLOptions{ + Method: "PUT", + Expires: time.Now().Add(time.Minute * 60), + } + + ctx := context.Background() + session, err := gcpic.GetSession(ctx) + if err != nil { + return "", err + } + + // TODO: make sure all cases are handled including the cases required by https://github.com/openshift/installer/pull/7697 + if session.Credentials.JSON != nil { + var credsMap map[string]interface{} + if err := json.Unmarshal(session.Credentials.JSON, &credsMap); err != nil { + return "", err + } + opts.GoogleAccessID = credsMap["client_email"].(string) + opts.PrivateKey = []byte(credsMap["private_key"].(string)) + } + + // The object has not been created yet. This is ok, it is expected to be created after this call. + // However, if the object is never created this could cause major issues. + url, err := handle.SignedURL(objectName, &opts) + if err != nil { + return "", fmt.Errorf("failed to create a signed url: %w", err) + } + + return url, nil +} + +// ProvisionBootstrapStorage will provision the required storage bucket and signed url for the bootstrap process. +func ProvisionBootstrapStorage(ic *installconfig.InstallConfig, clusterID string) (string, error) { + ctx := context.Background() + + if err := CreateStorage(ctx, ic, clusterID, BootstrapIgnitionBucket); err != nil { + return "", nil + } + + bucketHandle, err := CreateBucketHandle(ctx, GetBootstrapStorageName(clusterID)) + if err != nil { + return "", err + } + + url, err := CreateSignedURL(bucketHandle, BootstrapIgnitionBucket) + if err != nil { + return "", err + } + + return url, nil +} + +// FillBucket will add the contents to the bootstrap storage bucket object. The bucketName is the +// name of the bucket, and the object name refers to the object that should be filled within the bucket. +func FillBucket(ctx context.Context, bucketName, objectName, contents string) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*60) + defer cancel() + + bucketHandle, err := CreateBucketHandle(ctx, bucketName) + if err != nil { + return err + } + + objWriter := bucketHandle.Object(objectName).NewWriter(ctx) + if _, err := fmt.Fprintf(objWriter, contents); err != nil { + return fmt.Errorf("failed to store content in bucket object: %w", err) + } + + if err := objWriter.Close(); err != nil { + return fmt.Errorf("failed to close bucket object writer: %w", err) + } + + return nil +}