mirror of
https://github.com/opentofu/opentofu.git
synced 2026-02-18 18:17:54 -05:00
Refactor init command to use View instead of Ui (#3749)
Some checks are pending
build / Build for freebsd_386 (push) Waiting to run
build / Build for linux_386 (push) Waiting to run
build / Build for openbsd_386 (push) Waiting to run
build / Build for windows_386 (push) Waiting to run
build / Build for freebsd_amd64 (push) Waiting to run
build / Build for linux_amd64 (push) Waiting to run
build / Build for openbsd_amd64 (push) Waiting to run
build / Build for solaris_amd64 (push) Waiting to run
build / Build for windows_amd64 (push) Waiting to run
build / Build for freebsd_arm (push) Waiting to run
build / Build for linux_arm (push) Waiting to run
build / Build for linux_arm64 (push) Waiting to run
build / Build for darwin_amd64 (push) Waiting to run
build / Build for darwin_arm64 (push) Waiting to run
build / End-to-end Tests for linux_386 (push) Waiting to run
build / End-to-end Tests for windows_386 (push) Waiting to run
build / End-to-end Tests for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for linux_amd64 (push) Waiting to run
build / End-to-end Tests for windows_amd64 (push) Waiting to run
Quick Checks / List files changed for pull request (push) Waiting to run
Quick Checks / Unit tests for linux_386 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for windows_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm (push) Blocked by required conditions
Quick Checks / Unit tests for darwin_arm64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm64 (push) Blocked by required conditions
Quick Checks / Race Tests (push) Blocked by required conditions
Quick Checks / End-to-end Tests (push) Blocked by required conditions
Quick Checks / Code Consistency Checks (push) Blocked by required conditions
Quick Checks / License Checks (push) Waiting to run
Website checks / List files changed for pull request (push) Waiting to run
Website checks / Build (push) Blocked by required conditions
Website checks / Test Installation Instructions (push) Blocked by required conditions
Some checks are pending
build / Build for freebsd_386 (push) Waiting to run
build / Build for linux_386 (push) Waiting to run
build / Build for openbsd_386 (push) Waiting to run
build / Build for windows_386 (push) Waiting to run
build / Build for freebsd_amd64 (push) Waiting to run
build / Build for linux_amd64 (push) Waiting to run
build / Build for openbsd_amd64 (push) Waiting to run
build / Build for solaris_amd64 (push) Waiting to run
build / Build for windows_amd64 (push) Waiting to run
build / Build for freebsd_arm (push) Waiting to run
build / Build for linux_arm (push) Waiting to run
build / Build for linux_arm64 (push) Waiting to run
build / Build for darwin_amd64 (push) Waiting to run
build / Build for darwin_arm64 (push) Waiting to run
build / End-to-end Tests for linux_386 (push) Waiting to run
build / End-to-end Tests for windows_386 (push) Waiting to run
build / End-to-end Tests for darwin_amd64 (push) Waiting to run
build / End-to-end Tests for linux_amd64 (push) Waiting to run
build / End-to-end Tests for windows_amd64 (push) Waiting to run
Quick Checks / List files changed for pull request (push) Waiting to run
Quick Checks / Unit tests for linux_386 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for windows_amd64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm (push) Blocked by required conditions
Quick Checks / Unit tests for darwin_arm64 (push) Blocked by required conditions
Quick Checks / Unit tests for linux_arm64 (push) Blocked by required conditions
Quick Checks / Race Tests (push) Blocked by required conditions
Quick Checks / End-to-end Tests (push) Blocked by required conditions
Quick Checks / Code Consistency Checks (push) Blocked by required conditions
Quick Checks / License Checks (push) Waiting to run
Website checks / List files changed for pull request (push) Waiting to run
Website checks / Build (push) Blocked by required conditions
Website checks / Test Installation Instructions (push) Blocked by required conditions
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
parent
1e0a920a34
commit
5603b8a27c
23 changed files with 3774 additions and 817 deletions
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/mattn/go-shellwords"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"github.com/opentofu/opentofu/internal/command"
|
||||
"github.com/opentofu/opentofu/internal/command/workdir"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
|
|
@ -47,23 +48,8 @@ const (
|
|||
EnvCPUProfile = "TOFU_CPU_PROFILE"
|
||||
)
|
||||
|
||||
// ui wraps the primary output cli.Ui, and redirects Warn calls to Output
|
||||
// calls. This ensures that warnings are sent to stdout, and are properly
|
||||
// serialized within the stdout stream.
|
||||
type ui struct {
|
||||
cli.Ui
|
||||
}
|
||||
|
||||
func (u *ui) Warn(msg string) {
|
||||
u.Ui.Output(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Ui = &ui{&cli.BasicUi{
|
||||
Writer: os.Stdout,
|
||||
ErrorWriter: os.Stderr,
|
||||
Reader: os.Stdin,
|
||||
}}
|
||||
Ui = command.NewBasicUI()
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/command"
|
||||
)
|
||||
|
||||
func TestMain_cliArgsFromEnv(t *testing.T) {
|
||||
|
|
@ -291,7 +292,7 @@ func (c *testCommandCLI) Help() string { return "" }
|
|||
|
||||
func TestWarnOutput(t *testing.T) {
|
||||
mock := cli.NewMockUi()
|
||||
wrapped := &ui{mock}
|
||||
wrapped := command.NewWrappedUi(mock)
|
||||
wrapped.Warn("WARNING")
|
||||
|
||||
stderr := mock.ErrorWriter.String()
|
||||
|
|
|
|||
59
internal/command/arguments/backend.go
Normal file
59
internal/command/arguments/backend.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"time"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
IgnoreRemoteVersion bool
|
||||
// StateLock indicates if the state should be locked or not.
|
||||
StateLock bool
|
||||
// StateLockTimeout configures the duration that it waits for the state lock to be acquired.
|
||||
StateLockTimeout time.Duration
|
||||
// ForceInitCopy controls if the prompts for state migration should be skipped or not.
|
||||
ForceInitCopy bool
|
||||
// Reconfigure controls if the reconfiguration of the backend should happen with discarding the old configurations.
|
||||
Reconfigure bool
|
||||
// MigrateState controls if during the reconfiguration of the backend a migration should be attempted.
|
||||
MigrateState bool
|
||||
}
|
||||
|
||||
func (b *Backend) AddIgnoreRemoteVersionFlag(f *flag.FlagSet) {
|
||||
f.BoolVar(&b.IgnoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local OpenTofu versions are incompatible")
|
||||
}
|
||||
|
||||
func (b *Backend) AddStateFlags(f *flag.FlagSet) {
|
||||
f.BoolVar(&b.StateLock, "lock", true, "lock state")
|
||||
f.DurationVar(&b.StateLockTimeout, "lock-timeout", 0, "lock timeout")
|
||||
}
|
||||
|
||||
func (b *Backend) AddMigrationFlags(f *flag.FlagSet) {
|
||||
f.BoolVar(&b.ForceInitCopy, "force-copy", false, "suppress prompts about copying state data")
|
||||
f.BoolVar(&b.Reconfigure, "reconfigure", false, "reconfigure")
|
||||
f.BoolVar(&b.MigrateState, "migrate-state", false, "migrate state")
|
||||
}
|
||||
|
||||
func (b *Backend) migrationFlagsCheck() (diags tfdiags.Diagnostics) {
|
||||
if b.MigrateState && b.Reconfigure {
|
||||
return diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Wrong combination of options",
|
||||
"The -migrate-state and -reconfigure options are mutually-exclusive",
|
||||
))
|
||||
}
|
||||
|
||||
// Copying the state only happens during backend migration, so setting
|
||||
// -force-copy implies -migrate-state
|
||||
if b.ForceInitCopy {
|
||||
b.MigrateState = true
|
||||
}
|
||||
return diags
|
||||
}
|
||||
412
internal/command/arguments/backend_test.go
Normal file
412
internal/command/arguments/backend_test.go
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestBackend_AddIgnoreRemoteVersionFlag(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
"default value": {
|
||||
args: nil,
|
||||
want: false,
|
||||
},
|
||||
"flag not provided": {
|
||||
args: []string{},
|
||||
want: false,
|
||||
},
|
||||
"flag set to true": {
|
||||
args: []string{"-ignore-remote-version"},
|
||||
want: true,
|
||||
},
|
||||
"flag explicitly set to false": {
|
||||
args: []string{"-ignore-remote-version=false"},
|
||||
want: false,
|
||||
},
|
||||
"flag explicitly set to true": {
|
||||
args: []string{"-ignore-remote-version=true"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := &Backend{}
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
backend.AddIgnoreRemoteVersionFlag(fs)
|
||||
|
||||
if err := fs.Parse(tc.args); err != nil {
|
||||
t.Fatalf("unexpected error parsing flags: %v", err)
|
||||
}
|
||||
|
||||
if got := backend.IgnoreRemoteVersion; got != tc.want {
|
||||
t.Errorf("IgnoreRemoteVersion = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_AddStateFlags(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantLock bool
|
||||
wantLockTimeout time.Duration
|
||||
}{
|
||||
"default values": {
|
||||
args: nil,
|
||||
wantLock: true,
|
||||
wantLockTimeout: 0,
|
||||
},
|
||||
"lock set to false": {
|
||||
args: []string{"-lock=false"},
|
||||
wantLock: false,
|
||||
wantLockTimeout: 0,
|
||||
},
|
||||
"lock set to true explicitly": {
|
||||
args: []string{"-lock=true"},
|
||||
wantLock: true,
|
||||
wantLockTimeout: 0,
|
||||
},
|
||||
"lock-timeout set": {
|
||||
args: []string{"-lock-timeout=10s"},
|
||||
wantLock: true,
|
||||
wantLockTimeout: 10 * time.Second,
|
||||
},
|
||||
"lock-timeout set in minutes": {
|
||||
args: []string{"-lock-timeout=5m"},
|
||||
wantLock: true,
|
||||
wantLockTimeout: 5 * time.Minute,
|
||||
},
|
||||
"both flags set": {
|
||||
args: []string{"-lock=false", "-lock-timeout=30s"},
|
||||
wantLock: false,
|
||||
wantLockTimeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := &Backend{}
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
backend.AddStateFlags(fs)
|
||||
|
||||
if err := fs.Parse(tc.args); err != nil {
|
||||
t.Fatalf("unexpected error parsing flags: %v", err)
|
||||
}
|
||||
|
||||
if got := backend.StateLock; got != tc.wantLock {
|
||||
t.Errorf("StateLock = %v, want %v", got, tc.wantLock)
|
||||
}
|
||||
|
||||
if got := backend.StateLockTimeout; got != tc.wantLockTimeout {
|
||||
t.Errorf("StateLockTimeout = %v, want %v", got, tc.wantLockTimeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_AddMigrationFlags(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantForceInitCopy bool
|
||||
wantReconfigure bool
|
||||
wantMigrateState bool
|
||||
}{
|
||||
"default values": {
|
||||
args: nil,
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"force-copy set": {
|
||||
args: []string{"-force-copy"},
|
||||
wantForceInitCopy: true,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"force-copy explicitly true": {
|
||||
args: []string{"-force-copy=true"},
|
||||
wantForceInitCopy: true,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"force-copy explicitly false": {
|
||||
args: []string{"-force-copy=false"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"reconfigure set": {
|
||||
args: []string{"-reconfigure"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: true,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"reconfigure explicitly true": {
|
||||
args: []string{"-reconfigure=true"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: true,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"reconfigure explicitly false": {
|
||||
args: []string{"-reconfigure=false"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"migrate-state set": {
|
||||
args: []string{"-migrate-state"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"migrate-state explicitly true": {
|
||||
args: []string{"-migrate-state=true"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"migrate-state explicitly false": {
|
||||
args: []string{"-migrate-state=false"},
|
||||
wantForceInitCopy: false,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"force-copy and migrate-state set": {
|
||||
args: []string{"-force-copy", "-migrate-state"},
|
||||
wantForceInitCopy: true,
|
||||
wantReconfigure: false,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"all flags set": {
|
||||
args: []string{"-force-copy", "-reconfigure", "-migrate-state"},
|
||||
wantForceInitCopy: true,
|
||||
wantReconfigure: true,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := &Backend{}
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
backend.AddMigrationFlags(fs)
|
||||
|
||||
if err := fs.Parse(tc.args); err != nil {
|
||||
t.Fatalf("unexpected error parsing flags: %v", err)
|
||||
}
|
||||
|
||||
if got := backend.ForceInitCopy; got != tc.wantForceInitCopy {
|
||||
t.Errorf("ForceInitCopy = %v, want %v", got, tc.wantForceInitCopy)
|
||||
}
|
||||
|
||||
if got := backend.Reconfigure; got != tc.wantReconfigure {
|
||||
t.Errorf("Reconfigure = %v, want %v", got, tc.wantReconfigure)
|
||||
}
|
||||
|
||||
if got := backend.MigrateState; got != tc.wantMigrateState {
|
||||
t.Errorf("MigrateState = %v, want %v", got, tc.wantMigrateState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_migrationFlagsCheck(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
backend Backend
|
||||
wantDiags bool
|
||||
wantMigrateState bool
|
||||
diagsSummary string
|
||||
}{
|
||||
"no flags set": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: false,
|
||||
MigrateState: false,
|
||||
},
|
||||
wantDiags: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"only migrate-state set": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: false,
|
||||
MigrateState: true,
|
||||
},
|
||||
wantDiags: false,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"only reconfigure set": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: true,
|
||||
MigrateState: false,
|
||||
},
|
||||
wantDiags: false,
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"only force-copy set": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: true,
|
||||
Reconfigure: false,
|
||||
MigrateState: false,
|
||||
},
|
||||
wantDiags: false,
|
||||
wantMigrateState: true, // force-copy implies migrate-state
|
||||
},
|
||||
"force-copy and migrate-state set": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: true,
|
||||
Reconfigure: false,
|
||||
MigrateState: true,
|
||||
},
|
||||
wantDiags: false,
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"migrate-state and reconfigure set (mutually exclusive)": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: true,
|
||||
MigrateState: true,
|
||||
},
|
||||
wantDiags: true,
|
||||
wantMigrateState: true,
|
||||
diagsSummary: "Wrong combination of options",
|
||||
},
|
||||
"all flags set (error due to reconfigure + migrate-state)": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: true,
|
||||
Reconfigure: true,
|
||||
MigrateState: true,
|
||||
},
|
||||
wantDiags: true,
|
||||
wantMigrateState: true,
|
||||
diagsSummary: "Wrong combination of options",
|
||||
},
|
||||
"force-copy and reconfigure set (no error - check happens before force-copy sets migrate-state)": {
|
||||
backend: Backend{
|
||||
ForceInitCopy: true,
|
||||
Reconfigure: true,
|
||||
MigrateState: false,
|
||||
},
|
||||
wantDiags: false, // No error because MigrateState is false when check happens
|
||||
wantMigrateState: true, // force-copy sets this to true after the check
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := tc.backend
|
||||
diags := backend.migrationFlagsCheck()
|
||||
|
||||
if tc.wantDiags && len(diags) == 0 {
|
||||
t.Fatal("expected diagnostics but got none")
|
||||
}
|
||||
|
||||
if !tc.wantDiags && len(diags) != 0 {
|
||||
t.Fatalf("unexpected diagnostics: %v", diags)
|
||||
}
|
||||
|
||||
if tc.wantDiags && len(diags) == 1 {
|
||||
diag := diags[0]
|
||||
if diag.Description().Summary != tc.diagsSummary {
|
||||
t.Errorf("diagnostic summary = %q, want %q",
|
||||
diag.Description().Summary, tc.diagsSummary)
|
||||
}
|
||||
|
||||
// Verify it's an error
|
||||
if diag.Severity() != tfdiags.Error {
|
||||
t.Errorf("diagnostic severity = %v, want %v",
|
||||
diag.Severity(), tfdiags.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that MigrateState is set correctly
|
||||
if got := backend.MigrateState; got != tc.wantMigrateState {
|
||||
t.Errorf("MigrateState after check = %v, want %v", got, tc.wantMigrateState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_AllFlags(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want Backend
|
||||
}{
|
||||
"all defaults": {
|
||||
args: nil,
|
||||
want: Backend{
|
||||
IgnoreRemoteVersion: false,
|
||||
StateLock: true,
|
||||
StateLockTimeout: 0,
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: false,
|
||||
MigrateState: false,
|
||||
},
|
||||
},
|
||||
"all flags set": {
|
||||
args: []string{
|
||||
"-ignore-remote-version",
|
||||
"-lock=false",
|
||||
"-lock-timeout=1m",
|
||||
"-force-copy",
|
||||
"-reconfigure",
|
||||
"-migrate-state",
|
||||
},
|
||||
want: Backend{
|
||||
IgnoreRemoteVersion: true,
|
||||
StateLock: false,
|
||||
StateLockTimeout: time.Minute,
|
||||
ForceInitCopy: true,
|
||||
Reconfigure: true,
|
||||
MigrateState: true,
|
||||
},
|
||||
},
|
||||
"mixed flags": {
|
||||
args: []string{
|
||||
"-ignore-remote-version=true",
|
||||
"-lock-timeout=30s",
|
||||
"-migrate-state",
|
||||
},
|
||||
want: Backend{
|
||||
IgnoreRemoteVersion: true,
|
||||
StateLock: true,
|
||||
StateLockTimeout: 30 * time.Second,
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: false,
|
||||
MigrateState: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := &Backend{}
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
backend.AddIgnoreRemoteVersionFlag(fs)
|
||||
backend.AddStateFlags(fs)
|
||||
backend.AddMigrationFlags(fs)
|
||||
|
||||
if err := fs.Parse(tc.args); err != nil {
|
||||
t.Fatalf("unexpected error parsing flags: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.want, *backend); diff != "" {
|
||||
t.Errorf("unexpected result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
121
internal/command/arguments/init.go
Normal file
121
internal/command/arguments/init.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/opentofu/opentofu/internal/command/flags"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Init represents the command-line arguments for the init command.
|
||||
type Init struct {
|
||||
// Copy the contents of the given module into the target directory before initialisation
|
||||
FlagFromModule string
|
||||
// Lockfile operation mode. Currently only "readonly" is valid.
|
||||
FlagLockfile string
|
||||
// Set the OpenTofu test directory. When set, the
|
||||
// test command will search for test files in the current directory and
|
||||
// in the one specified by the flag.
|
||||
TestsDirectory string
|
||||
// When set to false, disables modules downloading for the current configuration
|
||||
FlagGet bool
|
||||
// Install the latest module and provider versions allowed within configured constraints, overriding the
|
||||
// default behavior of selecting exactly the version recorded in the dependency lockfile.
|
||||
FlagUpgrade bool
|
||||
// Directory containing plugin binaries. This overrides all default search paths for plugins, and prevents the
|
||||
// automatic installation of plugins. This flag can be used multiple times.
|
||||
FlagPluginPath flags.FlagStringSlice
|
||||
// Configuration to be merged with what is in the configuration file's 'backend' block. This can be
|
||||
// either a path to an HCL file with key/value assignments (same format as terraform.tfvars) or a
|
||||
// 'key=value' format, and can be specified multiple times. The backend type must be in the configuration itself.
|
||||
FlagConfigExtra flags.RawFlags
|
||||
// Disable backend or cloud backend initialization for this configuration and use what was previously
|
||||
// initialized instead. This and the FlagCloud cannot be toggled in the same time.
|
||||
FlagBackend bool
|
||||
FlagCloud bool
|
||||
|
||||
// Bools indicating that the FlagBackend and FlagCloud have been found into the arguments list of the
|
||||
// process.
|
||||
BackendFlagSet bool
|
||||
CloudFlagSet bool
|
||||
|
||||
// ViewOptions specifies which view options to use
|
||||
ViewOptions ViewOptions
|
||||
|
||||
// Vars holds and provides information for the flags related to variables that a user can give into the process
|
||||
Vars *Vars
|
||||
// Backend holds and providers information for the flags related to the backend operations, like locking
|
||||
// locking timeout, force migration, etc.
|
||||
Backend *Backend
|
||||
}
|
||||
|
||||
// ParseInit processes CLI arguments, returning an Init value, a closer function, and errors.
|
||||
// If errors are encountered, an Init value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseInit(args []string) (*Init, func(), tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
init := &Init{
|
||||
Vars: &Vars{},
|
||||
Backend: &Backend{},
|
||||
FlagConfigExtra: flags.NewRawFlags("-backend-config"),
|
||||
}
|
||||
|
||||
cmdFlags := extendedFlagSet("init", nil, nil, init.Vars)
|
||||
init.Backend.AddIgnoreRemoteVersionFlag(cmdFlags)
|
||||
init.Backend.AddStateFlags(cmdFlags)
|
||||
init.Backend.AddMigrationFlags(cmdFlags)
|
||||
cmdFlags.BoolVar(&init.FlagBackend, "backend", true, "")
|
||||
cmdFlags.BoolVar(&init.FlagCloud, "cloud", true, "")
|
||||
cmdFlags.Var(init.FlagConfigExtra, "backend-config", "")
|
||||
cmdFlags.StringVar(&init.FlagFromModule, "from-module", "", "copy the source of the given module into the directory before init")
|
||||
cmdFlags.BoolVar(&init.FlagGet, "get", true, "")
|
||||
cmdFlags.BoolVar(&init.FlagUpgrade, "upgrade", false, "")
|
||||
cmdFlags.Var(&init.FlagPluginPath, "plugin-dir", "plugin directory")
|
||||
cmdFlags.StringVar(&init.FlagLockfile, "lockfile", "", "Set a dependency lockfile mode")
|
||||
cmdFlags.StringVar(&init.TestsDirectory, "test-directory", "tests", "test-directory")
|
||||
|
||||
init.ViewOptions.AddFlags(cmdFlags, true)
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
closer, moreDiags := init.ViewOptions.Parse()
|
||||
diags = diags.Append(moreDiags)
|
||||
if diags.HasErrors() {
|
||||
return init, closer, diags
|
||||
}
|
||||
init.BackendFlagSet = flags.FlagIsSet(cmdFlags, "backend")
|
||||
init.CloudFlagSet = flags.FlagIsSet(cmdFlags, "cloud")
|
||||
|
||||
switch {
|
||||
case init.BackendFlagSet && init.CloudFlagSet:
|
||||
return init, closer, diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Wrong combination of options",
|
||||
"The -backend and -cloud options are aliases of one another and mutually-exclusive in their use",
|
||||
))
|
||||
case init.BackendFlagSet:
|
||||
init.FlagCloud = init.FlagBackend
|
||||
case init.CloudFlagSet:
|
||||
init.FlagBackend = init.FlagCloud
|
||||
}
|
||||
|
||||
diags = diags.Append(init.Backend.migrationFlagsCheck())
|
||||
|
||||
if len(cmdFlags.Args()) > 0 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unexpected argument",
|
||||
"Too many command line arguments. Did you mean to use -chdir?",
|
||||
))
|
||||
}
|
||||
return init, closer, diags
|
||||
}
|
||||
505
internal/command/arguments/init_test.go
Normal file
505
internal/command/arguments/init_test.go
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package arguments
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/opentofu/opentofu/internal/command/flags"
|
||||
)
|
||||
|
||||
func TestParseInit_basicValidation(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Init
|
||||
}{
|
||||
"defaults": {
|
||||
nil,
|
||||
initArgsWithDefaults(nil),
|
||||
},
|
||||
"upgrade flag": {
|
||||
[]string{"-upgrade"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagUpgrade = true
|
||||
}),
|
||||
},
|
||||
"get flag disabled": {
|
||||
[]string{"-get=false"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagGet = false
|
||||
}),
|
||||
},
|
||||
"from-module flag with value": {
|
||||
[]string{"-from-module=/path/to/module"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagFromModule = "/path/to/module"
|
||||
}),
|
||||
},
|
||||
"lockfile readonly": {
|
||||
[]string{"-lockfile=readonly"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagLockfile = "readonly"
|
||||
}),
|
||||
},
|
||||
"custom test-directory": {
|
||||
[]string{"-test-directory=integration"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.TestsDirectory = "integration"
|
||||
}),
|
||||
},
|
||||
"backend disabled": {
|
||||
[]string{"-backend=false"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagBackend = false
|
||||
init.FlagCloud = false
|
||||
init.BackendFlagSet = true
|
||||
}),
|
||||
},
|
||||
"cloud disabled": {
|
||||
[]string{"-cloud=false"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagBackend = false
|
||||
init.FlagCloud = false
|
||||
init.CloudFlagSet = true
|
||||
}),
|
||||
},
|
||||
"multiple flags combined": {
|
||||
[]string{"-upgrade", "-lockfile=readonly", "-get=false", "-from-module=/tmp/mod"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
init.FlagFromModule = "/tmp/mod"
|
||||
init.FlagLockfile = "readonly"
|
||||
init.FlagGet = false
|
||||
init.FlagUpgrade = true
|
||||
}),
|
||||
},
|
||||
"one plugin dir configured": {
|
||||
[]string{"-plugin-dir=/test1"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
_ = init.FlagPluginPath.Set("/test1")
|
||||
}),
|
||||
},
|
||||
"multiple plugin dirs configured": {
|
||||
[]string{"-plugin-dir=/test1", "-plugin-dir=/test2"},
|
||||
initArgsWithDefaults(func(init *Init) {
|
||||
_ = init.FlagPluginPath.Set("/test1")
|
||||
_ = init.FlagPluginPath.Set("/test2")
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
cmpOpts := cmpopts.IgnoreUnexported(Vars{}, ViewOptions{})
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
|
||||
t.Errorf("unexpected result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_backendCloudErrors(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantBackend bool
|
||||
wantCloud bool
|
||||
}{
|
||||
"both explicitly set to true": {
|
||||
args: []string{"-backend=true", "-cloud=true"},
|
||||
wantBackend: true,
|
||||
wantCloud: true,
|
||||
},
|
||||
"both explicitly set to false": {
|
||||
args: []string{"-backend=false", "-cloud=false"},
|
||||
wantBackend: false,
|
||||
wantCloud: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) == 0 {
|
||||
t.Fatal("expected diagnostics but got none")
|
||||
}
|
||||
if got, want := diags.Err().Error(), "Wrong combination of options"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := diags.Err().Error(), "mutually-exclusive"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got.FlagBackend != tc.wantBackend {
|
||||
t.Errorf("wrong FlagBackend. wanted %t but got %t", tc.wantBackend, got.FlagBackend)
|
||||
}
|
||||
if got.FlagCloud != tc.wantCloud {
|
||||
t.Errorf("wrong FlagCloud. wanted %t, want %t", tc.wantCloud, got.FlagCloud)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_backendCloudSynchronization(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantBackend bool
|
||||
wantCloud bool
|
||||
wantBackendSet bool
|
||||
wantCloudSet bool
|
||||
}{
|
||||
"backend=false only": {
|
||||
args: []string{"-backend=false"},
|
||||
wantBackend: false,
|
||||
wantCloud: false,
|
||||
wantBackendSet: true,
|
||||
wantCloudSet: false,
|
||||
},
|
||||
"backend=true only": {
|
||||
args: []string{"-backend=true"},
|
||||
wantBackend: true,
|
||||
wantCloud: true,
|
||||
wantBackendSet: true,
|
||||
wantCloudSet: false,
|
||||
},
|
||||
"cloud=false only": {
|
||||
args: []string{"-cloud=false"},
|
||||
wantBackend: false,
|
||||
wantCloud: false,
|
||||
wantBackendSet: false,
|
||||
wantCloudSet: true,
|
||||
},
|
||||
"cloud=true only": {
|
||||
args: []string{"-cloud=true"},
|
||||
wantBackend: true,
|
||||
wantCloud: true,
|
||||
wantBackendSet: false,
|
||||
wantCloudSet: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if got.FlagBackend != tc.wantBackend {
|
||||
t.Errorf("wrong FlagBackend. wanted %t but got %t", tc.wantBackend, got.FlagBackend)
|
||||
}
|
||||
if got.FlagCloud != tc.wantCloud {
|
||||
t.Errorf("wrong FlagCloud. wanted %t but got want %t", tc.wantCloud, got.FlagCloud)
|
||||
}
|
||||
if got.BackendFlagSet != tc.wantBackendSet {
|
||||
t.Errorf("wrong BackendFlagSet. wanted %t but got %t", tc.wantBackendSet, got.BackendFlagSet)
|
||||
}
|
||||
if got.CloudFlagSet != tc.wantCloudSet {
|
||||
t.Errorf("wrong CloudFlagSet. wanted %t but got %t", tc.wantCloudSet, got.CloudFlagSet)
|
||||
}
|
||||
if got.FlagBackend != got.FlagCloud {
|
||||
t.Errorf("wrong FlagBackend. expected to be in sync with FlagCloud, instead got FlagBackend=%t and FlagCloud=%t", got.FlagCloud, got.FlagBackend)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_backendFlags(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantBackend Backend
|
||||
}{
|
||||
"ignore-remote-version": {
|
||||
args: []string{"-ignore-remote-version"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.IgnoreRemoteVersion = true
|
||||
}),
|
||||
},
|
||||
"lock disabled": {
|
||||
args: []string{"-lock=false"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.StateLock = false
|
||||
}),
|
||||
},
|
||||
"lock-timeout": {
|
||||
args: []string{"-lock-timeout=30s"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.StateLockTimeout = 30 * time.Second
|
||||
}),
|
||||
},
|
||||
"migrate-state": {
|
||||
args: []string{"-migrate-state"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.MigrateState = true
|
||||
}),
|
||||
},
|
||||
"reconfigure": {
|
||||
args: []string{"-reconfigure"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.Reconfigure = true
|
||||
}),
|
||||
},
|
||||
"force-copy": {
|
||||
args: []string{"-force-copy"},
|
||||
wantBackend: backendWithDefaults(func(backend *Backend) {
|
||||
backend.ForceInitCopy = true
|
||||
backend.MigrateState = true
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantBackend, *got.Backend); diff != "" {
|
||||
t.Errorf("unexpected Backend result (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_migrationFlagsValidation(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantMigrateState bool
|
||||
wantDiagDetails string
|
||||
}{
|
||||
"force-copy implies migrate-state": {
|
||||
args: []string{"-force-copy"},
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"migrate-state set": {
|
||||
args: []string{"-migrate-state"},
|
||||
wantMigrateState: true,
|
||||
},
|
||||
"reconfigure set": {
|
||||
args: []string{"-reconfigure"},
|
||||
wantMigrateState: false,
|
||||
},
|
||||
"migration-state and reconfigure set": {
|
||||
args: []string{"-reconfigure", "-migrate-state"},
|
||||
wantMigrateState: true,
|
||||
wantDiagDetails: "The -migrate-state and -reconfigure options are mutually-exclusive",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
switch {
|
||||
case len(diags) == 0 && len(tc.wantDiagDetails) > 0:
|
||||
t.Fatal("expected to have a diagnostic but got none")
|
||||
case len(diags) > 0 && len(tc.wantDiagDetails) == 0:
|
||||
t.Fatalf("expected no diagnostic but got: %s", diags)
|
||||
case len(diags) > 0 && len(tc.wantDiagDetails) > 0:
|
||||
diag := diags[0]
|
||||
if diag.Description().Detail != tc.wantDiagDetails {
|
||||
t.Fatalf("Diagnostic Detail = %q; want %q", diag.Description().Detail, tc.wantDiagDetails)
|
||||
}
|
||||
}
|
||||
|
||||
if got.Backend.MigrateState != tc.wantMigrateState {
|
||||
t.Errorf("Backend.MigrateState = %v, want %v", got.Backend.MigrateState, tc.wantMigrateState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_backendConfig(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantCount int
|
||||
wantValues []string
|
||||
}{
|
||||
"no backend config": {
|
||||
args: nil,
|
||||
wantCount: 0,
|
||||
wantValues: nil,
|
||||
},
|
||||
"single backend config kv": {
|
||||
args: []string{"-backend-config=key=value"},
|
||||
wantCount: 1,
|
||||
wantValues: []string{"key=value"},
|
||||
},
|
||||
"backend config file": {
|
||||
args: []string{"-backend-config=/path/to/config.hcl"},
|
||||
wantCount: 1,
|
||||
wantValues: []string{"/path/to/config.hcl"},
|
||||
},
|
||||
"multiple backend configs": {
|
||||
args: []string{"-backend-config=k1=v1", "-backend-config=k2=v2", "-backend-config=/path/config.hcl"},
|
||||
wantCount: 3,
|
||||
wantValues: []string{"k1=v1", "k2=v2", "/path/config.hcl"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if tc.wantCount == 0 {
|
||||
if !got.FlagConfigExtra.Empty() {
|
||||
t.Error("FlagConfigExtra should be empty")
|
||||
}
|
||||
return
|
||||
}
|
||||
if got.FlagConfigExtra.Empty() {
|
||||
t.Error("FlagConfigExtra should not be empty")
|
||||
}
|
||||
items := got.FlagConfigExtra.AllItems()
|
||||
if len(items) != tc.wantCount {
|
||||
t.Errorf("len(FlagConfigExtra.AllItems()) = %d, want %d", len(items), tc.wantCount)
|
||||
}
|
||||
for i, want := range tc.wantValues {
|
||||
if items[i].Value != want {
|
||||
t.Errorf("FlagConfigExtra.AllItems()[%d].Value = %q, want %q", i, items[i].Value, want)
|
||||
}
|
||||
if items[i].Name != "-backend-config" {
|
||||
t.Errorf("FlagConfigExtra.AllItems()[%d].Name = %q, want %q", i, items[i].Name, "-backend-config")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_vars(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
wantCount int
|
||||
wantEmpty bool
|
||||
}{
|
||||
"no vars": {
|
||||
args: nil,
|
||||
wantCount: 0,
|
||||
wantEmpty: true,
|
||||
},
|
||||
"single var": {
|
||||
args: []string{"-var", "foo=bar"},
|
||||
wantCount: 1,
|
||||
wantEmpty: false,
|
||||
},
|
||||
"single var-file": {
|
||||
args: []string{"-var-file", "terraform.tfvars"},
|
||||
wantCount: 1,
|
||||
wantEmpty: false,
|
||||
},
|
||||
"multiple vars mixed": {
|
||||
args: []string{"-var", "a=1", "-var-file", "f.tfvars", "-var", "b=2"},
|
||||
wantCount: 3,
|
||||
wantEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if got.Vars.Empty() != tc.wantEmpty {
|
||||
t.Errorf("Vars.Empty() = %v, want %v", got.Vars.Empty(), tc.wantEmpty)
|
||||
}
|
||||
if len(got.Vars.All()) != tc.wantCount {
|
||||
t.Errorf("len(Vars.All()) = %d, want %d", len(got.Vars.All()), tc.wantCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInit_tooManyArguments(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
}{
|
||||
"one positional argument": {
|
||||
args: []string{"mydir"},
|
||||
},
|
||||
"multiple positional arguments": {
|
||||
args: []string{"dir1", "dir2"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, closer, diags := ParseInit(tc.args)
|
||||
defer closer()
|
||||
|
||||
if len(diags) == 0 {
|
||||
t.Fatal("expected diagnostics but got none")
|
||||
}
|
||||
if got, want := diags.Err().Error(), "Unexpected argument"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
if got, want := diags.Err().Error(), "Too many command line arguments"; !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initArgsWithDefaults(mutate func(init *Init)) *Init {
|
||||
ret := &Init{
|
||||
FlagFromModule: "",
|
||||
FlagLockfile: "",
|
||||
TestsDirectory: "tests",
|
||||
FlagGet: true,
|
||||
FlagUpgrade: false,
|
||||
FlagPluginPath: nil,
|
||||
FlagConfigExtra: flags.NewRawFlags("-backend-config"),
|
||||
FlagBackend: true,
|
||||
FlagCloud: true,
|
||||
BackendFlagSet: false,
|
||||
CloudFlagSet: false,
|
||||
ViewOptions: ViewOptions{
|
||||
ViewType: ViewHuman,
|
||||
InputEnabled: true,
|
||||
},
|
||||
Vars: &Vars{},
|
||||
Backend: &Backend{StateLock: true},
|
||||
}
|
||||
if mutate != nil {
|
||||
mutate(ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func backendWithDefaults(mutate func(backend *Backend)) Backend {
|
||||
ret := Backend{
|
||||
IgnoreRemoteVersion: false,
|
||||
StateLock: true,
|
||||
StateLockTimeout: 0,
|
||||
ForceInitCopy: false,
|
||||
Reconfigure: false,
|
||||
MigrateState: false,
|
||||
}
|
||||
if mutate != nil {
|
||||
mutate(&ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
|
@ -54,3 +55,29 @@ func (u *ColorizeUi) colorize(message string, color string) string {
|
|||
|
||||
return u.Colorize.Color(fmt.Sprintf("%s%s[reset]", color, message))
|
||||
}
|
||||
|
||||
// ui wraps the primary output [cli.Ui], and redirects Warn calls to Output
|
||||
// calls. This ensures that warnings are sent to stdout, and are properly
|
||||
// serialized within the stdout stream.
|
||||
type ui struct {
|
||||
cli.Ui
|
||||
}
|
||||
|
||||
func (u *ui) Warn(msg string) {
|
||||
u.Ui.Output(msg)
|
||||
}
|
||||
|
||||
// NewBasicUI returns a preconfigured [cli.Ui] that is meant to be used
|
||||
// as the primary Ui for OpenTofu.
|
||||
// TODO meta-refactor: this will have to be removed once everything is moved to views.
|
||||
func NewBasicUI() cli.Ui {
|
||||
return NewWrappedUi(&cli.BasicUi{
|
||||
Writer: os.Stdout,
|
||||
ErrorWriter: os.Stderr,
|
||||
Reader: os.Stdin,
|
||||
})
|
||||
}
|
||||
|
||||
func NewWrappedUi(u cli.Ui) cli.Ui {
|
||||
return &ui{u}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ func TestInitProvidersLocalOnly(t *testing.T) {
|
|||
t.Errorf("success message is missing from output:\n%s", stdout)
|
||||
}
|
||||
|
||||
if !strings.Contains(stdout, `{"@level":"info","@message":"- Installing example.com/awesomecorp/happycloud v1.2.0...","@module":"tofu.ui"`) {
|
||||
if !strings.Contains(stdout, `{"@level":"info","@message":"Installing example.com/awesomecorp/happycloud v1.2.0...","@module":"tofu.ui"`) {
|
||||
t.Errorf("provider download message is missing from output:\n%s", stdout)
|
||||
t.Logf("(this can happen if you have a conflicting copy of the plugin in one of the global plugin search dirs)")
|
||||
}
|
||||
|
|
@ -577,7 +577,7 @@ Initializing provider plugins...
|
|||
|
||||
// The following test is temporarily removed until the OpenTofu registry returns a deprecation warning
|
||||
// https://github.com/opentofu/registry/issues/108
|
||||
//func TestInitProviderWarnings(t *testing.T) {
|
||||
// func TestInitProviderWarnings(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// // This test will reach out to registry.terraform.io as one of the possible
|
||||
|
|
@ -596,7 +596,7 @@ Initializing provider plugins...
|
|||
// t.Errorf("expected warning message is missing from output:\n%s", stdout)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
// }
|
||||
|
||||
func escapeStringJSON(v string) string {
|
||||
b := &strings.Builder{}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package command
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
|
|
@ -45,116 +46,58 @@ type InitCommand struct {
|
|||
Meta
|
||||
}
|
||||
|
||||
func (c *InitCommand) Run(args []string) int {
|
||||
func (c *InitCommand) Run(rawArgs []string) int {
|
||||
ctx := c.CommandContext()
|
||||
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Init")
|
||||
defer span.End()
|
||||
|
||||
var flagFromModule, flagLockfile, testsDirectory string
|
||||
var flagBackend, flagCloud, flagGet, flagUpgrade bool
|
||||
var flagPluginPath flags.FlagStringSlice
|
||||
flagConfigExtra := flags.NewRawFlags("-backend-config")
|
||||
// new view
|
||||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
// Because the legacy UI was using println to show diagnostics and the new view is using, by default, print,
|
||||
// in order to keep functional parity, we setup the view to add a new line after each diagnostic.
|
||||
c.View.DiagsWithNewline()
|
||||
|
||||
args = c.Meta.process(args)
|
||||
cmdFlags := c.Meta.extendedFlagSet("init")
|
||||
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
|
||||
cmdFlags.BoolVar(&flagCloud, "cloud", true, "")
|
||||
cmdFlags.Var(flagConfigExtra, "backend-config", "")
|
||||
cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init")
|
||||
cmdFlags.BoolVar(&flagGet, "get", true, "")
|
||||
cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data")
|
||||
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
|
||||
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
|
||||
cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure")
|
||||
cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state")
|
||||
cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "")
|
||||
cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory")
|
||||
cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode")
|
||||
cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local OpenTofu versions are incompatible")
|
||||
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
||||
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
|
||||
cmdFlags.StringVar(&c.outputJSONInto, "json-into", "", "json-into")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
// Propagate -no-color for legacy use of Ui. The remote backend and
|
||||
// cloud package use this; it should be removed when/if they are
|
||||
// migrated to views.
|
||||
c.Meta.color = !common.NoColor
|
||||
c.Meta.Color = c.Meta.color
|
||||
|
||||
// Parse and validate flags
|
||||
args, closer, diags := arguments.ParseInit(rawArgs)
|
||||
defer closer()
|
||||
|
||||
// Instantiate the view, even if there are flag errors, so that we render
|
||||
// diagnostics according to the desired view
|
||||
view := views.NewInit(args.ViewOptions, c.View)
|
||||
// ... and initialise the Meta.Ui to wrap Meta.View into a new implementation
|
||||
// that is able to print by using View abstraction and use the Meta.Ui
|
||||
// to ask for the user input.
|
||||
c.Meta.configureUiFromView(args.ViewOptions)
|
||||
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
view.HelpPrompt()
|
||||
return 1
|
||||
}
|
||||
|
||||
if c.outputInJSON {
|
||||
c.Meta.color = false
|
||||
c.Meta.Color = false
|
||||
c.oldUi = c.Ui
|
||||
c.Ui = &WrappedUi{
|
||||
cliUi: c.oldUi,
|
||||
jsonView: views.NewJSONView(c.View, nil),
|
||||
onlyOutputInJSON: true,
|
||||
}
|
||||
// FIXME: the -input flag value is needed to initialize the backend and the
|
||||
// operation, but there is no clear path to pass this value down, so we
|
||||
// continue to mutate the Meta object state for now.
|
||||
c.Meta.input = args.ViewOptions.InputEnabled
|
||||
c.configureBackendFlags(args.Backend)
|
||||
|
||||
if len(args.FlagPluginPath) > 0 {
|
||||
c.pluginPath = args.FlagPluginPath
|
||||
}
|
||||
c.GatherVariables(args.Vars)
|
||||
|
||||
if c.outputJSONInto != "" {
|
||||
if c.outputInJSON {
|
||||
// Not a valid combination
|
||||
c.Ui.Error("The -json and -json-into options are mutually-exclusive in their use")
|
||||
return 1
|
||||
}
|
||||
|
||||
// NOTE: see meta_ui.go for color stripping in this legacy situation
|
||||
|
||||
out, closer, diags := arguments.OpenJSONIntoFile(c.outputJSONInto)
|
||||
defer closer()
|
||||
if diags.HasErrors() {
|
||||
c.Ui.Error(diags.Err().Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.oldUi = c.Ui
|
||||
c.Ui = &WrappedUi{
|
||||
cliUi: c.oldUi,
|
||||
jsonView: views.NewJSONView(c.View, out),
|
||||
onlyOutputInJSON: false,
|
||||
}
|
||||
}
|
||||
|
||||
backendFlagSet := flags.FlagIsSet(cmdFlags, "backend")
|
||||
cloudFlagSet := flags.FlagIsSet(cmdFlags, "cloud")
|
||||
|
||||
switch {
|
||||
case backendFlagSet && cloudFlagSet:
|
||||
c.Ui.Error("The -backend and -cloud options are aliases of one another and mutually-exclusive in their use")
|
||||
return 1
|
||||
case backendFlagSet:
|
||||
flagCloud = flagBackend
|
||||
case cloudFlagSet:
|
||||
flagBackend = flagCloud
|
||||
}
|
||||
|
||||
if c.migrateState && c.reconfigure {
|
||||
c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Copying the state only happens during backend migration, so setting
|
||||
// -force-copy implies -migrate-state
|
||||
if c.forceInitCopy {
|
||||
c.migrateState = true
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if len(flagPluginPath) > 0 {
|
||||
c.pluginPath = flagPluginPath
|
||||
}
|
||||
|
||||
// Validate the arg count and get the working directory
|
||||
args = cmdFlags.Args()
|
||||
path, err := modulePath(args)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
// This gets the current directory as full path.
|
||||
path := c.WorkingDir.NormalizePath(c.WorkingDir.RootModuleDir())
|
||||
|
||||
if err := c.storePluginPath(c.pluginPath); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error saving -plugin-path values: %s", err))
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error saving -plugin-path values: %w", err)))
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
@ -166,28 +109,24 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// to output a newline before the success message
|
||||
var header bool
|
||||
|
||||
if flagFromModule != "" {
|
||||
src := flagFromModule
|
||||
if args.FlagFromModule != "" {
|
||||
src := args.FlagFromModule
|
||||
|
||||
empty, err := configs.IsEmptyDir(path)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error validating destination directory: %s", err))
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error validating destination directory: %w", err)))
|
||||
return 1
|
||||
}
|
||||
if !empty {
|
||||
c.Ui.Error(strings.TrimSpace(errInitCopyNotEmpty))
|
||||
view.Diagnostics(diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
|
||||
"[reset][bold]Copying configuration[reset] from %q...", src,
|
||||
)))
|
||||
view.CopyFromModule(src)
|
||||
header = true
|
||||
|
||||
hooks := uiModuleInstallHooks{
|
||||
Ui: c.Ui,
|
||||
ShowLocalPaths: false, // since they are in a weird location for init
|
||||
}
|
||||
// do not show local directory, since they are in a weird location for init
|
||||
hooks := view.Hooks(false)
|
||||
|
||||
ctx, span := tracing.Tracer().Start(ctx, "From module", tracing.SpanAttributes(
|
||||
traceattrs.OpenTofuModuleSource(src),
|
||||
|
|
@ -197,46 +136,45 @@ func (c *InitCommand) Run(args []string) int {
|
|||
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
|
||||
diags = diags.Append(initDirFromModuleDiags)
|
||||
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
tracing.SetSpanError(span, initDirFromModuleDiags)
|
||||
span.End()
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output("")
|
||||
view.OutputNewline()
|
||||
}
|
||||
|
||||
// If our directory is empty, then we're done. We can't get or set up
|
||||
// the backend with an empty directory.
|
||||
empty, err := configs.IsEmptyDir(path)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Error checking configuration: %w", err))
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error checking configuration: %w", err)))
|
||||
return 1
|
||||
}
|
||||
if empty {
|
||||
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty)))
|
||||
view.InitialisedFromEmptyDir()
|
||||
return 0
|
||||
}
|
||||
|
||||
// Load just the root module to begin backend and module initialization
|
||||
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(ctx, path, testsDirectory)
|
||||
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(ctx, path, args.TestsDirectory)
|
||||
|
||||
// There may be parsing errors in config loading but these will be shown later _after_
|
||||
// checking for core version requirement errors. Not meeting the version requirement should
|
||||
// be the first error displayed if that is an issue, but other operations are required
|
||||
// before being able to check core version requirements.
|
||||
if rootModEarly == nil {
|
||||
c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError)))
|
||||
view.ConfigError()
|
||||
diags = diags.Append(earlyConfDiags)
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
var enc encryption.Encryption
|
||||
// If backend flag is explicitly set to false i.e -backend=false, we disable state and plan encryption
|
||||
if backendFlagSet && !flagBackend {
|
||||
if args.BackendFlagSet && !args.FlagBackend {
|
||||
enc = encryption.Disabled()
|
||||
} else {
|
||||
// Load the encryption configuration
|
||||
|
|
@ -244,7 +182,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
enc, encDiags = c.EncryptionFromModule(ctx, rootModEarly)
|
||||
diags = diags.Append(encDiags)
|
||||
if encDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
|
@ -257,10 +195,10 @@ func (c *InitCommand) Run(args []string) int {
|
|||
var backendOutput bool
|
||||
|
||||
switch {
|
||||
case flagCloud && rootModEarly.CloudConfig != nil:
|
||||
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, enc)
|
||||
case flagBackend:
|
||||
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, enc)
|
||||
case args.FlagCloud && rootModEarly.CloudConfig != nil:
|
||||
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, args.FlagConfigExtra, enc, view)
|
||||
case args.FlagBackend:
|
||||
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, args.FlagConfigExtra, enc, view)
|
||||
default:
|
||||
// load the previously-stored backend config
|
||||
back, backDiags = c.Meta.backendFromState(ctx, enc.State())
|
||||
|
|
@ -278,28 +216,28 @@ func (c *InitCommand) Run(args []string) int {
|
|||
c.ignoreRemoteVersionConflict(back)
|
||||
workspace, err := c.Workspace(ctx)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error selecting workspace: %w", err)))
|
||||
return 1
|
||||
}
|
||||
sMgr, err := back.StateMgr(ctx, workspace)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error loading state: %s", err)))
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := sMgr.RefreshState(context.TODO()); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||
view.Diagnostics(diags.Append(fmt.Errorf("Error refreshing state: %s", err)))
|
||||
return 1
|
||||
}
|
||||
|
||||
state = sMgr.State()
|
||||
}
|
||||
|
||||
if flagGet {
|
||||
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade)
|
||||
if args.FlagGet {
|
||||
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, args.TestsDirectory, rootModEarly, args.FlagUpgrade, view)
|
||||
diags = diags.Append(modsDiags)
|
||||
if modsAbort || modsDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
if modsOutput {
|
||||
|
|
@ -309,7 +247,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
|
||||
// With all of the modules (hopefully) installed, we can now try to load the
|
||||
// whole configuration tree.
|
||||
config, confDiags := c.loadConfigWithTests(ctx, path, testsDirectory)
|
||||
config, confDiags := c.loadConfigWithTests(ctx, path, args.TestsDirectory)
|
||||
// configDiags will be handled after the version constraint check, since an
|
||||
// incorrect version of tofu may be producing errors for configuration
|
||||
// constructs added in later versions.
|
||||
|
|
@ -320,7 +258,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// potentially-confusing downstream errors.
|
||||
versionDiags := tofu.CheckCoreVersionRequirements(config)
|
||||
if versionDiags.HasErrors() {
|
||||
c.showDiagnostics(versionDiags)
|
||||
view.Diagnostics(versionDiags)
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
@ -331,8 +269,8 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// backend.
|
||||
diags = diags.Append(earlyConfDiags.StrictDeduplicateMerge(backDiags))
|
||||
if earlyConfDiags.HasErrors() {
|
||||
c.Ui.Error(strings.TrimSpace(errInitConfigError))
|
||||
c.showDiagnostics(diags)
|
||||
view.ConfigError()
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +278,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// show the errInitConfigError preamble as we didn't detect problems with
|
||||
// the early configuration.
|
||||
if backDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
@ -348,8 +286,8 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// show other errors from loading the full configuration tree.
|
||||
diags = diags.Append(confDiags)
|
||||
if confDiags.HasErrors() {
|
||||
c.Ui.Error(strings.TrimSpace(errInitConfigError))
|
||||
c.showDiagnostics(diags)
|
||||
view.ConfigError()
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
@ -357,7 +295,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
if c.RunningInAutomation {
|
||||
if err := cb.AssertImportCompatible(config); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error()))
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
|
@ -369,17 +307,17 @@ func (c *InitCommand) Run(args []string) int {
|
|||
migratedState, migrateDiags := tofumigrate.MigrateStateProviderAddresses(config, state)
|
||||
diags = diags.Append(migrateDiags)
|
||||
if migrateDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
state = migratedState
|
||||
}
|
||||
|
||||
// Now that we have loaded all modules, check the module tree for missing providers.
|
||||
providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, flagUpgrade, flagPluginPath, flagLockfile)
|
||||
providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, args.FlagUpgrade, args.FlagPluginPath, args.FlagLockfile, view)
|
||||
diags = diags.Append(providerDiags)
|
||||
if providersAbort || providerDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
if providersOutput {
|
||||
|
|
@ -389,35 +327,25 @@ func (c *InitCommand) Run(args []string) int {
|
|||
// If we outputted information, then we need to output a newline
|
||||
// so that our success message is nicely spaced out from prior text.
|
||||
if header {
|
||||
c.Ui.Output("")
|
||||
view.OutputNewline()
|
||||
}
|
||||
|
||||
// If we accumulated any warnings along the way that weren't accompanied
|
||||
// by errors then we'll output them here so that the success message is
|
||||
// still the final thing shown.
|
||||
c.showDiagnostics(diags)
|
||||
_, cloud := back.(*cloud.Cloud)
|
||||
output := outputInitSuccess
|
||||
if cloud {
|
||||
output = outputInitSuccessCloud
|
||||
}
|
||||
|
||||
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output)))
|
||||
|
||||
view.Diagnostics(diags)
|
||||
_, isCloud := back.(*cloud.Cloud)
|
||||
view.InitSuccess(isCloud)
|
||||
if !c.RunningInAutomation {
|
||||
// If we're not running in an automation wrapper, give the user
|
||||
// some more detailed next steps that are appropriate for interactive
|
||||
// shell usage.
|
||||
output = outputInitSuccessCLI
|
||||
if cloud {
|
||||
output = outputInitSuccessCLICloud
|
||||
}
|
||||
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output)))
|
||||
view.InitSuccessCLI(isCloud)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) {
|
||||
func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool, view views.Init) (output bool, abort bool, diags tfdiags.Diagnostics) {
|
||||
testModules := false // We can also have modules buried in test files.
|
||||
for _, file := range earlyRoot.Tests {
|
||||
for _, run := range file.Runs {
|
||||
|
|
@ -437,16 +365,9 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear
|
|||
))
|
||||
defer span.End()
|
||||
|
||||
if upgrade {
|
||||
c.Ui.Output(c.Colorize().Color("[reset][bold]Upgrading modules..."))
|
||||
} else {
|
||||
c.Ui.Output(c.Colorize().Color("[reset][bold]Initializing modules..."))
|
||||
}
|
||||
view.InitializingModules(upgrade)
|
||||
|
||||
hooks := uiModuleInstallHooks{
|
||||
Ui: c.Ui,
|
||||
ShowLocalPaths: true,
|
||||
}
|
||||
hooks := view.Hooks(true)
|
||||
|
||||
installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, false, hooks)
|
||||
diags = diags.Append(installDiags)
|
||||
|
|
@ -471,12 +392,12 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear
|
|||
return true, installAbort, diags
|
||||
}
|
||||
|
||||
func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig flags.RawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
|
||||
func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig flags.RawFlags, enc encryption.Encryption, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Cloud backend init")
|
||||
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
|
||||
defer span.End()
|
||||
|
||||
c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing cloud backend..."))
|
||||
view.InitializingCloudBackend()
|
||||
|
||||
if len(extraConfig.AllItems()) != 0 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
|
|
@ -499,12 +420,12 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
|
|||
return back, true, diags
|
||||
}
|
||||
|
||||
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig flags.RawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
|
||||
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig flags.RawFlags, enc encryption.Encryption, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Backend init")
|
||||
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
|
||||
defer span.End()
|
||||
|
||||
c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing the backend..."))
|
||||
view.InitializingBackend()
|
||||
|
||||
var backendConfig *configs.Backend
|
||||
var backendConfigOverride hcl.Body
|
||||
|
|
@ -536,7 +457,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext
|
|||
return nil, true, diags
|
||||
}
|
||||
if backendType != canonType {
|
||||
c.Ui.Output(fmt.Sprintf("- %q is an alias for backend type %q", backendType, canonType))
|
||||
view.BackendTypeAlias(backendType, canonType)
|
||||
}
|
||||
|
||||
b := bf(nil) // This is only used to get the schema, encryption should panic if attempted
|
||||
|
|
@ -587,7 +508,7 @@ the backend configuration is present and valid.
|
|||
|
||||
// Load the complete module tree, and fetch any missing providers.
|
||||
// This method outputs its own Ui.
|
||||
func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) {
|
||||
func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output, abort bool, diags tfdiags.Diagnostics) {
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Get Providers")
|
||||
defer span.End()
|
||||
|
||||
|
|
@ -680,19 +601,13 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
// are shimming our vt100 output to the legacy console API on Windows.
|
||||
evts := &providercache.InstallerEvents{
|
||||
PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
|
||||
c.Ui.Output(c.Colorize().Color(
|
||||
"\n[reset][bold]Initializing provider plugins...",
|
||||
))
|
||||
view.InitializingProviderPlugins()
|
||||
},
|
||||
ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version, inProviderCache bool) {
|
||||
if inProviderCache {
|
||||
c.Ui.Info(fmt.Sprintf("- Detected previously-installed %s v%s in the shared cache directory", provider.ForDisplay(), selectedVersion))
|
||||
} else {
|
||||
c.Ui.Info(fmt.Sprintf("- Using previously-installed %s v%s", provider.ForDisplay(), selectedVersion))
|
||||
}
|
||||
view.ProviderAlreadyInstalled(provider.ForDisplay(), selectedVersion.String(), inProviderCache)
|
||||
},
|
||||
BuiltInProviderAvailable: func(provider addrs.Provider) {
|
||||
c.Ui.Info(fmt.Sprintf("- %s is built in to OpenTofu", provider.ForDisplay()))
|
||||
view.BuiltInProviderAvailable(provider.ForDisplay())
|
||||
},
|
||||
BuiltInProviderFailure: func(provider addrs.Provider, err error) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
|
|
@ -703,24 +618,20 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
},
|
||||
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
|
||||
if locked {
|
||||
c.Ui.Info(fmt.Sprintf("- Reusing previous version of %s from the dependency lock file", provider.ForDisplay()))
|
||||
view.ReusingLockFileVersion(provider.ForDisplay())
|
||||
} else {
|
||||
if len(versionConstraints) > 0 {
|
||||
c.Ui.Info(fmt.Sprintf("- Finding %s versions matching %q...", provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)))
|
||||
view.FindingProviderVersions(provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))
|
||||
} else {
|
||||
c.Ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay()))
|
||||
view.FindingLatestProviderVersion(provider.ForDisplay())
|
||||
}
|
||||
}
|
||||
},
|
||||
LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) {
|
||||
c.Ui.Info(fmt.Sprintf("- Using %s v%s from the shared cache directory", provider.ForDisplay(), version))
|
||||
view.UsingProviderFromCache(provider.ForDisplay(), version.String())
|
||||
},
|
||||
FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation, inProviderCache bool) {
|
||||
if inProviderCache {
|
||||
c.Ui.Info(fmt.Sprintf("- Installing %s v%s to the shared cache directory...", provider.ForDisplay(), version))
|
||||
} else {
|
||||
c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version))
|
||||
}
|
||||
view.InstallingProvider(provider.ForDisplay(), version.String(), inProviderCache)
|
||||
},
|
||||
QueryPackagesFailure: func(provider addrs.Provider, err error) {
|
||||
switch errorTy := err.(type) {
|
||||
|
|
@ -923,18 +834,15 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
if authResult != nil && authResult.Signed() {
|
||||
keyID = authResult.GPGKeyIDsString()
|
||||
}
|
||||
if keyID != "" {
|
||||
keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID))
|
||||
}
|
||||
|
||||
if authResult != nil && authResult.SigningSkipped() {
|
||||
c.Ui.Warn(fmt.Sprintf("- Installed %s v%s. Signature validation was skipped due to the registry not containing GPG keys for this provider", provider.ForDisplay(), version))
|
||||
view.ProviderInstalledSkippedSignature(provider.ForDisplay(), version.String())
|
||||
} else {
|
||||
c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID))
|
||||
view.ProviderInstalled(provider.ForDisplay(), version.String(), authResult.String(), keyID)
|
||||
}
|
||||
},
|
||||
CacheDirLockContended: func(cacheDir string) {
|
||||
c.Ui.Info(fmt.Sprintf("- Waiting for lock on cache directory %s", cacheDir))
|
||||
view.WaitingForCacheLock(cacheDir)
|
||||
},
|
||||
ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
|
||||
// We're going to use this opportunity to track if we have any
|
||||
|
|
@ -980,9 +888,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
}
|
||||
}
|
||||
if thirdPartySigned {
|
||||
c.Ui.Info(fmt.Sprintf("\nProviders are signed by their developers.\n" +
|
||||
"If you'd like to know more about provider signing, you can read about it here:\n" +
|
||||
"https://opentofu.org/docs/cli/plugins/signing/"))
|
||||
view.ProvidersSignedInfo()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -993,7 +899,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
mode := providercache.InstallNewProvidersOnly
|
||||
if upgrade {
|
||||
if flagLockfile == "readonly" {
|
||||
c.Ui.Error("The -upgrade flag conflicts with -lockfile=readonly.")
|
||||
view.ProviderUpgradeLockfileConflict()
|
||||
return true, true, diags
|
||||
}
|
||||
|
||||
|
|
@ -1001,8 +907,8 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
}
|
||||
newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode)
|
||||
if ctx.Err() == context.Canceled {
|
||||
c.showDiagnostics(diags)
|
||||
c.Ui.Error("Provider installation was canceled by an interrupt signal.")
|
||||
view.Diagnostics(diags)
|
||||
view.ProviderInstallationInterrupted()
|
||||
return true, true, diags
|
||||
}
|
||||
if err != nil {
|
||||
|
|
@ -1069,16 +975,9 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
|
|||
// say a little about what the dependency lock file is, for new
|
||||
// users or those who are upgrading from a previous Terraform
|
||||
// version that didn't have dependency lock files.
|
||||
c.Ui.Output(c.Colorize().Color(`
|
||||
OpenTofu has created a lock file [bold].terraform.lock.hcl[reset] to record the provider
|
||||
selections it made above. Include this file in your version control repository
|
||||
so that OpenTofu can guarantee to make the same selections by default when
|
||||
you run "tofu init" in the future.`))
|
||||
view.LockFileCreated()
|
||||
} else {
|
||||
c.Ui.Output(c.Colorize().Color(`
|
||||
OpenTofu has made some changes to the provider dependency selections recorded
|
||||
in the .terraform.lock.hcl file. Review those changes and commit them to your
|
||||
version control system if they represent changes you intended to make.`))
|
||||
view.LockFileChanged()
|
||||
}
|
||||
|
||||
moreDiags = c.replaceLockedDependencies(ctx, newLocks)
|
||||
|
|
@ -1234,6 +1133,39 @@ func (c *InitCommand) AutocompleteArgs() complete.Predictor {
|
|||
return complete.PredictDirs("")
|
||||
}
|
||||
|
||||
// TODO meta-refactor: move this to arguments once all commands are using the same shim logic
|
||||
func (c *InitCommand) GatherVariables(args *arguments.Vars) {
|
||||
// FIXME the arguments package currently trivially gathers variable related
|
||||
// arguments in a heterogeneous slice, in order to minimize the number of
|
||||
// code paths gathering variables during the transition to this structure.
|
||||
// Once all commands that gather variables have been converted to this
|
||||
// structure, we could move the variable gathering code to the arguments
|
||||
// package directly, removing this shim layer.
|
||||
|
||||
varArgs := args.All()
|
||||
items := make([]flags.RawFlag, len(varArgs))
|
||||
for i := range varArgs {
|
||||
items[i].Name = varArgs[i].Name
|
||||
items[i].Value = varArgs[i].Value
|
||||
}
|
||||
c.Meta.variableArgs = flags.RawFlags{Items: &items}
|
||||
}
|
||||
|
||||
// configureBackendFlags is a temporary shim until we move the backend migration logic away from the Meta fields.
|
||||
//
|
||||
// TODO meta-refactor: remove this when the Meta fields configured here will be removed and replaced
|
||||
// with proper arguments for the backend.
|
||||
func (c *InitCommand) configureBackendFlags(args *arguments.Backend) {
|
||||
c.forceInitCopy = args.ForceInitCopy
|
||||
c.reconfigure = args.Reconfigure
|
||||
c.migrateState = args.MigrateState
|
||||
c.Meta.ignoreRemoteVersion = args.IgnoreRemoteVersion
|
||||
// TODO meta-refactor: unify these 2 args attributes with the state flags in arguments.extendedFlagSet
|
||||
// https://github.com/opentofu/opentofu/blob/db8c872defd8666618649ef7e29fa2b809adfd5e/internal/command/arguments/extended.go#L320-L321
|
||||
c.Meta.stateLock = args.StateLock
|
||||
c.Meta.stateLockTimeout = args.StateLockTimeout
|
||||
}
|
||||
|
||||
func (c *InitCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-backend": completePredictBoolean,
|
||||
|
|
@ -1376,14 +1308,6 @@ func (c *InitCommand) Synopsis() string {
|
|||
return "Prepare your working directory for other commands"
|
||||
}
|
||||
|
||||
const errInitConfigError = `
|
||||
[reset]OpenTofu encountered problems during initialization, including problems
|
||||
with the configuration, described below.
|
||||
|
||||
The OpenTofu configuration must be valid before initialization so that
|
||||
OpenTofu can determine which modules and providers need to be installed.
|
||||
`
|
||||
|
||||
const errInitCopyNotEmpty = `
|
||||
The working directory already contains files. The -from-module option requires
|
||||
an empty directory into which a copy of the referenced module will be placed.
|
||||
|
|
@ -1392,39 +1316,6 @@ To initialize the configuration already in this working directory, omit the
|
|||
-from-module option.
|
||||
`
|
||||
|
||||
const outputInitEmpty = `
|
||||
[reset][bold]OpenTofu initialized in an empty directory![reset]
|
||||
|
||||
The directory has no OpenTofu configuration files. You may begin working
|
||||
with OpenTofu immediately by creating OpenTofu configuration files.
|
||||
`
|
||||
|
||||
const outputInitSuccess = `
|
||||
[reset][bold][green]OpenTofu has been successfully initialized![reset][green]
|
||||
`
|
||||
|
||||
const outputInitSuccessCloud = `
|
||||
[reset][bold][green]Cloud backend has been successfully initialized![reset][green]
|
||||
`
|
||||
|
||||
const outputInitSuccessCLI = `[reset][green]
|
||||
You may now begin working with OpenTofu. Try running "tofu plan" to see
|
||||
any changes that are required for your infrastructure. All OpenTofu commands
|
||||
should now work.
|
||||
|
||||
If you ever set or change modules or backend configuration for OpenTofu,
|
||||
rerun this command to reinitialize your working directory. If you forget, other
|
||||
commands will detect it and remind you to do so if necessary.
|
||||
`
|
||||
|
||||
const outputInitSuccessCLICloud = `[reset][green]
|
||||
You may now begin working with cloud backend. Try running "tofu plan" to
|
||||
see any changes that are required for your infrastructure.
|
||||
|
||||
If you ever set or change modules or OpenTofu Settings, run "tofu init"
|
||||
again to reinitialize your working directory.
|
||||
`
|
||||
|
||||
// providerProtocolTooOld is a message sent to the CLI UI if the provider's
|
||||
// supported protocol versions are too old for the user's version of tofu,
|
||||
// but a newer version of the provider is compatible.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -694,6 +694,39 @@ func (m *Meta) process(args []string) []string {
|
|||
return args
|
||||
}
|
||||
|
||||
// configureUiFromView is a shim method between now and the moment when
|
||||
// the remote backend and cloud package use the new View abstraction.
|
||||
// This method does several things:
|
||||
// - creates a new [NewBasicUI] if [Meta.Ui] is nil (needed for testing, see below)
|
||||
// - wraps the existing [Meta.Ui] into a new layer that uses the [views.View]
|
||||
// to print information and the existing [Meta.Ui] to ask for use input
|
||||
func (m *Meta) configureUiFromView(options arguments.ViewOptions) {
|
||||
// We do this so that we retain the ability to technically call
|
||||
// process multiple times, even if we have no plans to do so
|
||||
if m.oldUi != nil {
|
||||
m.Ui = m.oldUi
|
||||
}
|
||||
// This is a workaround to be able to get rid of the [Meta.Ui] slow and steady.
|
||||
// For the moment, this builds the Ui in the same way it's built in the main.go, but we want
|
||||
// it added here to remove the requirement of having the Ui initialised during tests.
|
||||
// The highlight here is that the "printing" is done through the [Meta.View] and
|
||||
// this Ui instance is used only to ask for user input.
|
||||
// Therefore, tests can initialise only the View and check the output from there.
|
||||
if m.Ui == nil {
|
||||
m.Ui = NewBasicUI()
|
||||
}
|
||||
|
||||
// Backup the current Ui to be used later
|
||||
m.oldUi = m.Ui
|
||||
|
||||
// Createa new ViewUi that wraps the View for printing and oldUi for user input
|
||||
m.Ui = &cli.ConcurrentUi{
|
||||
Ui: views.NewViewUI(options, m.View, m.oldUi),
|
||||
}
|
||||
// compared with Meta.process, this method does not configure the Meta.View, since that is the
|
||||
// responsibility of the caller of this method.
|
||||
}
|
||||
|
||||
// uiHook returns the UiHook to use with the context.
|
||||
func (m *Meta) uiHook() *views.UiHook {
|
||||
return views.NewUiHook(m.View)
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ func TestProvidersSchema_output(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
p := providersSchemaFixtureProvider()
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
m := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
}
|
||||
|
||||
|
|
@ -73,16 +73,23 @@ func TestProvidersSchema_output(t *testing.T) {
|
|||
ic := &InitCommand{
|
||||
Meta: m,
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
code := ic.Run([]string{})
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// flush the init output from the mock ui
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// `tofu provider schemas` command
|
||||
pc := &ProvidersSchemaCommand{Meta: m}
|
||||
if code := pc.Run([]string{"-json"}); code != 0 {
|
||||
// TODO meta-refactor-views: we need the ui here because the provider schema command is not yet migrated to views
|
||||
// Once the command is migrated, remove this part and use the testView
|
||||
ui := new(cli.MockUi)
|
||||
m.Ui = ui
|
||||
m.View = nil
|
||||
pc := &ProvidersSchemaCommand{
|
||||
Meta: m,
|
||||
}
|
||||
code = pc.Run([]string{"-json"})
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
var got, want providerSchemas
|
||||
|
|
|
|||
|
|
@ -73,24 +73,26 @@ func TestProviders_modules(t *testing.T) {
|
|||
t.Chdir(td)
|
||||
|
||||
// first run init with mock provider sources to install the module
|
||||
initUi := new(cli.MockUi)
|
||||
providerSource, close := newMockProviderSource(t, map[string][]string{
|
||||
"foo": {"1.0.0"},
|
||||
"bar": {"2.0.0"},
|
||||
"baz": {"1.2.2"},
|
||||
})
|
||||
defer close()
|
||||
m := Meta{
|
||||
view, done := testView(t)
|
||||
initMeta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: initUi,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
}
|
||||
ic := &InitCommand{
|
||||
Meta: m,
|
||||
Meta: initMeta,
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", initUi.ErrorWriter)
|
||||
code := ic.Run([]string{})
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// Providers command
|
||||
|
|
@ -110,14 +112,14 @@ func TestProviders_modules(t *testing.T) {
|
|||
wantOutput := []string{
|
||||
"provider[registry.opentofu.org/hashicorp/foo] 1.0.0", // from required_providers
|
||||
"provider[registry.opentofu.org/hashicorp/bar] 2.0.0", // from provider config
|
||||
"── module.kiddo", // tree node for child module
|
||||
"── module.kiddo", // tree node for child module
|
||||
"provider[registry.opentofu.org/hashicorp/baz]", // implied by a resource in the child module
|
||||
}
|
||||
|
||||
output := ui.OutputWriter.String()
|
||||
stdout := ui.OutputWriter.String()
|
||||
for _, want := range wantOutput {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Errorf("output missing %s:\n%s", want, output)
|
||||
if !strings.Contains(stdout, want) {
|
||||
t.Errorf("output missing %s:\n%s", want, stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/command/workdir"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
|
|
@ -586,21 +585,23 @@ func TestShow_json_output(t *testing.T) {
|
|||
p := showFixtureProvider()
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
view, done := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
code := ic.Run([]string{})
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
if expectError {
|
||||
// this should error, but not panic.
|
||||
return
|
||||
}
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// read expected output
|
||||
|
|
@ -634,7 +635,7 @@ func TestShow_json_output(t *testing.T) {
|
|||
"-out=tofu.plan",
|
||||
}
|
||||
|
||||
code := pc.Run(args)
|
||||
code = pc.Run(args)
|
||||
planOutput := planDone(t)
|
||||
|
||||
var wantedCode int
|
||||
|
|
@ -701,17 +702,19 @@ func TestShow_json_output_sensitive(t *testing.T) {
|
|||
p := showFixtureSensitiveProvider()
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
initView, initDone := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: initView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
code := ic.Run([]string{})
|
||||
output := initDone(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// plan
|
||||
|
|
@ -728,7 +731,7 @@ func TestShow_json_output_sensitive(t *testing.T) {
|
|||
args := []string{
|
||||
"-out=tofu.plan",
|
||||
}
|
||||
code := pc.Run(args)
|
||||
code = pc.Run(args)
|
||||
planOutput := planDone(t)
|
||||
|
||||
if code != 0 {
|
||||
|
|
@ -801,17 +804,19 @@ func TestShow_json_output_conditions_refresh_only(t *testing.T) {
|
|||
p := showFixtureSensitiveProvider()
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
initView, initDone := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
ProviderSource: providerSource,
|
||||
View: initView,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
initCode := ic.Run([]string{})
|
||||
output := initDone(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// plan
|
||||
|
|
@ -917,17 +922,19 @@ func TestShow_json_output_state(t *testing.T) {
|
|||
p := showFixtureProvider()
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
initView, initDone := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: initView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
initCode := ic.Run([]string{})
|
||||
output := initDone(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("init failed\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
// show
|
||||
|
|
@ -1365,17 +1372,19 @@ func TestShow_config(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
// Initialize the module
|
||||
ui := new(cli.MockUi)
|
||||
initView, initDone := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
Ui: ui,
|
||||
View: initView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
initCode := ic.Run([]string{})
|
||||
initOutput := initDone(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("init failed\n%s", initOutput.Stderr())
|
||||
}
|
||||
|
||||
view, done := testView(t)
|
||||
|
|
@ -1470,17 +1479,19 @@ func TestShow_config_withModule(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
// Initialize the module
|
||||
ui := new(cli.MockUi)
|
||||
initView, initDone := testView(t)
|
||||
ic := &InitCommand{
|
||||
Meta: Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
Ui: ui,
|
||||
View: initView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
initCode := ic.Run([]string{})
|
||||
initOutput := initDone(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("init failed\n%s", initOutput.Stderr())
|
||||
}
|
||||
|
||||
view, done := testView(t)
|
||||
|
|
|
|||
|
|
@ -766,12 +766,10 @@ can remove the provider configuration again.
|
|||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -781,23 +779,27 @@ can remove the provider configuration again.
|
|||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
initCode := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", initCode, initOutput.Stderr())
|
||||
}
|
||||
|
||||
streams, done = terminal.StreamsForTesting(t)
|
||||
meta.View = views.NewView(streams)
|
||||
c := &TestCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
code := c.Run([]string{"-no-color"})
|
||||
output := done(t)
|
||||
testOutput := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Errorf("expected status code 1 but got %d", code)
|
||||
}
|
||||
|
||||
actualOut, expectedOut := output.Stdout(), tc.expectedOut
|
||||
actualErr, expectedErr := output.Stderr(), tc.expectedErr
|
||||
actualOut, expectedOut := testOutput.Stdout(), tc.expectedOut
|
||||
actualErr, expectedErr := testOutput.Stderr(), tc.expectedErr
|
||||
|
||||
if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 {
|
||||
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff)
|
||||
|
|
@ -915,11 +917,9 @@ func TestTest_Modules(t *testing.T) {
|
|||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -929,26 +929,30 @@ func TestTest_Modules(t *testing.T) {
|
|||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
initCode := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", initCode, initOutput.Stderr())
|
||||
}
|
||||
|
||||
streams, done = terminal.StreamsForTesting(t)
|
||||
meta.View = views.NewView(streams)
|
||||
command := &TestCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
code := command.Run([]string{"-no-color"})
|
||||
output := done(t)
|
||||
testOutput := done(t)
|
||||
printedOutput := false
|
||||
|
||||
if code != tc.code {
|
||||
printedOutput = true
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, testOutput.All())
|
||||
}
|
||||
|
||||
// If we're not expecting a failure, we can compare the output.
|
||||
if code != 1 {
|
||||
actual := output.All()
|
||||
actual := testOutput.All()
|
||||
if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
|
||||
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
|
||||
}
|
||||
|
|
@ -962,7 +966,7 @@ func TestTest_Modules(t *testing.T) {
|
|||
|
||||
if provider.ResourceCount() > 0 {
|
||||
if !printedOutput {
|
||||
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All())
|
||||
t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), testOutput.All())
|
||||
} else {
|
||||
t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString())
|
||||
}
|
||||
|
|
@ -970,7 +974,7 @@ func TestTest_Modules(t *testing.T) {
|
|||
|
||||
if provider.DataSourceCount() > 0 {
|
||||
if !printedOutput {
|
||||
t.Errorf("should have deleted all data sources on completion but left %s\n\n%s", provider.DataSourceString(), output.All())
|
||||
t.Errorf("should have deleted all data sources on completion but left %s\n\n%s", provider.DataSourceString(), testOutput.All())
|
||||
} else {
|
||||
t.Errorf("should have deleted all data sources on completion but left %s", provider.DataSourceString())
|
||||
}
|
||||
|
|
@ -993,12 +997,10 @@ func TestTest_StatePropagation(t *testing.T) {
|
|||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -1008,16 +1010,21 @@ func TestTest_StatePropagation(t *testing.T) {
|
|||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
initCode := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", initCode, initOutput.Stderr())
|
||||
}
|
||||
|
||||
streams, done = terminal.StreamsForTesting(t)
|
||||
meta.View = views.NewView(streams)
|
||||
|
||||
c := &TestCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
code := c.Run([]string{"-verbose", "-no-color"})
|
||||
output := done(t)
|
||||
testOutput := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("expected status code 0 but got %d", code)
|
||||
|
|
@ -1085,7 +1092,7 @@ Plan: 0 to add, 1 to change, 0 to destroy.
|
|||
Success! 5 passed, 0 failed.
|
||||
`
|
||||
|
||||
actual := output.All()
|
||||
actual := testOutput.All()
|
||||
|
||||
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
|
||||
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
|
||||
|
|
@ -1291,11 +1298,9 @@ Success! 1 passed, 0 failed.
|
|||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -1305,22 +1310,26 @@ Success! 1 passed, 0 failed.
|
|||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
initCode := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", initCode, initOutput.Stderr())
|
||||
}
|
||||
|
||||
streams, done = terminal.StreamsForTesting(t)
|
||||
meta.View = views.NewView(streams)
|
||||
command := &TestCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
code := command.Run([]string{"-verbose", "-no-color"})
|
||||
output := done(t)
|
||||
testOutput := done(t)
|
||||
|
||||
if code != tc.code {
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, testOutput.All())
|
||||
}
|
||||
|
||||
actual := output.All()
|
||||
actual := testOutput.All()
|
||||
|
||||
if diff := cmp.Diff(actual, tc.expected); len(diff) > 0 {
|
||||
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expected, actual, diff)
|
||||
|
|
@ -1378,11 +1387,9 @@ func TestTest_InvalidLocalVariables(t *testing.T) {
|
|||
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -1392,32 +1399,37 @@ func TestTest_InvalidLocalVariables(t *testing.T) {
|
|||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
initCode := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if initCode != 0 {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", initCode, initOutput.Stderr())
|
||||
}
|
||||
|
||||
streams, done = terminal.StreamsForTesting(t)
|
||||
meta.View = views.NewView(streams)
|
||||
|
||||
command := &TestCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
code := command.Run([]string{"-verbose", "-no-color"})
|
||||
output := done(t)
|
||||
testOutput := done(t)
|
||||
|
||||
if code != tc.code {
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, output.All())
|
||||
t.Errorf("expected status code %d but got %d: %s", tc.code, code, testOutput.All())
|
||||
}
|
||||
|
||||
actual := output.All()
|
||||
actual := testOutput.All()
|
||||
|
||||
for _, containsString := range tc.contains {
|
||||
if !strings.Contains(actual, containsString) {
|
||||
t.Errorf("expected '%s' in output but didn't find it: \n%s", containsString, output.All())
|
||||
t.Errorf("expected '%s' in output but didn't find it: \n%s", containsString, testOutput.All())
|
||||
}
|
||||
}
|
||||
|
||||
for _, notContainsString := range tc.notContains {
|
||||
if strings.Contains(actual, notContainsString) {
|
||||
t.Errorf("expected not to find '%s' in output: \n%s", notContainsString, output.All())
|
||||
t.Errorf("expected not to find '%s' in output: \n%s", notContainsString, testOutput.All())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -1462,13 +1474,11 @@ digits, underscores, and dashes.
|
|||
})
|
||||
defer close()
|
||||
|
||||
streams, _ := terminal.StreamsForTesting(t)
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := views.NewView(streams)
|
||||
ui := new(cli.MockUi)
|
||||
meta := Meta{
|
||||
WorkingDir: workdir.NewDir("."),
|
||||
testingOverrides: metaOverridesForProvider(provider.Provider),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
Streams: streams,
|
||||
ProviderSource: providerSource,
|
||||
|
|
@ -1477,9 +1487,10 @@ digits, underscores, and dashes.
|
|||
init := &InitCommand{
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
if code := init.Run(nil); code != tc.code {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
|
||||
code := init.Run(nil)
|
||||
initOutput := done(t)
|
||||
if code != tc.code {
|
||||
t.Fatalf("expected status code 0 but got %d: %s", code, initOutput.Stderr())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
82
internal/command/views/hook_module_install.go
Normal file
82
internal/command/views/hook_module_install.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
)
|
||||
|
||||
// moduleInstallationHookHuman is the implementation of [initwd.ModuleInstallHooks] that prints the modules
|
||||
// installation progress information in human readable format.
|
||||
type moduleInstallationHookHuman struct {
|
||||
v *View
|
||||
showLocalPaths bool
|
||||
}
|
||||
|
||||
var _ initwd.ModuleInstallHooks = moduleInstallationHookHuman{}
|
||||
|
||||
func (h moduleInstallationHookHuman) Download(modulePath, packageAddr string, v *version.Version) {
|
||||
if v != nil {
|
||||
_, _ = h.v.streams.Println(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath))
|
||||
} else {
|
||||
_, _ = h.v.streams.Println(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath))
|
||||
}
|
||||
}
|
||||
|
||||
func (h moduleInstallationHookHuman) Install(modulePath string, v *version.Version, localDir string) {
|
||||
if h.showLocalPaths {
|
||||
_, _ = h.v.streams.Println(fmt.Sprintf("- %s in %s", modulePath, localDir))
|
||||
} else {
|
||||
_, _ = h.v.streams.Println(fmt.Sprintf("- %s", modulePath))
|
||||
}
|
||||
}
|
||||
|
||||
// moduleInstallationHookJSON is the implementation of [initwd.ModuleInstallHooks] that prints the modules
|
||||
// installation progress information in JSON format.
|
||||
type moduleInstallationHookJSON struct {
|
||||
v *JSONView
|
||||
showLocalPaths bool
|
||||
}
|
||||
|
||||
var _ initwd.ModuleInstallHooks = moduleInstallationHookJSON{}
|
||||
|
||||
func (h moduleInstallationHookJSON) Download(modulePath, packageAddr string, v *version.Version) {
|
||||
if v != nil {
|
||||
h.v.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath))
|
||||
} else {
|
||||
h.v.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath))
|
||||
}
|
||||
}
|
||||
|
||||
func (h moduleInstallationHookJSON) Install(modulePath string, _ *version.Version, localDir string) {
|
||||
if h.showLocalPaths {
|
||||
h.v.Info(fmt.Sprintf("installing %s in %s", modulePath, localDir))
|
||||
} else {
|
||||
h.v.Info(fmt.Sprintf("installing %s", modulePath))
|
||||
}
|
||||
}
|
||||
|
||||
// moduleInstallationHookMulti is the implementation of [initwd.ModuleInstallHooks] that wraps multiple
|
||||
// implementation of [initwd.ModuleInstallHooks] and acts as a proxy for all of those.
|
||||
// This is used for the `-json-into` flag.
|
||||
type moduleInstallationHookMulti []initwd.ModuleInstallHooks
|
||||
|
||||
var _ initwd.ModuleInstallHooks = moduleInstallationHookMulti(nil)
|
||||
|
||||
func (m moduleInstallationHookMulti) Download(modulePath, packageAddr string, v *version.Version) {
|
||||
for _, h := range m {
|
||||
h.Download(modulePath, packageAddr, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (m moduleInstallationHookMulti) Install(modulePath string, v *version.Version, localDir string) {
|
||||
for _, h := range m {
|
||||
h.Install(modulePath, v, localDir)
|
||||
}
|
||||
}
|
||||
147
internal/command/views/hook_module_install_test.go
Normal file
147
internal/command/views/hook_module_install_test.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
)
|
||||
|
||||
func TestModuleInstallationHooks(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
viewCall func(hook initwd.ModuleInstallHooks)
|
||||
showLocalPaths bool
|
||||
wantJson []map[string]any
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
"download_with_version": {
|
||||
viewCall: func(hook initwd.ModuleInstallHooks) {
|
||||
hook.Download("root.networking", "git::https://example.com/module.git", version.Must(version.NewVersion("2.5.3")))
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Downloading git::https://example.com/module.git 2.5.3 for root.networking...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("Downloading git::https://example.com/module.git 2.5.3 for root.networking..."),
|
||||
},
|
||||
"download_without_version": {
|
||||
viewCall: func(hook initwd.ModuleInstallHooks) {
|
||||
hook.Download("root.storage", "git::https://example.com/storage.git", nil)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Downloading git::https://example.com/storage.git for root.storage...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("Downloading git::https://example.com/storage.git for root.storage..."),
|
||||
},
|
||||
"install_without_local_path": {
|
||||
viewCall: func(hook initwd.ModuleInstallHooks) {
|
||||
hook.Install("root.networking", version.Must(version.NewVersion("2.5.3")), "/path/to/.terraform/modules/networking")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "installing root.networking",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- root.networking"),
|
||||
},
|
||||
"install_with_local_path": {
|
||||
viewCall: func(hook initwd.ModuleInstallHooks) {
|
||||
hook.Install("root.networking", version.Must(version.NewVersion("2.5.3")), "/path/to/.terraform/modules/networking")
|
||||
},
|
||||
showLocalPaths: true,
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "installing root.networking in /path/to/.terraform/modules/networking",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- root.networking in /path/to/.terraform/modules/networking"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testModuleInstallationHookHuman(t, tc.viewCall, tc.showLocalPaths, tc.wantStdout, tc.wantStderr)
|
||||
testModuleInstallationHookJson(t, tc.viewCall, tc.showLocalPaths, tc.wantJson)
|
||||
testModuleInstallationHookMulti(t, tc.viewCall, tc.showLocalPaths, tc.wantStdout, tc.wantStderr, tc.wantJson)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testModuleInstallationHookHuman(t *testing.T, call func(init initwd.ModuleInstallHooks), showLocalPaths bool, wantStdout, wantStderr string) {
|
||||
view, done := testView(t)
|
||||
moduleInstallationViewCall := moduleInstallationHookHuman{v: view, showLocalPaths: showLocalPaths}
|
||||
call(moduleInstallationViewCall)
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func testModuleInstallationHookJson(t *testing.T, call func(init initwd.ModuleInstallHooks), showLocalPaths bool, want []map[string]interface{}) {
|
||||
// New type just to assert the fields that we are interested in
|
||||
view, done := testView(t)
|
||||
moduleInstallationViewCall := moduleInstallationHookJSON{v: NewJSONView(view, nil), showLocalPaths: showLocalPaths}
|
||||
call(moduleInstallationViewCall)
|
||||
output := done(t)
|
||||
if output.Stderr() != "" {
|
||||
t.Errorf("expected no stderr but got:\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
testJSONViewOutputEquals(t, output.Stdout(), want)
|
||||
}
|
||||
|
||||
func testModuleInstallationHookMulti(t *testing.T, call func(init initwd.ModuleInstallHooks), showLocalPaths bool, wantStdout string, wantStderr string, want []map[string]interface{}) {
|
||||
jsonInto, err := os.CreateTemp(t.TempDir(), "json-into-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create the file to write json content into: %s", err)
|
||||
}
|
||||
view, done := testView(t)
|
||||
moduleInstallationViewCall := moduleInstallationHookMulti{
|
||||
moduleInstallationHookHuman{v: view, showLocalPaths: showLocalPaths},
|
||||
moduleInstallationHookJSON{v: NewJSONView(view, jsonInto), showLocalPaths: showLocalPaths},
|
||||
}
|
||||
call(moduleInstallationViewCall)
|
||||
{
|
||||
if err := jsonInto.Close(); err != nil {
|
||||
t.Fatalf("failed to close the jsonInto file: %s", err)
|
||||
}
|
||||
// check the fileInto content
|
||||
fileContent, err := os.ReadFile(jsonInto.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read the file content with the json output: %s", err)
|
||||
}
|
||||
testJSONViewOutputEquals(t, string(fileContent), want)
|
||||
}
|
||||
{
|
||||
// check the human output
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
592
internal/command/views/init.go
Normal file
592
internal/command/views/init.go
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
"github.com/opentofu/opentofu/internal/initwd"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
type Init interface {
|
||||
CopyFromModule(src string)
|
||||
InitialisedFromEmptyDir()
|
||||
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
HelpPrompt()
|
||||
|
||||
ConfigError()
|
||||
OutputNewline()
|
||||
InitSuccess(cloud bool)
|
||||
InitSuccessCLI(cloud bool)
|
||||
|
||||
InitializingModules(upgrade bool)
|
||||
|
||||
InitializingCloudBackend()
|
||||
InitializingBackend()
|
||||
BackendTypeAlias(backendType, canonType string)
|
||||
|
||||
InitializingProviderPlugins()
|
||||
ProviderAlreadyInstalled(provider string, version string, inCache bool)
|
||||
BuiltInProviderAvailable(provider string)
|
||||
ReusingLockFileVersion(provider string)
|
||||
FindingProviderVersions(provider string, constraints string)
|
||||
FindingLatestProviderVersion(provider string)
|
||||
UsingProviderFromCache(provider string, version string)
|
||||
InstallingProvider(provider string, version string, toCache bool)
|
||||
ProviderInstalled(provider string, version string, authResult string, keyID string)
|
||||
ProviderInstalledSkippedSignature(provider string, version string)
|
||||
WaitingForCacheLock(cacheDir string)
|
||||
ProvidersSignedInfo()
|
||||
ProviderUpgradeLockfileConflict()
|
||||
ProviderInstallationInterrupted()
|
||||
LockFileCreated()
|
||||
LockFileChanged()
|
||||
Hooks(showLocalDir bool) initwd.ModuleInstallHooks
|
||||
}
|
||||
|
||||
// NewInit returns an initialized Init implementation for the given ViewType.
|
||||
func NewInit(args arguments.ViewOptions, view *View) Init {
|
||||
var init Init
|
||||
switch args.ViewType {
|
||||
case arguments.ViewJSON:
|
||||
init = &InitJSON{view: NewJSONView(view, nil)}
|
||||
case arguments.ViewHuman:
|
||||
init = &InitHuman{view: view}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", args.ViewType))
|
||||
}
|
||||
|
||||
if args.JSONInto != nil {
|
||||
init = &InitMulti{init, &InitJSON{view: NewJSONView(view, args.JSONInto)}}
|
||||
}
|
||||
return init
|
||||
}
|
||||
|
||||
type InitMulti []Init
|
||||
|
||||
var _ Init = (InitMulti)(nil)
|
||||
|
||||
func (m InitMulti) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
for _, o := range m {
|
||||
o.Diagnostics(diags)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) HelpPrompt() {
|
||||
for _, o := range m {
|
||||
o.HelpPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) CopyFromModule(src string) {
|
||||
for _, o := range m {
|
||||
o.CopyFromModule(src)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitialisedFromEmptyDir() {
|
||||
for _, o := range m {
|
||||
o.InitialisedFromEmptyDir()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ConfigError() {
|
||||
for _, o := range m {
|
||||
o.ConfigError()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) OutputNewline() {
|
||||
for _, o := range m {
|
||||
o.OutputNewline()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitSuccess(cloud bool) {
|
||||
for _, o := range m {
|
||||
o.InitSuccess(cloud)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitSuccessCLI(cloud bool) {
|
||||
for _, o := range m {
|
||||
o.InitSuccessCLI(cloud)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitializingModules(upgrade bool) {
|
||||
for _, o := range m {
|
||||
o.InitializingModules(upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitializingCloudBackend() {
|
||||
for _, o := range m {
|
||||
o.InitializingCloudBackend()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitializingBackend() {
|
||||
for _, o := range m {
|
||||
o.InitializingBackend()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) BackendTypeAlias(backendType, canonType string) {
|
||||
for _, o := range m {
|
||||
o.BackendTypeAlias(backendType, canonType)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InitializingProviderPlugins() {
|
||||
for _, o := range m {
|
||||
o.InitializingProviderPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProviderAlreadyInstalled(provider string, version string, inCache bool) {
|
||||
for _, o := range m {
|
||||
o.ProviderAlreadyInstalled(provider, version, inCache)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) BuiltInProviderAvailable(provider string) {
|
||||
for _, o := range m {
|
||||
o.BuiltInProviderAvailable(provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ReusingLockFileVersion(provider string) {
|
||||
for _, o := range m {
|
||||
o.ReusingLockFileVersion(provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) FindingProviderVersions(provider string, constraints string) {
|
||||
for _, o := range m {
|
||||
o.FindingProviderVersions(provider, constraints)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) FindingLatestProviderVersion(provider string) {
|
||||
for _, o := range m {
|
||||
o.FindingLatestProviderVersion(provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) UsingProviderFromCache(provider string, version string) {
|
||||
for _, o := range m {
|
||||
o.UsingProviderFromCache(provider, version)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) InstallingProvider(provider string, version string, toCache bool) {
|
||||
for _, o := range m {
|
||||
o.InstallingProvider(provider, version, toCache)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProviderInstalled(provider string, version string, authResult string, keyID string) {
|
||||
for _, o := range m {
|
||||
o.ProviderInstalled(provider, version, authResult, keyID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProviderInstalledSkippedSignature(provider string, version string) {
|
||||
for _, o := range m {
|
||||
o.ProviderInstalledSkippedSignature(provider, version)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) WaitingForCacheLock(cacheDir string) {
|
||||
for _, o := range m {
|
||||
o.WaitingForCacheLock(cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProvidersSignedInfo() {
|
||||
for _, o := range m {
|
||||
o.ProvidersSignedInfo()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProviderUpgradeLockfileConflict() {
|
||||
for _, o := range m {
|
||||
o.ProviderUpgradeLockfileConflict()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) ProviderInstallationInterrupted() {
|
||||
for _, o := range m {
|
||||
o.ProviderInstallationInterrupted()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) LockFileCreated() {
|
||||
for _, o := range m {
|
||||
o.LockFileCreated()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) LockFileChanged() {
|
||||
for _, o := range m {
|
||||
o.LockFileChanged()
|
||||
}
|
||||
}
|
||||
|
||||
func (m InitMulti) Hooks(showLocalPath bool) initwd.ModuleInstallHooks {
|
||||
hooks := make([]initwd.ModuleInstallHooks, len(m))
|
||||
for i, o := range m {
|
||||
hooks[i] = o.Hooks(showLocalPath)
|
||||
}
|
||||
return moduleInstallationHookMulti(hooks)
|
||||
}
|
||||
|
||||
type InitHuman struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Init = (*InitHuman)(nil)
|
||||
|
||||
func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
func (v *InitHuman) HelpPrompt() {
|
||||
v.view.HelpPrompt("init")
|
||||
}
|
||||
|
||||
func (v *InitHuman) CopyFromModule(src string) {
|
||||
msg := v.view.colorize.Color(fmt.Sprintf("[reset][bold]Copying configuration[reset] from %q...", src))
|
||||
_, _ = v.view.streams.Println(msg)
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitialisedFromEmptyDir() {
|
||||
const outputInitEmpty = `
|
||||
[reset][bold]OpenTofu initialized in an empty directory![reset]
|
||||
|
||||
The directory has no OpenTofu configuration files. You may begin working
|
||||
with OpenTofu immediately by creating OpenTofu configuration files.`
|
||||
_, _ = v.view.streams.Println(strings.TrimSpace(v.view.colorize.Color(outputInitEmpty)))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ConfigError() {
|
||||
const errInitConfigError = `
|
||||
[reset]OpenTofu encountered problems during initialization, including problems
|
||||
with the configuration, described below.
|
||||
|
||||
The OpenTofu configuration must be valid before initialization so that
|
||||
OpenTofu can determine which modules and providers need to be installed.`
|
||||
|
||||
_, _ = v.view.streams.Eprintln(v.view.colorize.Color(errInitConfigError))
|
||||
}
|
||||
|
||||
func (v *InitHuman) OutputNewline() {
|
||||
_, _ = v.view.streams.Println("")
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitSuccess(cloud bool) {
|
||||
if cloud {
|
||||
const outputInitSuccessCloud = `[reset][bold][green]Cloud backend has been successfully initialized![reset][green]`
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(outputInitSuccessCloud))
|
||||
} else {
|
||||
const outputInitSuccess = `[reset][bold][green]OpenTofu has been successfully initialized![reset][green]`
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(outputInitSuccess))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitSuccessCLI(cloud bool) {
|
||||
if cloud {
|
||||
const outputInitSuccessCLICloud = `[reset][green]
|
||||
You may now begin working with cloud backend. Try running "tofu plan" to
|
||||
see any changes that are required for your infrastructure.
|
||||
|
||||
If you ever set or change modules or OpenTofu Settings, run "tofu init"
|
||||
again to reinitialize your working directory.`
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(outputInitSuccessCLICloud))
|
||||
} else {
|
||||
const outputInitSuccessCLI = `[reset][green]
|
||||
You may now begin working with OpenTofu. Try running "tofu plan" to see
|
||||
any changes that are required for your infrastructure. All OpenTofu commands
|
||||
should now work.
|
||||
|
||||
If you ever set or change modules or backend configuration for OpenTofu,
|
||||
rerun this command to reinitialize your working directory. If you forget, other
|
||||
commands will detect it and remind you to do so if necessary.`
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(outputInitSuccessCLI))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitializingModules(upgrade bool) {
|
||||
if upgrade {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color("[reset][bold]Upgrading modules..."))
|
||||
} else {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color("[reset][bold]Initializing modules..."))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitializingCloudBackend() {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color("\n[reset][bold]Initializing cloud backend..."))
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitializingBackend() {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color("\n[reset][bold]Initializing the backend..."))
|
||||
}
|
||||
|
||||
func (v *InitHuman) BackendTypeAlias(backendType, canonType string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- %q is an alias for backend type %q", backendType, canonType))
|
||||
}
|
||||
|
||||
func (v *InitHuman) InitializingProviderPlugins() {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color("\n[reset][bold]Initializing provider plugins..."))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProviderAlreadyInstalled(provider string, version string, inCache bool) {
|
||||
if inCache {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Detected previously-installed %s v%s in the shared cache directory", provider, version))
|
||||
} else {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Using previously-installed %s v%s", provider, version))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitHuman) BuiltInProviderAvailable(provider string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- %s is built in to OpenTofu", provider))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ReusingLockFileVersion(provider string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Reusing previous version of %s from the dependency lock file", provider))
|
||||
}
|
||||
|
||||
func (v *InitHuman) FindingProviderVersions(provider string, constraints string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Finding %s versions matching %q...", provider, constraints))
|
||||
}
|
||||
|
||||
func (v *InitHuman) FindingLatestProviderVersion(provider string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Finding latest version of %s...", provider))
|
||||
}
|
||||
|
||||
func (v *InitHuman) UsingProviderFromCache(provider string, version string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Using %s v%s from the shared cache directory", provider, version))
|
||||
}
|
||||
|
||||
func (v *InitHuman) InstallingProvider(provider string, version string, toCache bool) {
|
||||
if toCache {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Installing %s v%s to the shared cache directory...", provider, version))
|
||||
} else {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Installing %s v%s...", provider, version))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProviderInstalled(provider string, version string, authResult string, keyID string) {
|
||||
if keyID != "" {
|
||||
keyID = v.view.colorize.Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID))
|
||||
}
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Installed %s v%s (%s%s)", provider, version, authResult, keyID))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProviderInstalledSkippedSignature(provider string, version string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Installed %s v%s. Signature validation was skipped due to the registry not containing GPG keys for this provider", provider, version))
|
||||
}
|
||||
|
||||
func (v *InitHuman) WaitingForCacheLock(cacheDir string) {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("- Waiting for lock on cache directory %s", cacheDir))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProvidersSignedInfo() {
|
||||
_, _ = v.view.streams.Println(fmt.Sprintf("\nProviders are signed by their developers.\n" +
|
||||
"If you'd like to know more about provider signing, you can read about it here:\n" +
|
||||
"https://opentofu.org/docs/cli/plugins/signing/"))
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProviderUpgradeLockfileConflict() {
|
||||
_, _ = v.view.streams.Eprintln("The -upgrade flag conflicts with -lockfile=readonly.")
|
||||
}
|
||||
|
||||
func (v *InitHuman) ProviderInstallationInterrupted() {
|
||||
_, _ = v.view.streams.Eprintln("Provider installation was canceled by an interrupt signal.")
|
||||
}
|
||||
|
||||
func (v *InitHuman) LockFileCreated() {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(`
|
||||
OpenTofu has created a lock file [bold].terraform.lock.hcl[reset] to record the provider
|
||||
selections it made above. Include this file in your version control repository
|
||||
so that OpenTofu can guarantee to make the same selections by default when
|
||||
you run "tofu init" in the future.`))
|
||||
}
|
||||
|
||||
func (v *InitHuman) LockFileChanged() {
|
||||
_, _ = v.view.streams.Println(v.view.colorize.Color(`
|
||||
OpenTofu has made some changes to the provider dependency selections recorded
|
||||
in the .terraform.lock.hcl file. Review those changes and commit them to your
|
||||
version control system if they represent changes you intended to make.`))
|
||||
}
|
||||
|
||||
func (v *InitHuman) Hooks(showLocalPath bool) initwd.ModuleInstallHooks {
|
||||
return &moduleInstallationHookHuman{
|
||||
v: v.view,
|
||||
showLocalPaths: showLocalPath,
|
||||
}
|
||||
}
|
||||
|
||||
type InitJSON struct {
|
||||
view *JSONView
|
||||
}
|
||||
|
||||
var _ Init = (*InitJSON)(nil)
|
||||
|
||||
func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
func (v *InitJSON) HelpPrompt() {}
|
||||
|
||||
func (v *InitJSON) CopyFromModule(src string) {
|
||||
v.view.Info(fmt.Sprintf("Copying configuration from %q...", src))
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitialisedFromEmptyDir() {
|
||||
const outputInitEmpty = `OpenTofu initialized in an empty directory! The directory has no OpenTofu configuration files. You may begin working with OpenTofu immediately by creating OpenTofu configuration files.`
|
||||
v.view.Info(outputInitEmpty)
|
||||
}
|
||||
|
||||
func (v *InitJSON) ConfigError() {
|
||||
const errInitConfigError = `OpenTofu encountered problems during initialization, including problems with the configuration, described below. The OpenTofu configuration must be valid before initialization so that OpenTofu can determine which modules and providers need to be installed.`
|
||||
v.view.Error(errInitConfigError)
|
||||
}
|
||||
|
||||
func (v *InitJSON) OutputNewline() {
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitSuccess(cloud bool) {
|
||||
if cloud {
|
||||
v.view.Info(`Cloud backend has been successfully initialized!`)
|
||||
} else {
|
||||
v.view.Info(`OpenTofu has been successfully initialized!`)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitSuccessCLI(cloud bool) {
|
||||
if cloud {
|
||||
const outputInitSuccessCLICloud = `You may now begin working with cloud backend. Try running "tofu plan" to see any changes that are required for your infrastructure. If you ever set or change modules or OpenTofu Settings, run "tofu init" again to reinitialize your working directory.`
|
||||
v.view.Info(outputInitSuccessCLICloud)
|
||||
} else {
|
||||
const outputInitSuccessCLI = `You may now begin working with OpenTofu. Try running "tofu plan" to see any changes that are required for your infrastructure. All OpenTofu commands should now work. If you ever set or change modules or backend configuration for OpenTofu, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.`
|
||||
v.view.Info(outputInitSuccessCLI)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitializingModules(upgrade bool) {
|
||||
if upgrade {
|
||||
v.view.Info("Upgrading modules...")
|
||||
} else {
|
||||
v.view.Info("Initializing modules...")
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitializingCloudBackend() {
|
||||
v.view.Info("Initializing cloud backend...")
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitializingBackend() {
|
||||
v.view.Info("Initializing the backend...")
|
||||
}
|
||||
|
||||
func (v *InitJSON) BackendTypeAlias(backendType, canonType string) {
|
||||
v.view.Info(fmt.Sprintf("%q is an alias for backend type %q", backendType, canonType))
|
||||
}
|
||||
|
||||
func (v *InitJSON) InitializingProviderPlugins() {
|
||||
v.view.Info("Initializing provider plugins...")
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProviderAlreadyInstalled(provider string, version string, inCache bool) {
|
||||
if inCache {
|
||||
v.view.Info(fmt.Sprintf("Detected previously-installed %s v%s in the shared cache directory", provider, version))
|
||||
} else {
|
||||
v.view.Info(fmt.Sprintf("Using previously-installed %s v%s", provider, version))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitJSON) BuiltInProviderAvailable(provider string) {
|
||||
v.view.Info(fmt.Sprintf("%s is built in to OpenTofu", provider))
|
||||
}
|
||||
|
||||
func (v *InitJSON) ReusingLockFileVersion(provider string) {
|
||||
v.view.Info(fmt.Sprintf("Reusing previous version of %s from the dependency lock file", provider))
|
||||
}
|
||||
|
||||
func (v *InitJSON) FindingProviderVersions(provider string, constraints string) {
|
||||
v.view.Info(fmt.Sprintf("Finding %s versions matching %q...", provider, constraints))
|
||||
}
|
||||
|
||||
func (v *InitJSON) FindingLatestProviderVersion(provider string) {
|
||||
v.view.Info(fmt.Sprintf("Finding latest version of %s...", provider))
|
||||
}
|
||||
|
||||
func (v *InitJSON) UsingProviderFromCache(provider string, version string) {
|
||||
v.view.Info(fmt.Sprintf("Using %s v%s from the shared cache directory", provider, version))
|
||||
}
|
||||
|
||||
func (v *InitJSON) InstallingProvider(provider string, version string, toCache bool) {
|
||||
if toCache {
|
||||
v.view.Info(fmt.Sprintf("Installing %s v%s to the shared cache directory...", provider, version))
|
||||
} else {
|
||||
v.view.Info(fmt.Sprintf("Installing %s v%s...", provider, version))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProviderInstalled(provider string, version string, authResult string, keyID string) {
|
||||
if keyID != "" {
|
||||
keyID = fmt.Sprintf(", key ID %s", keyID)
|
||||
}
|
||||
v.view.Info(fmt.Sprintf("Installed %s v%s (%s%s)", provider, version, authResult, keyID))
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProviderInstalledSkippedSignature(provider string, version string) {
|
||||
v.view.Warn(fmt.Sprintf("Installed %s v%s. Signature validation was skipped due to the registry not containing GPG keys for this provider", provider, version))
|
||||
}
|
||||
|
||||
func (v *InitJSON) WaitingForCacheLock(cacheDir string) {
|
||||
v.view.Info(fmt.Sprintf("Waiting for lock on cache directory %s", cacheDir))
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProvidersSignedInfo() {
|
||||
v.view.Info("Providers are signed by their developers. " +
|
||||
"If you'd like to know more about provider signing, you can read about it here: " +
|
||||
"https://opentofu.org/docs/cli/plugins/signing/")
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProviderUpgradeLockfileConflict() {
|
||||
v.view.Error("The -upgrade flag conflicts with -lockfile=readonly.")
|
||||
}
|
||||
|
||||
func (v *InitJSON) ProviderInstallationInterrupted() {
|
||||
v.view.Error("Provider installation was canceled by an interrupt signal.")
|
||||
}
|
||||
|
||||
func (v *InitJSON) LockFileCreated() {
|
||||
v.view.Info("OpenTofu has created a lock file .terraform.lock.hcl to record the provider " +
|
||||
"selections it made above. Include this file in your version control repository " +
|
||||
"so that OpenTofu can guarantee to make the same selections by default when " +
|
||||
"you run \"tofu init\" in the future.")
|
||||
}
|
||||
|
||||
func (v *InitJSON) LockFileChanged() {
|
||||
v.view.Info("OpenTofu has made some changes to the provider dependency selections recorded " +
|
||||
"in the .terraform.lock.hcl file. Review those changes and commit them to your " +
|
||||
"version control system if they represent changes you intended to make.")
|
||||
}
|
||||
|
||||
func (v *InitJSON) Hooks(showLocalPath bool) initwd.ModuleInstallHooks {
|
||||
return &moduleInstallationHookJSON{
|
||||
v: v.view,
|
||||
showLocalPaths: showLocalPath,
|
||||
}
|
||||
}
|
||||
678
internal/command/views/init_test.go
Normal file
678
internal/command/views/init_test.go
Normal file
|
|
@ -0,0 +1,678 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
"github.com/opentofu/opentofu/internal/terminal"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestInitViews(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
viewCall func(init Init)
|
||||
wantJson []map[string]any
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
"copyFromModule": {
|
||||
viewCall: func(init Init) {
|
||||
init.CopyFromModule("my source")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Copying configuration from \"my source\"...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline(`Copying configuration from "my source"...`),
|
||||
},
|
||||
"fromEmptyDir": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitialisedFromEmptyDir()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "OpenTofu initialized in an empty directory! The directory has no OpenTofu configuration files. You may begin working with OpenTofu immediately by creating OpenTofu configuration files.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("OpenTofu initialized in an empty directory!\n\nThe directory has no OpenTofu configuration files. You may begin working\nwith OpenTofu immediately by creating OpenTofu configuration files."),
|
||||
},
|
||||
"outputNewline": {
|
||||
viewCall: func(init Init) {
|
||||
init.OutputNewline()
|
||||
},
|
||||
wantStdout: withNewline(""),
|
||||
wantStderr: "",
|
||||
wantJson: []map[string]any{{}},
|
||||
},
|
||||
"initSuccess_cloud": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitSuccess(true)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Cloud backend has been successfully initialized!",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("Cloud backend has been successfully initialized!"),
|
||||
},
|
||||
"initSuccess_non-cloud": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitSuccess(false)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "OpenTofu has been successfully initialized!",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("OpenTofu has been successfully initialized!"),
|
||||
},
|
||||
"initSuccessCLI_cloud": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitSuccessCLI(true)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "You may now begin working with cloud backend. Try running \"tofu plan\" to see any changes that are required for your infrastructure. If you ever set or change modules or OpenTofu Settings, run \"tofu init\" again to reinitialize your working directory.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nYou may now begin working with cloud backend. Try running \"tofu plan\" to\nsee any changes that are required for your infrastructure.\n\nIf you ever set or change modules or OpenTofu Settings, run \"tofu init\"\nagain to reinitialize your working directory."),
|
||||
},
|
||||
"initSuccessCLI_non-cloud": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitSuccessCLI(false)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "You may now begin working with OpenTofu. Try running \"tofu plan\" to see any changes that are required for your infrastructure. All OpenTofu commands should now work. If you ever set or change modules or backend configuration for OpenTofu, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nYou may now begin working with OpenTofu. Try running \"tofu plan\" to see\nany changes that are required for your infrastructure. All OpenTofu commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for OpenTofu,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary."),
|
||||
},
|
||||
"initializingModules_upgrade": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitializingModules(true)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Upgrading modules...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("Upgrading modules..."),
|
||||
},
|
||||
"initializingModules_init": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitializingModules(false)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Initializing modules...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("Initializing modules..."),
|
||||
},
|
||||
"initializingCloudBackend": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitializingCloudBackend()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Initializing cloud backend...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nInitializing cloud backend..."),
|
||||
},
|
||||
"initializingBackend": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitializingBackend()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Initializing the backend...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nInitializing the backend..."),
|
||||
},
|
||||
"backendTypeAlias": {
|
||||
viewCall: func(init Init) {
|
||||
init.BackendTypeAlias("s3", "aws_s3")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "\"s3\" is an alias for backend type \"aws_s3\"",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- \"s3\" is an alias for backend type \"aws_s3\""),
|
||||
},
|
||||
"initializingProviderPlugins": {
|
||||
viewCall: func(init Init) {
|
||||
init.InitializingProviderPlugins()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Initializing provider plugins...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nInitializing provider plugins..."),
|
||||
},
|
||||
"providerAlreadyInstalled_notInCache": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderAlreadyInstalled("hashicorp/aws", "5.0.0", false)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Using previously-installed hashicorp/aws v5.0.0",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Using previously-installed hashicorp/aws v5.0.0"),
|
||||
},
|
||||
"providerAlreadyInstalled_inCache": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderAlreadyInstalled("hashicorp/aws", "5.0.0", true)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Detected previously-installed hashicorp/aws v5.0.0 in the shared cache directory",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Detected previously-installed hashicorp/aws v5.0.0 in the shared cache directory"),
|
||||
},
|
||||
"builtInProviderAvailable": {
|
||||
viewCall: func(init Init) {
|
||||
init.BuiltInProviderAvailable("terraform")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "terraform is built in to OpenTofu",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- terraform is built in to OpenTofu"),
|
||||
},
|
||||
"reusingLockFileVersion": {
|
||||
viewCall: func(init Init) {
|
||||
init.ReusingLockFileVersion("hashicorp/random")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Reusing previous version of hashicorp/random from the dependency lock file",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Reusing previous version of hashicorp/random from the dependency lock file"),
|
||||
},
|
||||
"findingProviderVersions": {
|
||||
viewCall: func(init Init) {
|
||||
init.FindingProviderVersions("hashicorp/aws", "~> 5.0")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Finding hashicorp/aws versions matching \"~> 5.0\"...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Finding hashicorp/aws versions matching \"~> 5.0\"..."),
|
||||
},
|
||||
"findingLatestProviderVersion": {
|
||||
viewCall: func(init Init) {
|
||||
init.FindingLatestProviderVersion("hashicorp/null")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Finding latest version of hashicorp/null...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Finding latest version of hashicorp/null..."),
|
||||
},
|
||||
"usingProviderFromCache": {
|
||||
viewCall: func(init Init) {
|
||||
init.UsingProviderFromCache("hashicorp/aws", "5.0.0")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Using hashicorp/aws v5.0.0 from the shared cache directory",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Using hashicorp/aws v5.0.0 from the shared cache directory"),
|
||||
},
|
||||
"installingProvider_notToCache": {
|
||||
viewCall: func(init Init) {
|
||||
init.InstallingProvider("hashicorp/aws", "5.0.0", false)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Installing hashicorp/aws v5.0.0...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Installing hashicorp/aws v5.0.0..."),
|
||||
},
|
||||
"installingProvider_toCache": {
|
||||
viewCall: func(init Init) {
|
||||
init.InstallingProvider("hashicorp/aws", "5.0.0", true)
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Installing hashicorp/aws v5.0.0 to the shared cache directory...",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Installing hashicorp/aws v5.0.0 to the shared cache directory..."),
|
||||
},
|
||||
"providerInstalled_noKeyID": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderInstalled("hashicorp/aws", "5.0.0", "signed by HashiCorp", "")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Installed hashicorp/aws v5.0.0 (signed by HashiCorp)",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Installed hashicorp/aws v5.0.0 (signed by HashiCorp)"),
|
||||
},
|
||||
"providerInstalled_withKeyID": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderInstalled("hashicorp/aws", "5.0.0", "signed", "34365D9472D7468F")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Installed hashicorp/aws v5.0.0 (signed, key ID 34365D9472D7468F)",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Installed hashicorp/aws v5.0.0 (signed, key ID 34365D9472D7468F)"),
|
||||
},
|
||||
"waitingForCacheLock": {
|
||||
viewCall: func(init Init) {
|
||||
init.WaitingForCacheLock("/tmp/plugin-cache")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Waiting for lock on cache directory /tmp/plugin-cache",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Waiting for lock on cache directory /tmp/plugin-cache"),
|
||||
},
|
||||
"providersSignedInfo": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProvidersSignedInfo()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "Providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://opentofu.org/docs/cli/plugins/signing/",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nProviders are signed by their developers.\nIf you'd like to know more about provider signing, you can read about it here:\nhttps://opentofu.org/docs/cli/plugins/signing/"),
|
||||
},
|
||||
"lockFileCreated": {
|
||||
viewCall: func(init Init) {
|
||||
init.LockFileCreated()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "OpenTofu has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that OpenTofu can guarantee to make the same selections by default when you run \"tofu init\" in the future.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nOpenTofu has created a lock file .terraform.lock.hcl to record the provider\nselections it made above. Include this file in your version control repository\nso that OpenTofu can guarantee to make the same selections by default when\nyou run \"tofu init\" in the future."),
|
||||
},
|
||||
"lockFileChanged": {
|
||||
viewCall: func(init Init) {
|
||||
init.LockFileChanged()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "OpenTofu has made some changes to the provider dependency selections recorded in the .terraform.lock.hcl file. Review those changes and commit them to your version control system if they represent changes you intended to make.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("\nOpenTofu has made some changes to the provider dependency selections recorded\nin the .terraform.lock.hcl file. Review those changes and commit them to your\nversion control system if they represent changes you intended to make."),
|
||||
},
|
||||
// to stderr
|
||||
"configError": {
|
||||
viewCall: func(init Init) {
|
||||
init.ConfigError()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "OpenTofu encountered problems during initialization, including problems with the configuration, described below. The OpenTofu configuration must be valid before initialization so that OpenTofu can determine which modules and providers need to be installed.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("\nOpenTofu encountered problems during initialization, including problems\nwith the configuration, described below.\n\nThe OpenTofu configuration must be valid before initialization so that\nOpenTofu can determine which modules and providers need to be installed."),
|
||||
},
|
||||
"providerInstalledSkippedSignature": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderInstalledSkippedSignature("hashicorp/random", "3.0.0")
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "warn",
|
||||
"@message": "Installed hashicorp/random v3.0.0. Signature validation was skipped due to the registry not containing GPG keys for this provider",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: withNewline("- Installed hashicorp/random v3.0.0. Signature validation was skipped due to the registry not containing GPG keys for this provider"),
|
||||
wantStderr: "",
|
||||
},
|
||||
"providerUpgradeLockfileConflict": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderUpgradeLockfileConflict()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "The -upgrade flag conflicts with -lockfile=readonly.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("The -upgrade flag conflicts with -lockfile=readonly."),
|
||||
},
|
||||
"providerInstallationInterrupted": {
|
||||
viewCall: func(init Init) {
|
||||
init.ProviderInstallationInterrupted()
|
||||
},
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "Provider installation was canceled by an interrupt signal.",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("Provider installation was canceled by an interrupt signal."),
|
||||
},
|
||||
// Diagnostics
|
||||
"warning": {
|
||||
viewCall: func(init Init) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Warning, "A warning occurred", "foo bar"),
|
||||
}
|
||||
init.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: withNewline("\nWarning: A warning occurred\n\nfoo bar"),
|
||||
wantStderr: "",
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "warn",
|
||||
"@message": "Warning: A warning occurred",
|
||||
"@module": "tofu.ui",
|
||||
"diagnostic": map[string]any{
|
||||
"detail": "foo bar",
|
||||
"severity": "warning",
|
||||
"summary": "A warning occurred",
|
||||
},
|
||||
"type": "diagnostic",
|
||||
},
|
||||
},
|
||||
},
|
||||
"error": {
|
||||
viewCall: func(init Init) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Error, "An error occurred", "foo bar"),
|
||||
}
|
||||
init.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("\nError: An error occurred\n\nfoo bar"),
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "Error: An error occurred",
|
||||
"@module": "tofu.ui",
|
||||
"diagnostic": map[string]any{
|
||||
"detail": "foo bar",
|
||||
"severity": "error",
|
||||
"summary": "An error occurred",
|
||||
},
|
||||
"type": "diagnostic",
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple_diagnostics": {
|
||||
viewCall: func(init Init) {
|
||||
diags := tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(tfdiags.Warning, "A warning", "foo bar warning"),
|
||||
tfdiags.Sourceless(tfdiags.Error, "An error", "foo bar error"),
|
||||
}
|
||||
init.Diagnostics(diags)
|
||||
},
|
||||
wantStdout: withNewline("\nWarning: A warning\n\nfoo bar warning"),
|
||||
wantStderr: withNewline("\nError: An error\n\nfoo bar error"),
|
||||
wantJson: []map[string]any{
|
||||
{
|
||||
"@level": "warn",
|
||||
"@message": "Warning: A warning",
|
||||
"@module": "tofu.ui",
|
||||
"diagnostic": map[string]any{
|
||||
"detail": "foo bar warning",
|
||||
"severity": "warning",
|
||||
"summary": "A warning",
|
||||
},
|
||||
"type": "diagnostic",
|
||||
},
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "Error: An error",
|
||||
"@module": "tofu.ui",
|
||||
"diagnostic": map[string]any{
|
||||
"detail": "foo bar error",
|
||||
"severity": "error",
|
||||
"summary": "An error",
|
||||
},
|
||||
"type": "diagnostic",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Miscs
|
||||
"help prompt": {
|
||||
viewCall: func(init Init) {
|
||||
init.HelpPrompt()
|
||||
},
|
||||
wantStdout: "",
|
||||
wantStderr: withNewline("\nFor more help on using this command, run:\n tofu init -help"),
|
||||
wantJson: []map[string]any{{}},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testInitHuman(t, tc.viewCall, tc.wantStdout, tc.wantStderr)
|
||||
testInitJson(t, tc.viewCall, tc.wantJson)
|
||||
testInitMulti(t, tc.viewCall, tc.wantStdout, tc.wantStderr, tc.wantJson)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitViews_Hooks(t *testing.T) {
|
||||
t.Run("hooks_human_withLocalPath", func(t *testing.T) {
|
||||
view, _ := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewHuman}, view)
|
||||
hooks := initView.Hooks(true)
|
||||
|
||||
if hooks == nil {
|
||||
t.Fatal("expected hooks to be non-nil")
|
||||
}
|
||||
|
||||
// Verify it's the right type
|
||||
_, ok := hooks.(*moduleInstallationHookHuman)
|
||||
if !ok {
|
||||
t.Errorf("expected *moduleInstallationHookHuman, got %T", hooks)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hooks_human_withoutLocalPath", func(t *testing.T) {
|
||||
view, _ := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewHuman}, view)
|
||||
hooks := initView.Hooks(false)
|
||||
|
||||
if hooks == nil {
|
||||
t.Fatal("expected hooks to be non-nil")
|
||||
}
|
||||
|
||||
_, ok := hooks.(*moduleInstallationHookHuman)
|
||||
if !ok {
|
||||
t.Errorf("expected *moduleInstallationHookHuman, got %T", hooks)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hooks_json", func(t *testing.T) {
|
||||
view, _ := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewJSON}, view)
|
||||
hooks := initView.Hooks(true)
|
||||
|
||||
if hooks == nil {
|
||||
t.Fatal("expected hooks to be non-nil")
|
||||
}
|
||||
|
||||
_, ok := hooks.(*moduleInstallationHookJSON)
|
||||
if !ok {
|
||||
t.Errorf("expected *moduleInstallationHookJSON, got %T", hooks)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hooks_multi", func(t *testing.T) {
|
||||
jsonInto, err := os.CreateTemp(t.TempDir(), "json-into-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %s", err)
|
||||
}
|
||||
defer jsonInto.Close()
|
||||
|
||||
view, _ := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewHuman, JSONInto: jsonInto}, view)
|
||||
hooks := initView.Hooks(true)
|
||||
|
||||
if hooks == nil {
|
||||
t.Fatal("expected hooks to be non-nil")
|
||||
}
|
||||
|
||||
// Should be multi hook
|
||||
_, ok := hooks.(moduleInstallationHookMulti)
|
||||
if !ok {
|
||||
t.Errorf("expected moduleInstallationHookMulti, got %T", hooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testInitHuman(t *testing.T, call func(init Init), wantStdout, wantStderr string) {
|
||||
view, done := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewHuman}, view)
|
||||
call(initView)
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func testInitJson(t *testing.T, call func(init Init), want []map[string]interface{}) {
|
||||
// New type just to assert the fields that we are interested in
|
||||
view, done := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewJSON}, view)
|
||||
call(initView)
|
||||
output := done(t)
|
||||
if output.Stderr() != "" {
|
||||
t.Errorf("expected no stderr but got:\n%s", output.Stderr())
|
||||
}
|
||||
|
||||
testJSONViewOutputEquals(t, output.Stdout(), want)
|
||||
}
|
||||
|
||||
func testInitMulti(t *testing.T, call func(init Init), wantStdout string, wantStderr string, want []map[string]interface{}) {
|
||||
jsonInto, err := os.CreateTemp(t.TempDir(), "json-into-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create the file to write json content into: %s", err)
|
||||
}
|
||||
view, done := testView(t)
|
||||
initView := NewInit(arguments.ViewOptions{ViewType: arguments.ViewHuman, JSONInto: jsonInto}, view)
|
||||
call(initView)
|
||||
{
|
||||
if err := jsonInto.Close(); err != nil {
|
||||
t.Fatalf("failed to close the jsonInto file: %s", err)
|
||||
}
|
||||
// check the fileInto content
|
||||
fileContent, err := os.ReadFile(jsonInto.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read the file content with the json output: %s", err)
|
||||
}
|
||||
testJSONViewOutputEquals(t, string(fileContent), want)
|
||||
}
|
||||
{
|
||||
// check the human output
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(wantStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(wantStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testView(t *testing.T) (*View, func(*testing.T) *terminal.TestOutput) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
return view, done
|
||||
}
|
||||
|
||||
func withNewline(in string) string {
|
||||
return fmt.Sprintf("%s\n", in)
|
||||
}
|
||||
|
|
@ -458,7 +458,12 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
|
|||
break
|
||||
}
|
||||
wantStruct := want[i]
|
||||
|
||||
// When the json content generated is empty, there will be an empty one liner that can be matched
|
||||
// by a "want" slice with one empty element
|
||||
if len(gotLines[i]) == 0 && len(wantStruct) == 0 {
|
||||
t.Logf("json output empty and that matches the requirements")
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal([]byte(gotLines[i]), &gotStruct); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
|
|
@ -43,6 +45,10 @@ type View struct {
|
|||
// showSensitive is used to display the value of variables marked as sensitive.
|
||||
showSensitive bool
|
||||
|
||||
// Because some commands used before the UI to print diagnostics, those were printed using an [*ln] function, so
|
||||
// we want to be able to configure this for some of the commands to be able to keep the behavior consistent.
|
||||
diagsPrinter func(severity tfdiags.Severity, msg string)
|
||||
|
||||
// This unfortunate wart is required to enable rendering of diagnostics which
|
||||
// have associated source code in the configuration. This function pointer
|
||||
// will be dereferenced as late as possible when rendering diagnostics in
|
||||
|
|
@ -61,6 +67,13 @@ func NewView(streams *terminal.Streams) *View {
|
|||
Reset: true,
|
||||
},
|
||||
configSources: func() map[string]*hcl.File { return nil },
|
||||
diagsPrinter: func(severity tfdiags.Severity, msg string) {
|
||||
if severity == tfdiags.Error {
|
||||
_, _ = streams.Eprint(msg)
|
||||
} else {
|
||||
_, _ = streams.Print(msg)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +95,11 @@ func (v *View) RunningInAutomation() bool {
|
|||
|
||||
// Configure applies the global view configuration flags.
|
||||
func (v *View) Configure(view *arguments.View) {
|
||||
colors := maps.Clone(colorstring.DefaultColors)
|
||||
colors["purple"] = "38;5;57" // Add also purple to the colorise colors set
|
||||
|
||||
v.colorize.Disable = view.NoColor
|
||||
v.colorize.Colors = colors
|
||||
v.compactWarnings = view.CompactWarnings
|
||||
v.consolidateWarnings = view.ConsolidateWarnings
|
||||
v.consolidateErrors = view.ConsolidateErrors
|
||||
|
|
@ -90,6 +107,16 @@ func (v *View) Configure(view *arguments.View) {
|
|||
v.ModuleDeprecationWarnLvl = view.ModuleDeprecationWarnLvl
|
||||
}
|
||||
|
||||
func (v *View) DiagsWithNewline() {
|
||||
v.diagsPrinter = func(severity tfdiags.Severity, msg string) {
|
||||
if severity == tfdiags.Error {
|
||||
_, _ = v.streams.Eprintln(msg)
|
||||
} else {
|
||||
_, _ = v.streams.Println(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigSources overrides the default no-op callback with a new function
|
||||
// pointer, and should be called when the config loader is initialized.
|
||||
func (v *View) SetConfigSources(cb func() map[string]*hcl.File) {
|
||||
|
|
@ -159,6 +186,12 @@ func (v *View) Diagnostics(diags tfdiags.Diagnostics) {
|
|||
msg = format.Diagnostic(diag, v.configSources(), v.colorize, v.streams.Stderr.Columns())
|
||||
}
|
||||
|
||||
// TODO meta-refactor: once we are done with migrating all the commands to views, we should get rid
|
||||
// of the check and just allow the diagsPrinter to be called directly.
|
||||
if v.diagsPrinter != nil {
|
||||
v.diagsPrinter(diag.Severity(), msg)
|
||||
continue
|
||||
}
|
||||
if diag.Severity() == tfdiags.Error {
|
||||
v.streams.Eprint(msg)
|
||||
} else {
|
||||
|
|
|
|||
183
internal/command/views/view_ui.go
Normal file
183
internal/command/views/view_ui.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
)
|
||||
|
||||
var ErrorInputDisabled = fmt.Errorf("in this view cannot ask user input")
|
||||
|
||||
var _ cli.Ui = (*ViewUiHuman)(nil)
|
||||
var _ cli.Ui = (*ViewUiJSON)(nil)
|
||||
var _ cli.Ui = (*ViewUiMulti)(nil)
|
||||
|
||||
func NewViewUI(args arguments.ViewOptions, view *View, oldUi cli.Ui) cli.Ui {
|
||||
var ret cli.Ui
|
||||
switch args.ViewType {
|
||||
case arguments.ViewJSON:
|
||||
ret = &ViewUiJSON{
|
||||
view: NewJSONView(view, nil),
|
||||
}
|
||||
case arguments.ViewHuman:
|
||||
ret = &ViewUiHuman{
|
||||
errorColor: "[red]",
|
||||
warnColor: "[yellow]",
|
||||
ui: oldUi,
|
||||
view: view,
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", args.ViewType))
|
||||
}
|
||||
|
||||
if args.JSONInto != nil {
|
||||
ret = &ViewUiMulti{ret, &ViewUiJSON{view: NewJSONView(view, args.JSONInto)}}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ViewUiHuman is a Ui implementation that colors its output according
|
||||
// to the given color schemes for the given type of output.
|
||||
type ViewUiHuman struct {
|
||||
ui cli.Ui
|
||||
view *View
|
||||
errorColor string
|
||||
warnColor string
|
||||
outputColor string
|
||||
infoColor string
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) Ask(query string) (string, error) {
|
||||
return u.ui.Ask(u.colorize(query, u.outputColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) AskSecret(query string) (string, error) {
|
||||
return u.ui.AskSecret(u.colorize(query, u.outputColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) Output(message string) {
|
||||
_, _ = u.view.streams.Println(u.colorize(message, u.outputColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) Info(message string) {
|
||||
_, _ = u.view.streams.Println(u.colorize(message, u.infoColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) Error(message string) {
|
||||
_, _ = u.view.streams.Eprintln(u.colorize(message, u.errorColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) Warn(message string) {
|
||||
// Warning messages are meant to go to stdout as pointed out here: https://github.com/opentofu/opentofu/commit/0c3bb316ea56aacf5108883d1a269a53744fdd43
|
||||
_, _ = u.view.streams.Println(u.colorize(message, u.warnColor))
|
||||
}
|
||||
|
||||
func (u *ViewUiHuman) colorize(message string, color string) string {
|
||||
if color == "" {
|
||||
return message
|
||||
}
|
||||
|
||||
return u.view.colorize.Color(fmt.Sprintf("%s%s[reset]", color, message))
|
||||
}
|
||||
|
||||
// ViewUiJSON is a Ui implementation that colors its output according
|
||||
// to the given color schemes for the given type of output.
|
||||
type ViewUiJSON struct {
|
||||
view *JSONView
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) Ask(_ string) (string, error) {
|
||||
return "", ErrorInputDisabled
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) AskSecret(_ string) (string, error) {
|
||||
return "", ErrorInputDisabled
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) Output(message string) {
|
||||
u.view.Info(stripColor(message))
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) Info(message string) {
|
||||
u.view.Info(stripColor(message))
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) Error(message string) {
|
||||
u.view.Error(stripColor(message))
|
||||
}
|
||||
|
||||
func (u *ViewUiJSON) Warn(message string) {
|
||||
u.view.Warn(stripColor(message))
|
||||
}
|
||||
|
||||
// ViewUiMulti is a Ui implementation that colors its output according
|
||||
// to the given color schemes for the given type of output.
|
||||
type ViewUiMulti []cli.Ui
|
||||
|
||||
func (u ViewUiMulti) Ask(query string) (string, error) {
|
||||
var err error
|
||||
|
||||
for _, ui := range u {
|
||||
out, innerErr := ui.Ask(query)
|
||||
if innerErr == nil {
|
||||
return out, innerErr // Return first response
|
||||
}
|
||||
err = innerErr // Othwerise, store the error to be returned later in case it's needed
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (u ViewUiMulti) AskSecret(query string) (string, error) {
|
||||
var err error
|
||||
|
||||
for _, ui := range u {
|
||||
out, innerErr := ui.AskSecret(query)
|
||||
if innerErr == nil {
|
||||
return out, innerErr // Return first response
|
||||
}
|
||||
err = innerErr // Othwerise, store the error to be returned later in case it's needed
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (u ViewUiMulti) Output(message string) {
|
||||
for _, ui := range u {
|
||||
ui.Output(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (u ViewUiMulti) Info(message string) {
|
||||
for _, ui := range u {
|
||||
ui.Info(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (u ViewUiMulti) Error(message string) {
|
||||
for _, ui := range u {
|
||||
ui.Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (u ViewUiMulti) Warn(message string) {
|
||||
for _, ui := range u {
|
||||
ui.Warn(message)
|
||||
}
|
||||
}
|
||||
|
||||
var matchColorRe = regexp.MustCompile("\033\\[[\\d;]*m")
|
||||
|
||||
func stripColor(s string) string {
|
||||
// This is a workaround for supporting json-into in legacy UI code paths. Hopefully this will all be ripped out once rfc/20251105-use-cobra-instead-of-mitchellh.md
|
||||
// and related work is completed.
|
||||
//
|
||||
// NOTE: The regexp above is specifically tailored to the mitchellh colorstring.go implementation and will NOT work with the *full* set
|
||||
// of possible colorization chars.
|
||||
return matchColorRe.ReplaceAllString(s, "")
|
||||
}
|
||||
149
internal/command/views/view_ui_test.go
Normal file
149
internal/command/views/view_ui_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||
"github.com/opentofu/opentofu/internal/terminal"
|
||||
)
|
||||
|
||||
func TestViewUiHuman_OutputStreams(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fn func(ui cli.Ui)
|
||||
expectStdout string
|
||||
expectStderr string
|
||||
}{
|
||||
{
|
||||
name: "Output goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Output("test output message")
|
||||
},
|
||||
expectStdout: withNewline("test output message"),
|
||||
expectStderr: "",
|
||||
},
|
||||
{
|
||||
name: "Info goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Info("test info message")
|
||||
},
|
||||
expectStdout: withNewline("test info message"),
|
||||
expectStderr: "",
|
||||
},
|
||||
{
|
||||
name: "Warn goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Warn("test warning message")
|
||||
},
|
||||
expectStdout: withNewline("test warning message"),
|
||||
expectStderr: "",
|
||||
},
|
||||
{
|
||||
name: "Error goes to stderr",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Error("test error message")
|
||||
},
|
||||
expectStdout: "",
|
||||
expectStderr: withNewline("test error message"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
|
||||
ui := NewViewUI(arguments.ViewOptions{ViewType: arguments.ViewHuman}, view, nil) // testing output only, no need for Ui
|
||||
|
||||
tc.fn(ui)
|
||||
output := done(t)
|
||||
if diff := cmp.Diff(tc.expectStderr, output.Stderr()); diff != "" {
|
||||
t.Errorf("invalid stderr (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expectStdout, output.Stdout()); diff != "" {
|
||||
t.Errorf("invalid stdout (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewUiJSON_OutputStreams(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fn func(ui cli.Ui)
|
||||
expectStdout []map[string]any
|
||||
}{
|
||||
{
|
||||
name: "Output goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Output("test output")
|
||||
},
|
||||
expectStdout: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test output",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Info goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Info("test info")
|
||||
},
|
||||
expectStdout: []map[string]any{
|
||||
{
|
||||
"@level": "info",
|
||||
"@message": "test info",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Warn goes to stdout",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Warn("test warning")
|
||||
},
|
||||
expectStdout: []map[string]any{
|
||||
{
|
||||
"@level": "warn",
|
||||
"@message": "test warning",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Error goes to stderr (via JSON view)",
|
||||
fn: func(ui cli.Ui) {
|
||||
ui.Error("test error")
|
||||
},
|
||||
expectStdout: []map[string]any{
|
||||
{
|
||||
"@level": "error",
|
||||
"@message": "test error",
|
||||
"@module": "tofu.ui",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
|
||||
ui := NewViewUI(arguments.ViewOptions{ViewType: arguments.ViewJSON}, view, nil)
|
||||
|
||||
tc.fn(ui)
|
||||
output := done(t)
|
||||
testJSONViewOutputEquals(t, output.Stdout(), tc.expectStdout)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue