terraformutils/providerwrapper/provider.go (306 lines of code) (raw):

// Copyright 2018 The Terraformer Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package providerwrapper //nolint import ( "errors" "fmt" "log" "os" "os/exec" "runtime" "strings" "time" "github.com/GoogleCloudPlatform/terraformer/terraformutils/terraformerstring" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform/configs/configschema" tfplugin "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/version" ) // DefaultDataDir is the default directory for storing local data. const DefaultDataDir = ".terraform" // DefaultPluginVendorDir is the location in the config directory to look for // user-added plugin binaries. Terraform only reads from this path if it // exists, it is never created by terraform. const DefaultPluginVendorDirV12 = "terraform.d/plugins/" + pluginMachineName // pluginMachineName is the directory name used in new plugin paths. const pluginMachineName = runtime.GOOS + "_" + runtime.GOARCH type ProviderWrapper struct { Provider *tfplugin.GRPCProvider client *plugin.Client rpcClient plugin.ClientProtocol providerName string config cty.Value schema *providers.GetSchemaResponse retryCount int retrySleepMs int } func NewProviderWrapper(providerName string, providerConfig cty.Value, verbose bool, options ...map[string]int) (*ProviderWrapper, error) { p := &ProviderWrapper{retryCount: 5, retrySleepMs: 300} p.providerName = providerName p.config = providerConfig if len(options) > 0 { retryCount, hasOption := options[0]["retryCount"] if hasOption { p.retryCount = retryCount } retrySleepMs, hasOption := options[0]["retrySleepMs"] if hasOption { p.retrySleepMs = retrySleepMs } } err := p.initProvider(verbose) return p, err } func (p *ProviderWrapper) Kill() { p.client.Kill() } func (p *ProviderWrapper) GetSchema() *providers.GetSchemaResponse { if p.schema == nil { r := p.Provider.GetSchema() p.schema = &r } return p.schema } func (p *ProviderWrapper) GetReadOnlyAttributes(resourceTypes []string) (map[string][]string, error) { r := p.GetSchema() if r.Diagnostics.HasErrors() { return nil, r.Diagnostics.Err() } readOnlyAttributes := map[string][]string{} for resourceName, obj := range r.ResourceTypes { if terraformerstring.ContainsString(resourceTypes, resourceName) { readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^id$") for k, v := range obj.Block.Attributes { if !v.Optional && !v.Required { if v.Type.IsListType() || v.Type.IsSetType() { readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^"+k+"\\.(.*)") } else { readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^"+k+"$") } } } readOnlyAttributes[resourceName] = p.readObjBlocks(obj.Block.BlockTypes, readOnlyAttributes[resourceName], "-1") } } return readOnlyAttributes, nil } func (p *ProviderWrapper) readObjBlocks(block map[string]*configschema.NestedBlock, readOnlyAttributes []string, parent string) []string { for k, v := range block { if len(v.BlockTypes) > 0 { if parent == "-1" { readOnlyAttributes = p.readObjBlocks(v.BlockTypes, readOnlyAttributes, k) } else { readOnlyAttributes = p.readObjBlocks(v.BlockTypes, readOnlyAttributes, parent+"\\.[0-9]+\\."+k) } } fieldCount := 0 for key, l := range v.Attributes { if !l.Optional && !l.Required { fieldCount++ switch v.Nesting { case configschema.NestingList: if parent == "-1" { readOnlyAttributes = append(readOnlyAttributes, "^"+k+"\\.[0-9]+\\."+key+"($|\\.[0-9]+|\\.#)") } else { readOnlyAttributes = append(readOnlyAttributes, "^"+parent+"\\.(.*)\\."+key+"$") } case configschema.NestingSet: if parent == "-1" { readOnlyAttributes = append(readOnlyAttributes, "^"+k+"\\.[0-9]+\\."+key+"$") } else { readOnlyAttributes = append(readOnlyAttributes, "^"+parent+"\\.(.*)\\."+key+"($|\\.(.*))") } case configschema.NestingMap: readOnlyAttributes = append(readOnlyAttributes, parent+"\\."+key) default: readOnlyAttributes = append(readOnlyAttributes, parent+"\\."+key+"$") } } } if fieldCount == len(v.Block.Attributes) && fieldCount > 0 && len(v.BlockTypes) == 0 { readOnlyAttributes = append(readOnlyAttributes, "^"+k) } } return readOnlyAttributes } func (p *ProviderWrapper) Refresh(info *terraform.InstanceInfo, state *terraform.InstanceState) (*terraform.InstanceState, error) { schema := p.GetSchema() impliedType := schema.ResourceTypes[info.Type].Block.ImpliedType() priorState, err := state.AttrsAsObjectValue(impliedType) if err != nil { return nil, err } successReadResource := false resp := providers.ReadResourceResponse{} for i := 0; i < p.retryCount; i++ { resp = p.Provider.ReadResource(providers.ReadResourceRequest{ TypeName: info.Type, PriorState: priorState, Private: []byte{}, }) if resp.Diagnostics.HasErrors() { log.Println(resp.Diagnostics.Err()) log.Printf("WARN: Fail read resource from provider, wait %dms before retry\n", p.retrySleepMs) time.Sleep(time.Duration(p.retrySleepMs) * time.Millisecond) continue } else { successReadResource = true break } } if !successReadResource { log.Println("Fail read resource from provider, trying import command") // retry with regular import command - without resource attributes importResponse := p.Provider.ImportResourceState(providers.ImportResourceStateRequest{ TypeName: info.Type, ID: state.ID, }) if importResponse.Diagnostics.HasErrors() { return nil, resp.Diagnostics.Err() } if len(importResponse.ImportedResources) == 0 { return nil, errors.New("not able to import resource for a given ID") } return terraform.NewInstanceStateShimmedFromValue(importResponse.ImportedResources[0].State, int(schema.ResourceTypes[info.Type].Version)), nil } if resp.NewState.IsNull() { msg := fmt.Sprintf("ERROR: Read resource response is null for resource %s", info.Id) return nil, errors.New(msg) } return terraform.NewInstanceStateShimmedFromValue(resp.NewState, int(schema.ResourceTypes[info.Type].Version)), nil } func (p *ProviderWrapper) initProvider(verbose bool) error { providerFilePath, err := getProviderFileName(p.providerName) if err != nil { return err } options := hclog.LoggerOptions{ Name: "plugin", Level: hclog.Error, Output: os.Stdout, } if verbose { options.Level = hclog.Trace } logger := hclog.New(&options) p.client = plugin.NewClient( &plugin.ClientConfig{ Cmd: exec.Command(providerFilePath), HandshakeConfig: tfplugin.Handshake, VersionedPlugins: tfplugin.VersionedPlugins, Managed: true, Logger: logger, AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, AutoMTLS: true, }) p.rpcClient, err = p.client.Client() if err != nil { return err } raw, err := p.rpcClient.Dispense(tfplugin.ProviderPluginName) if err != nil { return err } p.Provider = raw.(*tfplugin.GRPCProvider) config, err := p.GetSchema().Provider.Block.CoerceValue(p.config) if err != nil { return err } p.Provider.Configure(providers.ConfigureRequest{ TerraformVersion: version.Version, Config: config, }) return nil } func getProviderFileName(providerName string) (string, error) { defaultDataDir := os.Getenv("TF_DATA_DIR") if defaultDataDir == "" { defaultDataDir = DefaultDataDir } providerFilePath, err := getProviderFileNameV13andV14(defaultDataDir, providerName) if err != nil || providerFilePath == "" { providerFilePath, err = getProviderFileNameV13andV14(os.Getenv("HOME")+string(os.PathSeparator)+ ".terraform.d", providerName) } if err != nil || providerFilePath == "" { return getProviderFileNameV12(providerName) } return providerFilePath, nil } func getProviderFileNameV13andV14(prefix, providerName string) (string, error) { // Read terraform v14 file path registryDir := prefix + string(os.PathSeparator) + "providers" + string(os.PathSeparator) + "registry.terraform.io" providerDirs, err := os.ReadDir(registryDir) if err != nil { // Read terraform v13 file path registryDir = prefix + string(os.PathSeparator) + "plugins" + string(os.PathSeparator) + "registry.terraform.io" providerDirs, err = os.ReadDir(registryDir) if err != nil { return "", err } } providerFilePath := "" for _, providerDir := range providerDirs { pluginPath := registryDir + string(os.PathSeparator) + providerDir.Name() + string(os.PathSeparator) + providerName dirs, err := os.ReadDir(pluginPath) if err != nil { continue } for _, dir := range dirs { if !dir.IsDir() { continue } for _, dir := range dirs { fullPluginPath := pluginPath + string(os.PathSeparator) + dir.Name() + string(os.PathSeparator) + runtime.GOOS + "_" + runtime.GOARCH files, err := os.ReadDir(fullPluginPath) if err == nil { for _, file := range files { if strings.HasPrefix(file.Name(), "terraform-provider-"+providerName) { providerFilePath = fullPluginPath + string(os.PathSeparator) + file.Name() } } } } } } return providerFilePath, nil } func getProviderFileNameV12(providerName string) (string, error) { defaultDataDir := os.Getenv("TF_DATA_DIR") if defaultDataDir == "" { defaultDataDir = DefaultDataDir } pluginPath := defaultDataDir + string(os.PathSeparator) + "plugins" + string(os.PathSeparator) + runtime.GOOS + "_" + runtime.GOARCH files, err := os.ReadDir(pluginPath) if err != nil { pluginPath = os.Getenv("HOME") + string(os.PathSeparator) + "." + DefaultPluginVendorDirV12 files, err = os.ReadDir(pluginPath) if err != nil { return "", err } } providerFilePath := "" for _, file := range files { if file.IsDir() { continue } if strings.HasPrefix(file.Name(), "terraform-provider-"+providerName) { providerFilePath = pluginPath + string(os.PathSeparator) + file.Name() } } return providerFilePath, nil } func GetProviderVersion(providerName string) string { providerFilePath, err := getProviderFileName(providerName) if err != nil { log.Println("Can't find provider file path. Ensure that you are following https://www.terraform.io/docs/configuration/providers.html#third-party-plugins.") return "" } t := strings.Split(providerFilePath, string(os.PathSeparator)) providerFileName := t[len(t)-1] providerFileNameParts := strings.Split(providerFileName, "_") if len(providerFileNameParts) < 2 { log.Println("Can't find provider version. Ensure that you are following https://www.terraform.io/docs/configuration/providers.html#plugin-names-and-versions.") return "" } providerVersion := providerFileNameParts[1] return "~> " + strings.TrimPrefix(providerVersion, "v") }