package google import ( "fmt" "log" "strconv" "strings" "google.golang.org/api/compute/v1" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/terraform" ) func resourceComputeInstanceMigrateState( v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { if is.Empty() { log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") return is, nil } var err error switch v { case 0: log.Println("[INFO] Found Compute Instance State v0; migrating to v1") is, err = migrateStateV0toV1(is) if err != nil { return is, err } fallthrough case 1: log.Println("[INFO] Found Compute Instance State v1; migrating to v2") is, err = migrateStateV1toV2(is) if err != nil { return is, err } fallthrough case 2: log.Println("[INFO] Found Compute Instance State v2; migrating to v3") is, err = migrateStateV2toV3(is) if err != nil { return is, err } fallthrough case 3: log.Println("[INFO] Found Compute Instance State v3; migrating to v4") is, err = migrateStateV3toV4(is, meta) if err != nil { return is, err } fallthrough case 4: log.Println("[INFO] Found Compute Instance State v4; migrating to v5") is, err = migrateStateV4toV5(is, meta) if err != nil { return is, err } fallthrough case 5: log.Println("[INFO] Found Compute Instance State v5; migrating to v6") is, err = migrateStateV5toV6(is) if err != nil { return is, err } // when adding case 6, make sure to turn this into a fallthrough return is, err default: return is, fmt.Errorf("Unexpected schema version: %d", v) } } func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) // Delete old count delete(is.Attributes, "metadata.#") newMetadata := make(map[string]string) for k, v := range is.Attributes { if !strings.HasPrefix(k, "metadata.") { continue } // We have a key that looks like "metadata.*" and we know it's not // metadata.# because we deleted it above, so it must be metadata.. // from the List of Maps. Just need to convert it to a single Map by // ditching the '' field. kParts := strings.SplitN(k, ".", 3) // Sanity check: all three parts should be there and should be a number badFormat := false if len(kParts) != 3 { badFormat = true } else if _, err := strconv.Atoi(kParts[1]); err != nil { badFormat = true } if badFormat { return is, fmt.Errorf( "migration error: found metadata key in unexpected format: %s", k) } // Rejoin as "metadata." newK := strings.Join([]string{kParts[0], kParts[2]}, ".") newMetadata[newK] = v delete(is.Attributes, k) } for k, v := range newMetadata { is.Attributes[k] = v } log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) return is, nil } func migrateStateV1toV2(is *terraform.InstanceState) (*terraform.InstanceState, error) { log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) // Maps service account index to list of scopes for that sccount newScopesMap := make(map[string][]string) for k, v := range is.Attributes { if !strings.HasPrefix(k, "service_account.") { continue } if k == "service_account.#" { continue } if strings.HasSuffix(k, ".scopes.#") { continue } if strings.HasSuffix(k, ".email") { continue } // Key is now of the form service_account.%d.scopes.%d kParts := strings.Split(k, ".") // Sanity check: all three parts should be there and should be a number badFormat := false if len(kParts) != 4 { badFormat = true } else if _, err := strconv.Atoi(kParts[1]); err != nil { badFormat = true } if badFormat { return is, fmt.Errorf( "migration error: found scope key in unexpected format: %s", k) } newScopesMap[kParts[1]] = append(newScopesMap[kParts[1]], v) delete(is.Attributes, k) } for service_acct_index, newScopes := range newScopesMap { for _, newScope := range newScopes { hash := hashcode.String(canonicalizeServiceScope(newScope)) newKey := fmt.Sprintf("service_account.%s.scopes.%d", service_acct_index, hash) is.Attributes[newKey] = newScope } } log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) return is, nil } func migrateStateV2toV3(is *terraform.InstanceState) (*terraform.InstanceState, error) { log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) is.Attributes["create_timeout"] = "4" log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) return is, nil } func migrateStateV3toV4(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) // Read instance from GCP. Since disks are not necessarily returned from the API in the order they were set, // we have no other way to know which source belongs to which attached disk. // Also note that the following code modifies the returned instance- if you need immutability, please change // this to make a copy of the needed data. config := meta.(*Config) instance, err := getInstanceFromInstanceState(config, is) if err != nil { return is, fmt.Errorf("migration error: %s", err) } diskList, err := getAllDisksFromInstanceState(config, is) if err != nil { return is, fmt.Errorf("migration error: %s", err) } allDisks := make(map[string]*compute.Disk) for _, disk := range diskList { allDisks[disk.Name] = disk } hasBootDisk := is.Attributes["boot_disk.#"] == "1" scratchDisks := 0 if v := is.Attributes["scratch_disk.#"]; v != "" { scratchDisks, err = strconv.Atoi(v) if err != nil { return is, fmt.Errorf("migration error: found scratch_disk.# value in unexpected format: %s", err) } } attachedDisks := 0 if v := is.Attributes["attached_disk.#"]; v != "" { attachedDisks, err = strconv.Atoi(v) if err != nil { return is, fmt.Errorf("migration error: found attached_disk.# value in unexpected format: %s", err) } } disks := 0 if v := is.Attributes["disk.#"]; v != "" { disks, err = strconv.Atoi(is.Attributes["disk.#"]) if err != nil { return is, fmt.Errorf("migration error: found disk.# value in unexpected format: %s", err) } } for i := 0; i < disks; i++ { if !hasBootDisk && i == 0 { is.Attributes["boot_disk.#"] = "1" // Note: the GCP API does not allow for scratch disks to be boot disks, so this situation // should never occur. if is.Attributes["disk.0.scratch_disk"] == "true" { return is, fmt.Errorf("migration error: found scratch disk at index 0") } for _, disk := range instance.Disks { if disk.Boot { is.Attributes["boot_disk.0.source"] = GetResourceNameFromSelfLink(disk.Source) is.Attributes["boot_disk.0.device_name"] = disk.DeviceName break } } is.Attributes["boot_disk.0.auto_delete"] = is.Attributes["disk.0.auto_delete"] is.Attributes["boot_disk.0.disk_encryption_key_raw"] = is.Attributes["disk.0.disk_encryption_key_raw"] is.Attributes["boot_disk.0.disk_encryption_key_sha256"] = is.Attributes["disk.0.disk_encryption_key_sha256"] if is.Attributes["disk.0.size"] != "" && is.Attributes["disk.0.size"] != "0" { is.Attributes["boot_disk.0.initialize_params.#"] = "1" is.Attributes["boot_disk.0.initialize_params.0.size"] = is.Attributes["disk.0.size"] } if is.Attributes["disk.0.type"] != "" { is.Attributes["boot_disk.0.initialize_params.#"] = "1" is.Attributes["boot_disk.0.initialize_params.0.type"] = is.Attributes["disk.0.type"] } if is.Attributes["disk.0.image"] != "" { is.Attributes["boot_disk.0.initialize_params.#"] = "1" is.Attributes["boot_disk.0.initialize_params.0.image"] = is.Attributes["disk.0.image"] } } else if is.Attributes[fmt.Sprintf("disk.%d.scratch", i)] == "true" { // Note: the GCP API does not allow for scratch disks without auto_delete, so this situation // should never occur. if is.Attributes[fmt.Sprintf("disk.%d.auto_delete", i)] != "true" { return is, fmt.Errorf("migration error: attempted to migrate scratch disk where auto_delete is not true") } is.Attributes[fmt.Sprintf("scratch_disk.%d.interface", scratchDisks)] = "SCSI" scratchDisks++ } else { // If disk is neither boot nor scratch, then it is attached. disk, err := getDiskFromAttributes(config, instance, allDisks, is.Attributes, i) if err != nil { return is, fmt.Errorf("migration error: %s", err) } is.Attributes[fmt.Sprintf("attached_disk.%d.source", attachedDisks)] = disk.Source is.Attributes[fmt.Sprintf("attached_disk.%d.device_name", attachedDisks)] = disk.DeviceName is.Attributes[fmt.Sprintf("attached_disk.%d.disk_encryption_key_raw", attachedDisks)] = is.Attributes[fmt.Sprintf("disk.%d.disk_encryption_key_raw", i)] is.Attributes[fmt.Sprintf("attached_disk.%d.disk_encryption_key_sha256", attachedDisks)] = is.Attributes[fmt.Sprintf("disk.%d.disk_encryption_key_sha256", i)] attachedDisks++ } } for k := range is.Attributes { if !strings.HasPrefix(k, "disk.") { continue } delete(is.Attributes, k) } if scratchDisks > 0 { is.Attributes["scratch_disk.#"] = strconv.Itoa(scratchDisks) } if attachedDisks > 0 { is.Attributes["attached_disk.#"] = strconv.Itoa(attachedDisks) } log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) return is, nil } func migrateStateV4toV5(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { if v := is.Attributes["disk.#"]; v != "" { return migrateStateV3toV4(is, meta) } return is, nil } func getInstanceFromInstanceState(config *Config, is *terraform.InstanceState) (*compute.Instance, error) { project, ok := is.Attributes["project"] if !ok { if config.Project == "" { return nil, fmt.Errorf("could not determine 'project'") } else { project = config.Project } } zone, ok := is.Attributes["zone"] if !ok { if config.Zone == "" { return nil, fmt.Errorf("could not determine 'zone'") } else { zone = config.Zone } } instance, err := config.clientCompute.Instances.Get( project, zone, is.ID).Do() if err != nil { return nil, fmt.Errorf("error reading instance: %s", err) } return instance, nil } func getAllDisksFromInstanceState(config *Config, is *terraform.InstanceState) ([]*compute.Disk, error) { project, ok := is.Attributes["project"] if !ok { if config.Project == "" { return nil, fmt.Errorf("could not determine 'project'") } else { project = config.Project } } zone, ok := is.Attributes["zone"] if !ok { if config.Zone == "" { return nil, fmt.Errorf("could not determine 'zone'") } else { zone = config.Zone } } diskList := []*compute.Disk{} token := "" for { disks, err := config.clientCompute.Disks.List(project, zone).PageToken(token).Do() if err != nil { return nil, fmt.Errorf("error reading disks: %s", err) } diskList = append(diskList, disks.Items...) token = disks.NextPageToken if token == "" { break } } return diskList, nil } func getDiskFromAttributes(config *Config, instance *compute.Instance, allDisks map[string]*compute.Disk, attributes map[string]string, i int) (*compute.AttachedDisk, error) { if diskSource := attributes[fmt.Sprintf("disk.%d.disk", i)]; diskSource != "" { return getDiskFromSource(instance, diskSource) } if deviceName := attributes[fmt.Sprintf("disk.%d.device_name", i)]; deviceName != "" { return getDiskFromDeviceName(instance, deviceName) } if encryptionKey := attributes[fmt.Sprintf("disk.%d.disk_encryption_key_raw", i)]; encryptionKey != "" { return getDiskFromEncryptionKey(instance, encryptionKey) } autoDelete, err := strconv.ParseBool(attributes[fmt.Sprintf("disk.%d.auto_delete", i)]) if err != nil { return nil, fmt.Errorf("error parsing auto_delete attribute of disk %d", i) } image := attributes[fmt.Sprintf("disk.%d.image", i)] // We know project and zone are set because we used them to read the instance project, ok := attributes["project"] if !ok { project = config.Project } zone := attributes["zone"] return getDiskFromAutoDeleteAndImage(config, instance, allDisks, autoDelete, image, project, zone) } func getDiskFromSource(instance *compute.Instance, source string) (*compute.AttachedDisk, error) { for _, disk := range instance.Disks { if disk.Boot == true || disk.Type == "SCRATCH" { // Ignore boot/scratch disks since this is just for finding attached disks continue } // we can just compare suffixes because terraform only allows setting "disk" by name and uses // the zone of the instance so we know there can be no duplicate names. if strings.HasSuffix(disk.Source, "/"+source) { return disk, nil } } return nil, fmt.Errorf("could not find attached disk with source %q", source) } func getDiskFromDeviceName(instance *compute.Instance, deviceName string) (*compute.AttachedDisk, error) { for _, disk := range instance.Disks { if disk.Boot == true || disk.Type == "SCRATCH" { // Ignore boot/scratch disks since this is just for finding attached disks continue } if disk.DeviceName == deviceName { return disk, nil } } return nil, fmt.Errorf("could not find attached disk with deviceName %q", deviceName) } func getDiskFromEncryptionKey(instance *compute.Instance, encryptionKey string) (*compute.AttachedDisk, error) { encryptionSha, err := hash256(encryptionKey) if err != nil { return nil, err } for _, disk := range instance.Disks { if disk.Boot == true || disk.Type == "SCRATCH" { // Ignore boot/scratch disks since this is just for finding attached disks continue } if disk.DiskEncryptionKey.Sha256 == encryptionSha { return disk, nil } } return nil, fmt.Errorf("could not find attached disk with encryption hash %q", encryptionSha) } func getDiskFromAutoDeleteAndImage(config *Config, instance *compute.Instance, allDisks map[string]*compute.Disk, autoDelete bool, image, project, zone string) (*compute.AttachedDisk, error) { img, err := resolveImage(config, project, image) if err != nil { return nil, err } imgParts := strings.Split(img, "/projects/") canonicalImage := imgParts[len(imgParts)-1] for i, disk := range instance.Disks { if disk.Boot == true || disk.Type == "SCRATCH" { // Ignore boot/scratch disks since this is just for finding attached disks continue } if disk.AutoDelete == autoDelete { // Read the disk to check if its image matches fullDisk := allDisks[GetResourceNameFromSelfLink(disk.Source)] sourceImage, err := getRelativePath(fullDisk.SourceImage) if err != nil { return nil, err } if canonicalImage == sourceImage { // Delete this disk because there might be multiple that match instance.Disks = append(instance.Disks[:i], instance.Disks[i+1:]...) return disk, nil } } } // We're not done! It's possible the disk was created with an image family rather than the image itself. // Now, do the exact same iteration but do some prefix matching to check if the families match. // This assumes that all disks with a given family have a sourceImage whose name starts with the name of // the image family. canonicalImage = strings.Replace(canonicalImage, "/family/", "/", -1) for i, disk := range instance.Disks { if disk.Boot == true || disk.Type == "SCRATCH" { // Ignore boot/scratch disks since this is just for finding attached disks continue } if disk.AutoDelete == autoDelete { // Read the disk to check if its image matches fullDisk := allDisks[GetResourceNameFromSelfLink(disk.Source)] sourceImage, err := getRelativePath(fullDisk.SourceImage) if err != nil { return nil, err } if strings.Contains(sourceImage, "/"+canonicalImage+"-") { // Delete this disk because there might be multiple that match instance.Disks = append(instance.Disks[:i], instance.Disks[i+1:]...) return disk, nil } } } return nil, fmt.Errorf("could not find attached disk with image %q", image) } func migrateStateV5toV6(is *terraform.InstanceState) (*terraform.InstanceState, error) { log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) if is.Attributes["boot_disk.0.initialize_params.#"] == "1" { if (is.Attributes["boot_disk.0.initialize_params.0.size"] == "0" || is.Attributes["boot_disk.0.initialize_params.0.size"] == "") && is.Attributes["boot_disk.0.initialize_params.0.type"] == "" && is.Attributes["boot_disk.0.initialize_params.0.image"] == "" { is.Attributes["boot_disk.0.initialize_params.#"] = "0" delete(is.Attributes, "boot_disk.0.initialize_params.0.size") delete(is.Attributes, "boot_disk.0.initialize_params.0.type") delete(is.Attributes, "boot_disk.0.initialize_params.0.image") } } log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes) return is, nil }