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 }