github.com/GoogleCloudPlatform/terraformer@v0.8.18/terraformutils/providerwrapper/provider.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  
    15  package providerwrapper //nolint
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"log"
    22  	"os"
    23  	"os/exec"
    24  	"runtime"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/GoogleCloudPlatform/terraformer/terraformutils/terraformerstring"
    29  
    30  	"github.com/zclconf/go-cty/cty"
    31  
    32  	"github.com/hashicorp/go-hclog"
    33  	"github.com/hashicorp/go-plugin"
    34  	"github.com/hashicorp/terraform/configs/configschema"
    35  	tfplugin "github.com/hashicorp/terraform/plugin"
    36  	"github.com/hashicorp/terraform/providers"
    37  	"github.com/hashicorp/terraform/terraform"
    38  	"github.com/hashicorp/terraform/version"
    39  )
    40  
    41  // DefaultDataDir is the default directory for storing local data.
    42  const DefaultDataDir = ".terraform"
    43  
    44  // DefaultPluginVendorDir is the location in the config directory to look for
    45  // user-added plugin binaries. Terraform only reads from this path if it
    46  // exists, it is never created by terraform.
    47  const DefaultPluginVendorDirV12 = "terraform.d/plugins/" + pluginMachineName
    48  
    49  // pluginMachineName is the directory name used in new plugin paths.
    50  const pluginMachineName = runtime.GOOS + "_" + runtime.GOARCH
    51  
    52  type ProviderWrapper struct {
    53  	Provider     *tfplugin.GRPCProvider
    54  	client       *plugin.Client
    55  	rpcClient    plugin.ClientProtocol
    56  	providerName string
    57  	config       cty.Value
    58  	schema       *providers.GetSchemaResponse
    59  	retryCount   int
    60  	retrySleepMs int
    61  }
    62  
    63  func NewProviderWrapper(providerName string, providerConfig cty.Value, verbose bool, options ...map[string]int) (*ProviderWrapper, error) {
    64  	p := &ProviderWrapper{retryCount: 5, retrySleepMs: 300}
    65  	p.providerName = providerName
    66  	p.config = providerConfig
    67  
    68  	if len(options) > 0 {
    69  		retryCount, hasOption := options[0]["retryCount"]
    70  		if hasOption {
    71  			p.retryCount = retryCount
    72  		}
    73  		retrySleepMs, hasOption := options[0]["retrySleepMs"]
    74  		if hasOption {
    75  			p.retrySleepMs = retrySleepMs
    76  		}
    77  	}
    78  
    79  	err := p.initProvider(verbose)
    80  
    81  	return p, err
    82  }
    83  
    84  func (p *ProviderWrapper) Kill() {
    85  	p.client.Kill()
    86  }
    87  
    88  func (p *ProviderWrapper) GetSchema() *providers.GetSchemaResponse {
    89  	if p.schema == nil {
    90  		r := p.Provider.GetSchema()
    91  		p.schema = &r
    92  	}
    93  	return p.schema
    94  }
    95  
    96  func (p *ProviderWrapper) GetReadOnlyAttributes(resourceTypes []string) (map[string][]string, error) {
    97  	r := p.GetSchema()
    98  
    99  	if r.Diagnostics.HasErrors() {
   100  		return nil, r.Diagnostics.Err()
   101  	}
   102  	readOnlyAttributes := map[string][]string{}
   103  	for resourceName, obj := range r.ResourceTypes {
   104  		if terraformerstring.ContainsString(resourceTypes, resourceName) {
   105  			readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^id$")
   106  			for k, v := range obj.Block.Attributes {
   107  				if !v.Optional && !v.Required {
   108  					if v.Type.IsListType() || v.Type.IsSetType() {
   109  						readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^"+k+"\\.(.*)")
   110  					} else {
   111  						readOnlyAttributes[resourceName] = append(readOnlyAttributes[resourceName], "^"+k+"$")
   112  					}
   113  				}
   114  			}
   115  			readOnlyAttributes[resourceName] = p.readObjBlocks(obj.Block.BlockTypes, readOnlyAttributes[resourceName], "-1")
   116  		}
   117  	}
   118  	return readOnlyAttributes, nil
   119  }
   120  
   121  func (p *ProviderWrapper) readObjBlocks(block map[string]*configschema.NestedBlock, readOnlyAttributes []string, parent string) []string {
   122  	for k, v := range block {
   123  		if len(v.BlockTypes) > 0 {
   124  			if parent == "-1" {
   125  				readOnlyAttributes = p.readObjBlocks(v.BlockTypes, readOnlyAttributes, k)
   126  			} else {
   127  				readOnlyAttributes = p.readObjBlocks(v.BlockTypes, readOnlyAttributes, parent+"\\.[0-9]+\\."+k)
   128  			}
   129  		}
   130  		fieldCount := 0
   131  		for key, l := range v.Attributes {
   132  			if !l.Optional && !l.Required {
   133  				fieldCount++
   134  				switch v.Nesting {
   135  				case configschema.NestingList:
   136  					if parent == "-1" {
   137  						readOnlyAttributes = append(readOnlyAttributes, "^"+k+"\\.[0-9]+\\."+key+"($|\\.[0-9]+|\\.#)")
   138  					} else {
   139  						readOnlyAttributes = append(readOnlyAttributes, "^"+parent+"\\.(.*)\\."+key+"$")
   140  					}
   141  				case configschema.NestingSet:
   142  					if parent == "-1" {
   143  						readOnlyAttributes = append(readOnlyAttributes, "^"+k+"\\.[0-9]+\\."+key+"$")
   144  					} else {
   145  						readOnlyAttributes = append(readOnlyAttributes, "^"+parent+"\\.(.*)\\."+key+"($|\\.(.*))")
   146  					}
   147  				case configschema.NestingMap:
   148  					readOnlyAttributes = append(readOnlyAttributes, parent+"\\."+key)
   149  				default:
   150  					readOnlyAttributes = append(readOnlyAttributes, parent+"\\."+key+"$")
   151  				}
   152  			}
   153  		}
   154  		if fieldCount == len(v.Block.Attributes) && fieldCount > 0 && len(v.BlockTypes) == 0 {
   155  			readOnlyAttributes = append(readOnlyAttributes, "^"+k)
   156  		}
   157  	}
   158  	return readOnlyAttributes
   159  }
   160  
   161  func (p *ProviderWrapper) Refresh(info *terraform.InstanceInfo, state *terraform.InstanceState) (*terraform.InstanceState, error) {
   162  	schema := p.GetSchema()
   163  	impliedType := schema.ResourceTypes[info.Type].Block.ImpliedType()
   164  	priorState, err := state.AttrsAsObjectValue(impliedType)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	successReadResource := false
   169  	resp := providers.ReadResourceResponse{}
   170  	for i := 0; i < p.retryCount; i++ {
   171  		resp = p.Provider.ReadResource(providers.ReadResourceRequest{
   172  			TypeName:   info.Type,
   173  			PriorState: priorState,
   174  			Private:    []byte{},
   175  		})
   176  		if resp.Diagnostics.HasErrors() {
   177  			log.Println(resp.Diagnostics.Err())
   178  			log.Printf("WARN: Fail read resource from provider, wait %dms before retry\n", p.retrySleepMs)
   179  			time.Sleep(time.Duration(p.retrySleepMs) * time.Millisecond)
   180  			continue
   181  		} else {
   182  			successReadResource = true
   183  			break
   184  		}
   185  	}
   186  
   187  	if !successReadResource {
   188  		log.Println("Fail read resource from provider, trying import command")
   189  		// retry with regular import command - without resource attributes
   190  		importResponse := p.Provider.ImportResourceState(providers.ImportResourceStateRequest{
   191  			TypeName: info.Type,
   192  			ID:       state.ID,
   193  		})
   194  		if importResponse.Diagnostics.HasErrors() {
   195  			return nil, resp.Diagnostics.Err()
   196  		}
   197  		if len(importResponse.ImportedResources) == 0 {
   198  			return nil, errors.New("not able to import resource for a given ID")
   199  		}
   200  		return terraform.NewInstanceStateShimmedFromValue(importResponse.ImportedResources[0].State, int(schema.ResourceTypes[info.Type].Version)), nil
   201  	}
   202  
   203  	if resp.NewState.IsNull() {
   204  		msg := fmt.Sprintf("ERROR: Read resource response is null for resource %s", info.Id)
   205  		return nil, errors.New(msg)
   206  	}
   207  
   208  	return terraform.NewInstanceStateShimmedFromValue(resp.NewState, int(schema.ResourceTypes[info.Type].Version)), nil
   209  }
   210  
   211  func (p *ProviderWrapper) initProvider(verbose bool) error {
   212  	providerFilePath, err := getProviderFileName(p.providerName)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	options := hclog.LoggerOptions{
   217  		Name:   "plugin",
   218  		Level:  hclog.Error,
   219  		Output: os.Stdout,
   220  	}
   221  	if verbose {
   222  		options.Level = hclog.Trace
   223  	}
   224  	logger := hclog.New(&options)
   225  	p.client = plugin.NewClient(
   226  		&plugin.ClientConfig{
   227  			Cmd:              exec.Command(providerFilePath),
   228  			HandshakeConfig:  tfplugin.Handshake,
   229  			VersionedPlugins: tfplugin.VersionedPlugins,
   230  			Managed:          true,
   231  			Logger:           logger,
   232  			AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   233  			AutoMTLS:         true,
   234  		})
   235  	p.rpcClient, err = p.client.Client()
   236  	if err != nil {
   237  		return err
   238  	}
   239  	raw, err := p.rpcClient.Dispense(tfplugin.ProviderPluginName)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	p.Provider = raw.(*tfplugin.GRPCProvider)
   245  
   246  	config, err := p.GetSchema().Provider.Block.CoerceValue(p.config)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	p.Provider.Configure(providers.ConfigureRequest{
   251  		TerraformVersion: version.Version,
   252  		Config:           config,
   253  	})
   254  
   255  	return nil
   256  }
   257  
   258  func getProviderFileName(providerName string) (string, error) {
   259  	defaultDataDir := os.Getenv("TF_DATA_DIR")
   260  	if defaultDataDir == "" {
   261  		defaultDataDir = DefaultDataDir
   262  	}
   263  	providerFilePath, err := getProviderFileNameV13andV14(defaultDataDir, providerName)
   264  	if err != nil || providerFilePath == "" {
   265  		providerFilePath, err = getProviderFileNameV13andV14(os.Getenv("HOME")+string(os.PathSeparator)+
   266  			".terraform.d", providerName)
   267  	}
   268  	if err != nil || providerFilePath == "" {
   269  		return getProviderFileNameV12(providerName)
   270  	}
   271  	return providerFilePath, nil
   272  }
   273  
   274  func getProviderFileNameV13andV14(prefix, providerName string) (string, error) {
   275  	// Read terraform v14 file path
   276  	registryDir := prefix + string(os.PathSeparator) + "providers" + string(os.PathSeparator) +
   277  		"registry.terraform.io"
   278  	providerDirs, err := ioutil.ReadDir(registryDir)
   279  	if err != nil {
   280  		// Read terraform v13 file path
   281  		registryDir = prefix + string(os.PathSeparator) + "plugins" + string(os.PathSeparator) +
   282  			"registry.terraform.io"
   283  		providerDirs, err = ioutil.ReadDir(registryDir)
   284  		if err != nil {
   285  			return "", err
   286  		}
   287  	}
   288  	providerFilePath := ""
   289  	for _, providerDir := range providerDirs {
   290  		pluginPath := registryDir + string(os.PathSeparator) + providerDir.Name() +
   291  			string(os.PathSeparator) + providerName
   292  		dirs, err := ioutil.ReadDir(pluginPath)
   293  		if err != nil {
   294  			continue
   295  		}
   296  		for _, dir := range dirs {
   297  			if !dir.IsDir() {
   298  				continue
   299  			}
   300  			for _, dir := range dirs {
   301  				fullPluginPath := pluginPath + string(os.PathSeparator) + dir.Name() +
   302  					string(os.PathSeparator) + runtime.GOOS + "_" + runtime.GOARCH
   303  				files, err := ioutil.ReadDir(fullPluginPath)
   304  				if err == nil {
   305  					for _, file := range files {
   306  						if strings.HasPrefix(file.Name(), "terraform-provider-"+providerName) {
   307  							providerFilePath = fullPluginPath + string(os.PathSeparator) + file.Name()
   308  						}
   309  					}
   310  				}
   311  			}
   312  		}
   313  	}
   314  	return providerFilePath, nil
   315  }
   316  
   317  func getProviderFileNameV12(providerName string) (string, error) {
   318  	defaultDataDir := os.Getenv("TF_DATA_DIR")
   319  	if defaultDataDir == "" {
   320  		defaultDataDir = DefaultDataDir
   321  	}
   322  	pluginPath := defaultDataDir + string(os.PathSeparator) + "plugins" + string(os.PathSeparator) + runtime.GOOS + "_" + runtime.GOARCH
   323  	files, err := ioutil.ReadDir(pluginPath)
   324  	if err != nil {
   325  		pluginPath = os.Getenv("HOME") + string(os.PathSeparator) + "." + DefaultPluginVendorDirV12
   326  		files, err = ioutil.ReadDir(pluginPath)
   327  		if err != nil {
   328  			return "", err
   329  		}
   330  	}
   331  	providerFilePath := ""
   332  	for _, file := range files {
   333  		if file.IsDir() {
   334  			continue
   335  		}
   336  		if strings.HasPrefix(file.Name(), "terraform-provider-"+providerName) {
   337  			providerFilePath = pluginPath + string(os.PathSeparator) + file.Name()
   338  		}
   339  	}
   340  	return providerFilePath, nil
   341  }
   342  
   343  func GetProviderVersion(providerName string) string {
   344  	providerFilePath, err := getProviderFileName(providerName)
   345  	if err != nil {
   346  		log.Println("Can't find provider file path. Ensure that you are following https://www.terraform.io/docs/configuration/providers.html#third-party-plugins.")
   347  		return ""
   348  	}
   349  	t := strings.Split(providerFilePath, string(os.PathSeparator))
   350  	providerFileName := t[len(t)-1]
   351  	providerFileNameParts := strings.Split(providerFileName, "_")
   352  	if len(providerFileNameParts) < 2 {
   353  		log.Println("Can't find provider version. Ensure that you are following https://www.terraform.io/docs/configuration/providers.html#plugin-names-and-versions.")
   354  		return ""
   355  	}
   356  	providerVersion := providerFileNameParts[1]
   357  	return "~> " + strings.TrimPrefix(providerVersion, "v")
   358  }