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