mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 15:40:07 -06:00
feat: init and get command support json format output (#1453)
Signed-off-by: Syasusu <syasusu@163.com>
This commit is contained in:
parent
046beee664
commit
bdab86962f
@ -35,6 +35,7 @@ ENHANCEMENTS:
|
|||||||
* Dump state file when `tofu test` fails to clean up resources. ([#1243](https://github.com/opentofu/opentofu/pull/1243))
|
* Dump state file when `tofu test` fails to clean up resources. ([#1243](https://github.com/opentofu/opentofu/pull/1243))
|
||||||
* Added aliases for `state list` (`state ls`), `state mv` (`state move`), and `state rm` (`state remove`) ([#1220](https://github.com/opentofu/opentofu/pull/1220))
|
* Added aliases for `state list` (`state ls`), `state mv` (`state move`), and `state rm` (`state remove`) ([#1220](https://github.com/opentofu/opentofu/pull/1220))
|
||||||
* Added mechanism to introduce automatic retries for provider installations, specifically targeting transient errors ([#1233](https://github.com/opentofu/opentofu/issues/1233))
|
* Added mechanism to introduce automatic retries for provider installations, specifically targeting transient errors ([#1233](https://github.com/opentofu/opentofu/issues/1233))
|
||||||
|
* Added `-json` flag to `tofu init` and `tofu get` to support output in json format. ([#1453](https://github.com/opentofu/opentofu/pull/1453))
|
||||||
|
|
||||||
BUG FIXES:
|
BUG FIXES:
|
||||||
* Fix view hooks unit test flakiness by deterministically waiting for heartbeats to execute ([$1153](https://github.com/opentofu/opentofu/issues/1153))
|
* Fix view hooks unit test flakiness by deterministically waiting for heartbeats to execute ([$1153](https://github.com/opentofu/opentofu/issues/1153))
|
||||||
|
2
go.mod
2
go.mod
@ -43,7 +43,7 @@ require (
|
|||||||
github.com/hashicorp/go-azure-helpers v0.43.0
|
github.com/hashicorp/go-azure-helpers v0.43.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||||
github.com/hashicorp/go-getter v1.7.3
|
github.com/hashicorp/go-getter v1.7.3
|
||||||
github.com/hashicorp/go-hclog v1.5.0
|
github.com/hashicorp/go-hclog v1.6.3
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-plugin v1.4.3
|
github.com/hashicorp/go-plugin v1.4.3
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.4
|
github.com/hashicorp/go-retryablehttp v0.7.4
|
||||||
|
4
go.sum
4
go.sum
@ -661,8 +661,8 @@ github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj
|
|||||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
@ -62,33 +62,67 @@ func TestInitProvidersInternal(t *testing.T) {
|
|||||||
// This test should _not_ reach out anywhere because the "terraform"
|
// This test should _not_ reach out anywhere because the "terraform"
|
||||||
// provider is internal to the core tofu binary.
|
// provider is internal to the core tofu binary.
|
||||||
|
|
||||||
fixturePath := filepath.Join("testdata", "tf-provider")
|
t.Run("output in human readable format", func(t *testing.T) {
|
||||||
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
fixturePath := filepath.Join("testdata", "tf-provider")
|
||||||
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
||||||
|
|
||||||
stdout, stderr, err := tf.Run("init")
|
stdout, stderr, err := tf.Run("init")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %s", err)
|
t.Errorf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stderr != "" {
|
if stderr != "" {
|
||||||
t.Errorf("unexpected stderr output:\n%s", stderr)
|
t.Errorf("unexpected stderr output:\n%s", stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") {
|
if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") {
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
t.Errorf("success message is missing from output:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(stdout, "Installing hashicorp/terraform") {
|
if strings.Contains(stdout, "Installing hashicorp/terraform") {
|
||||||
// Shouldn't have downloaded anything with this config, because the
|
// Shouldn't have downloaded anything with this config, because the
|
||||||
// provider is built in.
|
// provider is built in.
|
||||||
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") {
|
||||||
|
// Shouldn't have downloaded anything with this config, because the
|
||||||
|
// provider is built in.
|
||||||
|
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("output in machine readable format", func(t *testing.T) {
|
||||||
|
fixturePath := filepath.Join("testdata", "tf-provider")
|
||||||
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
||||||
|
|
||||||
|
stdout, stderr, err := tf.Run("init", "-json")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stderr != "" {
|
||||||
|
t.Errorf("unexpected stderr output:\n%s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can not check timestamp, so the sub string is not a valid json object
|
||||||
|
if !strings.Contains(stdout, `{"@level":"info","@message":"OpenTofu has been successfully initialized!","@module":"tofu.ui"`) {
|
||||||
|
t.Errorf("success message is missing from output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(stdout, "Installing hashicorp/terraform") {
|
||||||
|
// Shouldn't have downloaded anything with this config, because the
|
||||||
|
// provider is built in.
|
||||||
|
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") {
|
||||||
|
// Shouldn't have downloaded anything with this config, because the
|
||||||
|
// provider is built in.
|
||||||
|
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if strings.Contains(stdout, "Installing terraform.io/builtin/terraform") {
|
|
||||||
// Shouldn't have downloaded anything with this config, because the
|
|
||||||
// provider is built in.
|
|
||||||
t.Errorf("provider download message appeared in output:\n%s", stdout)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitProvidersVendored(t *testing.T) {
|
func TestInitProvidersVendored(t *testing.T) {
|
||||||
@ -144,42 +178,85 @@ func TestInitProvidersLocalOnly(t *testing.T) {
|
|||||||
// to the host "example.com", which is the placeholder domain we use in
|
// to the host "example.com", which is the placeholder domain we use in
|
||||||
// the test fixture.)
|
// the test fixture.)
|
||||||
|
|
||||||
fixturePath := filepath.Join("testdata", "local-only-provider")
|
t.Run("output in human readable format", func(t *testing.T) {
|
||||||
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
fixturePath := filepath.Join("testdata", "local-only-provider")
|
||||||
// If you run this test on a workstation with a plugin-cache directory
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
||||||
// configured, it will leave a bad directory behind and tofu init will
|
// If you run this test on a workstation with a plugin-cache directory
|
||||||
// not work until you remove it.
|
// configured, it will leave a bad directory behind and tofu init will
|
||||||
//
|
// not work until you remove it.
|
||||||
// To avoid this, we will "zero out" any existing cli config file.
|
//
|
||||||
tf.AddEnv("TF_CLI_CONFIG_FILE=")
|
// To avoid this, we will "zero out" any existing cli config file.
|
||||||
|
tf.AddEnv("TF_CLI_CONFIG_FILE=")
|
||||||
|
|
||||||
// Our fixture dir has a generic os_arch dir, which we need to customize
|
// Our fixture dir has a generic os_arch dir, which we need to customize
|
||||||
// to the actual OS/arch where this test is running in order to get the
|
// to the actual OS/arch where this test is running in order to get the
|
||||||
// desired result.
|
// desired result.
|
||||||
fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch")
|
fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch")
|
||||||
wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
|
wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
|
||||||
err := os.Rename(fixtMachineDir, wantMachineDir)
|
err := os.Rename(fixtMachineDir, wantMachineDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, stderr, err := tf.Run("init")
|
stdout, stderr, err := tf.Run("init")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %s", err)
|
t.Errorf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stderr != "" {
|
if stderr != "" {
|
||||||
t.Errorf("unexpected stderr output:\n%s", stderr)
|
t.Errorf("unexpected stderr output:\n%s", stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") {
|
if !strings.Contains(stdout, "OpenTofu has been successfully initialized!") {
|
||||||
t.Errorf("success message is missing from output:\n%s", stdout)
|
t.Errorf("success message is missing from output:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") {
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("output in machine readable format", func(t *testing.T) {
|
||||||
|
fixturePath := filepath.Join("testdata", "local-only-provider")
|
||||||
|
tf := e2e.NewBinary(t, tofuBin, fixturePath)
|
||||||
|
// If you run this test on a workstation with a plugin-cache directory
|
||||||
|
// configured, it will leave a bad directory behind and tofu init will
|
||||||
|
// not work until you remove it.
|
||||||
|
//
|
||||||
|
// To avoid this, we will "zero out" any existing cli config file.
|
||||||
|
tf.AddEnv("TF_CLI_CONFIG_FILE=")
|
||||||
|
|
||||||
|
// Our fixture dir has a generic os_arch dir, which we need to customize
|
||||||
|
// to the actual OS/arch where this test is running in order to get the
|
||||||
|
// desired result.
|
||||||
|
fixtMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/os_arch")
|
||||||
|
wantMachineDir := tf.Path("terraform.d/plugins/example.com/awesomecorp/happycloud/1.2.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
|
||||||
|
err := os.Rename(fixtMachineDir, wantMachineDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, stderr, err := tf.Run("init", "-json")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stderr != "" {
|
||||||
|
t.Errorf("unexpected stderr output:\n%s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can not check timestamp, so the sub string is not a valid json object
|
||||||
|
if !strings.Contains(stdout, `{"@level":"info","@message":"OpenTofu has been successfully initialized!","@module":"tofu.ui"`) {
|
||||||
|
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"`) {
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if !strings.Contains(stdout, "- Installing example.com/awesomecorp/happycloud v1.2.0") {
|
|
||||||
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)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitProvidersCustomMethod(t *testing.T) {
|
func TestInitProvidersCustomMethod(t *testing.T) {
|
||||||
@ -347,9 +424,21 @@ func TestInitProviderNotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("registry provider not found output in json format", func(t *testing.T) {
|
||||||
|
stdout, _, err := tf.Run("init", "-no-color", "-json")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got success")
|
||||||
|
}
|
||||||
|
|
||||||
|
oneLineStdout := strings.ReplaceAll(stdout, "\n", " ")
|
||||||
|
if !strings.Contains(oneLineStdout, `"diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/nonexist: provider registry registry.opentofu.org does not have a provider named registry.opentofu.org/hashicorp/nonexist\n\nAll modules should specify their required_providers so that external consumers will get the correct providers when using a module. To see which modules are currently depending on hashicorp/nonexist, run the following command:\n tofu providers\n\nIf you believe this provider is missing from the registry, please submit a issue on the OpenTofu Registry https://github.com/opentofu/registry/issues/"},"type":"diagnostic"}`) {
|
||||||
|
t.Errorf("expected error message is missing from output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("local provider not found", func(t *testing.T) {
|
t.Run("local provider not found", func(t *testing.T) {
|
||||||
// The -plugin-dir directory must exist for the provider installer to search it.
|
// The -plugin-dir directory must exist for the provider installer to search it.
|
||||||
pluginDir := tf.Path("empty")
|
pluginDir := tf.Path("empty-for-json")
|
||||||
if err := os.Mkdir(pluginDir, os.ModePerm); err != nil {
|
if err := os.Mkdir(pluginDir, os.ModePerm); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -364,6 +453,23 @@ func TestInitProviderNotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("local provider not found output in json format", func(t *testing.T) {
|
||||||
|
// The -plugin-dir directory must exist for the provider installer to search it.
|
||||||
|
pluginDir := tf.Path("empty")
|
||||||
|
if err := os.Mkdir(pluginDir, os.ModePerm); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, _, err := tf.Run("init", "-no-color", "-plugin-dir="+pluginDir, "-json")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got success")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(stdout, `"diagnostic":{"severity":"error","summary":"Failed to query available provider packages","detail":"Could not retrieve the list of available versions for provider hashicorp/nonexist: provider registry.opentofu.org/hashicorp/nonexist was not found in any of the search locations\n\n - `+pluginDir+`"},"type":"diagnostic"}`) {
|
||||||
|
t.Errorf("expected error message is missing from output:\n%s", stdout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("special characters enabled", func(t *testing.T) {
|
t.Run("special characters enabled", func(t *testing.T) {
|
||||||
_, stderr, err := tf.Run("init")
|
_, stderr, err := tf.Run("init")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/command/views"
|
||||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,11 +28,22 @@ func (c *GetCommand) Run(args []string) int {
|
|||||||
cmdFlags := c.Meta.defaultFlagSet("get")
|
cmdFlags := c.Meta.defaultFlagSet("get")
|
||||||
cmdFlags.BoolVar(&update, "update", false, "update")
|
cmdFlags.BoolVar(&update, "update", false, "update")
|
||||||
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
||||||
|
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
||||||
return 1
|
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),
|
||||||
|
outputInJSON: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialization can be aborted by interruption signals
|
// Initialization can be aborted by interruption signals
|
||||||
ctx, done := c.InterruptibleContext(c.CommandContext())
|
ctx, done := c.InterruptibleContext(c.CommandContext())
|
||||||
@ -81,6 +93,10 @@ Options:
|
|||||||
test command will search for test files in the current directory and
|
test command will search for test files in the current directory and
|
||||||
in the one specified by the flag.
|
in the one specified by the flag.
|
||||||
|
|
||||||
|
-json Produce output in a machine-readable JSON format,
|
||||||
|
suitable for use in text editor integrations and other
|
||||||
|
automated systems. Always disables color.
|
||||||
|
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
backendInit "github.com/opentofu/opentofu/internal/backend/init"
|
backendInit "github.com/opentofu/opentofu/internal/backend/init"
|
||||||
"github.com/opentofu/opentofu/internal/cloud"
|
"github.com/opentofu/opentofu/internal/cloud"
|
||||||
"github.com/opentofu/opentofu/internal/command/arguments"
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
||||||
|
"github.com/opentofu/opentofu/internal/command/views"
|
||||||
"github.com/opentofu/opentofu/internal/configs"
|
"github.com/opentofu/opentofu/internal/configs"
|
||||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||||
"github.com/opentofu/opentofu/internal/encryption"
|
"github.com/opentofu/opentofu/internal/encryption"
|
||||||
@ -67,11 +68,23 @@ func (c *InitCommand) Run(args []string) int {
|
|||||||
cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode")
|
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.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.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
||||||
|
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
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),
|
||||||
|
outputInJSON: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backendFlagSet := arguments.FlagIsSet(cmdFlags, "backend")
|
backendFlagSet := arguments.FlagIsSet(cmdFlags, "backend")
|
||||||
cloudFlagSet := arguments.FlagIsSet(cmdFlags, "cloud")
|
cloudFlagSet := arguments.FlagIsSet(cmdFlags, "cloud")
|
||||||
|
|
||||||
@ -1233,6 +1246,10 @@ Options:
|
|||||||
test command will search for test files in the current directory and
|
test command will search for test files in the current directory and
|
||||||
in the one specified by the flag.
|
in the one specified by the flag.
|
||||||
|
|
||||||
|
-json Produce output in a machine-readable JSON format,
|
||||||
|
suitable for use in text editor integrations and other
|
||||||
|
automated systems. Always disables color.
|
||||||
|
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
plugin "github.com/hashicorp/go-plugin"
|
"github.com/hashicorp/go-plugin"
|
||||||
"github.com/hashicorp/terraform-svchost/disco"
|
"github.com/hashicorp/terraform-svchost/disco"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
@ -266,6 +266,8 @@ type Meta struct {
|
|||||||
// Used with commands which write state to allow users to write remote
|
// Used with commands which write state to allow users to write remote
|
||||||
// state even if the remote and local OpenTofu versions don't match.
|
// state even if the remote and local OpenTofu versions don't match.
|
||||||
ignoreRemoteVersion bool
|
ignoreRemoteVersion bool
|
||||||
|
|
||||||
|
outputInJSON bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type testingOverrides struct {
|
type testingOverrides struct {
|
||||||
@ -693,6 +695,12 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.outputInJSON {
|
||||||
|
jsonView := views.NewJSONView(m.View)
|
||||||
|
jsonView.Diagnostics(diags)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
outputWidth := m.ErrorColumns()
|
outputWidth := m.ErrorColumns()
|
||||||
|
|
||||||
diags = diags.ConsolidateWarnings(1)
|
diags = diags.ConsolidateWarnings(1)
|
||||||
|
60
internal/command/meta_ui.go
Normal file
60
internal/command/meta_ui.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
|
||||||
|
"github.com/opentofu/opentofu/internal/command/views"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WrappedUi is a shim which adds json compatibility to those commands which
|
||||||
|
// have not yet been refactored to support output by views.View.
|
||||||
|
//
|
||||||
|
// For those not support json output command, all output is printed by cli.Ui.
|
||||||
|
// So we create WrappedUi, contains the old cli.Ui and views.JSONView,
|
||||||
|
// implement cli.Ui interface, so that we can make all command support json
|
||||||
|
// output in a short time.
|
||||||
|
type WrappedUi struct {
|
||||||
|
cliUi cli.Ui
|
||||||
|
jsonView *views.JSONView
|
||||||
|
outputInJSON bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) Ask(s string) (string, error) {
|
||||||
|
return m.cliUi.Ask(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) AskSecret(s string) (string, error) {
|
||||||
|
return m.cliUi.AskSecret(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) Output(s string) {
|
||||||
|
if m.outputInJSON {
|
||||||
|
m.jsonView.Output(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cliUi.Output(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) Info(s string) {
|
||||||
|
if m.outputInJSON {
|
||||||
|
m.jsonView.Info(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cliUi.Info(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) Error(s string) {
|
||||||
|
if m.outputInJSON {
|
||||||
|
m.jsonView.Error(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cliUi.Error(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WrappedUi) Warn(s string) {
|
||||||
|
if m.outputInJSON {
|
||||||
|
m.jsonView.Warn(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.cliUi.Warn(s)
|
||||||
|
}
|
@ -23,9 +23,10 @@ const JSON_UI_VERSION = "1.2"
|
|||||||
|
|
||||||
func NewJSONView(view *View) *JSONView {
|
func NewJSONView(view *View) *JSONView {
|
||||||
log := hclog.New(&hclog.LoggerOptions{
|
log := hclog.New(&hclog.LoggerOptions{
|
||||||
Name: "tofu.ui",
|
Name: "tofu.ui",
|
||||||
Output: view.streams.Stdout.File,
|
Output: view.streams.Stdout.File,
|
||||||
JSONFormat: true,
|
JSONFormat: true,
|
||||||
|
JSONEscapeDisabled: true,
|
||||||
})
|
})
|
||||||
jv := &JSONView{
|
jv := &JSONView{
|
||||||
log: log,
|
log: log,
|
||||||
@ -128,3 +129,23 @@ func (v *JSONView) Outputs(outputs json.Outputs) {
|
|||||||
"outputs", outputs,
|
"outputs", outputs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Output is designed for supporting command.WrappedUi
|
||||||
|
func (v *JSONView) Output(message string) {
|
||||||
|
v.log.Info(message, "type", "output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info is designed for supporting command.WrappedUi
|
||||||
|
func (v *JSONView) Info(message string) {
|
||||||
|
v.log.Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn is designed for supporting command.WrappedUi
|
||||||
|
func (v *JSONView) Warn(message string) {
|
||||||
|
v.log.Warn(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error is designed for supporting command.WrappedUi
|
||||||
|
func (v *JSONView) Error(message string) {
|
||||||
|
v.log.Error(message)
|
||||||
|
}
|
||||||
|
@ -44,20 +44,44 @@ func TestNewJSONView(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONView_Log(t *testing.T) {
|
func TestJSONView_Log(t *testing.T) {
|
||||||
streams, done := terminal.StreamsForTesting(t)
|
testCases := []struct {
|
||||||
jv := NewJSONView(NewView(streams))
|
caseName string
|
||||||
|
input string
|
||||||
jv.Log("hello, world")
|
want []map[string]interface{}
|
||||||
|
}{
|
||||||
want := []map[string]interface{}{
|
|
||||||
{
|
{
|
||||||
"@level": "info",
|
"log with regular character",
|
||||||
"@message": "hello, world",
|
"hello, world",
|
||||||
"@module": "tofu.ui",
|
[]map[string]interface{}{
|
||||||
"type": "log",
|
{
|
||||||
|
"@level": "info",
|
||||||
|
"@message": "hello, world",
|
||||||
|
"@module": "tofu.ui",
|
||||||
|
"type": "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"log with special character",
|
||||||
|
"hello, special char, <>&",
|
||||||
|
[]map[string]interface{}{
|
||||||
|
{
|
||||||
|
"@level": "info",
|
||||||
|
"@message": "hello, special char, <>&",
|
||||||
|
"@module": "tofu.ui",
|
||||||
|
"type": "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.caseName, func(t *testing.T) {
|
||||||
|
streams, done := terminal.StreamsForTesting(t)
|
||||||
|
jv := NewJSONView(NewView(streams))
|
||||||
|
jv.Log(tc.input)
|
||||||
|
testJSONViewOutputEquals(t, done(t).Stdout(), tc.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This test covers only the basics of JSON diagnostic rendering, as more
|
// This test covers only the basics of JSON diagnostic rendering, as more
|
||||||
|
Loading…
Reference in New Issue
Block a user