github.com/pix4d/terravalet@v0.8.1-0.20240131132849-abcd6a79eeeb/cmdimport.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  )
    10  
    11  type ResourcesBundle struct {
    12  	ResourceChanges []ResourceChange `json:"resource_changes"`
    13  }
    14  
    15  type ResourceChange struct {
    16  	Address      string `json:"address"`
    17  	Type         string `json:"type"`
    18  	ProviderName string `json:"provider_name"`
    19  	Change       struct {
    20  		Actions []string    `json:"actions"`
    21  		After   interface{} `json:"after"`
    22  	} `json:"change"`
    23  }
    24  
    25  type Definitions struct {
    26  	Separator string   `json:"separator"`
    27  	Priority  int      `json:"priority"`
    28  	Variables []string `json:"variables"`
    29  }
    30  
    31  // Keep track of the asymmetry of import subcommand.
    32  // When importing, the up direction wants two parameters:
    33  //
    34  //	terraform import res-address res-id
    35  //
    36  // while the down direction wants only one parameter:
    37  //
    38  //	terraform state rm res-address
    39  type ImportElement struct {
    40  	Addr string
    41  	ID   string
    42  }
    43  
    44  func doImport(upPath, downPath, srcPlanPath, resourcesDefinitions string) error {
    45  	definitionsFile, err := os.Open(resourcesDefinitions)
    46  	if err != nil {
    47  		return fmt.Errorf("opening the definitions file: %v", err)
    48  	}
    49  	defer definitionsFile.Close()
    50  
    51  	srcPlanFile, err := os.Open(srcPlanPath)
    52  	if err != nil {
    53  		return fmt.Errorf("opening the terraform plan file: %v", err)
    54  	}
    55  	defer srcPlanFile.Close()
    56  
    57  	upFile, err := os.Create(upPath)
    58  	if err != nil {
    59  		return fmt.Errorf("creating the up file: %v", err)
    60  	}
    61  	defer upFile.Close()
    62  
    63  	downFile, err := os.Create(downPath)
    64  	if err != nil {
    65  		return fmt.Errorf("creating the down file: %v", err)
    66  	}
    67  	defer downFile.Close()
    68  
    69  	imports, removals, err := Import(srcPlanFile, definitionsFile)
    70  	if err != nil {
    71  		return fmt.Errorf("parse src-plan: %v", err)
    72  	}
    73  
    74  	if err := importUpScript(imports, upFile); err != nil {
    75  		return fmt.Errorf("writing the up script: %v", err)
    76  	}
    77  	if err := importDownScript(removals, downFile); err != nil {
    78  		return fmt.Errorf("writing the down script: %v", err)
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  func Import(rd, definitionsFile io.Reader) ([]ImportElement, []ImportElement, error) {
    85  	var imports []ImportElement
    86  	var removals []ImportElement
    87  	var configs map[string]Definitions
    88  	var resourcesBundle ResourcesBundle
    89  	var filteredResources []ResourceChange
    90  
    91  	plan, err := io.ReadAll(rd)
    92  	if err != nil {
    93  		return imports, removals,
    94  			fmt.Errorf("reading the plan file: %s", err)
    95  	}
    96  	if err = json.Unmarshal(plan, &resourcesBundle); err != nil {
    97  		return imports, removals,
    98  			fmt.Errorf("parsing the plan: %s", err)
    99  	}
   100  
   101  	defs, err := io.ReadAll(definitionsFile)
   102  	if err != nil {
   103  		return imports, removals,
   104  			fmt.Errorf("reading the definitions file: %s", err)
   105  	}
   106  	if err = json.Unmarshal(defs, &configs); err != nil {
   107  		return imports, removals,
   108  			fmt.Errorf("parsing resources definitions: %s", err)
   109  	}
   110  
   111  	// Return objects in the correct order if 'priority' parameter is set in provider configuration.
   112  	// The remove order is reversed (LIFO logic).
   113  
   114  	// Filter all "create" resources before going further
   115  	for _, resource := range resourcesBundle.ResourceChanges {
   116  		if resource.Change.Actions[0] == "create" {
   117  			filteredResources = append(filteredResources, resource)
   118  		}
   119  	}
   120  
   121  	if len(filteredResources) == 0 {
   122  		return imports, removals,
   123  			fmt.Errorf("src-plan doesn't contains resources to create")
   124  	}
   125  
   126  	for _, resource := range filteredResources {
   127  		// Proceed only if type is declared in resources definitions
   128  		if _, ok := configs[resource.Type]; !ok {
   129  			msg := fmt.Sprintf("Warning: resource %s is not defined. Check %s documentation\n",
   130  				resource.Type, resource.ProviderName)
   131  			fmt.Printf("\033[1;33m%s\033[0m", msg)
   132  			continue
   133  		}
   134  		resourceParams := configs[resource.Type]
   135  		var resID []string
   136  		after := resource.Change.After.(map[string]interface{})
   137  		for _, field := range resourceParams.Variables {
   138  			if _, ok := after[field]; !ok {
   139  				return imports, removals,
   140  					fmt.Errorf(
   141  						"error in resources definition %s: field '%s' doesn't exist in plan",
   142  						resource.Type, field)
   143  			}
   144  			subID, ok := after[field].(string)
   145  			if !ok {
   146  				return imports, removals,
   147  					fmt.Errorf("resource_changes: after: %s: type is %T; want: string",
   148  						field, after[field])
   149  			}
   150  			resID = append(resID, subID)
   151  		}
   152  
   153  		elem := ImportElement{
   154  			Addr: resource.Address,
   155  			ID:   strings.Join(resID, resourceParams.Separator)}
   156  
   157  		if resourceParams.Priority == 1 {
   158  			// Prepend
   159  			imports = append([]ImportElement{elem}, imports...)
   160  		} else {
   161  			// Append
   162  			imports = append(imports, elem)
   163  		}
   164  	}
   165  
   166  	if len(imports) == 0 {
   167  		return imports, removals,
   168  			fmt.Errorf("src-plan contains only undefined resources")
   169  	}
   170  
   171  	// The removals are the reverse of the imports.
   172  	removals = make([]ImportElement, 0, len(imports))
   173  	for i := len(imports) - 1; i >= 0; i-- {
   174  		removals = append(removals, imports[i])
   175  	}
   176  
   177  	return imports, removals, nil
   178  }
   179  
   180  func importUpScript(elements []ImportElement, out io.Writer) error {
   181  	cmd := "terraform import"
   182  	fmt.Fprintf(out, importScriptHeader, cmd, len(elements))
   183  	for _, elem := range elements {
   184  		fmt.Fprintf(out, "%s \\\n    %q %q\n\n", cmd, elem.Addr, elem.ID)
   185  	}
   186  	return nil
   187  }
   188  
   189  func importDownScript(elements []ImportElement, out io.Writer) error {
   190  	cmd := "terraform state rm"
   191  	fmt.Fprintf(out, importScriptHeader, cmd, len(elements))
   192  	for _, elem := range elements {
   193  		fmt.Fprintf(out, "%s \\\n    %q\n\n", cmd, elem.Addr)
   194  	}
   195  	return nil
   196  }
   197  
   198  const importScriptHeader = `#! /bin/sh
   199  # DO NOT EDIT. Generated by terravalet.
   200  # WARNING: check the order of resources before running this script.
   201  #
   202  # This script will %q %d items.
   203  
   204  # Uncomment this if you want to stop the script at first error
   205  # set -e
   206  set -x
   207  
   208  `