1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-09-16 09:26:22 +02:00

Mta extension credentials handling (#2430)

Mta extension credentials handling

Co-authored-by: Stephan Aßmus <stephan.assmus@sap.com>
This commit is contained in:
Marcus Holl
2021-01-12 09:39:04 +01:00
committed by GitHub
parent 93330d5ed2
commit 65d22eb42a
4 changed files with 266 additions and 33 deletions

View File

@@ -15,17 +15,23 @@ import (
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
"unicode"
)
type cfFileUtil interface {
FileExists(string) (bool, error)
FileRename(string, string) error
FileRead(string) ([]byte, error)
FileWrite(path string, content []byte, perm os.FileMode) error
Getwd() (string, error)
Glob(string) ([]string, error)
Chmod(string, os.FileMode) error
Copy(string, string) (int64, error)
Stat(path string) (os.FileInfo, error)
}
var _now = time.Now
@@ -35,6 +41,7 @@ var _getManifest = getManifest
var _replaceVariables = yaml.Substitute
var _getVarsOptions = cloudfoundry.GetVarsOptions
var _getVarsFileOptions = cloudfoundry.GetVarsFileOptions
var _environ = os.Environ
var fileUtils cfFileUtil = piperutils.Files{}
// for simplify mocking. Maybe we find a more elegant way (mock for CFUtils)
@@ -668,21 +675,141 @@ func deployMta(config *cloudFoundryDeployOptions, mtarFilePath string, command c
cfDeployParams = append(cfDeployParams, deployParams...)
}
cfDeployParams = append(cfDeployParams, handleMtaExtensionDescriptors(config.MtaExtensionDescriptor)...)
extFileParams, extFiles := handleMtaExtensionDescriptors(config.MtaExtensionDescriptor)
return cfDeploy(config, cfDeployParams, nil, nil, command)
for _, extFile := range extFiles {
_, err := fileUtils.Copy(extFile, extFile+".original")
if err != nil {
return fmt.Errorf("Cannot prepare mta extension files: %w", err)
}
err = handleMtaExtensionCredentials(extFile, config.MtaExtensionCredentials)
if err != nil {
return fmt.Errorf("Cannot handle credentials inside mta extension files: %w", err)
}
}
cfDeployParams = append(cfDeployParams, extFileParams...)
err := cfDeploy(config, cfDeployParams, nil, nil, command)
for _, extFile := range extFiles {
renameError := fileUtils.FileRename(extFile+".original", extFile)
if err == nil && renameError != nil {
return renameError
}
}
return err
}
func handleMtaExtensionDescriptors(mtaExtensionDescriptor string) []string {
func handleMtaExtensionCredentials(extFile string, credentials map[string]interface{}) error {
log.Entry().Debugf("Inserting credentials into extension file '%s'", extFile)
b, err := fileUtils.FileRead(extFile)
if err != nil {
return errors.Wrapf(err, "Cannot handle credentials for mta extension file '%s'", extFile)
}
content := string(b)
env, err := toMap(_environ(), "=")
if err != nil {
errors.Wrap(err, "Cannot handle mta extension credentials.")
}
updated := false
missingCredentials := []string{}
for name, credentialKey := range credentials {
credKey, ok := credentialKey.(string)
if !ok {
return fmt.Errorf("Cannot handle mta extension credentials: Cannot cast '%v' (type %T) to string", credentialKey, credentialKey)
}
pattern := "<%= " + name + " %>"
if strings.Contains(content, pattern) {
cred := env[toEnvVarKey(credKey)]
if len(cred) == 0 {
missingCredentials = append(missingCredentials, credKey)
continue
}
content = strings.Replace(content, pattern, cred, -1)
updated = true
log.Entry().Debugf("Mta extension credentials handling: Placeholder '%s' has been replaced by credential denoted by '%s'/'%s' in file '%s'", name, credKey, toEnvVarKey(credKey), extFile)
}
}
if len(missingCredentials) > 0 {
missinCredsEnvVarKeyCompatible := []string{}
for _, missingKey := range missingCredentials {
missinCredsEnvVarKeyCompatible = append(missinCredsEnvVarKeyCompatible, toEnvVarKey(missingKey))
}
// ensure stable order of the entries. Needed e.g. for the tests.
sort.Strings(missingCredentials)
sort.Strings(missinCredsEnvVarKeyCompatible)
return fmt.Errorf("Cannot handle mta extension credentials: No credentials found for '%s'/'%s'. Are these credentials maintained?", missingCredentials, missinCredsEnvVarKeyCompatible)
}
if !updated {
log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has not been updated. Seems to contain no credentials.", extFile)
} else {
fInfo, err := fileUtils.Stat(extFile)
fMode := fInfo.Mode()
if err != nil {
errors.Wrap(err, "Cannot handle mta extension credentials.")
}
err = fileUtils.FileWrite(extFile, []byte(content), fMode)
if err != nil {
return errors.Wrap(err, "Cannot handle mta extension credentials.")
}
log.Entry().Debugf("Mta extension credentials handling: Extension file '%s' has been updated.", extFile)
}
re := regexp.MustCompile(`<%= .* %>`)
placeholders := re.FindAll([]byte(content), -1)
if len(placeholders) > 0 {
log.Entry().Warningf("mta extension credential handling: Unresolved placeholders found after inserting credentials: %s", placeholders)
}
return nil
}
func toEnvVarKey(key string) string {
key = regexp.MustCompile(`[^A-Za-z0-9]`).ReplaceAllString(key, "_")
// from here on we have only ascii
modifiedKey := ""
last := '_'
for _, runeVal := range key {
if unicode.IsUpper(runeVal) && last != '_' {
modifiedKey += "_"
}
modifiedKey += string(unicode.ToUpper(runeVal))
last = runeVal
}
return modifiedKey
// since golang regex does not support negative lookbehinds we have to code it ourselvs
}
func toMap(keyValue []string, separator string) (map[string]string, error) {
result := map[string]string{}
for _, entry := range keyValue {
kv := strings.Split(entry, separator)
if len(kv) < 2 {
return map[string]string{}, fmt.Errorf("Cannot convert to map: separator '%s' not found in entry '%s'", separator, entry)
}
result[kv[0]] = strings.Join(kv[1:], separator)
}
return result, nil
}
func handleMtaExtensionDescriptors(mtaExtensionDescriptor string) ([]string, []string) {
var result = []string{}
var extFiles = []string{}
for _, part := range strings.Fields(strings.Trim(mtaExtensionDescriptor, " ")) {
if part == "-e" || part == "" {
continue
}
// REVISIT: maybe check if the extension descriptor exists
result = append(result, "-e", part)
extFiles = append(extFiles, part)
}
return result
return result, extFiles
}
func cfDeploy(

View File

@@ -16,32 +16,33 @@ import (
)
type cloudFoundryDeployOptions struct {
APIEndpoint string `json:"apiEndpoint,omitempty"`
AppName string `json:"appName,omitempty"`
ArtifactVersion string `json:"artifactVersion,omitempty"`
CfHome string `json:"cfHome,omitempty"`
CfNativeDeployParameters string `json:"cfNativeDeployParameters,omitempty"`
CfPluginHome string `json:"cfPluginHome,omitempty"`
DeployDockerImage string `json:"deployDockerImage,omitempty"`
DeployTool string `json:"deployTool,omitempty"`
BuildTool string `json:"buildTool,omitempty"`
DeployType string `json:"deployType,omitempty"`
DockerPassword string `json:"dockerPassword,omitempty"`
DockerUsername string `json:"dockerUsername,omitempty"`
KeepOldInstance bool `json:"keepOldInstance,omitempty"`
LoginParameters string `json:"loginParameters,omitempty"`
Manifest string `json:"manifest,omitempty"`
ManifestVariables []string `json:"manifestVariables,omitempty"`
ManifestVariablesFiles []string `json:"manifestVariablesFiles,omitempty"`
MtaDeployParameters string `json:"mtaDeployParameters,omitempty"`
MtaExtensionDescriptor string `json:"mtaExtensionDescriptor,omitempty"`
MtaPath string `json:"mtaPath,omitempty"`
Org string `json:"org,omitempty"`
Password string `json:"password,omitempty"`
SmokeTestScript string `json:"smokeTestScript,omitempty"`
SmokeTestStatusCode int `json:"smokeTestStatusCode,omitempty"`
Space string `json:"space,omitempty"`
Username string `json:"username,omitempty"`
APIEndpoint string `json:"apiEndpoint,omitempty"`
AppName string `json:"appName,omitempty"`
ArtifactVersion string `json:"artifactVersion,omitempty"`
CfHome string `json:"cfHome,omitempty"`
CfNativeDeployParameters string `json:"cfNativeDeployParameters,omitempty"`
CfPluginHome string `json:"cfPluginHome,omitempty"`
DeployDockerImage string `json:"deployDockerImage,omitempty"`
DeployTool string `json:"deployTool,omitempty"`
BuildTool string `json:"buildTool,omitempty"`
DeployType string `json:"deployType,omitempty"`
DockerPassword string `json:"dockerPassword,omitempty"`
DockerUsername string `json:"dockerUsername,omitempty"`
KeepOldInstance bool `json:"keepOldInstance,omitempty"`
LoginParameters string `json:"loginParameters,omitempty"`
Manifest string `json:"manifest,omitempty"`
ManifestVariables []string `json:"manifestVariables,omitempty"`
ManifestVariablesFiles []string `json:"manifestVariablesFiles,omitempty"`
MtaDeployParameters string `json:"mtaDeployParameters,omitempty"`
MtaExtensionDescriptor string `json:"mtaExtensionDescriptor,omitempty"`
MtaExtensionCredentials map[string]interface{} `json:"mtaExtensionCredentials,omitempty"`
MtaPath string `json:"mtaPath,omitempty"`
Org string `json:"org,omitempty"`
Password string `json:"password,omitempty"`
SmokeTestScript string `json:"smokeTestScript,omitempty"`
SmokeTestStatusCode int `json:"smokeTestStatusCode,omitempty"`
Space string `json:"space,omitempty"`
Username string `json:"username,omitempty"`
}
type cloudFoundryDeployInflux struct {
@@ -175,6 +176,7 @@ func addCloudFoundryDeployFlags(cmd *cobra.Command, stepConfig *cloudFoundryDepl
cmd.Flags().StringSliceVar(&stepConfig.ManifestVariablesFiles, "manifestVariablesFiles", []string{`manifest-variables.yml`}, "path(s) of the Yaml file(s) containing the variable values to use as a replacement in the manifest file. The order of the files is relevant in case there are conflicting variable names and values within variable files. In such a case, the values of the last file win.")
cmd.Flags().StringVar(&stepConfig.MtaDeployParameters, "mtaDeployParameters", `-f`, "Additional parameters passed to mta deployment command")
cmd.Flags().StringVar(&stepConfig.MtaExtensionDescriptor, "mtaExtensionDescriptor", os.Getenv("PIPER_mtaExtensionDescriptor"), "Defines additional extension descriptor file for deployment with the mtaDeployPlugin")
cmd.Flags().StringVar(&stepConfig.MtaPath, "mtaPath", os.Getenv("PIPER_mtaPath"), "Defines the path to *.mtar for deployment with the mtaDeployPlugin")
cmd.Flags().StringVar(&stepConfig.Org, "org", os.Getenv("PIPER_org"), "Cloud Foundry target organization.")
cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password")
@@ -375,6 +377,14 @@ func cloudFoundryDeployMetadata() config.StepData {
Mandatory: false,
Aliases: []config.Alias{{Name: "cloudFoundry/mtaExtensionDescriptor"}},
},
{
Name: "mtaExtensionCredentials",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"},
Type: "map[string]interface{}",
Mandatory: false,
Aliases: []config.Alias{{Name: "cloudFoundry/mtaExtensionCredentials"}},
},
{
Name: "mtaPath",
ResourceRef: []config.ResourceReference{

View File

@@ -1216,7 +1216,7 @@ func TestDefaultManifestVariableFilesHandling(t *testing.T) {
func TestExtensionDescriptorsWithMinusE(t *testing.T) {
t.Run("ExtensionDescriptorsWithMinusE", func(t *testing.T) {
extDesc := handleMtaExtensionDescriptors("-e 1.yaml -e 2.yaml")
extDesc, _ := handleMtaExtensionDescriptors("-e 1.yaml -e 2.yaml")
assert.Equal(t, []string{
"-e",
"1.yaml",
@@ -1226,7 +1226,7 @@ func TestExtensionDescriptorsWithMinusE(t *testing.T) {
})
t.Run("ExtensionDescriptorsFirstOneWithoutMinusE", func(t *testing.T) {
extDesc := handleMtaExtensionDescriptors("1.yaml -e 2.yaml")
extDesc, _ := handleMtaExtensionDescriptors("1.yaml -e 2.yaml")
assert.Equal(t, []string{
"-e",
"1.yaml",
@@ -1236,7 +1236,7 @@ func TestExtensionDescriptorsWithMinusE(t *testing.T) {
})
t.Run("NoExtensionDescriptors", func(t *testing.T) {
extDesc := handleMtaExtensionDescriptors("")
extDesc, _ := handleMtaExtensionDescriptors("")
assert.Equal(t, []string{}, extDesc)
})
}
@@ -1277,3 +1277,88 @@ func TestAppNameChecks(t *testing.T) {
})
}
func TestMtaExtensionCredentials(t *testing.T) {
content := []byte(`'_schema-version: '3.1'
ID: test.ext
extends: test
parameters
test-credentials1: "<%= testCred1 %>"
test-credentials2: "<%= testCred2 %>"`)
filesMock := mock.FilesMock{}
filesMock.AddDir("/home/me")
filesMock.Chdir("/home/me")
filesMock.AddFile("mtaext1.mtaext", content)
filesMock.AddFile("mtaext2.mtaext", content)
filesMock.AddFile("mtaext3.mtaext", content)
fileUtils = &filesMock
_environ = func() []string {
return []string{
"MY_CRED_ENV_VAR1=******",
"MY_CRED_ENV_VAR2=++++++",
}
}
defer func() {
fileUtils = piperutils.Files{}
_environ = os.Environ
}()
t.Run("extension file does not exist", func(t *testing.T) {
err := handleMtaExtensionCredentials("mtaextDoesNotExist.mtaext", map[string]interface{}{})
assert.EqualError(t, err, "Cannot handle credentials for mta extension file 'mtaextDoesNotExist.mtaext': could not read 'mtaextDoesNotExist.mtaext'")
})
t.Run("credential cannot be retrieved", func(t *testing.T) {
err := handleMtaExtensionCredentials(
"mtaext1.mtaext",
map[string]interface{}{
"testCred1": "myCredEnvVar1NotDefined",
"testCred2": "myCredEnvVar2NotDefined",
},
)
assert.EqualError(t, err, "Cannot handle mta extension credentials: No credentials found for '[myCredEnvVar1NotDefined myCredEnvVar2NotDefined]'/'[MY_CRED_ENV_VAR1_NOT_DEFINED MY_CRED_ENV_VAR2_NOT_DEFINED]'. Are these credentials maintained?")
})
t.Run("irrelevant credentials does not cause failures", func(t *testing.T) {
err := handleMtaExtensionCredentials(
"mtaext2.mtaext",
map[string]interface{}{
"testCred1": "myCredEnvVar1",
"testCred2": "myCredEnvVar2",
"testCredNotUsed": "myCredEnvVarWhichDoesNotExist", //<-- This here is not used.
},
)
assert.NoError(t, err)
})
t.Run("replace straight forward", func(t *testing.T) {
mtaFileName := "mtaext3.mtaext"
err := handleMtaExtensionCredentials(
mtaFileName,
map[string]interface{}{
"testCred1": "myCredEnvVar1",
"testCred2": "myCredEnvVar2",
},
)
if assert.NoError(t, err) {
b, e := fileUtils.FileRead(mtaFileName)
if e != nil {
assert.Fail(t, "Cannot read mta extension file: %v", e)
}
content := string(b)
assert.Contains(t, content, "test-credentials1: \"******\"")
assert.Contains(t, content, "test-credentials2: \"++++++\"")
}
})
}
func TestEnvVarKeyModification(t *testing.T) {
envVarCompatibleKey := toEnvVarKey("Mta.ExtensionCredential~Credential_Id1")
assert.Equal(t, "MTA_EXTENSION_CREDENTIAL_CREDENTIAL_ID1", envVarCompatibleKey)
}

View File

@@ -259,6 +259,17 @@ spec:
mandatory: false
aliases:
- name: cloudFoundry/mtaExtensionDescriptor
- name: mtaExtensionCredentials
type: "map[string]interface{}"
description: "Defines a map of credentials that need to be replaced in the `mtaExtensionDescriptor`. This map needs to be created as `value-to-be-replaced`:`id-of-a-credential-in-jenkins`"
scope:
- PARAMETERS
- STAGES
- STEPS
- GENERAL
mandatory: false
aliases:
- name: cloudFoundry/mtaExtensionCredentials
- name: mtaPath
type: string
description: "Defines the path to *.mtar for deployment with the mtaDeployPlugin"