pkg/infrastructure/authorizationCheckers/terraform/terraformAuthorizationChecker.go (241 lines of code) (raw):

// MIT License // // Copyright (c) Microsoft Corporation. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE package terraform import ( "context" "os" "strings" "github.com/Azure/mpf/pkg/domain" "github.com/hashicorp/terraform-exec/tfexec" log "github.com/sirupsen/logrus" ) type terraformDeploymentConfig struct { ctx context.Context workingDir string execPath string varFilePath string importExistingResourcesToState bool targetModule string } var inDestroyPhase bool // This file is created once the destroy phase is entered const ( TFDestroyStateEnteredFileName = ".azmpfEnteredDestroyPhase.txt" TFExistingResourceErrorMsg = "to be managed via Terraform this resource needs to be imported into the State" // Returning this response will trigger a retry RetryDeploymentResponseErrorMessage = "RetryGetDeploymentAuthorizationErrors" // Retryable errors BillingFeaturesPayloadError = "CurrentBillingFeatures is required in payload" WaitingForDataplaneError = "waiting for the Data Plane" ) func NewTerraformAuthorizationChecker(workDir string, execPath string, varFilePath string, importExistingResources bool, targetModule string) *terraformDeploymentConfig { err := deleteEnteredDestroyPhaseStateFile(workDir, TFDestroyStateEnteredFileName) if err != nil { log.Warnf("error deleting enteredDestroyPhaseStateFile: %s", err) } return &terraformDeploymentConfig{ workingDir: workDir, execPath: execPath, ctx: context.Background(), varFilePath: varFilePath, importExistingResourcesToState: importExistingResources, targetModule: targetModule, } } func (a *terraformDeploymentConfig) GetDeploymentAuthorizationErrors(mpfConfig domain.MPFConfig) (string, error) { return a.deployTerraform(mpfConfig) } func (a *terraformDeploymentConfig) CleanDeployment(mpfConfig domain.MPFConfig) error { err := deleteEnteredDestroyPhaseStateFile(a.workingDir, TFDestroyStateEnteredFileName) if err != nil { log.Warnf("error deleting enteredDestroyPhaseStateFile: %s", err) } tf, err := tfexec.NewTerraform(a.workingDir, a.execPath) if err != nil { log.Fatalf("error running NewTerraform: %s", err) } err = tf.Init(context.Background()) if err != nil { log.Warnf("error running Init: %s", err) return err } switch { case a.varFilePath == "" && a.targetModule == "": err = tf.Destroy(a.ctx) case a.varFilePath != "" && a.targetModule == "": err = tf.Destroy(a.ctx, tfexec.VarFile(a.varFilePath)) case a.varFilePath == "" && a.targetModule != "": err = tf.Destroy(a.ctx, tfexec.Target(a.targetModule)) case a.varFilePath != "" && a.targetModule != "": err = tf.Destroy(a.ctx, tfexec.VarFile(a.varFilePath), tfexec.Target(a.targetModule)) } if err != nil { log.Warnf("error running terraform destroy: %s", err) } return err } func (a *terraformDeploymentConfig) setTFConfig(mpfConfig domain.MPFConfig) (*tfexec.Terraform, error) { log.Infof("workingDir: %s", a.workingDir) log.Infof("varfilePath: %s", a.varFilePath) log.Infof("execPath: %s", a.execPath) tf, err := tfexec.NewTerraform(a.workingDir, a.execPath) if err != nil { log.Fatalf("error running NewTerraform: %s", err) } pathEnvVal := os.Getenv("PATH") var tfLogLevel string tfLogPathEnvVal := os.Getenv("TF_LOG_PATH") if tfLogPathEnvVal == "" { tfLogPathEnvVal = a.workingDir + "/terraform.log" } tfReattachProviders := os.Getenv("TF_REATTACH_PROVIDERS") switch log.GetLevel() { case log.InfoLevel: tfLogLevel = "INFO" case log.WarnLevel: tfLogLevel = "WARN" case log.DebugLevel: tfLogLevel = "DEBUG" case log.TraceLevel: tfLogLevel = "TRACE" default: tfLogLevel = "ERROR" } if tfLogLevel != "ERROR" { err := tf.SetLog(tfLogLevel) if err != nil { log.Warnf("error setting Terraform log level: %s", err) } err = tf.SetLogPath(tfLogPathEnvVal) if err != nil { log.Warnf("error setting Terraform log path: %s", err) } tf.SetStderr(os.Stderr) tf.SetStdout(os.Stdout) } envVars := map[string]string{ "ARM_CLIENT_ID": mpfConfig.SP.SPClientID, "ARM_CLIENT_SECRET": mpfConfig.SP.SPClientSecret, "ARM_SUBSCRIPTION_ID": mpfConfig.SubscriptionID, "ARM_TENANT_ID": mpfConfig.TenantID, "PATH": pathEnvVal, } if tfReattachProviders != "" { envVars["TF_REATTACH_PROVIDERS"] = tfReattachProviders } err = tf.SetEnv(envVars) if err != nil { log.Warnf("error setting Terraform env vars: %s", err) } return tf, nil } func (a *terraformDeploymentConfig) deployTerraform(mpfConfig domain.MPFConfig) (string, error) { tf, err := a.setTFConfig(mpfConfig) if err != nil { log.Fatalf("error setting Terraform start config: %s", err) } inDestroyPhase = doesEnteredDestroyPhaseStateFileExist(a.workingDir, TFDestroyStateEnteredFileName) if !inDestroyPhase { log.Infof("destroy phase file does not exist, in apply phase") msg, err := a.terraformApply(mpfConfig, tf) if err != nil || msg != "" { return msg, err } } return a.terraformDestroy(mpfConfig, tf) } func (a *terraformDeploymentConfig) terraformApply(mpfConfig domain.MPFConfig, tf *tfexec.Terraform) (string, error) { err := tf.Init(context.Background()) if err != nil { log.Warnf("error running Init: %s", err) return "", err } log.Infoln("in apply phase") switch { case a.varFilePath == "" && a.targetModule == "": err = tf.Apply(a.ctx) case a.varFilePath != "" && a.targetModule == "": err = tf.Apply(a.ctx, tfexec.VarFile(a.varFilePath)) case a.varFilePath == "" && a.targetModule != "": err = tf.Apply(a.ctx, tfexec.Target(a.targetModule)) case a.varFilePath != "" && a.targetModule != "": err = tf.Apply(a.ctx, tfexec.VarFile(a.varFilePath), tfexec.Target(a.targetModule)) } if err == nil { return "", nil } errorMsg := err.Error() log.Debugln("terraform apply error: ", errorMsg) // Temporary fix to workaround issue https://github.com/hashicorp/terraform-provider-azurerm/issues/27961 // It is observed only once, so retrying works if strings.Contains(errorMsg, BillingFeaturesPayloadError) { return RetryDeploymentResponseErrorMessage, nil } // import errors can occur for some resources, when identity does not have all required permissions, // as described in https://github.com/hashicorp/terraform-provider-azurerm/issues/27961#issuecomment-2467392936 if a.importExistingResourcesToState && strings.Contains(errorMsg, TFExistingResourceErrorMsg) { msg, err := a.terraformImport(tf, errorMsg) if err != nil || msg != "" { if strings.Contains(msg, "Authorization") { return msg, nil } return msg, err } return a.terraformApply(mpfConfig, tf) } if strings.Contains(errorMsg, "Authorization") || strings.Contains(errorMsg, "LinkedAccessCheckFailed") { if strings.Contains(errorMsg, WaitingForDataplaneError) { log.Warnln("terraform apply: waiting for dataplane error occured, requesting retry") return RetryDeploymentResponseErrorMessage, nil } log.Debug("terraform apply: authorization error occured") return errorMsg, nil } log.Warnf("terraform apply: non authorizaton error occured: %s", errorMsg) return errorMsg, err } func (a *terraformDeploymentConfig) terraformImport(tf *tfexec.Terraform, existingResErrMesg string) (string, error) { log.Warnf("terraform apply: existing resource error occured:|| %s ||\n\n", existingResErrMesg) log.Warn("importing existing resources to state") exstResAddrAndResIDs, err := GetAddressAndResourceIDFromExistingResourceError(existingResErrMesg) if err != nil { log.Warnf("error getting existing resource address and resource ID: %s \n", err) return existingResErrMesg, err } for addr, resID := range exstResAddrAndResIDs { log.Warnf("importing existing resource: %s, %s ||\n", addr, resID) if a.varFilePath != "" { err = tf.Import(a.ctx, addr, resID, tfexec.VarFile(a.varFilePath)) } else { err = tf.Import(a.ctx, addr, resID) } if err != nil { log.Warnf("error importing existing resource: %s \n", err) return err.Error(), err } log.Warnf("imported existing resource: %s, %s \n", addr, resID) } log.Warnf("imported all existing resources to state, triggering deployTerrafom again \n") return "", nil } func (a *terraformDeploymentConfig) terraformDestroy(mpfConfig domain.MPFConfig, tf *tfexec.Terraform) (string, error) { var err error log.Infoln("in destroy phase") if !inDestroyPhase { err = createEnteredDestroyPhaseStateFile(a.workingDir, TFDestroyStateEnteredFileName) if err != nil { log.Warnf("error creating enteredDestroyPhaseStateFile: %s", err) } } switch { case a.varFilePath == "" && a.targetModule == "": err = tf.Destroy(a.ctx) case a.varFilePath != "" && a.targetModule == "": err = tf.Destroy(a.ctx, tfexec.VarFile(a.varFilePath)) case a.varFilePath == "" && a.targetModule != "": err = tf.Destroy(a.ctx, tfexec.Target(a.targetModule)) case a.varFilePath != "" && a.targetModule != "": err = tf.Destroy(a.ctx, tfexec.VarFile(a.varFilePath), tfexec.Target(a.targetModule)) } if err != nil { errorMsg := err.Error() log.Debugln(errorMsg) if strings.Contains(errorMsg, "Authorization") { return errorMsg, nil } log.Warnf("terraform destroy: non authorizaton error occured: %s", errorMsg) return errorMsg, err } return "", nil }