//go:build !providerless // +build !providerless /* Copyright 2018 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 gce import ( "context" "fmt" "math/rand" "net/http" "os/exec" "regexp" "strings" "time" compute "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" clientset "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/test/e2e/framework" e2epv "k8s.io/kubernetes/test/e2e/framework/pv" e2eservice "k8s.io/kubernetes/test/e2e/framework/service" gcecloud "k8s.io/legacy-cloud-providers/gce" ) func init() { framework.RegisterProvider("gce", factory) framework.RegisterProvider("gke", factory) } func factory() (framework.ProviderInterface, error) { framework.Logf("Fetching cloud provider for %q\r", framework.TestContext.Provider) zone := framework.TestContext.CloudConfig.Zone region := framework.TestContext.CloudConfig.Region allowedZones := framework.TestContext.CloudConfig.Zones // ensure users don't specify a zone outside of the requested zones if len(zone) > 0 && len(allowedZones) > 0 { var found bool for _, allowedZone := range allowedZones { if zone == allowedZone { found = true break } } if !found { return nil, fmt.Errorf("the provided zone %q must be included in the list of allowed zones %v", zone, allowedZones) } } var err error if region == "" { region, err = gcecloud.GetGCERegion(zone) if err != nil { return nil, fmt.Errorf("error parsing GCE/GKE region from zone %q: %w", zone, err) } } managedZones := []string{} // Manage all zones in the region if !framework.TestContext.CloudConfig.MultiZone { managedZones = []string{zone} } if len(allowedZones) > 0 { managedZones = allowedZones } gceCloud, err := gcecloud.CreateGCECloud(&gcecloud.CloudConfig{ APIEndpoint: framework.TestContext.CloudConfig.APIEndpoint, ProjectID: framework.TestContext.CloudConfig.ProjectID, Region: region, Zone: zone, ManagedZones: managedZones, NetworkName: "", // TODO: Change this to use framework.TestContext.CloudConfig.Network? SubnetworkName: "", NodeTags: nil, NodeInstancePrefix: "", TokenSource: nil, UseMetadataServer: false, AlphaFeatureGate: gcecloud.NewAlphaFeatureGate([]string{}), }) if err != nil { return nil, fmt.Errorf("Error building GCE/GKE provider: %w", err) } // Arbitrarily pick one of the zones we have nodes in, looking at prepopulated zones first. if framework.TestContext.CloudConfig.Zone == "" && len(managedZones) > 0 { framework.TestContext.CloudConfig.Zone = managedZones[rand.Intn(len(managedZones))] } if framework.TestContext.CloudConfig.Zone == "" && framework.TestContext.CloudConfig.MultiZone { zones, err := gceCloud.GetAllZonesFromCloudProvider() if err != nil { return nil, err } framework.TestContext.CloudConfig.Zone, _ = zones.PopAny() } return NewProvider(gceCloud), nil } // NewProvider returns a cloud provider interface for GCE func NewProvider(gceCloud *gcecloud.Cloud) framework.ProviderInterface { return &Provider{ gceCloud: gceCloud, } } // Provider is a structure to handle GCE clouds for e2e testing type Provider struct { framework.NullProvider gceCloud *gcecloud.Cloud } // ResizeGroup resizes an instance group func (p *Provider) ResizeGroup(group string, size int32) error { // TODO: make this hit the compute API directly instead of shelling out to gcloud. // TODO: make gce/gke implement InstanceGroups, so we can eliminate the per-provider logic zone, err := getGCEZoneForGroup(group) if err != nil { return err } output, err := exec.Command("gcloud", "compute", "instance-groups", "managed", "resize", group, fmt.Sprintf("--size=%v", size), "--project="+framework.TestContext.CloudConfig.ProjectID, "--zone="+zone).CombinedOutput() if err != nil { return fmt.Errorf("Failed to resize node instance group %s: %s", group, output) } return nil } // GetGroupNodes returns a node name for the specified node group func (p *Provider) GetGroupNodes(group string) ([]string, error) { // TODO: make this hit the compute API directly instead of shelling out to gcloud. // TODO: make gce/gke implement InstanceGroups, so we can eliminate the per-provider logic zone, err := getGCEZoneForGroup(group) if err != nil { return nil, err } output, err := exec.Command("gcloud", "compute", "instance-groups", "managed", "list-instances", group, "--project="+framework.TestContext.CloudConfig.ProjectID, "--zone="+zone).CombinedOutput() if err != nil { return nil, fmt.Errorf("Failed to get nodes in instance group %s: %s", group, output) } re := regexp.MustCompile(".*RUNNING") lines := re.FindAllString(string(output), -1) for i, line := range lines { lines[i] = line[:strings.Index(line, " ")] } return lines, nil } // GroupSize returns the size of an instance group func (p *Provider) GroupSize(group string) (int, error) { // TODO: make this hit the compute API directly instead of shelling out to gcloud. // TODO: make gce/gke implement InstanceGroups, so we can eliminate the per-provider logic zone, err := getGCEZoneForGroup(group) if err != nil { return -1, err } output, err := exec.Command("gcloud", "compute", "instance-groups", "managed", "list-instances", group, "--project="+framework.TestContext.CloudConfig.ProjectID, "--zone="+zone).CombinedOutput() if err != nil { return -1, fmt.Errorf("Failed to get group size for group %s: %s", group, output) } re := regexp.MustCompile("RUNNING") return len(re.FindAllString(string(output), -1)), nil } // EnsureLoadBalancerResourcesDeleted ensures that cloud load balancer resources that were created func (p *Provider) EnsureLoadBalancerResourcesDeleted(ctx context.Context, ip, portRange string) error { project := framework.TestContext.CloudConfig.ProjectID region, err := gcecloud.GetGCERegion(framework.TestContext.CloudConfig.Zone) if err != nil { return fmt.Errorf("could not get region for zone %q: %w", framework.TestContext.CloudConfig.Zone, err) } return wait.PollWithContext(ctx, 10*time.Second, 5*time.Minute, func(ctx context.Context) (bool, error) { computeservice := p.gceCloud.ComputeServices().GA list, err := computeservice.ForwardingRules.List(project, region).Do() if err != nil { return false, err } for _, item := range list.Items { if item.PortRange == portRange && item.IPAddress == ip { framework.Logf("found a load balancer: %v", item) return false, nil } } return true, nil }) } func getGCEZoneForGroup(group string) (string, error) { output, err := exec.Command("gcloud", "compute", "instance-groups", "managed", "list", "--project="+framework.TestContext.CloudConfig.ProjectID, "--format=value(zone)", "--filter=name="+group).Output() if err != nil { return "", fmt.Errorf("Failed to get zone for node group %s: %s", group, output) } return strings.TrimSpace(string(output)), nil } // DeleteNode deletes a node which is specified as the argument func (p *Provider) DeleteNode(node *v1.Node) error { zone := framework.TestContext.CloudConfig.Zone project := framework.TestContext.CloudConfig.ProjectID return p.gceCloud.DeleteInstance(project, zone, node.Name) } func (p *Provider) CreateShare() (string, string, string, error) { return "", "", "", nil } func (p *Provider) DeleteShare(accountName, shareName string) error { return nil } // CreatePD creates a persistent volume func (p *Provider) CreatePD(zone string) (string, error) { pdName := fmt.Sprintf("%s-%s", framework.TestContext.Prefix, string(uuid.NewUUID())) if zone == "" && framework.TestContext.CloudConfig.MultiZone { zones, err := p.gceCloud.GetAllZonesFromCloudProvider() if err != nil { return "", err } zone, _ = zones.PopAny() } tags := map[string]string{} if _, err := p.gceCloud.CreateDisk(pdName, gcecloud.DiskTypeStandard, zone, 2 /* sizeGb */, tags); err != nil { return "", err } return pdName, nil } // DeletePD deletes a persistent volume func (p *Provider) DeletePD(pdName string) error { err := p.gceCloud.DeleteDisk(pdName) if err != nil { if gerr, ok := err.(*googleapi.Error); ok && len(gerr.Errors) > 0 && gerr.Errors[0].Reason == "notFound" { // PD already exists, ignore error. return nil } framework.Logf("error deleting PD %q: %v", pdName, err) } return err } // CreatePVSource creates a persistent volume source func (p *Provider) CreatePVSource(ctx context.Context, zone, diskName string) (*v1.PersistentVolumeSource, error) { return &v1.PersistentVolumeSource{ GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{ PDName: diskName, FSType: "ext3", ReadOnly: false, }, }, nil } // DeletePVSource deletes a persistent volume source func (p *Provider) DeletePVSource(ctx context.Context, pvSource *v1.PersistentVolumeSource) error { return e2epv.DeletePDWithRetry(ctx, pvSource.GCEPersistentDisk.PDName) } // CleanupServiceResources cleans up GCE Service Type=LoadBalancer resources with // the given name. The name is usually the UUID of the Service prefixed with an // alpha-numeric character ('a') to work around cloudprovider rules. func (p *Provider) CleanupServiceResources(ctx context.Context, c clientset.Interface, loadBalancerName, region, zone string) { if pollErr := wait.PollWithContext(ctx, 5*time.Second, e2eservice.LoadBalancerCleanupTimeout, func(ctx context.Context) (bool, error) { if err := p.cleanupGCEResources(ctx, c, loadBalancerName, region, zone); err != nil { framework.Logf("Still waiting for glbc to cleanup: %v", err) return false, nil } return true, nil }); pollErr != nil { framework.Failf("Failed to cleanup service GCE resources.") } } func (p *Provider) cleanupGCEResources(ctx context.Context, c clientset.Interface, loadBalancerName, region, zone string) (retErr error) { if region == "" { // Attempt to parse region from zone if no region is given. var err error region, err = gcecloud.GetGCERegion(zone) if err != nil { return fmt.Errorf("error parsing GCE/GKE region from zone %q: %w", zone, err) } } if err := p.gceCloud.DeleteFirewall(gcecloud.MakeFirewallName(loadBalancerName)); err != nil && !IsGoogleAPIHTTPErrorCode(err, http.StatusNotFound) { retErr = err } if err := p.gceCloud.DeleteRegionForwardingRule(loadBalancerName, region); err != nil && !IsGoogleAPIHTTPErrorCode(err, http.StatusNotFound) { retErr = fmt.Errorf("%v\n%v", retErr, err) } if err := p.gceCloud.DeleteRegionAddress(loadBalancerName, region); err != nil && !IsGoogleAPIHTTPErrorCode(err, http.StatusNotFound) { retErr = fmt.Errorf("%v\n%v", retErr, err) } clusterID, err := GetClusterID(ctx, c) if err != nil { retErr = fmt.Errorf("%v\n%v", retErr, err) return } hcNames := []string{gcecloud.MakeNodesHealthCheckName(clusterID)} hc, getErr := p.gceCloud.GetHTTPHealthCheck(loadBalancerName) if getErr != nil && !IsGoogleAPIHTTPErrorCode(getErr, http.StatusNotFound) { retErr = fmt.Errorf("%v\n%v", retErr, getErr) return } if hc != nil { hcNames = append(hcNames, hc.Name) } if err := p.gceCloud.DeleteExternalTargetPoolAndChecks(&v1.Service{}, loadBalancerName, region, clusterID, hcNames...); err != nil && !IsGoogleAPIHTTPErrorCode(err, http.StatusNotFound) { retErr = fmt.Errorf("%v\n%v", retErr, err) } return } // L4LoadBalancerSrcRanges contains the ranges of ips used by the GCE L4 load // balancers for proxying client requests and performing health checks. func (p *Provider) L4LoadBalancerSrcRanges() []string { return gcecloud.L4LoadBalancerSrcRanges() } // EnableAndDisableInternalLB returns functions for both enabling and disabling internal Load Balancer func (p *Provider) EnableAndDisableInternalLB() (enable, disable func(svc *v1.Service)) { enable = func(svc *v1.Service) { svc.ObjectMeta.Annotations = map[string]string{gcecloud.ServiceAnnotationLoadBalancerType: string(gcecloud.LBTypeInternal)} } disable = func(svc *v1.Service) { delete(svc.ObjectMeta.Annotations, gcecloud.ServiceAnnotationLoadBalancerType) } return } // GetInstanceTags gets tags from GCE instance with given name. func GetInstanceTags(cloudConfig framework.CloudConfig, instanceName string) *compute.Tags { gceCloud := cloudConfig.Provider.(*Provider).gceCloud res, err := gceCloud.ComputeServices().GA.Instances.Get(cloudConfig.ProjectID, cloudConfig.Zone, instanceName).Do() if err != nil { framework.Failf("Failed to get instance tags for %v: %v", instanceName, err) } return res.Tags } // SetInstanceTags sets tags on GCE instance with given name. func SetInstanceTags(cloudConfig framework.CloudConfig, instanceName, zone string, tags []string) []string { gceCloud := cloudConfig.Provider.(*Provider).gceCloud // Re-get instance everytime because we need the latest fingerprint for updating metadata resTags := GetInstanceTags(cloudConfig, instanceName) _, err := gceCloud.ComputeServices().GA.Instances.SetTags( cloudConfig.ProjectID, zone, instanceName, &compute.Tags{Fingerprint: resTags.Fingerprint, Items: tags}).Do() if err != nil { framework.Failf("failed to set instance tags: %v", err) } framework.Logf("Sent request to set tags %v on instance: %v", tags, instanceName) return resTags.Items } // IsGoogleAPIHTTPErrorCode returns true if the error is a google api // error matching the corresponding HTTP error code. func IsGoogleAPIHTTPErrorCode(err error, code int) bool { apiErr, ok := err.(*googleapi.Error) return ok && apiErr.Code == code } // GetGCECloud returns GCE cloud provider func GetGCECloud() (*gcecloud.Cloud, error) { p, ok := framework.TestContext.CloudConfig.Provider.(*Provider) if !ok { return nil, fmt.Errorf("failed to convert CloudConfig.Provider to GCE provider: %#v", framework.TestContext.CloudConfig.Provider) } return p.gceCloud, nil } // GetClusterID returns cluster ID func GetClusterID(ctx context.Context, c clientset.Interface) (string, error) { cm, err := c.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, gcecloud.UIDConfigMapName, metav1.GetOptions{}) if err != nil || cm == nil { return "", fmt.Errorf("error getting cluster ID: %w", err) } clusterID, clusterIDExists := cm.Data[gcecloud.UIDCluster] providerID, providerIDExists := cm.Data[gcecloud.UIDProvider] if !clusterIDExists { return "", fmt.Errorf("cluster ID not set") } if providerIDExists { return providerID, nil } return clusterID, nil }