1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-10-30 23:57:50 +02:00

Cleanup outdated blue green support for cf native build tools (#4965)

* Remove blue green deployment support for cf native build tools

* Empty for testing

* Remove obsolete dependency

* feedback from code review

* Fix IT's run

* Add test
This commit is contained in:
Srinikitha Kondreddy
2024-07-04 11:13:36 +02:00
committed by GitHub
parent e2f1c13b75
commit 4a4c13ff03
6 changed files with 94 additions and 769 deletions

View File

@@ -2,14 +2,11 @@ package cmd
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
@@ -19,7 +16,6 @@ import (
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/yaml"
"github.com/elliotchance/orderedmap"
"github.com/pkg/errors"
)
@@ -57,10 +53,6 @@ func cfLogout(c command.ExecRunner) error {
return cf.Logout()
}
const defaultSmokeTestScript = `#!/usr/bin/env bash
# this is simply testing if the application root returns HTTP STATUS_CODE
curl -so /dev/null -w '%{response_code}' https://$1 | grep $STATUS_CODE`
func cloudFoundryDeploy(config cloudFoundryDeployOptions, telemetryData *telemetry.CustomData, influxData *cloudFoundryDeployInflux) {
// for command execution use Command
c := command.Command{}
@@ -226,46 +218,33 @@ func handleMTADeployment(config *cloudFoundryDeployOptions, command command.Exec
}
type deployConfig struct {
DeployCommand string
DeployOptions []string
AppName string
ManifestFile string
SmokeTestScript []string
DeployCommand string
DeployOptions []string
AppName string
ManifestFile string
}
func handleCFNativeDeployment(config *cloudFoundryDeployOptions, command command.ExecRunner) error {
deployType, err := checkAndUpdateDeployTypeForNotSupportedManifest(config)
if err != nil {
return err
}
var deployCommand string
var smokeTestScript []string
var deployOptions []string
var err error
// deploy command will be provided by the prepare functions below
if deployType == "blue-green" {
log.Entry().Warn("[WARN] Blue-green deployment type is deprecated for cf native builds " +
"and will be completely removed by 15.06.2024" +
if config.DeployType == "blue-green" {
return fmt.Errorf("Blue-green deployment type is deprecated for cf native builds." +
"Instead set parameter `cfNativeDeployParameters: '--strategy rolling'`. " +
"Please refer to the Cloud Foundry documentation for further information: " +
"https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html." +
"Or alternatively, switch to mta build tool. Please refer to mta build tool" +
"documentation for further information: https://sap.github.io/cloud-mta-build-tool/configuration/.")
deployCommand, deployOptions, smokeTestScript, err = prepareBlueGreenCfNativeDeploy(config)
} else if config.DeployType == "standard" {
deployCommand, deployOptions, err = prepareCfPushCfNativeDeploy(config)
if err != nil {
return errors.Wrapf(err, "Cannot prepare cf native deployment. DeployType '%s'", deployType)
}
} else if deployType == "standard" {
deployCommand, deployOptions, smokeTestScript, err = prepareCfPushCfNativeDeploy(config)
if err != nil {
return errors.Wrapf(err, "Cannot prepare cf push native deployment. DeployType '%s'", deployType)
return errors.Wrapf(err, "Cannot prepare cf push native deployment. DeployType '%s'", config.DeployType)
}
} else {
return fmt.Errorf("Invalid deploy type received: '%s'. Supported values: %v", deployType, []string{"blue-green", "standard"})
return fmt.Errorf("Invalid deploy type received: '%s'. Supported value: standard", config.DeployType)
}
appName, err := getAppName(config)
@@ -281,22 +260,18 @@ func handleCFNativeDeployment(config *cloudFoundryDeployOptions, command command
log.Entry().Infof("cfManifestVariables: '%v'", config.ManifestVariables)
log.Entry().Infof("cfManifestVariablesFiles: '%v'", config.ManifestVariablesFiles)
log.Entry().Infof("cfdeployDockerImage: '%s'", config.DeployDockerImage)
log.Entry().Infof("smokeTestScript: '%s'", config.SmokeTestScript)
additionalEnvironment := []string{
"STATUS_CODE=" + strconv.FormatInt(int64(config.SmokeTestStatusCode), 10),
}
var additionalEnvironment []string
if len(config.DockerPassword) > 0 {
additionalEnvironment = append(additionalEnvironment, "CF_DOCKER_PASSWORD="+config.DockerPassword)
additionalEnvironment = []string{("CF_DOCKER_PASSWORD=" + config.DockerPassword)}
}
myDeployConfig := deployConfig{
DeployCommand: deployCommand,
DeployOptions: deployOptions,
AppName: config.AppName,
ManifestFile: config.Manifest,
SmokeTestScript: smokeTestScript,
DeployCommand: deployCommand,
DeployOptions: deployOptions,
AppName: config.AppName,
ManifestFile: config.Manifest,
}
log.Entry().Infof("DeployConfig: %v", myDeployConfig)
@@ -323,55 +298,19 @@ func deployCfNative(deployConfig deployConfig, config *cloudFoundryDeployOptions
deployStatement = append(deployStatement, deployConfig.ManifestFile)
}
if len(config.DeployDockerImage) > 0 && config.DeployType != "blue-green" {
if len(config.DeployDockerImage) > 0 {
deployStatement = append(deployStatement, "--docker-image", config.DeployDockerImage)
}
if len(config.DockerUsername) > 0 && config.DeployType != "blue-green" {
if len(config.DockerUsername) > 0 {
deployStatement = append(deployStatement, "--docker-username", config.DockerUsername)
}
if len(deployConfig.SmokeTestScript) > 0 {
deployStatement = append(deployStatement, deployConfig.SmokeTestScript...)
}
if len(config.CfNativeDeployParameters) > 0 {
deployStatement = append(deployStatement, strings.Fields(config.CfNativeDeployParameters)...)
}
stopOldAppIfRunning := func(_cmd command.ExecRunner) error {
if config.KeepOldInstance && config.DeployType == "blue-green" {
oldAppName := deployConfig.AppName + "-old"
var buff bytes.Buffer
_cmd.Stdout(&buff)
defer func() {
_cmd.Stdout(log.Writer())
}()
err := _cmd.RunExecutable("cf", "stop", oldAppName)
if err != nil {
cfStopLog := buff.String()
if !strings.Contains(cfStopLog, oldAppName+" not found") {
return fmt.Errorf("Could not stop application '%s'. Error: %s", oldAppName, cfStopLog)
}
log.Entry().Infof("Cannot stop application '%s' since this appliation was not found.", oldAppName)
} else {
log.Entry().Infof("Old application '%s' has been stopped.", oldAppName)
}
}
return nil
}
return cfDeploy(config, deployStatement, additionalEnvironment, stopOldAppIfRunning, cmd)
return cfDeploy(config, deployStatement, additionalEnvironment, cmd)
}
func getManifest(name string) (cloudfoundry.Manifest, error) {
@@ -392,9 +331,7 @@ func getAppName(config *cloudFoundryDeployOptions) (string, error) {
if len(config.AppName) > 0 {
return config.AppName, nil
}
if config.DeployType == "blue-green" {
return "", fmt.Errorf("Blue-green plugin requires app name to be passed (see https://github.com/bluemixgaragelondon/cf-blue-green-deploy/issues/27)")
}
manifestFile, err := getManifestFileName(config)
fileExists, err := fileUtils.FileExists(manifestFile)
@@ -438,153 +375,12 @@ func getAppName(config *cloudFoundryDeployOptions) (string, error) {
return name, nil
}
func handleSmokeTestScript(smokeTestScript string) ([]string, error) {
if smokeTestScript == "blueGreenCheckScript.sh" {
// what should we do if there is already a script with the given name? Should we really overwrite ...
err := fileUtils.FileWrite(smokeTestScript, []byte(defaultSmokeTestScript), 0755)
if err != nil {
return []string{}, fmt.Errorf("failed to write default smoke-test script: %w", err)
}
log.Entry().Debugf("smoke test script '%s' has been written.", smokeTestScript)
}
if len(smokeTestScript) > 0 {
err := fileUtils.Chmod(smokeTestScript, 0755)
if err != nil {
return []string{}, fmt.Errorf("failed to make smoke-test script executable: %w", err)
}
pwd, err := fileUtils.Getwd()
if err != nil {
return []string{}, fmt.Errorf("failed to get current working directory for execution of smoke-test script: %w", err)
}
return []string{"--smoke-test", filepath.Join(pwd, smokeTestScript)}, nil
}
return []string{}, nil
}
func prepareBlueGreenCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
smokeTest, err := handleSmokeTestScript(config.SmokeTestScript)
if err != nil {
return "", []string{}, []string{}, err
}
var deployOptions = []string{}
if !config.KeepOldInstance {
deployOptions = append(deployOptions, "--delete-old-apps")
}
manifestFile, err := getManifestFileName(config)
manifestFileExists, err := fileUtils.FileExists(manifestFile)
if err != nil {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", manifestFile)
}
if !manifestFileExists {
log.Entry().Infof("Manifest file '%s' does not exist", manifestFile)
} else {
manifestVariables, err := toStringInterfaceMap(toParameterMap(config.ManifestVariables))
if err != nil {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare manifest variables: '%v'", config.ManifestVariables)
}
manifestVariablesFiles, err := validateManifestVariablesFiles(config.ManifestVariablesFiles)
if err != nil {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot validate manifest variables files '%v'", config.ManifestVariablesFiles)
}
modified, err := _replaceVariables(manifestFile, manifestVariables, manifestVariablesFiles)
if err != nil {
return "", []string{}, []string{}, errors.Wrap(err, "Cannot prepare manifest file")
}
if modified {
log.Entry().Infof("Manifest file '%s' has been updated (variable substitution)", manifestFile)
} else {
log.Entry().Infof("Manifest file '%s' has not been updated (no variable substitution)", manifestFile)
}
err = handleLegacyCfManifest(manifestFile)
if err != nil {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot handle legacy manifest '%s'", manifestFile)
}
}
return "blue-green-deploy", deployOptions, smokeTest, nil
}
// validateManifestVariablesFiles: in case the only provided file is 'manifest-variables.yml' and this file does not
// exist we ignore that file. For any other file there is no check if that file exists. In case several files are
// provided we also do not check for the default file 'manifest-variables.yml'
func validateManifestVariablesFiles(manifestVariablesFiles []string) ([]string, error) {
const defaultManifestVariableFileName = "manifest-variables.yml"
if len(manifestVariablesFiles) == 1 && manifestVariablesFiles[0] == defaultManifestVariableFileName {
// we have only the default file. Most likely this is not configured, but we simply have the default.
// In case this file does not exist we ignore that file.
exists, err := fileUtils.FileExists(defaultManifestVariableFileName)
if err != nil {
return []string{}, errors.Wrapf(err, "Cannot check if file '%s' exists", defaultManifestVariableFileName)
}
if !exists {
return []string{}, nil
}
}
return manifestVariablesFiles, nil
}
func toParameterMap(parameters []string) (*orderedmap.OrderedMap, error) {
parameterMap := orderedmap.NewOrderedMap()
for _, p := range parameters {
keyVal := strings.Split(p, "=")
if len(keyVal) != 2 {
return nil, fmt.Errorf("Invalid parameter provided (expected format <key>=<val>: '%s'", p)
}
parameterMap.Set(keyVal[0], keyVal[1])
}
return parameterMap, nil
}
func handleLegacyCfManifest(manifestFile string) error {
manifest, err := _getManifest(manifestFile)
if err != nil {
return err
}
err = manifest.Transform()
if err != nil {
return err
}
if manifest.IsModified() {
err = manifest.WriteManifest()
if err != nil {
return err
}
log.Entry().Infof("Manifest file '%s' was in legacy format has been transformed and updated.", manifestFile)
} else {
log.Entry().Debugf("Manifest file '%s' was not in legacy format. No transformation needed, no update performed.", manifestFile)
}
return nil
}
func prepareCfPushCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, []string, error) {
func prepareCfPushCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []string, error) {
deployOptions := []string{}
varOptions, err := _getVarsOptions(config.ManifestVariables)
if err != nil {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-options: '%v'", config.ManifestVariables)
return "", []string{}, errors.Wrapf(err, "Cannot prepare var-options: '%v'", config.ManifestVariables)
}
varFileOptions, err := _getVarsFileOptions(config.ManifestVariablesFiles)
@@ -594,73 +390,14 @@ func prepareCfPushCfNativeDeploy(config *cloudFoundryDeployOptions) (string, []s
log.Entry().Warningf("We skip adding not-existing file '%s' as a vars-file to the cf create-service-push call", missingVarFile)
}
} else {
return "", []string{}, []string{}, errors.Wrapf(err, "Cannot prepare var-file-options: '%v'", config.ManifestVariablesFiles)
return "", []string{}, errors.Wrapf(err, "Cannot prepare var-file-options: '%v'", config.ManifestVariablesFiles)
}
}
deployOptions = append(deployOptions, varOptions...)
deployOptions = append(deployOptions, varFileOptions...)
return "push", deployOptions, []string{}, nil
}
func toStringInterfaceMap(in *orderedmap.OrderedMap, err error) (map[string]interface{}, error) {
out := map[string]interface{}{}
if err == nil {
for _, key := range in.Keys() {
if k, ok := key.(string); ok {
val, exists := in.Get(key)
if exists {
out[k] = val
} else {
return nil, fmt.Errorf("No entry found for '%v'", key)
}
} else {
return nil, fmt.Errorf("Cannot cast key '%v' to string", key)
}
}
}
return out, err
}
func checkAndUpdateDeployTypeForNotSupportedManifest(config *cloudFoundryDeployOptions) (string, error) {
manifestFile, err := getManifestFileName(config)
manifestFileExists, err := fileUtils.FileExists(manifestFile)
if err != nil {
return "", err
}
if config.DeployType == "blue-green" && manifestFileExists {
manifest, _ := _getManifest(manifestFile)
apps, err := manifest.GetApplications()
if err != nil {
return "", fmt.Errorf("failed to obtain applications from manifest: %w", err)
}
if len(apps) > 1 {
return "", fmt.Errorf("Your manifest contains more than one application. For blue green deployments your manifest file may contain only one application")
}
hasNoRouteProperty, err := manifest.ApplicationHasProperty(0, "no-route")
if err != nil {
return "", errors.Wrap(err, "Failed to obtain 'no-route' property from manifest")
}
if len(apps) == 1 && hasNoRouteProperty {
const deployTypeStandard = "standard"
log.Entry().Warningf("Blue green deployment is not possible for application without route. Using deployment type '%s' instead.", deployTypeStandard)
return deployTypeStandard, nil
}
}
return config.DeployType, nil
return "push", deployOptions, nil
}
func deployMta(config *cloudFoundryDeployOptions, mtarFilePath string, command command.ExecRunner) error {
@@ -706,7 +443,7 @@ func deployMta(config *cloudFoundryDeployOptions, mtarFilePath string, command c
cfDeployParams = append(cfDeployParams, extFileParams...)
err := cfDeploy(config, cfDeployParams, nil, nil, command)
err := cfDeploy(config, cfDeployParams, nil, command)
for _, extFile := range extFiles {
renameError := fileUtils.FileRename(extFile+".original", extFile)
@@ -833,7 +570,6 @@ func cfDeploy(
config *cloudFoundryDeployOptions,
cfDeployParams []string,
additionalEnvironment []string,
postDeployAction func(command command.ExecRunner) error,
command command.ExecRunner) error {
const cfLogFile = "cf.log"
@@ -883,10 +619,6 @@ func cfDeploy(
}
}
if err == nil && postDeployAction != nil {
err = postDeployAction(command)
}
if loginPerformed {
logoutErr := _cfLogout(command)

View File

@@ -42,8 +42,6 @@ type cloudFoundryDeployOptions struct {
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"`
}
@@ -214,10 +212,10 @@ func addCloudFoundryDeployFlags(cmd *cobra.Command, stepConfig *cloudFoundryDepl
cmd.Flags().StringVar(&stepConfig.DeployDockerImage, "deployDockerImage", os.Getenv("PIPER_deployDockerImage"), "Docker image deployments are supported (via manifest file in general)[https://docs.cloudfoundry.org/devguide/deploy-apps/manifest-attributes.html#docker]. If no manifest is used, this parameter defines the image to be deployed. The specified name of the image is passed to the `--docker-image` parameter of the cf CLI and must adhere it's naming pattern (e.g. REPO/IMAGE:TAG). See (cf CLI documentation)[https://docs.cloudfoundry.org/devguide/deploy-apps/push-docker.html] for details. Note: The used Docker registry must be visible for the targeted Cloud Foundry instance.")
cmd.Flags().StringVar(&stepConfig.DeployTool, "deployTool", os.Getenv("PIPER_deployTool"), "Defines the tool which should be used for deployment.")
cmd.Flags().StringVar(&stepConfig.BuildTool, "buildTool", os.Getenv("PIPER_buildTool"), "Defines the tool which is used for building the artifact. If provided, `deployTool` is automatically derived from it. For MTA projects, `deployTool` defaults to `mtaDeployPlugin`. For other projects `cf_native` will be used.")
cmd.Flags().StringVar(&stepConfig.DeployType, "deployType", `standard`, "Defines the type of deployment, either `standard` deployment which results in a system downtime or a zero-downtime `blue-green` deployment. If 'cf_native' as deployTool and 'blue-green' as deployType is used in combination, your manifest.yaml may only contain one application. If this application has the option 'no-route' active the deployType will be changed to 'standard'.")
cmd.Flags().StringVar(&stepConfig.DeployType, "deployType", `standard`, "Defines the type of deployment, for example, `standard` deployment which results in a system downtime, `blue-green` deployment which results in zero downtime for mta deploy tool. - For mta build tool, possible values are `standard`, `blue-green` or `bg-deploy`. - For cf native build tools, possible value is `standard`. To eliminate system downtime, an alternative is to pass '--strategy rolling' to the parameter `cfNativeDeployParameters`.")
cmd.Flags().StringVar(&stepConfig.DockerPassword, "dockerPassword", os.Getenv("PIPER_dockerPassword"), "If the specified image in `deployDockerImage` is contained in a Docker registry, which requires authorization, this defines the password to be used.")
cmd.Flags().StringVar(&stepConfig.DockerUsername, "dockerUsername", os.Getenv("PIPER_dockerUsername"), "If the specified image in `deployDockerImage` is contained in a Docker registry, which requires authorization, this defines the username to be used.")
cmd.Flags().BoolVar(&stepConfig.KeepOldInstance, "keepOldInstance", false, "In case of a `blue-green` deployment the old instance will be deleted by default. If this option is set to true the old instance will remain stopped in the Cloud Foundry space.")
cmd.Flags().BoolVar(&stepConfig.KeepOldInstance, "keepOldInstance", false, "If this option is set to true the old instance will remain stopped in the Cloud Foundry space.\"")
cmd.Flags().StringVar(&stepConfig.LoginParameters, "loginParameters", os.Getenv("PIPER_loginParameters"), "Addition command line options for cf login command. No escaping/quoting is performed. Not recommended for productive environments.")
cmd.Flags().StringVar(&stepConfig.Manifest, "manifest", os.Getenv("PIPER_manifest"), "Defines the manifest to be used for deployment to Cloud Foundry.")
cmd.Flags().StringSliceVar(&stepConfig.ManifestVariables, "manifestVariables", []string{}, "Defines a list of variables in the form `key=value` which are used for variable substitution within the file given by manifest.")
@@ -228,8 +226,6 @@ func addCloudFoundryDeployFlags(cmd *cobra.Command, stepConfig *cloudFoundryDepl
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")
cmd.Flags().StringVar(&stepConfig.SmokeTestScript, "smokeTestScript", `blueGreenCheckScript.sh`, "Allows to specify a script which performs a check during blue-green deployment. The script gets the FQDN as parameter and returns `exit code 0` in case check returned `smokeTestStatusCode`. More details can be found [here](https://github.com/bluemixgaragelondon/cf-blue-green-deploy#how-to-use). Currently this option is only considered for deployTool `cf_native`.")
cmd.Flags().IntVar(&stepConfig.SmokeTestStatusCode, "smokeTestStatusCode", 200, "Expected status code returned by the check.")
cmd.Flags().StringVar(&stepConfig.Space, "space", os.Getenv("PIPER_space"), "Cloud Foundry target space")
cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User name used for deployment")
@@ -514,24 +510,6 @@ func cloudFoundryDeployMetadata() config.StepData {
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_password"),
},
{
Name: "smokeTestScript",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `blueGreenCheckScript.sh`,
},
{
Name: "smokeTestStatusCode",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"},
Type: "int",
Mandatory: false,
Aliases: []config.Alias{},
Default: 200,
},
{
Name: "space",
ResourceRef: []config.ResourceReference{},

View File

@@ -6,7 +6,6 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
@@ -77,8 +76,7 @@ func TestCfDeployment(t *testing.T) {
Username: "me",
Password: "******",
APIEndpoint: "https://examples.sap.com/cf",
SmokeTestStatusCode: 200, // default
Manifest: "manifest.yml", //default
Manifest: "manifest.yml", // default
MtaDeployParameters: "-f", // default
DeployType: "standard", // default
}
@@ -167,73 +165,6 @@ func TestCfDeployment(t *testing.T) {
assert.EqualError(t, err, "Your application name 'a_z' contains a '_' (underscore) which is not allowed, only letters, dashes and numbers can be used. Please change the name to fit this requirement(s). For more details please visit https://docs.cloudfoundry.org/devguide/deploy-apps/deploy-app.html#basic-settings.")
})
t.Run("Manifest substitution", func(t *testing.T) {
defer func() {
cleanup()
_replaceVariables = func(manifest string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) {
return false, nil
}
}()
s := mock.ExecMockRunner{}
var manifestForSubstitution string
var replacements map[string]interface{}
var replacementFiles []string
defer prepareDefaultManifestMocking("substitute-manifest.yml", []string{"testAppName"})()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.AppName = "myApp"
config.Manifest = "substitute-manifest.yml"
_replaceVariables = func(manifest string, _replacements map[string]interface{}, _replacementsFiles []string) (bool, error) {
manifestForSubstitution = manifest
replacements = _replacements
replacementFiles = _replacementsFiles
return false, nil
}
t.Run("straight forward", func(t *testing.T) {
defer func() {
config.ManifestVariables = []string{}
config.ManifestVariablesFiles = []string{}
}()
config.ManifestVariables = []string{"k1=v1"}
config.ManifestVariablesFiles = []string{"myVars.yml"}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.NoError(t, err) {
assert.Equal(t, "substitute-manifest.yml", manifestForSubstitution)
assert.Equal(t, map[string]interface{}{"k1": "v1"}, replacements)
assert.Equal(t, []string{"myVars.yml"}, replacementFiles)
}
})
t.Run("empty", func(t *testing.T) {
defer func() {
config.ManifestVariables = []string{}
config.ManifestVariablesFiles = []string{}
}()
config.ManifestVariables = []string{}
config.ManifestVariablesFiles = []string{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.NoError(t, err) {
assert.Equal(t, "substitute-manifest.yml", manifestForSubstitution)
assert.Equal(t, map[string]interface{}{}, replacements)
assert.Equal(t, []string{}, replacementFiles)
}
})
})
t.Run("Invalid deploytool", func(t *testing.T) {
defer cleanup()
@@ -279,7 +210,6 @@ func TestCfDeployment(t *testing.T) {
t.Run("check environment variables", func(t *testing.T) {
assert.Contains(t, s.Env, "CF_HOME=/home/me1")
assert.Contains(t, s.Env, "CF_PLUGIN_HOME=/home/me2")
assert.Contains(t, s.Env, "STATUS_CODE=200")
})
}
})
@@ -404,55 +334,7 @@ func TestCfDeployment(t *testing.T) {
})
t.Run("check environment variables", func(t *testing.T) {
//REVISIT: in the corresponding groovy test we checked for "${'********'}"
// I don't understand why, but we should discuss ...
assert.Contains(t, s.Env, "CF_DOCKER_PASSWORD=********")
})
}
})
t.Run("deploy cf native blue green with manifest and docker credentials", func(t *testing.T) {
defer cleanup()
// Blue Green Deploy cf cli plugin does not support --docker-username and --docker-image parameters
// docker username and docker image have to be set in the manifest file
// if a private docker repository is used the CF_DOCKER_PASSWORD env variable must be set
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.DockerUsername = "test_cf_docker"
config.DockerPassword = "********"
config.AppName = "testAppName"
defer prepareDefaultManifestMocking("manifest.yml", []string{"testAppName"})()
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.NoError(t, err) {
t.Run("check shell calls", func(t *testing.T) {
withLoginAndLogout(t, func(t *testing.T) {
assert.Equal(t, []mock.ExecCall{
{Exec: "cf", Params: []string{"version"}},
{Exec: "cf", Params: []string{"plugins"}},
{Exec: "cf", Params: []string{
"blue-green-deploy",
"testAppName",
"--delete-old-apps",
"-f",
"manifest.yml",
}},
}, s.Calls)
})
})
t.Run("check environment variables", func(t *testing.T) {
//REVISIT: in the corresponding groovy test we checked for "${'********'}"
// REVISIT: in the corresponding groovy test we checked for "${'********'}"
// I don't understand why, but we should discuss ...
assert.Contains(t, s.Env, "CF_DOCKER_PASSWORD=********")
})
@@ -503,8 +385,8 @@ func TestCfDeployment(t *testing.T) {
config.Manifest = ""
config.AppName = ""
//app name does not need to be set if it can be found in the manifest.yml
//manifest name does not need to be set- the default manifest.yml will be used if not set
// app name does not need to be set if it can be found in the manifest.yml
// manifest name does not need to be set- the default manifest.yml will be used if not set
defer prepareDefaultManifestMocking("manifest.yml", []string{"newAppName"})()
s := mock.ExecMockRunner{}
@@ -530,6 +412,61 @@ func TestCfDeployment(t *testing.T) {
}
})
t.Run("cf native deploy fail when deployType is blue-green", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.Manifest = ""
config.AppName = ""
// app name does not need to be set if it can be found in the manifest.yml
// manifest name does not need to be set- the default manifest.yml will be used if not set
defer prepareDefaultManifestMocking("manifest.yml", []string{"newAppName"})()
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.EqualError(t, err, "Blue-green deployment type is deprecated for cf native builds."+
"Instead set parameter `cfNativeDeployParameters: '--strategy rolling'`. "+
"Please refer to the Cloud Foundry documentation for further information: "+
"https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html."+
"Or alternatively, switch to mta build tool. Please refer to mta build tool"+
"documentation for further information: https://sap.github.io/cloud-mta-build-tool/configuration/.") {
t.Run("check shell calls", func(t *testing.T) {
noopCfAPICalls(t, s)
})
}
})
t.Run("cf native deploy fail when unknown deployType is set", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue"
config.Manifest = ""
config.AppName = ""
// app name does not need to be set if it can be found in the manifest.yml
// manifest name does not need to be set- the default manifest.yml will be used if not set
defer prepareDefaultManifestMocking("manifest.yml", []string{"newAppName"})()
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.EqualError(t, err, "Invalid deploy type received: 'blue'. Supported value: standard") {
t.Run("check shell calls", func(t *testing.T) {
noopCfAPICalls(t, s)
})
}
})
t.Run("deploy cf native without app name", func(t *testing.T) {
defer cleanup()
@@ -553,131 +490,12 @@ func TestCfDeployment(t *testing.T) {
}
})
// tests from groovy checking for keep old instances are already contained above. Search for '--delete-old-apps'
t.Run("deploy cf native blue green keep old instance", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.Manifest = "test-manifest.yml"
config.AppName = "myTestApp"
config.KeepOldInstance = true
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.NoError(t, err) {
t.Run("check shell calls", func(t *testing.T) {
withLoginAndLogout(t, func(t *testing.T) {
assert.Equal(t, []mock.ExecCall{
{Exec: "cf", Params: []string{"version"}},
{Exec: "cf", Params: []string{"plugins"}},
{Exec: "cf", Params: []string{
"blue-green-deploy",
"myTestApp",
"-f",
"test-manifest.yml",
}},
{Exec: "cf", Params: []string{
"stop",
"myTestApp-old",
// MIGRATE FFROM GROOVY: in contrast to groovy there is not redirect of everything &> to a file since we
// read the stream directly now.
}},
}, s.Calls)
})
})
}
})
t.Run("cf deploy blue green multiple applications", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.Manifest = "test-manifest.yml"
config.AppName = "myTestApp"
defer prepareDefaultManifestMocking("test-manifest.yml", []string{"app1", "app2"})()
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.EqualError(t, err, "Your manifest contains more than one application. For blue green deployments your manifest file may contain only one application") {
t.Run("check shell calls", func(t *testing.T) {
noopCfAPICalls(t, s)
})
}
})
t.Run("cf native deploy blue green with no route", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.Manifest = "test-manifest.yml"
config.AppName = "myTestApp"
defer func() {
_ = filesMock.FileRemove("test-manifest.yml")
_getManifest = getManifest
}()
filesMock.AddFile("test-manifest.yml", []byte("Content does not matter"))
_getManifest = func(name string) (cloudfoundry.Manifest, error) {
return manifestMock{
manifestFileName: "test-manifest.yml",
apps: []map[string]interface{}{
{
"name": "app1",
"no-route": true,
},
},
},
nil
}
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.NoError(t, err) {
t.Run("check shell calls", func(t *testing.T) {
withLoginAndLogout(t, func(t *testing.T) {
assert.Equal(t, []mock.ExecCall{
{Exec: "cf", Params: []string{"version"}},
{Exec: "cf", Params: []string{"plugins"}},
{Exec: "cf", Params: []string{
"push",
"myTestApp",
"-f",
"test-manifest.yml",
}},
}, s.Calls)
})
})
}
})
t.Run("cf native deployment failure", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.DeployType = "standard"
config.Manifest = "test-manifest.yml"
config.AppName = "myTestApp"
@@ -685,7 +503,7 @@ func TestCfDeployment(t *testing.T) {
s := mock.ExecMockRunner{}
s.ShouldFailOnCommand = map[string]error{"cf.*deploy.*": fmt.Errorf("cf deploy failed")}
s.ShouldFailOnCommand = map[string]error{"cf.*push.*": fmt.Errorf("cf deploy failed")}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.EqualError(t, err, "cf deploy failed") {
@@ -702,7 +520,7 @@ func TestCfDeployment(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.DeployType = "standard"
config.Manifest = "test-manifest.yml"
config.AppName = "myTestApp"
@@ -739,8 +557,6 @@ func TestCfDeployment(t *testing.T) {
}
})
// TODO testCfNativeBlueGreenKeepOldInstanceShouldThrowErrorOnStopError
t.Run("cf native deploy standard should not stop instance", func(t *testing.T) {
defer cleanup()
@@ -783,45 +599,6 @@ func TestCfDeployment(t *testing.T) {
}
})
t.Run("testCfNativeWithoutAppNameBlueGreen", func(t *testing.T) {
defer cleanup()
config.DeployTool = "cf_native"
config.DeployType = "blue-green"
config.Manifest = "test-manifest.yml"
defer func() {
_ = filesMock.FileRemove("test-manifest.yml")
_getManifest = getManifest
}()
filesMock.AddFile("test-manifest.yml", []byte("The content does not matter"))
_getManifest = func(name string) (cloudfoundry.Manifest, error) {
return manifestMock{
manifestFileName: "test-manifest.yml",
apps: []map[string]interface{}{
{
"there-is": "no-app-name",
},
},
},
nil
}
s := mock.ExecMockRunner{}
err := runCloudFoundryDeploy(&config, nil, nil, &s)
if assert.EqualError(t, err, "Blue-green plugin requires app name to be passed (see https://github.com/bluemixgaragelondon/cf-blue-green-deploy/issues/27)") {
t.Run("check shell calls", func(t *testing.T) {
noopCfAPICalls(t, s)
})
}
})
// TODO add test for testCfNativeFailureInShellCall
t.Run("deploytool mtaDeployPlugin blue green", func(t *testing.T) {
@@ -1019,10 +796,6 @@ func TestCfDeployment(t *testing.T) {
// TODO: testCfPushDeploymentWithoutVariableSubstitution is already handled above (?)
// TODO: testCfBlueGreenDeploymentWithVariableSubstitution variable substitution is not handled at the moment (pr pending).
// but anyway we should not test the full cycle here, but only that the variables substitution tool is called in the appropriate way.
// variable substitution should be tested at the variables substitution tool itself (yaml util)
t.Run("deploytool mtaDeployPlugin", func(t *testing.T) {
defer cleanup()
@@ -1144,135 +917,6 @@ func TestMtarLookup(t *testing.T) {
})
}
func TestSmokeTestScriptHandling(t *testing.T) {
filesMock := mock.FilesMock{}
filesMock.AddDir("/home/me")
err := filesMock.Chdir("/home/me")
assert.NoError(t, err)
filesMock.AddFileWithMode("mySmokeTestScript.sh", []byte("Content does not matter"), 0644)
fileUtils = &filesMock
var canExec os.FileMode = 0755
t.Run("non default existing smoke test file", func(t *testing.T) {
parts, err := handleSmokeTestScript("mySmokeTestScript.sh")
if assert.NoError(t, err) {
// when the none-default file name is provided the file must already exist
// in the project sources.
assert.False(t, filesMock.HasWrittenFile("mySmokeTestScript.sh"))
info, e := filesMock.Stat("mySmokeTestScript.sh")
if assert.NoError(t, e) {
assert.Equal(t, canExec, info.Mode())
}
assert.Equal(t, []string{
"--smoke-test",
filepath.FromSlash("/home/me/mySmokeTestScript.sh"),
}, parts)
}
})
t.Run("non default not existing smoke test file", func(t *testing.T) {
parts, err := handleSmokeTestScript("notExistingSmokeTestScript.sh")
if assert.EqualError(t, err, "failed to make smoke-test script executable: chmod: notExistingSmokeTestScript.sh: No such file or directory") {
assert.False(t, filesMock.HasWrittenFile("notExistingSmokeTestScript.sh"))
assert.Equal(t, []string{}, parts)
}
})
t.Run("default smoke test file", func(t *testing.T) {
parts, err := handleSmokeTestScript("blueGreenCheckScript.sh")
if assert.NoError(t, err) {
info, e := filesMock.Stat("blueGreenCheckScript.sh")
if assert.NoError(t, e) {
assert.Equal(t, canExec, info.Mode())
}
// in this case we provide the file. We overwrite in case there is already such a file ...
assert.True(t, filesMock.HasWrittenFile("blueGreenCheckScript.sh"))
content, e := filesMock.FileRead("blueGreenCheckScript.sh")
if assert.NoError(t, e) {
assert.Equal(t, "#!/usr/bin/env bash\n# this is simply testing if the application root returns HTTP STATUS_CODE\ncurl -so /dev/null -w '%{response_code}' https://$1 | grep $STATUS_CODE", string(content))
}
assert.Equal(t, []string{
"--smoke-test",
filepath.FromSlash("/home/me/blueGreenCheckScript.sh"),
}, parts)
}
})
}
func TestDefaultManifestVariableFilesHandling(t *testing.T) {
filesMock := mock.FilesMock{}
filesMock.AddDir("/home/me")
err := filesMock.Chdir("/home/me")
assert.NoError(t, err)
fileUtils = &filesMock
t.Run("default manifest variable file is the only one and exists", func(t *testing.T) {
defer func() {
_ = filesMock.FileRemove("manifest-variables.yml")
}()
filesMock.AddFile("manifest-variables.yml", []byte("Content does not matter"))
manifestFiles, err := validateManifestVariablesFiles(
[]string{
"manifest-variables.yml",
},
)
if assert.NoError(t, err) {
assert.Equal(t,
[]string{
"manifest-variables.yml",
}, manifestFiles)
}
})
t.Run("default manifest variable file is the only one and does not exist", func(t *testing.T) {
manifestFiles, err := validateManifestVariablesFiles(
[]string{
"manifest-variables.yml",
},
)
if assert.NoError(t, err) {
assert.Equal(t, []string{}, manifestFiles)
}
})
t.Run("default manifest variable file among others remains if it does not exist", func(t *testing.T) {
// in this case we might fail later.
manifestFiles, err := validateManifestVariablesFiles(
[]string{
"manifest-variables.yml",
"a-second-file.yml",
},
)
if assert.NoError(t, err) {
// the order in which the files are returned is significant.
assert.Equal(t, []string{
"manifest-variables.yml",
"a-second-file.yml",
}, manifestFiles)
}
})
}
func TestExtensionDescriptorsWithMinusE(t *testing.T) {
t.Run("ExtensionDescriptorsWithMinusE", func(t *testing.T) {

1
go.mod
View File

@@ -22,7 +22,6 @@ require (
github.com/buildpacks/lifecycle v0.18.4
github.com/cloudevents/sdk-go/v2 v2.10.1
github.com/docker/cli v24.0.6+incompatible
github.com/elliotchance/orderedmap v1.4.0
github.com/evanphx/json-patch v5.7.0+incompatible
github.com/getsentry/sentry-go v0.26.0
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32

2
go.sum
View File

@@ -279,8 +279,6 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A=
github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=

View File

@@ -136,10 +136,10 @@ spec:
- name: deployType
type: string
description:
"Defines the type of deployment, either `standard` deployment which results in a system
downtime or a zero-downtime `blue-green` deployment. If 'cf_native' as deployTool and 'blue-green'
as deployType is used in combination, your manifest.yaml may only contain one application.
If this application has the option 'no-route' active the deployType will be changed to 'standard'."
"Defines the type of deployment, for example, `standard` deployment which results in a system
downtime, `blue-green` deployment which results in zero downtime for mta deploy tool.
- For mta build tool, possible values are `standard`, `blue-green` or `bg-deploy`.
- For cf native build tools, possible value is `standard`. To eliminate system downtime, an alternative is to pass '--strategy rolling' to the parameter `cfNativeDeployParameters`."
scope:
- PARAMETERS
- STAGES
@@ -180,7 +180,6 @@ spec:
- name: keepOldInstance
type: bool
description:
"In case of a `blue-green` deployment the old instance will be deleted by default.
If this option is set to true the old instance will remain stopped in the Cloud Foundry space."
scope:
- PARAMETERS
@@ -339,31 +338,6 @@ spec:
- type: vaultSecret
default: cloudfoundry-$(org)-$(space)
name: cloudfoundryVaultSecretName
- name: smokeTestScript
type: string
description:
"Allows to specify a script which performs a check during blue-green deployment.
The script gets the FQDN as parameter and returns `exit code 0` in case check returned
`smokeTestStatusCode`.
More details can be found [here](https://github.com/bluemixgaragelondon/cf-blue-green-deploy#how-to-use).
Currently this option is only considered for deployTool `cf_native`."
scope:
- PARAMETERS
- STAGES
- STEPS
- GENERAL
mandatory: false
default: "blueGreenCheckScript.sh"
- name: smokeTestStatusCode
type: int
description: "Expected status code returned by the check."
scope:
- PARAMETERS
- STAGES
- STEPS
- GENERAL
mandatory: false
default: 200
- name: space
type: string
description: "Cloud Foundry target space"