github.com/openshift/installer@v1.4.17/pkg/terraform/terraform.go (about)

     1  package terraform
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  
    10  	"github.com/hashicorp/terraform-exec/tfexec"
    11  	"github.com/pkg/errors"
    12  	"github.com/sirupsen/logrus"
    13  
    14  	"github.com/openshift/installer/pkg/asset"
    15  	"github.com/openshift/installer/pkg/asset/cluster/tfvars"
    16  	"github.com/openshift/installer/pkg/infrastructure"
    17  	"github.com/openshift/installer/pkg/lineprinter"
    18  	"github.com/openshift/installer/pkg/metrics/timer"
    19  	"github.com/openshift/installer/pkg/types"
    20  )
    21  
    22  const (
    23  	tfVarsFileName         = "terraform.tfvars.json"
    24  	tfPlatformVarsFileName = "terraform.platform.auto.tfvars.json"
    25  )
    26  
    27  // Provider implements the infrastructure.Provider interface.
    28  type Provider struct {
    29  	stages []Stage
    30  }
    31  
    32  // InitializeProvider creates a concrete infrastructure.Provider for the given platform.
    33  func InitializeProvider(stages []Stage) infrastructure.Provider {
    34  	return &Provider{stages}
    35  }
    36  
    37  // Provision implements pkg/infrastructure/provider.Provision. Provision iterates
    38  // through each of the stages and applies the Terraform config for the stage.
    39  func (p *Provider) Provision(_ context.Context, dir string, parents asset.Parents) ([]*asset.File, error) {
    40  	tfVars := &tfvars.TerraformVariables{}
    41  	parents.Get(tfVars)
    42  	vars := tfVars.Files()
    43  
    44  	fileList := []*asset.File{}
    45  	terraformDir := filepath.Join(dir, "terraform")
    46  	if err := os.Mkdir(terraformDir, 0777); err != nil {
    47  		return nil, fmt.Errorf("could not create the terraform directory: %w", err)
    48  	}
    49  
    50  	terraformDirPath, err := filepath.Abs(terraformDir)
    51  	if err != nil {
    52  		return nil, fmt.Errorf("cannot get absolute path of terraform directory: %w", err)
    53  	}
    54  
    55  	defer os.RemoveAll(terraformDir)
    56  	if err = UnpackTerraform(terraformDirPath, p.stages); err != nil {
    57  		return nil, fmt.Errorf("error unpacking terraform: %w", err)
    58  	}
    59  
    60  	for _, stage := range p.stages {
    61  		outputs, stateFile, err := applyStage(stage.Platform(), stage, terraformDirPath, vars)
    62  		if err != nil {
    63  			// Write the state file to the install directory even if the apply failed.
    64  			if stateFile != nil {
    65  				fileList = append(fileList, stateFile)
    66  			}
    67  			return fileList, fmt.Errorf("failure applying terraform for %q stage: %w", stage.Name(), err)
    68  		}
    69  		vars = append(vars, outputs)
    70  		fileList = append(fileList, outputs)
    71  		fileList = append(fileList, stateFile)
    72  
    73  		_, extErr := stage.ExtractLBConfig(dir, terraformDirPath, outputs, vars[0])
    74  		if extErr != nil {
    75  			return fileList, fmt.Errorf("failed to extract load balancer information: %w", extErr)
    76  		}
    77  	}
    78  	return fileList, nil
    79  }
    80  
    81  // DestroyBootstrap implements pkg/infrastructure/provider.DestroyBootstrap.
    82  // DestroyBootstrap iterates through each stage, and will run the destroy
    83  // command when defined on a stage.
    84  func (p *Provider) DestroyBootstrap(ctx context.Context, dir string) error {
    85  	varFiles := []string{tfVarsFileName, tfPlatformVarsFileName}
    86  	for _, stage := range p.stages {
    87  		varFiles = append(varFiles, stage.OutputsFilename())
    88  	}
    89  
    90  	terraformDir := filepath.Join(dir, "terraform")
    91  	if err := os.Mkdir(terraformDir, 0777); err != nil {
    92  		return fmt.Errorf("could not create the terraform directory: %w", err)
    93  	}
    94  
    95  	terraformDirPath, err := filepath.Abs(terraformDir)
    96  	if err != nil {
    97  		return fmt.Errorf("could not get absolute path of terraform directory: %w", err)
    98  	}
    99  
   100  	defer os.RemoveAll(terraformDirPath)
   101  	if err = UnpackTerraform(terraformDirPath, p.stages); err != nil {
   102  		return fmt.Errorf("error unpacking terraform: %w", err)
   103  	}
   104  
   105  	for i := len(p.stages) - 1; i >= 0; i-- {
   106  		stage := p.stages[i]
   107  
   108  		if !stage.DestroyWithBootstrap() {
   109  			continue
   110  		}
   111  
   112  		tempDir, err := os.MkdirTemp("", fmt.Sprintf("openshift-install-%s-", stage.Name()))
   113  		if err != nil {
   114  			return fmt.Errorf("failed to create temporary directory for Terraform execution: %w", err)
   115  		}
   116  		defer os.RemoveAll(tempDir)
   117  
   118  		stateFilePathInInstallDir := filepath.Join(dir, stage.StateFilename())
   119  		stateFilePathInTempDir := filepath.Join(tempDir, StateFilename)
   120  		if err := copyFile(stateFilePathInInstallDir, stateFilePathInTempDir); err != nil {
   121  			return fmt.Errorf("failed to copy state file to the temporary directory: %w", err)
   122  		}
   123  
   124  		targetVarFiles := make([]string, 0, len(varFiles))
   125  		for _, filename := range varFiles {
   126  			sourcePath := filepath.Join(dir, filename)
   127  			targetPath := filepath.Join(tempDir, filename)
   128  			if err := copyFile(sourcePath, targetPath); err != nil {
   129  				// platform may not need platform-specific Terraform variables
   130  				if filename == tfPlatformVarsFileName {
   131  					var pErr *os.PathError
   132  					if errors.As(err, &pErr) && pErr.Path == sourcePath {
   133  						continue
   134  					}
   135  				}
   136  				return fmt.Errorf("failed to copy %s to the temporary directory: %w", filename, err)
   137  			}
   138  			targetVarFiles = append(targetVarFiles, targetPath)
   139  		}
   140  
   141  		if err := stage.Destroy(tempDir, terraformDirPath, targetVarFiles); err != nil {
   142  			return err
   143  		}
   144  
   145  		if err := copyFile(stateFilePathInTempDir, stateFilePathInInstallDir); err != nil {
   146  			return fmt.Errorf("failed to copy state file from the temporary directory: %w", err)
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  // ExtractHostAddresses implements pkg/infrastructure/provider.ExtractHostAddresses. Extracts the addresses to be used
   153  // for gathering debug logs by inspecting the Terraform output files.
   154  func (p *Provider) ExtractHostAddresses(dir string, config *types.InstallConfig, ha *infrastructure.HostAddresses) error {
   155  	for _, stage := range p.stages {
   156  		stageBootstrap, stagePort, stageMasters, err := stage.ExtractHostAddresses(dir, config)
   157  		if err != nil {
   158  			logrus.Warnf("Failed to extract host addresses: %s", err.Error())
   159  		} else {
   160  			if stageBootstrap != "" {
   161  				ha.Bootstrap = stageBootstrap
   162  			}
   163  			if stagePort != 0 {
   164  				ha.Port = stagePort
   165  			}
   166  			if len(stageMasters) > 0 {
   167  				ha.Masters = stageMasters
   168  			}
   169  		}
   170  	}
   171  	return nil
   172  }
   173  
   174  // newTFExec creates a tfexec.Terraform for executing Terraform CLI commands.
   175  // The `datadir` is the location to which the terraform plan (tf files, etc) has been unpacked.
   176  // The `terraformDir` is the location to which Terraform, provider binaries, & .terraform data dir have been unpacked.
   177  // The stdout and stderr will be sent to the logger at the debug and error levels,
   178  // respectively.
   179  func newTFExec(datadir string, terraformDir string) (*tfexec.Terraform, error) {
   180  	tfPath := filepath.Join(terraformDir, "bin", "terraform")
   181  	tf, err := tfexec.NewTerraform(datadir, tfPath)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	// terraform-exec will not accept debug logs unless a log file path has
   187  	// been specified. And it makes sense since the logging is very verbose.
   188  	if path, ok := os.LookupEnv("TF_LOG_PATH"); ok {
   189  		// These might fail if tf cli does not have a compatible version. Since
   190  		// the exact same check is repeated, we just have to verify error once
   191  		// for all calls
   192  		if err := tf.SetLog(os.Getenv("TF_LOG")); err != nil {
   193  			// We want to skip setting the log path since tf-exec lib will
   194  			// default to TRACE log levels which can risk leaking sensitive
   195  			// data
   196  			logrus.Infof("Skipping setting terraform log levels: %v", err)
   197  		} else {
   198  			tf.SetLogCore(os.Getenv("TF_LOG_CORE"))         //nolint:errcheck
   199  			tf.SetLogProvider(os.Getenv("TF_LOG_PROVIDER")) //nolint:errcheck
   200  			// This never returns any errors despite its signature
   201  			tf.SetLogPath(path) //nolint:errcheck
   202  		}
   203  	}
   204  
   205  	// Add terraform info logs to the installer log
   206  	lpDebug := &lineprinter.LinePrinter{Print: (&lineprinter.Trimmer{WrappedPrint: logrus.Debug}).Print}
   207  	lpError := &lineprinter.LinePrinter{Print: (&lineprinter.Trimmer{WrappedPrint: logrus.Error}).Print}
   208  	defer lpDebug.Close()
   209  	defer lpError.Close()
   210  
   211  	tf.SetStdout(lpDebug)
   212  	tf.SetStderr(lpError)
   213  	tf.SetLogger(newPrintfer())
   214  
   215  	// Set the Terraform data dir to be the same as the terraformDir so that
   216  	// files we unpack are contained and, more importantly, we can ensure the
   217  	// provider binaries unpacked in the Terraform data dir have the same permission
   218  	// levels as the Terraform binary.
   219  	dd := path.Join(terraformDir, ".terraform")
   220  	os.Setenv("TF_DATA_DIR", dd)
   221  
   222  	return tf, nil
   223  }
   224  
   225  // Apply unpacks the platform-specific Terraform modules into the
   226  // given directory and then runs 'terraform init' and 'terraform
   227  // apply'.
   228  func Apply(dir string, platform string, stage Stage, terraformDir string, extraOpts ...tfexec.ApplyOption) error {
   229  	if err := unpackAndInit(dir, platform, stage.Name(), terraformDir, stage.Providers()); err != nil {
   230  		return err
   231  	}
   232  
   233  	tf, err := newTFExec(dir, terraformDir)
   234  	if err != nil {
   235  		return errors.Wrap(err, "failed to create a new tfexec")
   236  	}
   237  	err = tf.Apply(context.Background(), extraOpts...)
   238  	return errors.Wrap(diagnoseApplyError(err), "failed to apply Terraform")
   239  }
   240  
   241  // Destroy unpacks the platform-specific Terraform modules into the
   242  // given directory and then runs 'terraform init' and 'terraform
   243  // destroy'.
   244  func Destroy(dir string, platform string, stage Stage, terraformDir string, extraOpts ...tfexec.DestroyOption) error {
   245  	if err := unpackAndInit(dir, platform, stage.Name(), terraformDir, stage.Providers()); err != nil {
   246  		return err
   247  	}
   248  
   249  	tf, err := newTFExec(dir, terraformDir)
   250  	if err != nil {
   251  		return errors.Wrap(err, "failed to create a new tfexec")
   252  	}
   253  	return errors.Wrap(
   254  		tf.Destroy(context.Background(), extraOpts...),
   255  		"failed doing terraform destroy",
   256  	)
   257  }
   258  
   259  func applyStage(platform string, stage Stage, terraformDir string, tfvarsFiles []*asset.File) (*asset.File, *asset.File, error) {
   260  	// Copy the terraform.tfvars to a temp directory which will contain the terraform plan.
   261  	tmpDir, err := os.MkdirTemp("", fmt.Sprintf("openshift-install-%s-", stage.Name()))
   262  	if err != nil {
   263  		return nil, nil, errors.Wrap(err, "failed to create temp dir for terraform execution")
   264  	}
   265  	defer os.RemoveAll(tmpDir)
   266  
   267  	extraOpts := []tfexec.ApplyOption{}
   268  	for _, file := range tfvarsFiles {
   269  		if err := os.WriteFile(filepath.Join(tmpDir, file.Filename), file.Data, 0o600); err != nil {
   270  			return nil, nil, err
   271  		}
   272  		extraOpts = append(extraOpts, tfexec.VarFile(filepath.Join(tmpDir, file.Filename)))
   273  	}
   274  
   275  	return applyTerraform(tmpDir, platform, stage, terraformDir, extraOpts...)
   276  }
   277  
   278  func applyTerraform(tmpDir string, platform string, stage Stage, terraformDir string, opts ...tfexec.ApplyOption) (outputsFile, stateFile *asset.File, err error) {
   279  	timer.StartTimer(stage.Name())
   280  	defer timer.StopTimer(stage.Name())
   281  
   282  	applyErr := Apply(tmpDir, platform, stage, terraformDir, opts...)
   283  
   284  	if data, err := os.ReadFile(filepath.Join(tmpDir, StateFilename)); err == nil {
   285  		stateFile = &asset.File{
   286  			Filename: stage.StateFilename(),
   287  			Data:     data,
   288  		}
   289  	} else if !os.IsNotExist(err) {
   290  		logrus.Errorf("Failed to read tfstate: %v", err)
   291  		return nil, nil, errors.Wrap(err, "failed to read tfstate")
   292  	}
   293  
   294  	if applyErr != nil {
   295  		return nil, stateFile, fmt.Errorf("error applying Terraform configs: %w", applyErr)
   296  	}
   297  
   298  	outputs, err := Outputs(tmpDir, terraformDir)
   299  	if err != nil {
   300  		return nil, stateFile, errors.Wrapf(err, "could not get outputs from stage %q", stage.Name())
   301  	}
   302  
   303  	outputsFile = &asset.File{
   304  		Filename: stage.OutputsFilename(),
   305  		Data:     outputs,
   306  	}
   307  	return outputsFile, stateFile, nil
   308  }
   309  
   310  func copyFile(from string, to string) error {
   311  	data, err := os.ReadFile(from)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	return os.WriteFile(to, data, 0o666) //nolint:gosec // state file doesn't need to be 0600
   317  }