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 | ||||
|  | ||||
| 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...) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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"]) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										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: | ||||
|         - name: TF_IN_AUTOMATION | ||||
|           value: piper | ||||
|   outputs: | ||||
|     resources: | ||||
|       - name: commonPipelineEnvironment | ||||
|         type: piperEnvironment | ||||
|         params: | ||||
|           - name: custom/terraformOutputs | ||||
|             type: 'map[string]interface{}' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user