mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
write generated config when using the cloud integration
This commit is contained in:
parent
b88bae2ec4
commit
cdce4c4a6d
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/hashicorp/terraform
|
||||
|
||||
replace github.com/hashicorp/go-tfe v1.24.0 => github.com/hashicorp/go-tfe v1.25.2-0.20230526220312-15160638651d
|
||||
replace github.com/hashicorp/go-tfe v1.24.0 => github.com/hashicorp/go-tfe v1.25.2-0.20230530060311-414a2bce2afa
|
||||
|
||||
require (
|
||||
cloud.google.com/go/kms v1.6.0
|
||||
|
4
go.sum
4
go.sum
@ -542,8 +542,8 @@ github.com/hashicorp/go-slug v0.11.1/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu4
|
||||
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-tfe v1.25.2-0.20230526220312-15160638651d h1:M2fcs6fQ6Nq7mmOzsH/VI0m66q8XTz65jk0DzQ74gmw=
|
||||
github.com/hashicorp/go-tfe v1.25.2-0.20230526220312-15160638651d/go.mod h1:1Y6nsdMuJ14lYdc1VMLl/erlthvMzUsJn+WYWaAdSc4=
|
||||
github.com/hashicorp/go-tfe v1.25.2-0.20230530060311-414a2bce2afa h1:MDW7I6XkDAypUklJjsUlsMg4nfZgBYucTY7BppEhJ4w=
|
||||
github.com/hashicorp/go-tfe v1.25.2-0.20230530060311-414a2bce2afa/go.mod h1:1Y6nsdMuJ14lYdc1VMLl/erlthvMzUsJn+WYWaAdSc4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
|
@ -6,6 +6,7 @@ package local
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
@ -191,7 +192,7 @@ func (b *Local) opPlan(
|
||||
}
|
||||
|
||||
// Write out any generated config, before we render the plan.
|
||||
wroteConfig, moreDiags := genconfig.MaybeWriteGeneratedConfig(plan, op.GenerateConfigOut)
|
||||
wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
op.ReportResult(runningOp, diags)
|
||||
@ -215,3 +216,38 @@ func (b *Local) opPlan(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) {
|
||||
if genconfig.ShouldWriteConfig(out) {
|
||||
diags := genconfig.ValidateTargetFile(out)
|
||||
if diags.HasErrors() {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
for _, c := range plan.Changes.Resources {
|
||||
change := genconfig.Change{
|
||||
Addr: c.Addr.String(),
|
||||
GeneratedConfig: c.GeneratedConfig,
|
||||
}
|
||||
if c.Importing != nil {
|
||||
change.ImportID = c.Importing.ID
|
||||
}
|
||||
|
||||
var moreDiags tfdiags.Diagnostics
|
||||
writer, wroteConfig, moreDiags = change.MaybeWriteConfig(writer, out)
|
||||
if moreDiags.HasErrors() {
|
||||
return false, diags.Append(moreDiags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wroteConfig {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Config generation is experimental",
|
||||
"Generating configuration during import is currently experimental, and the generated configuration format may change in future versions."))
|
||||
}
|
||||
|
||||
return wroteConfig, diags
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonformat"
|
||||
"github.com/hashicorp/terraform/internal/genconfig"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
@ -82,6 +83,10 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation
|
||||
))
|
||||
}
|
||||
|
||||
if len(op.GenerateConfigOut) > 0 {
|
||||
diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut))
|
||||
}
|
||||
|
||||
// Return if there are any errors.
|
||||
if diags.HasErrors() {
|
||||
return nil, diags.Err()
|
||||
@ -250,6 +255,10 @@ in order to capture the filesystem context the remote workspace expects:
|
||||
}
|
||||
runOptions.Variables = runVariables
|
||||
|
||||
if len(op.GenerateConfigOut) > 0 {
|
||||
runOptions.AllowConfigGeneration = tfe.Bool(true)
|
||||
}
|
||||
|
||||
r, err := b.client.Runs.Create(stopCtx, runOptions)
|
||||
if err != nil {
|
||||
return r, generalError("Failed to create run", err)
|
||||
@ -419,43 +428,91 @@ func (b *Cloud) renderPlanLogs(ctx context.Context, op *backend.Operation, run *
|
||||
}
|
||||
}
|
||||
|
||||
// Get the run's current status and include the workspace. We will check if
|
||||
// the run has errored and if structured output is enabled.
|
||||
// Get the run's current status and include the workspace and plan. We will check if
|
||||
// the run has errored, if structured output is enabled, and if the plan
|
||||
run, err = b.client.Runs.ReadWithOptions(ctx, run.ID, &tfe.RunReadOptions{
|
||||
Include: []tfe.RunIncludeOpt{tfe.RunWorkspace},
|
||||
Include: []tfe.RunIncludeOpt{tfe.RunWorkspace, tfe.RunPlan},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the run was errored, canceled, or discarded we will not resume the rest
|
||||
// of this logic and attempt to render the plan.
|
||||
if run.Status == tfe.RunErrored || run.Status == tfe.RunCanceled ||
|
||||
run.Status == tfe.RunDiscarded {
|
||||
// of this logic and attempt to render the plan, except in certain special circumstances
|
||||
// where the plan errored but successfully generated configuration during an
|
||||
// import operation. In that case, we need to keep going so we can load the JSON plan
|
||||
// and use it to write the generated config to the specified output file.
|
||||
shouldGenerateConfig := shouldGenerateConfig(op.GenerateConfigOut, run)
|
||||
shouldRenderPlan := shouldRenderPlan(run)
|
||||
if !shouldRenderPlan && !shouldGenerateConfig {
|
||||
// We won't return an error here since we need to resume the logic that
|
||||
// follows after rendering the logs (run tasks, cost estimation, etc.)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine whether we should call the renderer to generate the plan output
|
||||
// in human readable format. Otherwise we risk duplicate plan output since
|
||||
// plan output may be contained in the streamed log file.
|
||||
if ok, err := b.shouldRenderStructuredRunOutput(run); ok {
|
||||
// Fetch the redacted plan.
|
||||
redacted, err := readRedactedPlan(ctx, b.client.BaseURL(), b.token, run.Plan.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Render plan output.
|
||||
b.renderer.RenderHumanPlan(*redacted, op.PlanMode)
|
||||
} else if err != nil {
|
||||
// Fetch the redacted JSON plan if we need it for either rendering the plan
|
||||
// or writing out generated configuration.
|
||||
var redactedPlan *jsonformat.Plan
|
||||
renderSRO, err := b.shouldRenderStructuredRunOutput(run)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if renderSRO || shouldGenerateConfig {
|
||||
redactedPlan, err = readRedactedPlan(ctx, b.client.BaseURL(), b.token, run.Plan.ID)
|
||||
if err != nil {
|
||||
return generalError("Failed to read JSON plan", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write any generated config before rendering the plan, so we can stop in case of errors
|
||||
if shouldGenerateConfig {
|
||||
diags := maybeWriteGeneratedConfig(redactedPlan, op.GenerateConfigOut)
|
||||
if diags.HasErrors() {
|
||||
return diags.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Only generate the human readable output from the plan if structured run output is
|
||||
// enabled. Otherwise we risk duplicate plan output since plan output may also be
|
||||
// shown in the streamed logs.
|
||||
if shouldRenderPlan && renderSRO {
|
||||
b.renderer.RenderHumanPlan(*redactedPlan, op.PlanMode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// maybeWriteGeneratedConfig attempts to write any generated configuration from the JSON plan
|
||||
// to the specified output file, if generated configuration exists and the correct flag was
|
||||
// passed to the plan command.
|
||||
func maybeWriteGeneratedConfig(plan *jsonformat.Plan, out string) (diags tfdiags.Diagnostics) {
|
||||
if genconfig.ShouldWriteConfig(out) {
|
||||
diags := genconfig.ValidateTargetFile(out)
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
for _, c := range plan.ResourceChanges {
|
||||
change := genconfig.Change{
|
||||
Addr: c.Address,
|
||||
GeneratedConfig: c.Change.GeneratedConfig,
|
||||
}
|
||||
if c.Change.Importing != nil {
|
||||
change.ImportID = c.Change.Importing.ID
|
||||
}
|
||||
|
||||
var moreDiags tfdiags.Diagnostics
|
||||
writer, _, moreDiags = change.MaybeWriteConfig(writer, out)
|
||||
if moreDiags.HasErrors() {
|
||||
return diags.Append(moreDiags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// shouldRenderStructuredRunOutput ensures the remote workspace has structured
|
||||
// run output enabled and, if using Terraform Enterprise, ensures it is a release
|
||||
// that supports enabling SRO for CLI-driven runs. The plan output will have
|
||||
@ -496,6 +553,16 @@ func (b *Cloud) shouldRenderStructuredRunOutput(run *tfe.Run) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func shouldRenderPlan(run *tfe.Run) bool {
|
||||
return !(run.Status == tfe.RunErrored || run.Status == tfe.RunCanceled ||
|
||||
run.Status == tfe.RunDiscarded)
|
||||
}
|
||||
|
||||
func shouldGenerateConfig(out string, run *tfe.Run) bool {
|
||||
return (run.Plan.Status == tfe.PlanErrored || run.Plan.Status == tfe.PlanFinished) &&
|
||||
run.Plan.GeneratedConfiguration == true && len(out) > 0
|
||||
}
|
||||
|
||||
const planDefaultHeader = `
|
||||
[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
|
||||
will stop streaming the logs, but will not stop the plan running remotely.[reset]
|
||||
|
@ -1011,16 +1011,17 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
||||
}
|
||||
|
||||
r := &tfe.Run{
|
||||
ID: GenerateID("run-"),
|
||||
Actions: &tfe.RunActions{IsCancelable: true},
|
||||
Apply: a,
|
||||
CostEstimate: ce,
|
||||
HasChanges: false,
|
||||
Permissions: &tfe.RunPermissions{},
|
||||
Plan: p,
|
||||
ReplaceAddrs: options.ReplaceAddrs,
|
||||
Status: tfe.RunPending,
|
||||
TargetAddrs: options.TargetAddrs,
|
||||
ID: GenerateID("run-"),
|
||||
Actions: &tfe.RunActions{IsCancelable: true},
|
||||
Apply: a,
|
||||
CostEstimate: ce,
|
||||
HasChanges: false,
|
||||
Permissions: &tfe.RunPermissions{},
|
||||
Plan: p,
|
||||
ReplaceAddrs: options.ReplaceAddrs,
|
||||
Status: tfe.RunPending,
|
||||
TargetAddrs: options.TargetAddrs,
|
||||
AllowConfigGeneration: options.AllowConfigGeneration,
|
||||
}
|
||||
|
||||
if options.Message != nil {
|
||||
@ -1043,6 +1044,10 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
||||
r.RefreshOnly = *options.RefreshOnly
|
||||
}
|
||||
|
||||
if options.AllowConfigGeneration != nil && *options.AllowConfigGeneration == true {
|
||||
r.Plan.GeneratedConfiguration = true
|
||||
}
|
||||
|
||||
w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID]
|
||||
if !ok {
|
||||
return nil, tfe.ErrResourceNotFound
|
||||
|
@ -5,12 +5,18 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func ValidateTargetFile(out string) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
func ShouldWriteConfig(out string) bool {
|
||||
if len(out) == 0 {
|
||||
// No specified out file, so don't write anything.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ValidateTargetFile(out string) (diags tfdiags.Diagnostics) {
|
||||
if _, err := os.Stat(out); !os.IsNotExist(err) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
@ -21,68 +27,56 @@ func ValidateTargetFile(out string) tfdiags.Diagnostics {
|
||||
return diags
|
||||
}
|
||||
|
||||
func MaybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) {
|
||||
if len(out) == 0 {
|
||||
// No specified out file, so don't write anything.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
diags = ValidateTargetFile(out)
|
||||
if diags.HasErrors() {
|
||||
return false, diags
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
|
||||
for _, change := range plan.Changes.Resources {
|
||||
if len(change.GeneratedConfig) > 0 {
|
||||
if writer == nil {
|
||||
// Lazily create the generated file, in case we have no
|
||||
// generated config to create.
|
||||
var err error
|
||||
if writer, err = os.Create(out); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to create target generated file",
|
||||
fmt.Sprintf("Terraform did not have permission to create the generated file (%s) in the target directory. Please modify permissions over the target directory, and try again.", out)))
|
||||
return false, diags
|
||||
}
|
||||
type Change struct {
|
||||
Addr string
|
||||
ImportID string
|
||||
GeneratedConfig string
|
||||
}
|
||||
|
||||
func (c *Change) MaybeWriteConfig(writer io.Writer, out string) (io.Writer, bool, tfdiags.Diagnostics) {
|
||||
var wroteConfig bool
|
||||
var diags tfdiags.Diagnostics
|
||||
if len(c.GeneratedConfig) > 0 {
|
||||
if writer == nil {
|
||||
// Lazily create the generated file, in case we have no
|
||||
// generated config to create.
|
||||
if w, err := os.Create(out); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to create target generated file",
|
||||
fmt.Sprintf("Terraform could not create the generated file (%s) in the target directory: %v. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", out, err)))
|
||||
return false, diags
|
||||
fmt.Sprintf("Terraform did not have permission to create the generated file (%s) in the target directory. Please modify permissions over the target directory, and try again.", out)))
|
||||
return nil, false, diags
|
||||
}
|
||||
|
||||
header := "# __generated__ by Terraform\n# Please review these resources and move them into your main configuration files.\n"
|
||||
// Missing the header from the file, isn't the end of the world
|
||||
// so if this did return an error, then we will just ignore it.
|
||||
_, _ = writer.Write([]byte(header))
|
||||
}
|
||||
|
||||
header := "\n# __generated__ by Terraform"
|
||||
if change.Importing != nil && len(change.Importing.ID) > 0 {
|
||||
header += fmt.Sprintf(" from %q", change.Importing.ID)
|
||||
}
|
||||
header += "\n"
|
||||
if _, err := writer.Write([]byte(fmt.Sprintf("%s%s\n", header, change.GeneratedConfig))); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Failed to save generated config",
|
||||
fmt.Sprintf("Terraform encountered an error while writing generated config: %v. The config for %s must be created manually before applying. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", err, change.Addr.String())))
|
||||
tfdiags.Error,
|
||||
"Failed to create target generated file",
|
||||
fmt.Sprintf("Terraform could not create the generated file (%s) in the target directory: %v. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", out, err)))
|
||||
return nil, false, diags
|
||||
} else {
|
||||
writer = w
|
||||
}
|
||||
wroteConfig = true
|
||||
|
||||
header := "# __generated__ by Terraform\n# Please review these resources and move them into your main configuration files.\n"
|
||||
// Missing the header from the file, isn't the end of the world
|
||||
// so if this did return an error, then we will just ignore it.
|
||||
_, _ = writer.Write([]byte(header))
|
||||
}
|
||||
|
||||
header := "\n# __generated__ by Terraform"
|
||||
if len(c.ImportID) > 0 {
|
||||
header += fmt.Sprintf(" from %q", c.ImportID)
|
||||
}
|
||||
header += "\n"
|
||||
if _, err := writer.Write([]byte(fmt.Sprintf("%s%s\n", header, c.GeneratedConfig))); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Failed to save generated config",
|
||||
fmt.Sprintf("Terraform encountered an error while writing generated config: %v. The config for %s must be created manually before applying. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", err, c.Addr)))
|
||||
}
|
||||
wroteConfig = true
|
||||
}
|
||||
|
||||
if wroteConfig {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Config generation is experimental",
|
||||
"Generating configuration during import is currently experimental, and the generated configuration format may change in future versions."))
|
||||
}
|
||||
|
||||
return wroteConfig, diags
|
||||
return writer, wroteConfig, diags
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user