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 }