github.com/GoogleCloudPlatform/terraformer@v0.8.18/cmd/import.go (about)

     1  // Copyright 2018 The Terraformer Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  package cmd
    15  
    16  import (
    17  	"fmt"
    18  	"io/ioutil"
    19  	"log"
    20  	"os"
    21  	"sort"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/GoogleCloudPlatform/terraformer/terraformutils/terraformerstring"
    26  
    27  	"github.com/GoogleCloudPlatform/terraformer/terraformutils/providerwrapper"
    28  
    29  	"github.com/spf13/pflag"
    30  
    31  	"github.com/GoogleCloudPlatform/terraformer/terraformutils"
    32  	"github.com/GoogleCloudPlatform/terraformer/terraformutils/terraformoutput"
    33  
    34  	"github.com/spf13/cobra"
    35  )
    36  
    37  type ImportOptions struct {
    38  	Resources     []string
    39  	Excludes      []string
    40  	PathPattern   string
    41  	PathOutput    string
    42  	State         string
    43  	Bucket        string
    44  	Profile       string
    45  	Verbose       bool
    46  	Zone          string
    47  	Regions       []string
    48  	Projects      []string
    49  	ResourceGroup string
    50  	Connect       bool
    51  	Compact       bool
    52  	Filter        []string
    53  	Plan          bool `json:"-"`
    54  	Output        string
    55  	RetryCount    int
    56  	RetrySleepMs  int
    57  }
    58  
    59  const DefaultPathPattern = "{output}/{provider}/{service}/"
    60  const DefaultPathOutput = "generated"
    61  const DefaultState = "local"
    62  
    63  func newImportCmd() *cobra.Command {
    64  	options := ImportOptions{}
    65  	cmd := &cobra.Command{
    66  		Use:           "import",
    67  		Short:         "Import current state to Terraform configuration",
    68  		Long:          "Import current state to Terraform configuration",
    69  		SilenceUsage:  true,
    70  		SilenceErrors: false,
    71  		//Version:       version.String(),
    72  	}
    73  
    74  	cmd.AddCommand(newCmdPlanImporter(options))
    75  	for _, subcommand := range providerImporterSubcommands() {
    76  		providerCommand := subcommand(options)
    77  		_ = providerCommand.MarkPersistentFlagRequired("resources")
    78  		cmd.AddCommand(providerCommand)
    79  	}
    80  	return cmd
    81  }
    82  
    83  func Import(provider terraformutils.ProviderGenerator, options ImportOptions, args []string) error {
    84  
    85  	providerWrapper, options, err := initOptionsAndWrapper(provider, options, args)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	defer providerWrapper.Kill()
    90  	providerMapping := terraformutils.NewProvidersMapping(provider)
    91  
    92  	err = initAllServicesResources(providerMapping, options, args, providerWrapper)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	err = terraformutils.RefreshResourcesByProvider(providerMapping, providerWrapper)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	providerMapping.ConvertTFStates(providerWrapper)
   103  	// change structs with additional data for each resource
   104  	providerMapping.CleanupProviders()
   105  
   106  	err = importFromPlan(providerMapping, options, args)
   107  
   108  	return err
   109  }
   110  
   111  func initOptionsAndWrapper(provider terraformutils.ProviderGenerator, options ImportOptions, args []string) (*providerwrapper.ProviderWrapper, ImportOptions, error) {
   112  	err := provider.Init(args)
   113  	if err != nil {
   114  		return nil, options, err
   115  	}
   116  
   117  	if terraformerstring.ContainsString(options.Resources, "*") {
   118  		log.Println("Attempting an import of ALL resources in " + provider.GetName())
   119  		options.Resources = providerServices(provider)
   120  	}
   121  
   122  	if options.Excludes != nil {
   123  		localSlice := []string{}
   124  		for _, r := range options.Resources {
   125  			remove := false
   126  			for _, e := range options.Excludes {
   127  				if r == e {
   128  					remove = true
   129  					log.Println("Excluding resource " + e)
   130  				}
   131  			}
   132  			if !remove {
   133  				localSlice = append(localSlice, r)
   134  			}
   135  		}
   136  		options.Resources = localSlice
   137  	}
   138  
   139  	providerWrapper, err := providerwrapper.NewProviderWrapper(provider.GetName(), provider.GetConfig(), options.Verbose, map[string]int{"retryCount": options.RetryCount, "retrySleepMs": options.RetrySleepMs})
   140  	if err != nil {
   141  		return nil, options, err
   142  	}
   143  
   144  	return providerWrapper, options, nil
   145  }
   146  
   147  func initAllServicesResources(providersMapping *terraformutils.ProvidersMapping, options ImportOptions, args []string, providerWrapper *providerwrapper.ProviderWrapper) error {
   148  	numOfResources := len(options.Resources)
   149  	var wg sync.WaitGroup
   150  	wg.Add(numOfResources)
   151  
   152  	var failedServices []string
   153  
   154  	for _, service := range options.Resources {
   155  		serviceProvider := providersMapping.AddServiceToProvider(service)
   156  		err := serviceProvider.Init(args)
   157  		if err != nil {
   158  			return err
   159  		}
   160  		err = initServiceResources(service, serviceProvider, options, providerWrapper)
   161  		if err != nil {
   162  			failedServices = append(failedServices, service)
   163  		}
   164  	}
   165  
   166  	// remove providers that failed to init their service
   167  	providersMapping.RemoveServices(failedServices)
   168  	providersMapping.ProcessResources(false)
   169  
   170  	return nil
   171  }
   172  
   173  func importFromPlan(providerMapping *terraformutils.ProvidersMapping, options ImportOptions, args []string) error {
   174  	plan := &ImportPlan{
   175  		Provider:         providerMapping.GetBaseProvider().GetName(),
   176  		Options:          options,
   177  		Args:             args,
   178  		ImportedResource: map[string][]terraformutils.Resource{},
   179  	}
   180  
   181  	resourcesByService := providerMapping.GetResourcesByService()
   182  	for service := range resourcesByService {
   183  		plan.ImportedResource[service] = append(plan.ImportedResource[service], resourcesByService[service]...)
   184  	}
   185  
   186  	if options.Plan {
   187  		path := Path(options.PathPattern, providerMapping.GetBaseProvider().GetName(), "terraformer", options.PathOutput)
   188  		return ExportPlanFile(plan, path, "plan.json")
   189  	}
   190  
   191  	return ImportFromPlan(providerMapping.GetBaseProvider(), plan)
   192  }
   193  
   194  func initServiceResources(service string, provider terraformutils.ProviderGenerator,
   195  	options ImportOptions, providerWrapper *providerwrapper.ProviderWrapper) error {
   196  	log.Println(provider.GetName() + " importing... " + service)
   197  	err := provider.InitService(service, options.Verbose)
   198  	if err != nil {
   199  		log.Printf("%s error importing %s, err: %s\n", provider.GetName(), service, err)
   200  		return err
   201  	}
   202  	provider.GetService().ParseFilters(options.Filter)
   203  	err = provider.GetService().InitResources()
   204  	if err != nil {
   205  		log.Printf("%s error initializing resources in service %s, err: %s\n", provider.GetName(), service, err)
   206  		return err
   207  	}
   208  
   209  	provider.GetService().PopulateIgnoreKeys(providerWrapper)
   210  	provider.GetService().InitialCleanup()
   211  	log.Println(provider.GetName() + " done importing " + service)
   212  
   213  	return nil
   214  }
   215  
   216  func ImportFromPlan(provider terraformutils.ProviderGenerator, plan *ImportPlan) error {
   217  	options := plan.Options
   218  	importedResource := plan.ImportedResource
   219  	isServicePath := strings.Contains(options.PathPattern, "{service}")
   220  
   221  	if options.Connect {
   222  		log.Println(provider.GetName() + " Connecting.... ")
   223  		importedResource = terraformutils.ConnectServices(importedResource, isServicePath, provider.GetResourceConnections())
   224  	}
   225  
   226  	if !isServicePath {
   227  		var compactedResources []terraformutils.Resource
   228  		for _, resources := range importedResource {
   229  			compactedResources = append(compactedResources, resources...)
   230  		}
   231  		e := printService(provider, "", options, compactedResources, importedResource)
   232  		if e != nil {
   233  			return e
   234  		}
   235  	} else {
   236  		for serviceName, resources := range importedResource {
   237  			e := printService(provider, serviceName, options, resources, importedResource)
   238  			if e != nil {
   239  				return e
   240  			}
   241  		}
   242  	}
   243  	return nil
   244  }
   245  
   246  func printService(provider terraformutils.ProviderGenerator, serviceName string, options ImportOptions, resources []terraformutils.Resource, importedResource map[string][]terraformutils.Resource) error {
   247  	log.Println(provider.GetName() + " save " + serviceName)
   248  	// Print HCL files for Resources
   249  	path := Path(options.PathPattern, provider.GetName(), serviceName, options.PathOutput)
   250  	err := terraformoutput.OutputHclFiles(resources, provider, path, serviceName, options.Compact, options.Output)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	tfStateFile, err := terraformutils.PrintTfState(resources)
   255  	if err != nil {
   256  		return err
   257  	}
   258  	// print or upload State file
   259  	if options.State == "bucket" {
   260  		log.Println(provider.GetName() + " upload tfstate to  bucket " + options.Bucket)
   261  		bucket := terraformoutput.BucketState{
   262  			Name: options.Bucket,
   263  		}
   264  		if err := bucket.BucketUpload(path, tfStateFile); err != nil {
   265  			return err
   266  		}
   267  		// create Bucket file
   268  		if bucketStateDataFile, err := terraformutils.Print(bucket.BucketGetTfData(path), map[string]struct{}{}, options.Output); err == nil {
   269  			terraformoutput.PrintFile(path+"/bucket.tf", bucketStateDataFile)
   270  		}
   271  	} else {
   272  		if serviceName == "" {
   273  			log.Println(provider.GetName() + " save tfstate")
   274  		} else {
   275  			log.Println(provider.GetName() + " save tfstate for " + serviceName)
   276  		}
   277  		if err := ioutil.WriteFile(path+"/terraform.tfstate", tfStateFile, os.ModePerm); err != nil {
   278  			return err
   279  		}
   280  	}
   281  	// Print hcl variables.tf
   282  	if serviceName != "" {
   283  		if options.Connect && len(provider.GetResourceConnections()[serviceName]) > 0 {
   284  			variables := map[string]map[string]map[string]interface{}{}
   285  			variables["data"] = map[string]map[string]interface{}{}
   286  			variables["data"]["terraform_remote_state"] = map[string]interface{}{}
   287  			if options.State == "bucket" {
   288  				bucket := terraformoutput.BucketState{
   289  					Name: options.Bucket,
   290  				}
   291  				for k := range provider.GetResourceConnections()[serviceName] {
   292  					if _, exist := importedResource[k]; !exist {
   293  						continue
   294  					}
   295  					variables["data"]["terraform_remote_state"][k] = map[string]interface{}{
   296  						"backend": "gcs",
   297  						"config":  bucket.BucketGetTfData(strings.ReplaceAll(path, serviceName, k)),
   298  					}
   299  				}
   300  			} else {
   301  				for k := range provider.GetResourceConnections()[serviceName] {
   302  					if _, exist := importedResource[k]; !exist {
   303  						continue
   304  					}
   305  					variables["data"]["terraform_remote_state"][k] = map[string]interface{}{
   306  						"backend": "local",
   307  						"config": map[string]interface{}{
   308  							"path": strings.Repeat("../", strings.Count(path, "/")) + strings.ReplaceAll(path, serviceName, k) + "terraform.tfstate",
   309  						},
   310  					}
   311  				}
   312  			}
   313  			// create variables file
   314  			if len(provider.GetResourceConnections()[serviceName]) > 0 && options.Connect && len(variables["data"]["terraform_remote_state"]) > 0 {
   315  				variablesFile, err := terraformutils.Print(variables, map[string]struct{}{"config": {}}, options.Output)
   316  				if err != nil {
   317  					return err
   318  				}
   319  				terraformoutput.PrintFile(path+"/variables."+terraformoutput.GetFileExtension(options.Output), variablesFile)
   320  			}
   321  		}
   322  	} else {
   323  		if options.Connect {
   324  			variables := map[string]map[string]map[string]interface{}{}
   325  			variables["data"] = map[string]map[string]interface{}{}
   326  			variables["data"]["terraform_remote_state"] = map[string]interface{}{}
   327  			if options.State == "bucket" {
   328  				bucket := terraformoutput.BucketState{
   329  					Name: options.Bucket,
   330  				}
   331  				variables["data"]["terraform_remote_state"]["local"] = map[string]interface{}{
   332  					"backend": "gcs",
   333  					"config":  bucket.BucketGetTfData(path),
   334  				}
   335  			} else {
   336  				variables["data"]["terraform_remote_state"]["local"] = map[string]interface{}{
   337  					"backend": "local",
   338  					"config": map[string]interface{}{
   339  						"path": "terraform.tfstate",
   340  					},
   341  				}
   342  			}
   343  			// create variables file
   344  			if options.Connect {
   345  				variablesFile, err := terraformutils.Print(variables, map[string]struct{}{"config": {}}, options.Output)
   346  				if err != nil {
   347  					return err
   348  				}
   349  				terraformoutput.PrintFile(path+"/variables."+terraformoutput.GetFileExtension(options.Output), variablesFile)
   350  			}
   351  		}
   352  	}
   353  	return nil
   354  }
   355  
   356  func Path(pathPattern, providerName, serviceName, output string) string {
   357  	return strings.NewReplacer(
   358  		"{provider}", providerName,
   359  		"{service}", serviceName,
   360  		"{output}", output,
   361  	).Replace(pathPattern)
   362  }
   363  
   364  func listCmd(provider terraformutils.ProviderGenerator) *cobra.Command {
   365  	cmd := &cobra.Command{
   366  		Use:   "list",
   367  		Short: "List supported resources for " + provider.GetName() + " provider",
   368  		Long:  "List supported resources for " + provider.GetName() + " provider",
   369  		RunE: func(cmd *cobra.Command, args []string) error {
   370  			services := providerServices(provider)
   371  			for _, k := range services {
   372  				fmt.Println(k)
   373  			}
   374  			return nil
   375  		},
   376  	}
   377  	cmd.Flags().AddFlag(&pflag.Flag{Name: "resources"})
   378  	return cmd
   379  }
   380  
   381  func providerServices(provider terraformutils.ProviderGenerator) []string {
   382  	var services []string
   383  	for k := range provider.GetSupportedService() {
   384  		services = append(services, k)
   385  	}
   386  	sort.Strings(services)
   387  	return services
   388  }
   389  
   390  func baseProviderFlags(flag *pflag.FlagSet, options *ImportOptions, sampleRes, sampleFilters string) {
   391  	flag.BoolVarP(&options.Connect, "connect", "c", true, "")
   392  	flag.BoolVarP(&options.Compact, "compact", "C", false, "")
   393  	flag.StringSliceVarP(&options.Resources, "resources", "r", []string{}, sampleRes)
   394  	flag.StringSliceVarP(&options.Excludes, "excludes", "x", []string{}, sampleRes)
   395  	flag.StringVarP(&options.PathPattern, "path-pattern", "p", DefaultPathPattern, "{output}/{provider}/")
   396  	flag.StringVarP(&options.PathOutput, "path-output", "o", DefaultPathOutput, "")
   397  	flag.StringVarP(&options.State, "state", "s", DefaultState, "local or bucket")
   398  	flag.StringVarP(&options.Bucket, "bucket", "b", "", "gs://terraform-state")
   399  	flag.StringSliceVarP(&options.Filter, "filter", "f", []string{}, sampleFilters)
   400  	flag.BoolVarP(&options.Verbose, "verbose", "v", false, "")
   401  	flag.StringVarP(&options.Output, "output", "O", "hcl", "output format hcl or json")
   402  	flag.IntVarP(&options.RetryCount, "retry-number", "n", 5, "number of retries to perform when refresh fails")
   403  	flag.IntVarP(&options.RetrySleepMs, "retry-sleep-ms", "m", 300, "time in ms to sleep between retries")
   404  }