diff --git a/cmd/terraformExecute.go b/cmd/terraformExecute.go index 16d262213..381af7bce 100644 --- a/cmd/terraformExecute.go +++ b/cmd/terraformExecute.go @@ -1,11 +1,13 @@ package cmd import ( + "bytes" "fmt" "github.com/SAP/jenkins-library/pkg/command" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/SAP/jenkins-library/pkg/terraform" ) type terraformExecuteUtils interface { @@ -30,16 +32,16 @@ func newTerraformExecuteUtils() terraformExecuteUtils { return &utils } -func terraformExecute(config terraformExecuteOptions, telemetryData *telemetry.CustomData) { +func terraformExecute(config terraformExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *terraformExecuteCommonPipelineEnvironment) { utils := newTerraformExecuteUtils() - err := runTerraformExecute(&config, telemetryData, utils) + err := runTerraformExecute(&config, telemetryData, utils, commonPipelineEnvironment) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } -func runTerraformExecute(config *terraformExecuteOptions, telemetryData *telemetry.CustomData, utils terraformExecuteUtils) error { +func runTerraformExecute(config *terraformExecuteOptions, telemetryData *telemetry.CustomData, utils terraformExecuteUtils, commonPipelineEnvironment *terraformExecuteCommonPipelineEnvironment) error { if len(config.CliConfigFile) > 0 { utils.AppendEnv([]string{fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", config.CliConfigFile)}) } @@ -66,21 +68,38 @@ func runTerraformExecute(config *terraformExecuteOptions, telemetryData *telemet } } - return runTerraform(utils, config.Command, args, config.GlobalOptions) -} + err := runTerraform(utils, config.Command, args, config.GlobalOptions) -func runTerraform(utils terraformExecuteUtils, command string, args []string, globalOptions []string) error { - cliArgs := []string{} - - if globalOptions != nil { - cliArgs = append(cliArgs, globalOptions...) + if err != nil { + return err } - cliArgs = append(cliArgs, command) + var outputBuffer bytes.Buffer + utils.Stdout(&outputBuffer) - if args != nil { - cliArgs = append(cliArgs, args...) + err = runTerraform(utils, "output", []string{"-json"}, config.GlobalOptions) + + if err != nil { + return err } - return utils.RunExecutable("terraform", cliArgs...) + commonPipelineEnvironment.custom.terraformOutputs, err = terraform.ReadOutputs(outputBuffer.String()) + + return err +} + +func runTerraform(utils terraformExecuteUtils, command string, additionalArgs []string, globalOptions []string) error { + args := []string{} + + if len(globalOptions) > 0 { + args = append(args, globalOptions...) + } + + args = append(args, command) + + if len(additionalArgs) > 0 { + args = append(args, additionalArgs...) + } + + return utils.RunExecutable("terraform", args...) } diff --git a/cmd/terraformExecute_generated.go b/cmd/terraformExecute_generated.go index b749e4d37..5ee0fc37e 100644 --- a/cmd/terraformExecute_generated.go +++ b/cmd/terraformExecute_generated.go @@ -5,10 +5,12 @@ package cmd import ( "fmt" "os" + "path/filepath" "time" "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/piperenv" "github.com/SAP/jenkins-library/pkg/splunk" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/SAP/jenkins-library/pkg/validation" @@ -24,6 +26,34 @@ type terraformExecuteOptions struct { CliConfigFile string `json:"cliConfigFile,omitempty"` } +type terraformExecuteCommonPipelineEnvironment struct { + custom struct { + terraformOutputs map[string]interface{} + } +} + +func (p *terraformExecuteCommonPipelineEnvironment) persist(path, resourceName string) { + content := []struct { + category string + name string + value interface{} + }{ + {category: "custom", name: "terraformOutputs", value: p.custom.terraformOutputs}, + } + + errCount := 0 + for _, param := range content { + err := piperenv.SetResourceParameter(path, resourceName, filepath.Join(param.category, param.name), param.value) + if err != nil { + log.Entry().WithError(err).Error("Error persisting piper environment.") + errCount++ + } + } + if errCount > 0 { + log.Entry().Fatal("failed to persist Piper environment") + } +} + // TerraformExecuteCommand Executes Terraform func TerraformExecuteCommand() *cobra.Command { const STEP_NAME = "terraformExecute" @@ -31,6 +61,7 @@ func TerraformExecuteCommand() *cobra.Command { metadata := terraformExecuteMetadata() var stepConfig terraformExecuteOptions var startTime time.Time + var commonPipelineEnvironment terraformExecuteCommonPipelineEnvironment var logCollector *log.CollectorHook var createTerraformExecuteCmd = &cobra.Command{ @@ -81,6 +112,7 @@ func TerraformExecuteCommand() *cobra.Command { telemetryData.ErrorCode = "1" handler := func() { config.RemoveVaultSecretFiles() + commonPipelineEnvironment.persist(GeneralConfig.EnvRootPath, "commonPipelineEnvironment") telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) telemetryData.ErrorCategory = log.GetErrorCategory().String() telemetry.Send(&telemetryData) @@ -98,7 +130,7 @@ func TerraformExecuteCommand() *cobra.Command { GeneralConfig.HookConfig.SplunkConfig.Index, GeneralConfig.HookConfig.SplunkConfig.SendLogs) } - terraformExecute(stepConfig, &telemetryData) + terraformExecute(stepConfig, &telemetryData, &commonPipelineEnvironment) telemetryData.ErrorCode = "0" log.Entry().Info("SUCCESS") }, @@ -208,6 +240,17 @@ func terraformExecuteMetadata() config.StepData { Containers: []config.Container{ {Name: "terraform", Image: "hashicorp/terraform:0.14.7", EnvVars: []config.EnvVar{{Name: "TF_IN_AUTOMATION", Value: "piper"}}, Options: []config.Option{{Name: "--entrypoint", Value: ""}}}, }, + Outputs: config.StepOutputs{ + Resources: []config.StepResources{ + { + Name: "commonPipelineEnvironment", + Type: "piperEnvironment", + Parameters: []map[string]interface{}{ + {"Name": "custom/terraformOutputs"}, + }, + }, + }, + }, }, } return theMetaData diff --git a/cmd/terraformExecute_test.go b/cmd/terraformExecute_test.go index 102d0b086..e753640c2 100644 --- a/cmd/terraformExecute_test.go +++ b/cmd/terraformExecute_test.go @@ -94,15 +94,19 @@ func TestRunTerraformExecute(t *testing.T) { } for i, test := range tt { - t.Run(fmt.Sprintf("That arguemtns are correct %d", i), func(t *testing.T) { + t.Run(fmt.Sprintf("That arguments are correct %d", i), func(t *testing.T) { t.Parallel() // init config := test.terraformExecuteOptions utils := newTerraformExecuteTestsUtils() + utils.StdoutReturn = map[string]string{} + utils.StdoutReturn["terraform output -json"] = "{}" + utils.StdoutReturn["terraform -chgdir=src output -json"] = "{}" + runner := utils.ExecMockRunner // test - err := runTerraformExecute(&config, nil, utils) + err := runTerraformExecute(&config, nil, utils, &terraformExecuteCommonPipelineEnvironment{}) // assert assert.NoError(t, err) @@ -117,4 +121,32 @@ func TestRunTerraformExecute(t *testing.T) { assert.Subset(t, runner.Env, test.expectedEnvVars) }) } + + t.Run("Outputs get injected into CPE", func(t *testing.T) { + t.Parallel() + + cpe := terraformExecuteCommonPipelineEnvironment{} + + config := terraformExecuteOptions{ + Command: "plan", + } + utils := newTerraformExecuteTestsUtils() + utils.StdoutReturn = map[string]string{} + utils.StdoutReturn["terraform output -json"] = `{ + "sample_var": { + "sensitive": true, + "value": "a secret value", + "type": "string" + } +} + ` + + // test + err := runTerraformExecute(&config, nil, utils, &cpe) + + // assert + assert.NoError(t, err) + assert.Equal(t, 1, len(cpe.custom.terraformOutputs)) + assert.Equal(t, "a secret value", cpe.custom.terraformOutputs["sample_var"]) + }) } diff --git a/pkg/terraform/terraform.go b/pkg/terraform/terraform.go new file mode 100644 index 000000000..ad90f8f94 --- /dev/null +++ b/pkg/terraform/terraform.go @@ -0,0 +1,28 @@ +package terraform + +import ( + "encoding/json" +) + +type TerraformOutput struct { + Sensitive bool `json:"sensitive"` + ObjType interface{} `json:"type"` + Value interface{} `json:"value"` +} + +func ReadOutputs(tfOutputJson string) (map[string]interface{}, error) { + var objmap map[string]TerraformOutput + err := json.Unmarshal([]byte(tfOutputJson), &objmap) + + if err != nil { + return nil, err + } + + retmap := make(map[string]interface{}) + + for tfoutvarname, tfoutvar := range objmap { + retmap[tfoutvarname] = tfoutvar.Value + } + + return retmap, nil +} diff --git a/pkg/terraform/terraform_test.go b/pkg/terraform/terraform_test.go new file mode 100644 index 000000000..10e5bf028 --- /dev/null +++ b/pkg/terraform/terraform_test.go @@ -0,0 +1,117 @@ +package terraform + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestReadOutputs(t *testing.T) { + terraformOutputsJson := `{ + "boolean": { + "sensitive": false, + "type": "bool", + "value": true + }, + "list_any": { + "sensitive": false, + "type": [ + "tuple", + [ + "bool", + "string", + "number", + [ + "tuple", + [] + ] + ] + ], + "value": [ + true, + "2", + 3, + [] + ] + }, + "list_numbers": { + "sensitive": false, + "type": [ + "tuple", + [ + "number", + "number", + "number" + ] + ], + "value": [ + 1, + 2, + 3 + ] + }, + "list_string": { + "sensitive": false, + "type": [ + "tuple", + [ + "string", + "string", + "string" + ] + ], + "value": [ + "1", + "2", + "3" + ] + }, + "map": { + "sensitive": false, + "type": [ + "object", + { + "ATTR1": "string", + "ATTR2": [ + "object", + { + "ATTR3": [ + "tuple", + [] + ] + } + ] + } + ], + "value": { + "ATTR1": "", + "ATTR2": { + "ATTR3": [] + } + } + }, + "secret": { + "sensitive": true, + "type": "string", + "value": "this-could-be-a-password" + }, + "string": { + "sensitive": false, + "type": "string", + "value": "string" + }, + "number": { + "sensitive": false, + "type": "number", + "value": 1 + } +} +` + outputs, err := ReadOutputs(terraformOutputsJson) + assert.NoError(t, err) + + assert.Equal(t, 8, len(outputs)) + + assert.Equal(t, true, outputs["boolean"]) + assert.Equal(t, "string", outputs["string"]) + assert.Equal(t, float64(1), outputs["number"]) +} diff --git a/resources/metadata/terraformExecute.yaml b/resources/metadata/terraformExecute.yaml index f3b61b6d3..4612d9431 100644 --- a/resources/metadata/terraformExecute.yaml +++ b/resources/metadata/terraformExecute.yaml @@ -70,3 +70,10 @@ spec: env: - name: TF_IN_AUTOMATION value: piper + outputs: + resources: + - name: commonPipelineEnvironment + type: piperEnvironment + params: + - name: custom/terraformOutputs + type: 'map[string]interface{}'