You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	feat(terraformExecute): pass tf outputs to cpe (#3241)
* feat(terraformExecute): pass tf outputs to cpe * cleanup Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
This commit is contained in:
		| @@ -1,11 +1,13 @@ | |||||||
| package cmd | package cmd | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/command" | 	"github.com/SAP/jenkins-library/pkg/command" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/log" | 	"github.com/SAP/jenkins-library/pkg/log" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||||
|  | 	"github.com/SAP/jenkins-library/pkg/terraform" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type terraformExecuteUtils interface { | type terraformExecuteUtils interface { | ||||||
| @@ -30,16 +32,16 @@ func newTerraformExecuteUtils() terraformExecuteUtils { | |||||||
| 	return &utils | 	return &utils | ||||||
| } | } | ||||||
|  |  | ||||||
| func terraformExecute(config terraformExecuteOptions, telemetryData *telemetry.CustomData) { | func terraformExecute(config terraformExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *terraformExecuteCommonPipelineEnvironment) { | ||||||
| 	utils := newTerraformExecuteUtils() | 	utils := newTerraformExecuteUtils() | ||||||
|  |  | ||||||
| 	err := runTerraformExecute(&config, telemetryData, utils) | 	err := runTerraformExecute(&config, telemetryData, utils, commonPipelineEnvironment) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Entry().WithError(err).Fatal("step execution failed") | 		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 { | 	if len(config.CliConfigFile) > 0 { | ||||||
| 		utils.AppendEnv([]string{fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", config.CliConfigFile)}) | 		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 { | 	if err != nil { | ||||||
| 	cliArgs := []string{} | 		return err | ||||||
|  |  | ||||||
| 	if globalOptions != nil { |  | ||||||
| 		cliArgs = append(cliArgs, globalOptions...) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cliArgs = append(cliArgs, command) | 	var outputBuffer bytes.Buffer | ||||||
|  | 	utils.Stdout(&outputBuffer) | ||||||
|  |  | ||||||
| 	if args != nil { | 	err = runTerraform(utils, "output", []string{"-json"}, config.GlobalOptions) | ||||||
| 		cliArgs = append(cliArgs, args...) |  | ||||||
|  | 	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...) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,10 +5,12 @@ package cmd | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/SAP/jenkins-library/pkg/config" | 	"github.com/SAP/jenkins-library/pkg/config" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/log" | 	"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/splunk" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||||
| 	"github.com/SAP/jenkins-library/pkg/validation" | 	"github.com/SAP/jenkins-library/pkg/validation" | ||||||
| @@ -24,6 +26,34 @@ type terraformExecuteOptions struct { | |||||||
| 	CliConfigFile    string   `json:"cliConfigFile,omitempty"` | 	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 | // TerraformExecuteCommand Executes Terraform | ||||||
| func TerraformExecuteCommand() *cobra.Command { | func TerraformExecuteCommand() *cobra.Command { | ||||||
| 	const STEP_NAME = "terraformExecute" | 	const STEP_NAME = "terraformExecute" | ||||||
| @@ -31,6 +61,7 @@ func TerraformExecuteCommand() *cobra.Command { | |||||||
| 	metadata := terraformExecuteMetadata() | 	metadata := terraformExecuteMetadata() | ||||||
| 	var stepConfig terraformExecuteOptions | 	var stepConfig terraformExecuteOptions | ||||||
| 	var startTime time.Time | 	var startTime time.Time | ||||||
|  | 	var commonPipelineEnvironment terraformExecuteCommonPipelineEnvironment | ||||||
| 	var logCollector *log.CollectorHook | 	var logCollector *log.CollectorHook | ||||||
|  |  | ||||||
| 	var createTerraformExecuteCmd = &cobra.Command{ | 	var createTerraformExecuteCmd = &cobra.Command{ | ||||||
| @@ -81,6 +112,7 @@ func TerraformExecuteCommand() *cobra.Command { | |||||||
| 			telemetryData.ErrorCode = "1" | 			telemetryData.ErrorCode = "1" | ||||||
| 			handler := func() { | 			handler := func() { | ||||||
| 				config.RemoveVaultSecretFiles() | 				config.RemoveVaultSecretFiles() | ||||||
|  | 				commonPipelineEnvironment.persist(GeneralConfig.EnvRootPath, "commonPipelineEnvironment") | ||||||
| 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||||
| 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||||
| 				telemetry.Send(&telemetryData) | 				telemetry.Send(&telemetryData) | ||||||
| @@ -98,7 +130,7 @@ func TerraformExecuteCommand() *cobra.Command { | |||||||
| 					GeneralConfig.HookConfig.SplunkConfig.Index, | 					GeneralConfig.HookConfig.SplunkConfig.Index, | ||||||
| 					GeneralConfig.HookConfig.SplunkConfig.SendLogs) | 					GeneralConfig.HookConfig.SplunkConfig.SendLogs) | ||||||
| 			} | 			} | ||||||
| 			terraformExecute(stepConfig, &telemetryData) | 			terraformExecute(stepConfig, &telemetryData, &commonPipelineEnvironment) | ||||||
| 			telemetryData.ErrorCode = "0" | 			telemetryData.ErrorCode = "0" | ||||||
| 			log.Entry().Info("SUCCESS") | 			log.Entry().Info("SUCCESS") | ||||||
| 		}, | 		}, | ||||||
| @@ -208,6 +240,17 @@ func terraformExecuteMetadata() config.StepData { | |||||||
| 			Containers: []config.Container{ | 			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: ""}}}, | 				{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 | 	return theMetaData | ||||||
|   | |||||||
| @@ -94,15 +94,19 @@ func TestRunTerraformExecute(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i, test := range tt { | 	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() | 			t.Parallel() | ||||||
| 			// init | 			// init | ||||||
| 			config := test.terraformExecuteOptions | 			config := test.terraformExecuteOptions | ||||||
| 			utils := newTerraformExecuteTestsUtils() | 			utils := newTerraformExecuteTestsUtils() | ||||||
|  | 			utils.StdoutReturn = map[string]string{} | ||||||
|  | 			utils.StdoutReturn["terraform output -json"] = "{}" | ||||||
|  | 			utils.StdoutReturn["terraform -chgdir=src output -json"] = "{}" | ||||||
|  |  | ||||||
| 			runner := utils.ExecMockRunner | 			runner := utils.ExecMockRunner | ||||||
|  |  | ||||||
| 			// test | 			// test | ||||||
| 			err := runTerraformExecute(&config, nil, utils) | 			err := runTerraformExecute(&config, nil, utils, &terraformExecuteCommonPipelineEnvironment{}) | ||||||
|  |  | ||||||
| 			// assert | 			// assert | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| @@ -117,4 +121,32 @@ func TestRunTerraformExecute(t *testing.T) { | |||||||
| 			assert.Subset(t, runner.Env, test.expectedEnvVars) | 			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"]) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								pkg/terraform/terraform.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/terraform/terraform.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								pkg/terraform/terraform_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								pkg/terraform/terraform_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"]) | ||||||
|  | } | ||||||
| @@ -70,3 +70,10 @@ spec: | |||||||
|       env: |       env: | ||||||
|         - name: TF_IN_AUTOMATION |         - name: TF_IN_AUTOMATION | ||||||
|           value: piper |           value: piper | ||||||
|  |   outputs: | ||||||
|  |     resources: | ||||||
|  |       - name: commonPipelineEnvironment | ||||||
|  |         type: piperEnvironment | ||||||
|  |         params: | ||||||
|  |           - name: custom/terraformOutputs | ||||||
|  |             type: 'map[string]interface{}' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user