diff --git a/src/cloud-api-adaptor/go.mod b/src/cloud-api-adaptor/go.mod index 8f62f83bf1..f78cd7b687 100644 --- a/src/cloud-api-adaptor/go.mod +++ b/src/cloud-api-adaptor/go.mod @@ -58,6 +58,7 @@ require ( github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.24.0 golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 + google.golang.org/api v0.162.0 google.golang.org/protobuf v1.33.0 k8s.io/api v0.26.2 k8s.io/apimachinery v0.26.2 diff --git a/src/cloud-api-adaptor/ibmcloud/cluster/node/version.tf b/src/cloud-api-adaptor/ibmcloud/cluster/node/version.tf index 4e80892aa4..f70a2f32b2 100644 --- a/src/cloud-api-adaptor/ibmcloud/cluster/node/version.tf +++ b/src/cloud-api-adaptor/ibmcloud/cluster/node/version.tf @@ -7,7 +7,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "~> 1.50.0" + version = "~> 1.74.0" } } } diff --git a/src/cloud-api-adaptor/ibmcloud/cluster/version.tf b/src/cloud-api-adaptor/ibmcloud/cluster/version.tf index 4e80892aa4..f70a2f32b2 100644 --- a/src/cloud-api-adaptor/ibmcloud/cluster/version.tf +++ b/src/cloud-api-adaptor/ibmcloud/cluster/version.tf @@ -7,7 +7,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "~> 1.50.0" + version = "~> 1.74.0" } } } diff --git a/src/cloud-api-adaptor/ibmcloud/cluster/vpc/version.tf b/src/cloud-api-adaptor/ibmcloud/cluster/vpc/version.tf index 4e80892aa4..f70a2f32b2 100644 --- a/src/cloud-api-adaptor/ibmcloud/cluster/vpc/version.tf +++ b/src/cloud-api-adaptor/ibmcloud/cluster/vpc/version.tf @@ -7,7 +7,7 @@ terraform { required_providers { ibm = { source = "IBM-Cloud/ibm" - version = "~> 1.50.0" + version = "~> 1.74.0" } } } diff --git a/src/cloud-api-adaptor/test/e2e/gcp_common.go b/src/cloud-api-adaptor/test/e2e/gcp_common.go new file mode 100644 index 0000000000..fd581d6203 --- /dev/null +++ b/src/cloud-api-adaptor/test/e2e/gcp_common.go @@ -0,0 +1,56 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "testing" + "strings" + "time" + + pv "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/test/provisioner/gcp" + "google.golang.org/api/compute/v1" +) + +// GCPAssert implements the CloudAssert interface. +type GCPAssert struct { + Vpc *pv.GCPVPC +} + +func NewGCPAssert() GCPAssert { + return GCPAssert{ + Vpc: pv.GCPProps.GcpVPC, + } +} + +func (aa GCPAssert) DefaultTimeout() time.Duration { + return 1 * time.Minute +} + +func (aa GCPAssert) HasPodVM(t *testing.T, id string) { + podvmPrefix := "podvm-" + id + + // Create a request to list instances in the specified project and zone. + req := pv.GCPProps.ComputeService.Instances.List(pv.GCPProps.ProjectID, pv.GCPProps.Zone) + instances, err := req.Do() + if err != nil { + t.Errorf("Failed to list instances: %v", err) + return + } + + found := false + for _, instance := range instances.Items { + if instance.Status != "TERMINATED" && strings.HasPrefix(instance.Name, podvmPrefix) { + found = true + break + } + } + + if !found { + t.Errorf("Podvm name=%s not found", id) + } +} + +func (aa GCPAssert) GetInstanceType(t *testing.T, podName string) (string, error) { + return "", nil +} diff --git a/src/cloud-api-adaptor/test/e2e/gcp_test.go b/src/cloud-api-adaptor/test/e2e/gcp_test.go new file mode 100644 index 0000000000..b637040300 --- /dev/null +++ b/src/cloud-api-adaptor/test/e2e/gcp_test.go @@ -0,0 +1,104 @@ +//go:build gcp + +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "testing" +) + +func TestGCPCreateSimplePod(t *testing.T) { + assert := GCPAssert{} + DoTestCreateSimplePod(t, testEnv, assert) +} + +func TestGCPCreatePodWithConfigMap(t *testing.T) { + t.Skip("Test not passing") + assert := NewGCPAssert() + + DoTestCreatePodWithConfigMap(t, testEnv, assert) +} + +func TestGCPCreatePodWithSecret(t *testing.T) { + t.Skip("Test not passing") + assert := NewGCPAssert() + + DoTestCreatePodWithSecret(t, testEnv, assert) +} + +// func TestAwsCreatePeerPodContainerWithExternalIPAccess(t *testing.T) { +// t.Skip("Test not passing") +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodContainerWithExternalIPAccess(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodWithJob(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodWithJob(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodAndCheckUserLogs(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodAndCheckUserLogs(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodAndCheckWorkDirLogs(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodAndCheckWorkDirLogs(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodAndCheckEnvVariableLogsWithImageOnly(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodAndCheckEnvVariableLogsWithImageOnly(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodAndCheckEnvVariableLogsWithDeploymentOnly(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodAndCheckEnvVariableLogsWithDeploymentOnly(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodAndCheckEnvVariableLogsWithImageAndDeployment(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodAndCheckEnvVariableLogsWithImageAndDeployment(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodWithLargeImage(t *testing.T) { +// assert := NewAWSAssert() +// +// DoTestCreatePeerPodWithLargeImage(t, testEnv, assert) +// } +// +// func TestAwsCreatePeerPodWithPVC(t *testing.T) { +// t.Skip("To be implemented") +// } +// +// func TestAwsCreatePeerPodWithAuthenticatedImagewithValidCredentials(t *testing.T) { +// t.Skip("To be implemented") +// } +// +// func TestAwsCreatePeerPodWithAuthenticatedImageWithInvalidCredentials(t *testing.T) { +// t.Skip("To be implemented") +// } +// +// func TestAwsCreatePeerPodWithAuthenticatedImageWithoutCredentials(t *testing.T) { +// t.Skip("To be implemented") +// } +// +// func TestAwsDeletePod(t *testing.T) { +// assert := NewAWSAssert() +// DoTestDeleteSimplePod(t, testEnv, assert) +// } +// +// func TestAwsCreateNginxDeployment(t *testing.T) { +// assert := NewAWSAssert() +// DoTestNginxDeployment(t, testEnv, assert) +// } diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/cluster.go b/src/cloud-api-adaptor/test/provisioner/gcp/cluster.go new file mode 100644 index 0000000000..23fd311113 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/cluster.go @@ -0,0 +1,225 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "context" + "fmt" + "os" + "os/exec" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/api/container/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/option" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/retry" + kconf "sigs.k8s.io/e2e-framework/klient/conf" +) + +// GKECluster implements the basic GKE Cluster client operations. +type GKECluster struct { + Srv *container.Service + Properties map[string]string +} + +// NewGKECluster creates a new GKECluster with the given properties +func NewGKECluster(properties map[string]string) (*GKECluster, error) { + srv, err := container.NewService( + context.Background(), + option.WithCredentialsFile(properties["credentialsPath"]), + ) + if err != nil { + return nil, fmt.Errorf("failed to create GKE service: %w", err) + } + + return &GKECluster{ + Srv: srv, + Properties: properties, + }, nil +} + +// Create creates the GKE cluster +func (g *GKECluster) Create(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, time.Hour) + defer cancel() + + cluster := &container.Cluster{ + Name: g.clusterName, + InitialNodeCount: g.nodeCount, + NodeConfig: &container.NodeConfig{ + MachineType: g.machineType, + ImageType: "UBUNTU_CONTAINERD", // Default CO OS has a ro fs. + }, + } + + req := &container.CreateClusterRequest{ + Cluster: cluster, + } + + op, err := g.Client.Projects.Zones.Clusters.Create( + g.projectID, g.zone, req, + ).Context(ctx).Do() + if err != nil { + return fmt.Errorf("GKE: Projects.Zones.Clusters.Create: %v", err) + } + + log.Infof("GKE: Cluster creation operation: %v\n", op.Name) + + _, err = g.WaitForActive(ctx, 30*time.Minute) + if err != nil { + return fmt.Errorf("GKE: Error waiting for cluster to become active: %v", err) + } + + err = g.ApplyNodeLabels(ctx) + if err != nil { + return fmt.Errorf("GKE: Error applying node labels: %v", err) + } + return nil +} + +// Delete deletes the GKE cluster +func (g *GKECluster) Delete(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, time.Hour) + defer cancel() + + op, err := g.Client.Projects.Zones.Clusters.Delete( + g.projectID, g.zone, g.clusterName, + ).Context(ctx).Do() + if err != nil { + return fmt.Errorf("GKE: Projects.Zones.Clusters.Delete: %v", err) + } + + log.Infof("GKE: Cluster deletion operation: %v\n", op.Name) + + // Wait for the cluster to be deleted + activationTimeout := 30 * time.Minute + err = g.WaitForDeleted(ctx, activationTimeout) + if err != nil { + return fmt.Errorf("GKE: error waiting for cluster to be deleted: %v", err) + } + return nil +} + +// WaitForActive waits until the GKE cluster is active +func (g *GKECluster) WaitForActive( + ctx context.Context, activationTimeout time.Duration, +) (*container.Cluster, error) { + + timeoutCtx, cancel := context.WithTimeout(ctx, activationTimeout) + defer cancel() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeoutCtx.Done(): + return nil, fmt.Errorf("GKE: Reached timeout waiting for cluster.") + case <-ticker.C: + cluster, err := g.Client.Projects.Zones.Clusters.Get(g.projectID, g.zone, g.clusterName).Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("GKE: Projects.Zones.Clusters.Get: %v", err) + } + + if cluster.Status == "RUNNING" { + log.Info("GKE: Cluster is now active") + return cluster, nil + } + + log.Info("GKE: Waiting for cluster to become active...") + } + } +} + +// WaitForDeleted waits until the GKE cluster is deleted +func (g *GKECluster) WaitForDeleted( + ctx context.Context, activationTimeout time.Duration, +) error { + timeoutCtx, cancel := context.WithTimeout(ctx, activationTimeout) + defer cancel() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeoutCtx.Done(): + return fmt.Errorf("GKE: timeout waiting for cluster deletion") + case <-ticker.C: + _, err := g.Client.Projects.Zones.Clusters.Get(g.projectID, g.zone, g.clusterName).Context(ctx).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + log.Info("GKE: Cluster deleted successfully") + return nil + } + return fmt.Errorf("GKE: Projects.Zones.Clusters.Get: %v", err) + } + + log.Info("GKE: Waiting for cluster to be deleted...") + } + } +} + +func (g *GKECluster) ApplyNodeLabels(ctx context.Context) error { + kubeconfigPath, err := g.GetKubeconfigFile(ctx) + if err != nil { + return err + } + + config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return fmt.Errorf("failed to build kubeconfig: %v", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create clientset: %v", err) + } + + nodes, err := clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("failed to list nodes: %v", err) + } + + for _, node := range nodes.Items { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + n, err := clientset.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get node: %v", err) + } + + n.Labels["node.kubernetes.io/worker"] = "" + n.Labels["node-role.kubernetes.io/worker"] = "" + _, err = clientset.CoreV1().Nodes().Update(ctx, n, metav1.UpdateOptions{}) + return err + }) + if err != nil { + return fmt.Errorf("Failed to label node %s: %v\n", node.Name, err) + } + log.Infof("Successfully labeled node %s\n", node.Name) + } + return nil +} + +// GetKubeconfigFile retrieves the path to the kubeconfig file +func (g *GKECluster) GetKubeconfigFile(ctx context.Context) (string, error) { + cmd := exec.CommandContext(ctx, "gcloud", "container", "clusters", "get-credentials", g.clusterName, "--zone", g.zone, "--project", g.projectID) + output, err := cmd.CombinedOutput() + + if err != nil { + return "", fmt.Errorf("Failed to get cluster credentials: %v\nOutput: %s", err, output) + } + + kubeconfigPath := kconf.ResolveKubeConfigFile() + _, err = os.Stat(kubeconfigPath) + if err != nil { + return "", fmt.Errorf("Failed to resolve KubeConfigfile: %v", err) + } + return kubeconfigPath, nil +} diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/image.go b/src/cloud-api-adaptor/test/provisioner/gcp/image.go new file mode 100644 index 0000000000..33ed6e91d1 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/image.go @@ -0,0 +1,23 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "google.golang.org/api/compute/v1" +) + +type GCPImage struct { + Client *compute.Service +} + +func NewGCPImage(credentialsPath string) *GCPImage { + client, err := compute.NewService(context.TODO(), option.WithCredentialsFile(credentialsPath)) + if err != nil { + return nil, fmt.Errorf("GKE: failed to create GCP compute service: %v", err) + } + + return &GCPImage{ + Client: client, + } +} diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/overlay.go b/src/cloud-api-adaptor/test/provisioner/gcp/overlay.go new file mode 100644 index 0000000000..ca2a4db884 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/overlay.go @@ -0,0 +1,88 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "context" + pv "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/test/provisioner" + log "github.com/sirupsen/logrus" + "path/filepath" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +type GCPInstallOverlay struct { + Overlay *pv.KustomizeOverlay +} + +func NewGCPInstallOverlay(installDir, provider string) (pv.InstallOverlay, error) { + overlay, err := pv.NewKustomizeOverlay(filepath.Join(installDir, "overlays", provider)) + if err != nil { + return nil, err + } + + return &GCPInstallOverlay{ + Overlay: overlay, + }, nil +} + +func (a *GCPInstallOverlay) Apply(ctx context.Context, cfg *envconf.Config) error { + return a.Overlay.Apply(ctx, cfg) +} + +func (a *GCPInstallOverlay) Delete(ctx context.Context, cfg *envconf.Config) error { + return a.Overlay.Delete(ctx, cfg) +} + +func (a *GCPInstallOverlay) Edit(ctx context.Context, cfg *envconf.Config, properties map[string]string) error { + var err error + + image := properties["caa_image_name"] + log.Infof("Updating caa image with %s", image) + if image != "" { + err = a.Overlay.SetKustomizeImage("cloud-api-adaptor", "newName", image) + if err != nil { + return err + } + } + + // Mapping the internal properties to ConfigMapGenerator properties. + mapProps := map[string]string{ + "pause_image": "PAUSE_IMAGE", + "podvm_image_name": "PODVM_IMAGE_NAME", + "machine_type": "GCP_MACHINE_TYPE", + "project_id": "GCP_PROJECT_ID", + "zone": "GCP_ZONE", + "network": "GCP_NETWORK", + "vxlan_port": "VXLAN_PORT", + } + + for k, v := range mapProps { + if properties[k] != "" { + if err = a.Overlay.SetKustomizeConfigMapGeneratorLiteral("peer-pods-cm", + v, properties[k]); err != nil { + return err + } + } + } + + // Mapping the internal properties to SecretGenerator properties. + mapProps = map[string]string{ + "credentials": "GCP_CREDENTIALS", + } + for k, _ := range mapProps { + if properties[k] != "" { + log.Info(properties[k]) + if err = a.Overlay.SetKustomizeSecretGeneratorFile("peer-pods-secret", + properties[k]); err != nil { + return err + } + } + } + + if err = a.Overlay.YamlReload(); err != nil { + return err + } + + return nil +} diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/provision.go b/src/cloud-api-adaptor/test/provisioner/gcp/provision.go new file mode 100644 index 0000000000..ad4399de43 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/provision.go @@ -0,0 +1,15 @@ +//go:build gcp + +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + pv "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/test/provisioner" +) + +func init() { + pv.NewProvisionerFunctions["gcp"] = NewGCPProvisioner + pv.NewInstallOverlayFunctions["gcp"] = NewGCPInstallOverlay +} diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/provision_common.go b/src/cloud-api-adaptor/test/provisioner/gcp/provision_common.go new file mode 100644 index 0000000000..944ae47d92 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/provision_common.go @@ -0,0 +1,97 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "context" + "fmt" + "google.golang.org/api/option" + + pv "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/test/provisioner" + // log "github.com/sirupsen/logrus" + "google.golang.org/api/compute/v1" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +var GCPProps = &GCPProvisioner{} + +// GCPProvisioner implements the CloudProvisioner interface. +type GCPProvisioner struct { + GkeCluster *GKECluster + GcpVPC *GCPVPC + PodvmImage *GCPImage +} + +// NewGCPProvisioner creates a new GCPProvisioner with the given properties. +func NewGCPProvisioner(properties map[string]string) (pv.CloudProvisioner, error) { + credentials_path = properties['credentials_path'] + + gkeCluster, err := NewGKECluster(credentials_path) + if err != nil { + return nil, err + } + + gcpVPC, err := NewGCPVPC(credentials_path, properties['vpc_name']) + if err != nil { + return nil, err + } + + gcpImage, err := NewGCPImage(credentials_path) + if err != nil { + return nil, err + } + + GCPProps = &GCPProvisioner{ + GkeCluster: gkeCluster, + GcpVPC: gcpVPC, + PodvmImage: gcpImage, + } + return GCPProps, nil +} + +// CreateCluster creates a new GKE cluster. +func (p *GCPProvisioner) CreateCluster(ctx context.Context, cfg *envconf.Config) error { + err := p.GkeCluster.Create(ctx) + if err != nil { + return err + } + + kubeconfigPath, err := p.GkeCluster.GetKubeconfigFile(ctx) + if err != nil { + return err + } + *cfg = *envconf.NewWithKubeConfig(kubeconfigPath) + + return nil +} + +// CreateVPC creates a new VPC in Google Cloud. +func (p *GCPProvisioner) CreateVPC(ctx context.Context, cfg *envconf.Config) error { + return p.GcpVPC.Create(ctx, cfg) +} + +// DeleteCluster deletes the GKE cluster. +func (p *GCPProvisioner) DeleteCluster(ctx context.Context, cfg *envconf.Config) error { + return p.GkeCluster.Delete(ctx) +} + +// DeleteVPC deletes the VPC in Google Cloud. +func (p *GCPProvisioner) DeleteVPC(ctx context.Context, cfg *envconf.Config) error { + return p.GcpVPC.Delete(ctx, cfg) +} + +func (p *GCPProvisioner) GetProperties(ctx context.Context, cfg *envconf.Config) map[string]string { + return map[string]string{ + "podvm_image_name": p.PodvmImage.Name, + "machine_type": p.GkeCluster.machineType, + "project_id": p.GkeCluster.projectID, + "zone": p.GkeCluster.zone, + "network": p.GcpVPC.vpcName, + } +} + +func (p *GCPProvisioner) UploadPodvm(imagePath string, ctx context.Context, cfg *envconf.Config) error { + // To be Implemented + return nil +} diff --git a/src/cloud-api-adaptor/test/provisioner/gcp/vpc.go b/src/cloud-api-adaptor/test/provisioner/gcp/vpc.go new file mode 100644 index 0000000000..85ae4873f0 --- /dev/null +++ b/src/cloud-api-adaptor/test/provisioner/gcp/vpc.go @@ -0,0 +1,145 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "context" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/option" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +// GCPVPC implements the Google Compute VPC interface. +type GCPVPC struct { + Client *compute.Service + CredentialsPath string + Name string +} + +// NewGCPVPC creates a new GCPVPC object. +func NewGCPVPC(credentialsPath string, name string) (*GCPVPC, error) { + client, err := compute.NewService(context.TODO(), option.WithCredentialsFile(credentialsPath)) + if err != nil { + return nil, fmt.Errorf("GKE: failed to create GCP compute service: %v", err) + } + + return &GCPVPC{ + Client: client, + CredentialsPath: credentialsPath, + Name: name, + }, nil +} + +// Create creates a new VPC in Google Cloud. +func (g *GCPVPC) Create( + ctx context.Context, cfg *envconf.Config, +) error { + ctx, cancel := context.WithTimeout(ctx, time.Hour) + defer cancel() + + _, err = g.Client.Networks.Get(g.projectID, g.Name).Context(ctx).Do() + if err == nil { + log.Infof("GKE: Using existing VPC %s.\n", g.Name) + return nil + } + + network := &compute.Network{ + Name: g.Name, + AutoCreateSubnetworks: true, + } + + op, err := g.Client.Networks.Insert(g.projectID, network).Context(ctx).Do() + if err != nil { + return fmt.Errorf("GKE: Networks.Insert: %v", err) + } + + log.Infof("GKE: VPC creation operation started: %v\n", op.Name) + + err = g.WaitForCreation(ctx, 30*time.Minute) + if err != nil { + return fmt.Errorf("GKE: Error waiting for VPC to be created: %v", err) + } + return nil +} + +// Delete deletes a VPC in Google Cloud. +func (g *GCPVPC) Delete(ctx context.Context, cfg *envconf.Config) error { + op, err := g.Client.Networks.Delete(g.projectID, g.Name).Context(ctx).Do() + if err != nil { + return fmt.Errorf("GKE: Networks.Delete: %v", err) + } + + log.Infof("GKE: VPC deletion operation started: %v\n", op.Name) + + err = g.WaitForDeleted(ctx, 30*time.Minute) + if err != nil { + return fmt.Errorf("GKE: Error waiting for VPC to be deleted: %v", err) + } + + return nil +} + +// WaitForCreation waits until the VPC is created and available. +func (g *GCPVPC) WaitForCreation( + ctx context.Context, timeout time.Duration, +) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for VPC creation") + case <-ticker.C: + network, err := g.Client.Networks.Get(g.projectID, g.Name).Context(ctx).Do() + if err != nil { + if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code == 404 { + log.Info("Waiting for VPC to be created...") + continue + } + return fmt.Errorf("Networks.Get: %v", err) + } + if network.SelfLink != "" { + log.Info("VPC created successfully") + return nil + } + } + } +} + +// WaitForDeleted waits until the VPC is deleted. +func (g *GCPVPC) WaitForDeleted( + ctx context.Context, timeout time.Duration, +) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("GKE: timeout waiting for VPC deletion") + case <-ticker.C: + _, err := g.Client.Networks.Get(g.projectID, g.Name).Context(ctx).Do() + if err != nil { + if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code == 404 { + log.Info("GKE: VPC deleted successfully") + return nil + } + return fmt.Errorf("GKE: Networks.Get: %v", err) + } + log.Info("GKE: Waiting for VPC to be deleted...") + } + } +}