github.com/mponton/terratest@v0.44.0/modules/gcp/compute_test.go (about)

     1  //go:build gcp
     2  // +build gcp
     3  
     4  // NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.
     5  
     6  package gcp
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"reflect"
    12  	"regexp"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/mponton/terratest/modules/retry"
    17  	"github.com/stretchr/testify/assert"
    18  	"google.golang.org/api/compute/v1"
    19  )
    20  
    21  const DEFAULT_MACHINE_TYPE = "f1-micro"
    22  const DEFAULT_IMAGE_FAMILY_PROJECT_NAME = "ubuntu-os-cloud"
    23  const DEFAULT_IMAGE_FAMILY_NAME = "family/ubuntu-2204-lts"
    24  
    25  // Zones that support running f1-micro instances
    26  var ZonesThatSupportF1Micro = []string{"us-central1-a", "us-east1-b", "us-west1-a", "europe-north1-a", "europe-west1-b", "europe-central2-a"}
    27  
    28  func TestGetPublicIpOfInstance(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	instanceName := RandomValidGcpName()
    32  	projectID := GetGoogleProjectIDFromEnvVar(t)
    33  	zone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)
    34  
    35  	createComputeInstance(t, projectID, zone, instanceName)
    36  	defer deleteComputeInstance(t, projectID, zone, instanceName)
    37  
    38  	// Now that our Instance is launched, attempt to query the public IP
    39  	maxRetries := 10
    40  	sleepBetweenRetries := 3 * time.Second
    41  
    42  	ip := retry.DoWithRetry(t, "Read IP address of Compute Instance", maxRetries, sleepBetweenRetries, func() (string, error) {
    43  		// Consider attempting to connect to the Compute Instance at this IP in the future, but for now, we just call the
    44  		// the function to ensure we don't have errors
    45  		instance := FetchInstance(t, projectID, instanceName)
    46  		ip := instance.GetPublicIp(t)
    47  
    48  		if ip == "" {
    49  			return "", fmt.Errorf("Got blank IP. Retrying.\n")
    50  		}
    51  		return ip, nil
    52  	})
    53  
    54  	fmt.Printf("Public IP of Compute Instance %s = %s\n", instanceName, ip)
    55  }
    56  
    57  func TestZoneUrlToZone(t *testing.T) {
    58  	t.Parallel()
    59  
    60  	testCases := []struct {
    61  		zoneUrl      string
    62  		expectedZone string
    63  	}{
    64  		{"https://www.googleapis.com/compute/v1/projects/terratest-123456/zones/asia-east1-b", "asia-east1-b"},
    65  		{"https://www.googleapis.com/compute/v1/projects/terratest-123456/zones/us-east1-a", "us-east1-a"},
    66  	}
    67  
    68  	for _, tc := range testCases {
    69  		zone := ZoneUrlToZone(tc.zoneUrl)
    70  		assert.Equal(t, zone, tc.expectedZone, "Zone not extracted successfully from Zone URL")
    71  	}
    72  }
    73  
    74  func TestGetAndSetLabels(t *testing.T) {
    75  	t.Parallel()
    76  
    77  	instanceName := RandomValidGcpName()
    78  	projectID := GetGoogleProjectIDFromEnvVar(t)
    79  
    80  	zone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)
    81  
    82  	createComputeInstance(t, projectID, zone, instanceName)
    83  	defer deleteComputeInstance(t, projectID, zone, instanceName)
    84  
    85  	// Now that our Instance is launched, set the labels. Note that in GCP label keys and values can only contain
    86  	// lowercase letters, numeric characters, underscores and dashes.
    87  	instance := FetchInstance(t, projectID, instanceName)
    88  
    89  	labelsToWrite := map[string]string{
    90  		"context": "terratest",
    91  	}
    92  	instance.SetLabels(t, labelsToWrite)
    93  
    94  	// Now attempt to read the labels we just set.
    95  	maxRetries := 30
    96  	sleepBetweenRetries := 3 * time.Second
    97  
    98  	retry.DoWithRetry(t, "Read newly set labels", maxRetries, sleepBetweenRetries, func() (string, error) {
    99  		instance := FetchInstance(t, projectID, instanceName)
   100  		labelsFromRead := instance.GetLabels(t)
   101  		if !reflect.DeepEqual(labelsFromRead, labelsToWrite) {
   102  			return "", fmt.Errorf("Labels that were written did not match labels that were read. Retrying.\n")
   103  		}
   104  
   105  		return "", nil
   106  	})
   107  }
   108  
   109  // Set custom metadata on a Compute Instance, and then verify it was set as expected
   110  func TestGetAndSetMetadata(t *testing.T) {
   111  	t.Parallel()
   112  
   113  	projectID := GetGoogleProjectIDFromEnvVar(t)
   114  	instanceName := RandomValidGcpName()
   115  
   116  	zone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)
   117  
   118  	// Create a new Compute Instance
   119  	createComputeInstance(t, projectID, zone, instanceName)
   120  	defer deleteComputeInstance(t, projectID, zone, instanceName)
   121  
   122  	// Set the metadata
   123  	instance := FetchInstance(t, projectID, instanceName)
   124  
   125  	metadataToWrite := map[string]string{
   126  		"foo": "bar",
   127  	}
   128  	instance.SetMetadata(t, metadataToWrite)
   129  
   130  	// Now attempt to read the metadata we just set
   131  	maxRetries := 30
   132  	sleepBetweenRetries := 3 * time.Second
   133  
   134  	retry.DoWithRetry(t, "Read newly set metadata", maxRetries, sleepBetweenRetries, func() (string, error) {
   135  		instance := FetchInstance(t, projectID, instanceName)
   136  		metadataFromRead := instance.GetMetadata(t)
   137  		for _, metadataItem := range metadataFromRead {
   138  			for key, val := range metadataToWrite {
   139  				if metadataItem.Key == key && *metadataItem.Value == val {
   140  					return "", nil
   141  				}
   142  			}
   143  		}
   144  
   145  		fmt.Printf("Metadata to write: %+v\nMetadata from read: %+v\n", metadataToWrite, metadataFromRead)
   146  
   147  		return "", fmt.Errorf("Metadata that was written was not found in metadata that was read. Retrying.\n")
   148  	})
   149  }
   150  
   151  // Helper function to launch a Compute Instance. This function is useful for quickly iterating on automated tests. But
   152  // if you're writing a test that resembles real-world code that Terratest users may write, you should create a Compute
   153  // Instance using a Terraform apply, similar to the tests in /test.
   154  func createComputeInstance(t *testing.T, projectID string, zone string, name string) {
   155  	t.Logf("Launching new Compute Instance %s\n", name)
   156  
   157  	// This RegEx was pulled straight from the GCP API error messages that complained when it's not honored
   158  	validNameExp := `^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$`
   159  	regEx := regexp.MustCompile(validNameExp)
   160  
   161  	if !regEx.MatchString(name) {
   162  		t.Fatalf("Invalid Compute Instance name: %s. Must match RegEx %s\n", name, validNameExp)
   163  	}
   164  
   165  	machineType := DEFAULT_MACHINE_TYPE
   166  	sourceImageFamilyProjectName := DEFAULT_IMAGE_FAMILY_PROJECT_NAME
   167  	sourceImageFamilyName := DEFAULT_IMAGE_FAMILY_NAME
   168  
   169  	// Per GCP docs (https://cloud.google.com/compute/docs/reference/rest/v1/instances/setMachineType), the MachineType
   170  	// is actually specified as a partial URL
   171  	machineTypeURL := fmt.Sprintf("zones/%s/machineTypes/%s", zone, machineType)
   172  	sourceImageURL := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", sourceImageFamilyProjectName, sourceImageFamilyName)
   173  
   174  	// Based on the properties listed as required at https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert
   175  	// plus a somewhat painful cycle of add-next-property-try-fix-error-message-repeat.
   176  	instanceConfig := &compute.Instance{
   177  		Name:        name,
   178  		MachineType: machineTypeURL,
   179  		NetworkInterfaces: []*compute.NetworkInterface{
   180  			&compute.NetworkInterface{
   181  				AccessConfigs: []*compute.AccessConfig{
   182  					&compute.AccessConfig{},
   183  				},
   184  			},
   185  		},
   186  		Disks: []*compute.AttachedDisk{
   187  			&compute.AttachedDisk{
   188  				AutoDelete: true,
   189  				Boot:       true,
   190  				InitializeParams: &compute.AttachedDiskInitializeParams{
   191  					SourceImage: sourceImageURL,
   192  				},
   193  			},
   194  		},
   195  	}
   196  
   197  	service, err := NewComputeServiceE(t)
   198  	if err != nil {
   199  		t.Fatal(err)
   200  	}
   201  
   202  	// Create the Compute Instance
   203  	ctx := context.Background()
   204  	_, err = service.Instances.Insert(projectID, zone, instanceConfig).Context(ctx).Do()
   205  	if err != nil {
   206  		t.Fatalf("Error launching new Compute Instance: %s", err)
   207  	}
   208  }
   209  
   210  // Helper function that destroys the given Compute Instance and all of its attached disks.
   211  func deleteComputeInstance(t *testing.T, projectID string, zone string, name string) {
   212  	t.Logf("Deleting Compute Instance %s\n", name)
   213  
   214  	service, err := NewComputeServiceE(t)
   215  	if err != nil {
   216  		t.Fatal(err)
   217  	}
   218  
   219  	// Delete the Compute Instance
   220  	ctx := context.Background()
   221  	_, err = service.Instances.Delete(projectID, zone, name).Context(ctx).Do()
   222  	if err != nil {
   223  		t.Fatalf("Error deleting Compute Instance: %s", err)
   224  	}
   225  }
   226  
   227  // TODO: Add additional automated tests to cover remaining functions in compute.go