1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-05 13:25:19 +02:00

Add kubernetesDeploy step (#1073)

* Add kubernetesDeploy step

Co-authored-by: Sven Merk <33895725+nevskrem@users.noreply.github.com>
This commit is contained in:
Oliver Nocon 2020-01-24 14:30:27 +01:00 committed by GitHub
parent 7ead134d68
commit 73ab887f25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1369 additions and 28 deletions

View File

@ -9,7 +9,7 @@ RUN go test ./... -cover
# execute build
RUN export GIT_COMMIT=$(git rev-parse HEAD) && \
export GIT_REPOSITORY=$(git config --get remote.origin.url) && \
go build \
CGO_ENABLED=0 go build \
-ldflags \
"-X github.com/SAP/jenkins-library/cmd.GitCommit=${GIT_COMMIT} \
-X github.com/SAP/jenkins-library/pkg/log.LibraryRepository=${GIT_REPOSITORY}" \

View File

@ -98,6 +98,11 @@ func generateConfig() error {
return errors.Wrap(err, "getting step config failed")
}
// apply context conditions if context configuration is requested
if configOptions.contextConfig {
applyContextConditions(metadata, &stepConfig)
}
myConfigJSON, _ := config.GetJSON(stepConfig.Config)
fmt.Println(myConfigJSON)
@ -129,3 +134,33 @@ func defaultsAndFilters(metadata *config.StepData, stepName string) ([]io.ReadCl
//ToDo: retrieve default values from metadata
return nil, metadata.GetParameterFilters(), nil
}
func applyContextConditions(metadata config.StepData, stepConfig *config.StepConfig) {
//consider conditions for context configuration
//containers
applyContainerConditions(metadata.Spec.Containers, stepConfig)
//sidecars
applyContainerConditions(metadata.Spec.Sidecars, stepConfig)
//ToDo: remove all unnecessary sub maps?
// e.g. extract delete() from applyContainerConditions - loop over all stepConfig.Config[param.Value] and remove ...
}
func applyContainerConditions(containers []config.Container, stepConfig *config.StepConfig) {
for _, container := range containers {
if len(container.Conditions) > 0 {
for _, param := range container.Conditions[0].Params {
if container.Conditions[0].ConditionRef == "strings-equal" && stepConfig.Config[param.Name] == param.Value {
var containerConf map[string]interface{}
containerConf = stepConfig.Config[param.Value].(map[string]interface{})
for key, value := range containerConf {
stepConfig.Config[key] = value
}
delete(stepConfig.Config, param.Value)
}
}
}
}
}

View File

@ -1,6 +1,7 @@
package cmd
import (
"fmt"
"io"
"io/ioutil"
"strings"
@ -86,5 +87,93 @@ func TestDefaultsAndFilters(t *testing.T) {
assert.Equal(t, 1, len(filters.All), "wrong number of filter values")
assert.NoError(t, err, "error occured but none expected")
})
}
func TestApplyContextConditions(t *testing.T) {
tt := []struct {
metadata config.StepData
conf config.StepConfig
expected map[string]interface{}
}{
{
metadata: config.StepData{Spec: config.StepSpec{Containers: []config.Container{}}},
conf: config.StepConfig{Config: map[string]interface{}{}},
expected: map[string]interface{}{},
},
{
metadata: config.StepData{Spec: config.StepSpec{Containers: []config.Container{
{
Image: "myTestImage:latest",
Conditions: []config.Condition{
{
ConditionRef: "strings-equal",
Params: []config.Param{
{Name: "param1", Value: "val2"},
},
},
},
},
}}},
conf: config.StepConfig{Config: map[string]interface{}{
"param1": "val1",
"val1": map[string]interface{}{"dockerImage": "myTestImage:latest"},
}},
expected: map[string]interface{}{
"param1": "val1",
"val1": map[string]interface{}{"dockerImage": "myTestImage:latest"},
},
},
{
metadata: config.StepData{Spec: config.StepSpec{Containers: []config.Container{
{
Image: "myTestImage:latest",
Conditions: []config.Condition{
{
ConditionRef: "strings-equal",
Params: []config.Param{
{Name: "param1", Value: "val1"},
},
},
},
},
}}},
conf: config.StepConfig{Config: map[string]interface{}{
"param1": "val1",
"val1": map[string]interface{}{"dockerImage": "myTestImage:latest"},
}},
expected: map[string]interface{}{
"param1": "val1",
"dockerImage": "myTestImage:latest",
},
},
{
metadata: config.StepData{Spec: config.StepSpec{Sidecars: []config.Container{
{
Image: "myTestImage:latest",
Conditions: []config.Condition{
{
ConditionRef: "strings-equal",
Params: []config.Param{
{Name: "param1", Value: "val1"},
},
},
},
},
}}},
conf: config.StepConfig{Config: map[string]interface{}{
"param1": "val1",
"val1": map[string]interface{}{"dockerImage": "myTestImage:latest"},
}},
expected: map[string]interface{}{
"param1": "val1",
"dockerImage": "myTestImage:latest",
},
},
}
for run, test := range tt {
applyContextConditions(test.metadata, &test.conf)
assert.Equalf(t, test.expected, test.conf.Config, fmt.Sprintf("Run %v failed", run))
}
}

View File

@ -11,6 +11,14 @@ type execRunner interface {
Stderr(err io.Writer)
}
type envExecRunner interface {
RunExecutable(e string, p ...string) error
Dir(d string)
Env(e []string)
Stdout(out io.Writer)
Stderr(err io.Writer)
}
type shellRunner interface {
RunShell(s string, c string) error
Dir(d string)

231
cmd/kubernetesDeploy.go Normal file
View File

@ -0,0 +1,231 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"regexp"
"strconv"
"strings"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/log"
)
func kubernetesDeploy(config kubernetesDeployOptions) error {
c := command.Command{}
// reroute stderr output to logging framework, stdout will be used for command interactions
c.Stderr(log.Entry().Writer())
runKubernetesDeploy(config, &c, log.Entry().Writer())
return nil
}
func runKubernetesDeploy(config kubernetesDeployOptions, command envExecRunner, stdout io.Writer) {
if config.DeployTool == "helm" {
runHelmDeploy(config, command, stdout)
} else {
runKubectlDeploy(config, command)
}
}
func runHelmDeploy(config kubernetesDeployOptions, command envExecRunner, stdout io.Writer) {
_, containerRegistry, err := splitRegistryURL(config.ContainerRegistryURL)
if err != nil {
log.Entry().WithError(err).Fatalf("Container registry url '%v' incorrect", config.ContainerRegistryURL)
}
containerImageName, containerImageTag, err := splitFullImageName(config.Image)
if err != nil {
log.Entry().WithError(err).Fatalf("Container image '%v' incorrect", config.Image)
}
helmLogFields := map[string]interface{}{}
helmLogFields["Chart Path"] = config.ChartPath
helmLogFields["Namespace"] = config.Namespace
helmLogFields["Deployment Name"] = config.DeploymentName
helmLogFields["Context"] = config.KubeContext
helmLogFields["Kubeconfig"] = config.KubeConfig
log.Entry().WithFields(helmLogFields).Debug("Calling Helm")
helmEnv := []string{fmt.Sprintf("KUBECONFIG=%v", config.KubeConfig)}
if len(config.TillerNamespace) > 0 {
helmEnv = append(helmEnv, fmt.Sprintf("TILLER_NAMESPACE=%v", config.TillerNamespace))
}
log.Entry().Debugf("Helm Env: %v", helmEnv)
command.Env(helmEnv)
command.Stdout(stdout)
initParams := []string{"init", "--client-only"}
if err := command.RunExecutable("helm", initParams...); err != nil {
log.Entry().WithError(err).Fatal("Helm init called failed")
}
var dockerRegistrySecret bytes.Buffer
command.Stdout(&dockerRegistrySecret)
kubeParams := []string{
"--insecure-skip-tls-verify=true",
"create",
"secret",
"docker-registry",
"regsecret",
fmt.Sprintf("--docker-server=%v", containerRegistry),
fmt.Sprintf("--docker-username=%v", config.ContainerRegistryUser),
fmt.Sprintf("--docker-password=%v", config.ContainerRegistryPassword),
"--dry-run=true",
"--output=json",
}
log.Entry().Infof("Calling kubectl create secret --dry-run=true ...")
log.Entry().Debugf("kubectl parameters %v", kubeParams)
if err := command.RunExecutable("kubectl", kubeParams...); err != nil {
log.Entry().WithError(err).Fatal("Retrieving Docker config via kubectl failed")
}
log.Entry().Debugf("Secret created: %v", string(dockerRegistrySecret.Bytes()))
var dockerRegistrySecretData struct {
Kind string `json:"kind"`
Data struct {
DockerConfJSON string `json:".dockerconfigjson"`
} `json:"data"`
Type string `json:"type"`
}
if err := json.Unmarshal(dockerRegistrySecret.Bytes(), &dockerRegistrySecretData); err != nil {
log.Entry().WithError(err).Fatal("Reading docker registry secret json failed")
}
ingressHosts := ""
for i, h := range config.IngressHosts {
ingressHosts += fmt.Sprintf(",ingress.hosts[%v]=%v", i, h)
}
upgradeParams := []string{
"upgrade",
config.DeploymentName,
config.ChartPath,
"--install",
"--force",
"--namespace",
config.Namespace,
"--wait",
"--timeout",
strconv.Itoa(config.HelmDeployWaitSeconds),
"--set",
fmt.Sprintf("image.repository=%v/%v,image.tag=%v,secret.dockerconfigjson=%v%v", containerRegistry, containerImageName, containerImageTag, dockerRegistrySecretData.Data.DockerConfJSON, ingressHosts),
}
if len(config.KubeContext) > 0 {
upgradeParams = append(upgradeParams, "--kube-context", config.KubeContext)
}
if len(config.AdditionalParameters) > 0 {
upgradeParams = append(upgradeParams, config.AdditionalParameters...)
}
command.Stdout(stdout)
log.Entry().Info("Calling helm upgrade ...")
log.Entry().Debugf("Helm parameters %v", upgradeParams)
command.RunExecutable("helm", upgradeParams...)
if err := command.RunExecutable("helm", upgradeParams...); err != nil {
log.Entry().WithError(err).Fatal("Helm upgrade call failed")
}
}
func runKubectlDeploy(config kubernetesDeployOptions, command envExecRunner) {
_, containerRegistry, err := splitRegistryURL(config.ContainerRegistryURL)
if err != nil {
log.Entry().WithError(err).Fatalf("Container registry url '%v' incorrect", config.ContainerRegistryURL)
}
kubeParams := []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", config.Namespace),
}
if len(config.KubeConfig) > 0 {
log.Entry().Info("Using KUBECONFIG environment for authentication.")
kubeEnv := []string{fmt.Sprintf("KUBECONFIG=%v", config.KubeConfig)}
command.Env(kubeEnv)
if len(config.KubeContext) > 0 {
kubeParams = append(kubeParams, fmt.Sprintf("--context=%v", config.KubeContext))
}
} else {
log.Entry().Info("Using --token parameter for authentication.")
kubeParams = append(kubeParams, fmt.Sprintf("--server=%v", config.APIServer))
kubeParams = append(kubeParams, fmt.Sprintf("--token=%v", config.KubeToken))
}
if config.CreateDockerRegistrySecret {
if len(config.ContainerRegistryUser)+len(config.ContainerRegistryPassword) == 0 {
log.Entry().Fatal("Cannot create Container registry secret without proper registry username/password")
}
// first check if secret already exists
kubeCheckParams := append(kubeParams, "get", "secret", config.ContainerRegistrySecret)
if err := command.RunExecutable("kubectl", kubeCheckParams...); err != nil {
log.Entry().Infof("Registry secret '%v' does not exist, let's create it ...", config.ContainerRegistrySecret)
kubeSecretParams := append(
kubeParams,
"create",
"secret",
"docker-registry",
config.ContainerRegistrySecret,
fmt.Sprintf("--docker-server=%v", containerRegistry),
fmt.Sprintf("--docker-username=%v", config.ContainerRegistryUser),
fmt.Sprintf("--docker-password=%v", config.ContainerRegistryPassword),
)
log.Entry().Infof("Creating container registry secret '%v'", config.ContainerRegistrySecret)
log.Entry().Debugf("Running kubectl with following parameters: %v", kubeSecretParams)
if err := command.RunExecutable("kubectl", kubeSecretParams...); err != nil {
log.Entry().WithError(err).Fatal("Creating container registry secret failed")
}
}
}
appTemplate, err := ioutil.ReadFile(config.AppTemplate)
if err != nil {
log.Entry().WithError(err).Fatalf("Error when reading appTemplate '%v'", config.AppTemplate)
}
// Update image name in deployment yaml, expects placeholder like 'image: <image-name>'
re := regexp.MustCompile(`image:[ ]*<image-name>`)
appTemplate = []byte(re.ReplaceAllString(string(appTemplate), fmt.Sprintf("image: %v/%v", containerRegistry, config.Image)))
err = ioutil.WriteFile(config.AppTemplate, appTemplate, 0700)
if err != nil {
log.Entry().WithError(err).Fatalf("Error when updating appTemplate '%v'", config.AppTemplate)
}
kubeApplyParams := append(kubeParams, "apply", "--filename", config.AppTemplate)
if len(config.AdditionalParameters) > 0 {
kubeApplyParams = append(kubeApplyParams, config.AdditionalParameters...)
}
if err := command.RunExecutable("kubectl", kubeApplyParams...); err != nil {
log.Entry().Debugf("Running kubectl with following parameters: %v", kubeApplyParams)
log.Entry().WithError(err).Fatal("Deployment with kubectl failed.")
}
}
func splitRegistryURL(registryURL string) (protocol, registry string, err error) {
parts := strings.Split(registryURL, "://")
if len(parts) != 2 || len(parts[1]) == 0 {
return "", "", fmt.Errorf("Failed to split registry url '%v'", registryURL)
}
return parts[0], parts[1], nil
}
func splitFullImageName(image string) (imageName, tag string, err error) {
parts := strings.Split(image, ":")
switch len(parts) {
case 0:
return "", "", fmt.Errorf("Failed to split image name '%v'", image)
case 1:
if len(parts[0]) > 0 {
return parts[0], "", nil
}
return "", "", fmt.Errorf("Failed to split image name '%v'", image)
case 2:
return parts[0], parts[1], nil
}
return "", "", fmt.Errorf("Failed to split image name '%v'", image)
}

View File

@ -0,0 +1,267 @@
package cmd
import (
"os"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/spf13/cobra"
)
type kubernetesDeployOptions struct {
AdditionalParameters []string `json:"additionalParameters,omitempty"`
APIServer string `json:"apiServer,omitempty"`
AppTemplate string `json:"appTemplate,omitempty"`
ChartPath string `json:"chartPath,omitempty"`
ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
ContainerRegistryUser string `json:"containerRegistryUser,omitempty"`
ContainerRegistrySecret string `json:"containerRegistrySecret,omitempty"`
CreateDockerRegistrySecret bool `json:"createDockerRegistrySecret,omitempty"`
DeploymentName string `json:"deploymentName,omitempty"`
DeployTool string `json:"deployTool,omitempty"`
HelmDeployWaitSeconds int `json:"helmDeployWaitSeconds,omitempty"`
Image string `json:"image,omitempty"`
IngressHosts []string `json:"ingressHosts,omitempty"`
KubeConfig string `json:"kubeConfig,omitempty"`
KubeContext string `json:"kubeContext,omitempty"`
KubeToken string `json:"kubeToken,omitempty"`
Namespace string `json:"namespace,omitempty"`
TillerNamespace string `json:"tillerNamespace,omitempty"`
}
var myKubernetesDeployOptions kubernetesDeployOptions
// KubernetesDeployCommand Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster.
func KubernetesDeployCommand() *cobra.Command {
metadata := kubernetesDeployMetadata()
var createKubernetesDeployCmd = &cobra.Command{
Use: "kubernetesDeploy",
Short: "Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster.",
Long: `Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster.
!!! note "Deployment supports multiple deployment tools"
Currently the following are supported:
* [Helm](https://helm.sh/) command line tool and [Helm Charts](https://docs.helm.sh/developing_charts/#charts).
* [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) and ` + "`" + `kubectl apply` + "`" + ` command.
## Helm
Following helm command will be executed by default:
` + "`" + `` + "`" + `` + "`" + `
helm upgrade <deploymentName> <chartPath> --install --force --namespace <namespace> --wait --timeout <helmDeployWaitSeconds> --set "image.repository=<yourRegistry>/<yourImageName>,image.tag=<yourImageTag>,secret.dockerconfigjson=<dockerSecret>,ingress.hosts[0]=<ingressHosts[0]>,,ingress.hosts[1]=<ingressHosts[1]>,...
` + "`" + `` + "`" + `` + "`" + `
* ` + "`" + `yourRegistry` + "`" + ` will be retrieved from ` + "`" + `containerRegistryUrl` + "`" + `
* ` + "`" + `yourImageName` + "`" + `, ` + "`" + `yourImageTag` + "`" + ` will be retrieved from ` + "`" + `image` + "`" + `
* ` + "`" + `dockerSecret` + "`" + ` will be calculated with a call to ` + "`" + `kubectl create secret docker-registry regsecret --docker-server=<yourRegistry> --docker-username=<containerRegistryUser> --docker-password=<containerRegistryPassword> --dry-run=true --output=json'` + "`" + ``,
PreRunE: func(cmd *cobra.Command, args []string) error {
log.SetStepName("kubernetesDeploy")
log.SetVerbose(GeneralConfig.Verbose)
return PrepareConfig(cmd, &metadata, "kubernetesDeploy", &myKubernetesDeployOptions, config.OpenPiperFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
return kubernetesDeploy(myKubernetesDeployOptions)
},
}
addKubernetesDeployFlags(createKubernetesDeployCmd)
return createKubernetesDeployCmd
}
func addKubernetesDeployFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceVar(&myKubernetesDeployOptions.AdditionalParameters, "additionalParameters", []string{}, "Defines additional parameters for \"helm install\" or \"kubectl apply\" command.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.APIServer, "apiServer", os.Getenv("PIPER_apiServer"), "Defines the Url of the API Server of the Kubernetes cluster.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.AppTemplate, "appTemplate", os.Getenv("PIPER_appTemplate"), "Defines the filename for the kubernetes app template (e.g. k8s_apptemplate.yaml)")
cmd.Flags().StringVar(&myKubernetesDeployOptions.ChartPath, "chartPath", os.Getenv("PIPER_chartPath"), "Defines the chart path for deployments using helm.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.ContainerRegistryPassword, "containerRegistryPassword", os.Getenv("PIPER_containerRegistryPassword"), "Password for container registry access - typically provided by the CI/CD environment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "http(s) url of the Container registry.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.ContainerRegistryUser, "containerRegistryUser", os.Getenv("PIPER_containerRegistryUser"), "Username for container registry access - typically provided by the CI/CD environment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.ContainerRegistrySecret, "containerRegistrySecret", "regsecret", "Name of the container registry secret used for pulling containers from the registry.")
cmd.Flags().BoolVar(&myKubernetesDeployOptions.CreateDockerRegistrySecret, "createDockerRegistrySecret", false, "Toggle to turn on Regsecret creation with a \"deployTool:kubectl\" deployment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.DeploymentName, "deploymentName", os.Getenv("PIPER_deploymentName"), "Defines the name of the deployment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.DeployTool, "deployTool", "kubectl", "Defines the tool which should be used for deployment.")
cmd.Flags().IntVar(&myKubernetesDeployOptions.HelmDeployWaitSeconds, "helmDeployWaitSeconds", 300, "Number of seconds before helm deploy returns.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.Image, "image", os.Getenv("PIPER_image"), "Full name of the image to be deployed.")
cmd.Flags().StringSliceVar(&myKubernetesDeployOptions.IngressHosts, "ingressHosts", []string{}, "List of ingress hosts to be exposed via helm deployment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.KubeConfig, "kubeConfig", os.Getenv("PIPER_kubeConfig"), "Defines the path to the \"kubeconfig\" file.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.KubeContext, "kubeContext", os.Getenv("PIPER_kubeContext"), "Defines the context to use from the \"kubeconfig\" file.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.KubeToken, "kubeToken", os.Getenv("PIPER_kubeToken"), "Contains the id_token used by kubectl for authentication. Consider using kubeConfig parameter instead.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.Namespace, "namespace", "default", "Defines the target Kubernetes namespace for the deployment.")
cmd.Flags().StringVar(&myKubernetesDeployOptions.TillerNamespace, "tillerNamespace", os.Getenv("PIPER_tillerNamespace"), "Defines optional tiller namespace for deployments using helm.")
cmd.MarkFlagRequired("chartPath")
cmd.MarkFlagRequired("containerRegistryUrl")
cmd.MarkFlagRequired("deploymentName")
cmd.MarkFlagRequired("deployTool")
cmd.MarkFlagRequired("image")
}
// retrieve step metadata
func kubernetesDeployMetadata() config.StepData {
var theMetaData = config.StepData{
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{
Name: "additionalParameters",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{{Name: "helmDeploymentParameters"}},
},
{
Name: "apiServer",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "k8sAPIServer"}},
},
{
Name: "appTemplate",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "k8sAppTemplate"}},
},
{
Name: "chartPath",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{{Name: "helmChartPath"}},
},
{
Name: "containerRegistryPassword",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "containerRegistryUrl",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{{Name: "dockerRegistryUrl"}},
},
{
Name: "containerRegistryUser",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "containerRegistrySecret",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "createDockerRegistrySecret",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "deploymentName",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{{Name: "helmDeploymentName"}},
},
{
Name: "deployTool",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{},
},
{
Name: "helmDeployWaitSeconds",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "int",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "image",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: true,
Aliases: []config.Alias{{Name: "deployImage"}},
},
{
Name: "ingressHosts",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "kubeConfig",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "kubeContext",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "kubeToken",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "namespace",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "helmDeploymentNamespace"}, {Name: "k8sDeploymentNamespace"}},
},
{
Name: "tillerNamespace",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "helmTillerNamespace"}},
},
},
},
},
}
return theMetaData
}

View File

@ -0,0 +1,16 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestKubernetesDeployCommand(t *testing.T) {
testCmd := KubernetesDeployCommand()
// only high level testing performed - details are tested in step generation procudure
assert.Equal(t, "kubernetesDeploy", testCmd.Use, "command name incorrect")
}

View File

@ -0,0 +1,273 @@
package cmd
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRunKubernetesDeploy(t *testing.T) {
t.Run("test helm", func(t *testing.T) {
opts := kubernetesDeployOptions{
ContainerRegistryURL: "https://my.registry:55555",
ContainerRegistryUser: "registryUser",
ContainerRegistryPassword: "********",
ChartPath: "path/to/chart",
DeploymentName: "deploymentName",
DeployTool: "helm",
HelmDeployWaitSeconds: 400,
IngressHosts: []string{"ingress.host1", "ingress.host2"},
Image: "path/to/Image:latest",
AdditionalParameters: []string{"--testParam", "testValue"},
KubeContext: "testCluster",
Namespace: "deploymentNamespace",
}
dockerConfigJSON := `{"kind": "Secret","data":{".dockerconfigjson": "ThisIsOurBase64EncodedSecret=="}}`
e := execMockRunner{
stdoutReturn: map[string]string{
"kubectl --insecure-skip-tls-verify=true create secret docker-registry regsecret --docker-server=my.registry:55555 --docker-username=registryUser --docker-password=******** --dry-run=true --output=json": dockerConfigJSON,
},
}
var stdout bytes.Buffer
runKubernetesDeploy(opts, &e, &stdout)
assert.Equal(t, "helm", e.calls[0].exec, "Wrong init command")
assert.Equal(t, []string{"init", "--client-only"}, e.calls[0].params, "Wrong init parameters")
assert.Equal(t, "kubectl", e.calls[1].exec, "Wrong secret creation command")
assert.Equal(t, []string{"--insecure-skip-tls-verify=true", "create", "secret", "docker-registry", "regsecret", "--docker-server=my.registry:55555", "--docker-username=registryUser", "--docker-password=********", "--dry-run=true", "--output=json"}, e.calls[1].params, "Wrong secret creation parameters")
assert.Equal(t, "helm", e.calls[2].exec, "Wrong upgrade command")
assert.Equal(t, []string{
"upgrade",
"deploymentName",
"path/to/chart",
"--install",
"--force",
"--namespace",
"deploymentNamespace",
"--wait",
"--timeout",
"400",
"--set",
"image.repository=my.registry:55555/path/to/Image,image.tag=latest,secret.dockerconfigjson=ThisIsOurBase64EncodedSecret==,ingress.hosts[0]=ingress.host1,ingress.hosts[1]=ingress.host2",
"--kube-context",
"testCluster",
"--testParam",
"testValue",
}, e.calls[2].params, "Wrong upgrade parameters")
})
t.Run("test kubectl - create secret/kubeconfig", func(t *testing.T) {
dir, err := ioutil.TempDir("", "")
defer os.RemoveAll(dir) // clean up
assert.NoError(t, err, "Error when creating temp dir")
opts := kubernetesDeployOptions{
AppTemplate: filepath.Join(dir, "test.yaml"),
ContainerRegistryURL: "https://my.registry:55555",
ContainerRegistryUser: "registryUser",
ContainerRegistryPassword: "********",
ContainerRegistrySecret: "regSecret",
CreateDockerRegistrySecret: true,
DeployTool: "kubectl",
Image: "path/to/Image:latest",
AdditionalParameters: []string{"--testParam", "testValue"},
KubeConfig: "This is my kubeconfig",
KubeContext: "testCluster",
Namespace: "deploymentNamespace",
}
kubeYaml := `kind: Deployment
metadata:
spec:
spec:
image: <image-name>`
ioutil.WriteFile(opts.AppTemplate, []byte(kubeYaml), 0755)
e := execMockRunner{
shouldFailOnCommand: map[string]error{
"kubectl --insecure-skip-tls-verify=true --namespace=deploymentNamespace --context=testCluster get secret regSecret": fmt.Errorf("secret not found"),
},
}
var stdout bytes.Buffer
runKubernetesDeploy(opts, &e, &stdout)
assert.Equal(t, e.env[0], []string{"KUBECONFIG=This is my kubeconfig"})
assert.Equal(t, "kubectl", e.calls[0].exec, "Wrong secret lookup command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
fmt.Sprintf("--context=%v", opts.KubeContext),
"get",
"secret",
opts.ContainerRegistrySecret,
}, e.calls[0].params, "kubectl parameters incorrect")
assert.Equal(t, "kubectl", e.calls[1].exec, "Wrong secret create command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
fmt.Sprintf("--context=%v", opts.KubeContext),
"create",
"secret",
"docker-registry",
opts.ContainerRegistrySecret,
"--docker-server=my.registry:55555",
fmt.Sprintf("--docker-username=%v", opts.ContainerRegistryUser),
fmt.Sprintf("--docker-password=%v", opts.ContainerRegistryPassword),
}, e.calls[1].params, "kubectl parameters incorrect")
assert.Equal(t, "kubectl", e.calls[2].exec, "Wrong apply command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
fmt.Sprintf("--context=%v", opts.KubeContext),
"apply",
"--filename",
opts.AppTemplate,
"--testParam",
"testValue",
}, e.calls[2].params, "kubectl parameters incorrect")
appTemplate, err := ioutil.ReadFile(opts.AppTemplate)
assert.Contains(t, string(appTemplate), "my.registry:55555/path/to/Image:latest")
})
t.Run("test kubectl - lookup secret/kubeconfig", func(t *testing.T) {
dir, err := ioutil.TempDir("", "")
defer os.RemoveAll(dir) // clean up
assert.NoError(t, err, "Error when creating temp dir")
opts := kubernetesDeployOptions{
AppTemplate: filepath.Join(dir, "test.yaml"),
ContainerRegistryURL: "https://my.registry:55555",
ContainerRegistryUser: "registryUser",
ContainerRegistryPassword: "********",
ContainerRegistrySecret: "regSecret",
CreateDockerRegistrySecret: true,
DeployTool: "kubectl",
Image: "path/to/Image:latest",
KubeConfig: "This is my kubeconfig",
Namespace: "deploymentNamespace",
}
ioutil.WriteFile(opts.AppTemplate, []byte("testYaml"), 0755)
e := execMockRunner{}
var stdout bytes.Buffer
runKubernetesDeploy(opts, &e, &stdout)
assert.Equal(t, "kubectl", e.calls[0].exec, "Wrong secret lookup command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
"get",
"secret",
opts.ContainerRegistrySecret,
}, e.calls[0].params, "kubectl parameters incorrect")
assert.Equal(t, "kubectl", e.calls[1].exec, "Wrong apply command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
"apply",
"--filename",
opts.AppTemplate,
}, e.calls[1].params, "kubectl parameters incorrect")
})
t.Run("test kubectl - token only", func(t *testing.T) {
dir, err := ioutil.TempDir("", "")
defer os.RemoveAll(dir) // clean up
assert.NoError(t, err, "Error when creating temp dir")
opts := kubernetesDeployOptions{
APIServer: "https://my.api.server",
AppTemplate: filepath.Join(dir, "test.yaml"),
ContainerRegistryURL: "https://my.registry:55555",
ContainerRegistryUser: "registryUser",
ContainerRegistryPassword: "********",
ContainerRegistrySecret: "regSecret",
DeployTool: "kubectl",
Image: "path/to/Image:latest",
KubeToken: "testToken",
Namespace: "deploymentNamespace",
}
ioutil.WriteFile(opts.AppTemplate, []byte("testYaml"), 0755)
e := execMockRunner{
shouldFailOnCommand: map[string]error{},
}
var stdout bytes.Buffer
runKubernetesDeploy(opts, &e, &stdout)
assert.Equal(t, "kubectl", e.calls[0].exec, "Wrong apply command")
assert.Equal(t, []string{
"--insecure-skip-tls-verify=true",
fmt.Sprintf("--namespace=%v", opts.Namespace),
fmt.Sprintf("--server=%v", opts.APIServer),
fmt.Sprintf("--token=%v", opts.KubeToken),
"apply",
"--filename",
opts.AppTemplate,
}, e.calls[0].params, "kubectl parameters incorrect")
})
}
func TestSplitRegistryURL(t *testing.T) {
tt := []struct {
in string
outProtocol string
outRegistry string
outError error
}{
{in: "https://my.registry.com", outProtocol: "https", outRegistry: "my.registry.com", outError: nil},
{in: "https://", outProtocol: "", outRegistry: "", outError: fmt.Errorf("Failed to split registry url 'https://'")},
{in: "my.registry.com", outProtocol: "", outRegistry: "", outError: fmt.Errorf("Failed to split registry url 'my.registry.com'")},
{in: "", outProtocol: "", outRegistry: "", outError: fmt.Errorf("Failed to split registry url ''")},
{in: "https://https://my.registry.com", outProtocol: "", outRegistry: "", outError: fmt.Errorf("Failed to split registry url 'https://https://my.registry.com'")},
}
for _, test := range tt {
p, r, err := splitRegistryURL(test.in)
assert.Equal(t, test.outProtocol, p, "Protocol value unexpected")
assert.Equal(t, test.outRegistry, r, "Registry value unexpected")
assert.Equal(t, test.outError, err, "Error value not as expected")
}
}
func TestSplitImageName(t *testing.T) {
tt := []struct {
in string
outImage string
outTag string
outError error
}{
{in: "", outImage: "", outTag: "", outError: fmt.Errorf("Failed to split image name ''")},
{in: "path/to/image", outImage: "path/to/image", outTag: "", outError: nil},
{in: "path/to/image:tag", outImage: "path/to/image", outTag: "tag", outError: nil},
{in: "https://my.registry.com/path/to/image:tag", outImage: "", outTag: "", outError: fmt.Errorf("Failed to split image name 'https://my.registry.com/path/to/image:tag'")},
}
for _, test := range tt {
i, tag, err := splitFullImageName(test.in)
assert.Equal(t, test.outImage, i, "Image value unexpected")
assert.Equal(t, test.outTag, tag, "Tag value unexpected")
assert.Equal(t, test.outError, err, "Error value not as expected")
}
}

View File

@ -46,6 +46,7 @@ func Execute() {
rootCmd.AddCommand(VersionCommand())
rootCmd.AddCommand(DetectExecuteScanCommand())
rootCmd.AddCommand(KarmaExecuteTestsCommand())
rootCmd.AddCommand(KubernetesDeployCommand())
rootCmd.AddCommand(XsDeployCommand())
rootCmd.AddCommand(GithubPublishReleaseCommand())
rootCmd.AddCommand(GithubCreatePullRequestCommand())

View File

@ -13,11 +13,14 @@ import (
)
type execMockRunner struct {
dir []string
calls []execCall
stdout io.Writer
stderr io.Writer
shouldFailWith error
dir []string
env [][]string
calls []execCall
stdout io.Writer
stderr io.Writer
stdoutReturn map[string]string
shouldFailWith error
shouldFailOnCommand map[string]error
}
type execCall struct {
@ -27,6 +30,7 @@ type execCall struct {
type shellMockRunner struct {
dir string
env [][]string
calls []string
shell []string
stdout io.Writer
@ -38,12 +42,26 @@ func (m *execMockRunner) Dir(d string) {
m.dir = append(m.dir, d)
}
func (m *execMockRunner) Env(e []string) {
m.env = append(m.env, e)
}
func (m *execMockRunner) RunExecutable(e string, p ...string) error {
if m.shouldFailWith != nil {
return m.shouldFailWith
}
exec := execCall{exec: e, params: p}
m.calls = append(m.calls, exec)
if c := strings.Join(append([]string{e}, p...), " "); m.shouldFailOnCommand != nil && m.shouldFailOnCommand[c] != nil {
return m.shouldFailOnCommand[c]
}
if c := strings.Join(append([]string{e}, p...), " "); m.stdoutReturn != nil && len(m.stdoutReturn[c]) > 0 {
m.stdout.Write([]byte(m.stdoutReturn[c]))
}
return nil
}
@ -59,6 +77,10 @@ func (m *shellMockRunner) Dir(d string) {
m.dir = d
}
func (m *shellMockRunner) Env(e []string) {
m.env = append(m.env, e)
}
func (m *shellMockRunner) RunShell(s string, c string) error {
if m.shouldFailWith != nil {

View File

@ -16,6 +16,7 @@ type Command struct {
dir string
stdout io.Writer
stderr io.Writer
env []string
}
// Dir sets the working directory for the execution
@ -23,6 +24,11 @@ func (c *Command) Dir(d string) {
c.dir = d
}
// Env sets explicit environment variables to be used for execution
func (c *Command) Env(e []string) {
c.env = e
}
// Stdout ..
func (c *Command) Stdout(stdout io.Writer) {
c.stdout = stdout
@ -43,7 +49,14 @@ func (c *Command) RunShell(shell, script string) error {
cmd := ExecCommand(shell)
cmd.Dir = c.dir
if len(c.dir) > 0 {
cmd.Dir = c.dir
}
if len(c.env) > 0 {
cmd.Env = c.env
}
in := bytes.Buffer{}
in.Write([]byte(script))
cmd.Stdin = &in
@ -65,6 +78,10 @@ func (c *Command) RunExecutable(executable string, params ...string) error {
cmd.Dir = c.dir
}
if len(c.env) > 0 {
cmd.Env = c.env
}
if err := runCmd(cmd, _out, _err); err != nil {
return errors.Wrapf(err, "running command '%v' failed", executable)
}

View File

@ -208,6 +208,7 @@ func (m *StepData) GetContextParameterFilters() StepFilters {
for _, condition := range container.Conditions {
for _, dependentParam := range condition.Params {
parameterKeys = append(parameterKeys, dependentParam.Value)
parameterKeys = append(parameterKeys, dependentParam.Name)
}
}
}
@ -216,6 +217,7 @@ func (m *StepData) GetContextParameterFilters() StepFilters {
if len(m.Spec.Sidecars) > 0 {
//ToDo: support fallback for "dockerName" configuration property -> via aliasing?
containerFilters = append(containerFilters, []string{"containerName", "containerPortMappings", "dockerName", "sidecarEnvVars", "sidecarImage", "sidecarName", "sidecarOptions", "sidecarPullImage", "sidecarReadyCommand", "sidecarVolumeBind", "sidecarWorkspace"}...)
//ToDo: add condition param.Value and param.Name to filter as for Containers
}
if len(containerFilters) > 0 {
filters.All = append(filters.All, containerFilters...)

View File

@ -266,12 +266,12 @@ func TestGetContextParameterFilters(t *testing.T) {
t.Run("Containers", func(t *testing.T) {
filters := metadata2.GetContextParameterFilters()
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.All, "incorrect filter All")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.General, "incorrect filter General")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.Steps, "incorrect filter Steps")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.Stages, "incorrect filter Stages")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.Parameters, "incorrect filter Parameters")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip"}, filters.Env, "incorrect filter Env")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.All, "incorrect filter All")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.General, "incorrect filter General")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.Steps, "incorrect filter Steps")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.Stages, "incorrect filter Stages")
assert.Equal(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.Parameters, "incorrect filter Parameters")
assert.NotEqual(t, []string{"containerCommand", "containerShell", "dockerEnvVars", "dockerImage", "dockerOptions", "dockerPullImage", "dockerVolumeBind", "dockerWorkspace", "pip", "scanType"}, filters.Env, "incorrect filter Env")
})
t.Run("Sidecars", func(t *testing.T) {

View File

@ -68,12 +68,16 @@ func setDefaultStepParameters(stepData *config.StepData) {
switch param.Type {
case "bool":
param.Default = "false"
case "int":
param.Default = "0"
}
} else {
switch param.Type {
case "string":
case "bool":
param.Default = fmt.Sprintf("\"%v\"", param.Default)
case "int":
param.Default = fmt.Sprintf("%v", param.Default)
}
}

View File

@ -214,12 +214,14 @@ func setDefaultParameters(stepData *config.StepData) (bool, error) {
if param.Default == nil {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
osImportRequired = true
case "bool":
// ToDo: Check if default should be read from env
param.Default = "false"
case "int":
param.Default = "0"
case "string":
param.Default = fmt.Sprintf("os.Getenv(\"PIPER_%v\")", param.Name)
osImportRequired = true
case "[]string":
// ToDo: Check if default should be read from env
param.Default = "[]string{}"
@ -228,14 +230,16 @@ func setDefaultParameters(stepData *config.StepData) (bool, error) {
}
} else {
switch param.Type {
case "string":
param.Default = fmt.Sprintf("\"%v\"", param.Default)
case "bool":
boolVal := "false"
if param.Default.(bool) == true {
boolVal = "true"
}
param.Default = boolVal
case "int":
param.Default = fmt.Sprintf("%v", param.Default)
case "string":
param.Default = fmt.Sprintf("\"%v\"", param.Default)
case "[]string":
param.Default = fmt.Sprintf("[]string{\"%v\"}", strings.Join(getStringSliceFromInterface(param.Default), "\", \""))
default:
@ -430,6 +434,8 @@ func flagType(paramType string) string {
switch paramType {
case "bool":
theFlagType = "BoolVar"
case "int":
theFlagType = "IntVar"
case "string":
theFlagType = "StringVar"
case "[]string":

View File

@ -115,12 +115,14 @@ func TestSetDefaultParameters(t *testing.T) {
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "string", Default: "val0"},
{Name: "param1", Scope: []string{"STEPS"}, Type: "string"},
{Name: "param2", Scope: []string{"STAGES"}, Type: "bool", Default: true},
{Name: "param3", Scope: []string{"PARAMETERS"}, Type: "bool"},
{Name: "param4", Scope: []string{"ENV"}, Type: "[]string", Default: stringSliceDefault},
{Name: "param5", Scope: []string{"ENV"}, Type: "[]string"},
{Name: "param0", Type: "string", Default: "val0"},
{Name: "param1", Type: "string"},
{Name: "param2", Type: "bool", Default: true},
{Name: "param3", Type: "bool"},
{Name: "param4", Type: "[]string", Default: stringSliceDefault},
{Name: "param5", Type: "[]string"},
{Name: "param6", Type: "int"},
{Name: "param7", Type: "int", Default: 1},
},
},
},
@ -133,6 +135,8 @@ func TestSetDefaultParameters(t *testing.T) {
"false",
"[]string{\"val4_1\", \"val4_2\"}",
"[]string{}",
"0",
"1",
}
osImport, err := setDefaultParameters(&stepData)
@ -152,8 +156,8 @@ func TestSetDefaultParameters(t *testing.T) {
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param0", Scope: []string{"GENERAL"}, Type: "int", Default: 10},
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
{Name: "param0", Type: "n/a", Default: 10},
{Name: "param1", Type: "n/a"},
},
},
},
@ -162,7 +166,7 @@ func TestSetDefaultParameters(t *testing.T) {
Spec: config.StepSpec{
Inputs: config.StepInputs{
Parameters: []config.StepParameters{
{Name: "param1", Scope: []string{"GENERAL"}, Type: "int"},
{Name: "param1", Type: "n/a"},
},
},
},
@ -246,6 +250,7 @@ func TestFlagType(t *testing.T) {
expected string
}{
{input: "bool", expected: "BoolVar"},
{input: "int", expected: "IntVar"},
{input: "string", expected: "StringVar"},
{input: "[]string", expected: "StringSliceVar"},
}

View File

@ -0,0 +1,220 @@
metadata:
name: kubernetesDeploy
description: Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster.
longDescription: |-
Deployment to Kubernetes test or production namespace within the specified Kubernetes cluster.
!!! note "Deployment supports multiple deployment tools"
Currently the following are supported:
* [Helm](https://helm.sh/) command line tool and [Helm Charts](https://docs.helm.sh/developing_charts/#charts).
* [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) and `kubectl apply` command.
## Helm
Following helm command will be executed by default:
```
helm upgrade <deploymentName> <chartPath> --install --force --namespace <namespace> --wait --timeout <helmDeployWaitSeconds> --set "image.repository=<yourRegistry>/<yourImageName>,image.tag=<yourImageTag>,secret.dockerconfigjson=<dockerSecret>,ingress.hosts[0]=<ingressHosts[0]>,,ingress.hosts[1]=<ingressHosts[1]>,...
```
* `yourRegistry` will be retrieved from `containerRegistryUrl`
* `yourImageName`, `yourImageTag` will be retrieved from `image`
* `dockerSecret` will be calculated with a call to `kubectl create secret docker-registry regsecret --docker-server=<yourRegistry> --docker-username=<containerRegistryUser> --docker-password=<containerRegistryPassword> --dry-run=true --output=json'`
spec:
inputs:
secrets:
- name: kubeConfigFileCredentialsId
type: jenkins
- name: kubeTokenCredentialsId
type: jenkins
- name: dockerCredentialsId
type: jenkins
resources:
- name: deployDescriptor
type: stash
params:
- name: additionalParameters
aliases:
- name: helmDeploymentParameters
type: '[]string'
description: Defines additional parameters for \"helm install\" or \"kubectl apply\" command.
scope:
- PARAMETERS
- STAGES
- STEPS
- name: apiServer
aliases:
- name: k8sAPIServer
type: string
description: Defines the Url of the API Server of the Kubernetes cluster.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
- name: appTemplate
aliases:
- name: k8sAppTemplate
type: string
description: Defines the filename for the kubernetes app template (e.g. k8s_apptemplate.yaml)
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
- name: chartPath
aliases:
- name: helmChartPath
type: string
description: Defines the chart path for deployments using helm.
mandatory: true
scope:
- PARAMETERS
- STAGES
- STEPS
- name: containerRegistryPassword
description: Password for container registry access - typically provided by the CI/CD environment.
type: string
scope:
- PARAMETERS
- STAGES
- STEPS
- name: containerRegistryUrl
aliases:
- name: dockerRegistryUrl
type: string
description: http(s) url of the Container registry.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
mandatory: true
- name: containerRegistryUser
description: Username for container registry access - typically provided by the CI/CD environment.
type: string
scope:
- PARAMETERS
- STAGES
- STEPS
- name: containerRegistrySecret
description: Name of the container registry secret used for pulling containers from the registry.
type: string
scope:
- PARAMETERS
- STAGES
- STEPS
default: regsecret
- name: createDockerRegistrySecret
type: bool
description: Toggle to turn on Regsecret creation with a \"deployTool:kubectl\" deployment.
scope:
- PARAMETERS
- STAGES
- STEPS
default: false
- name: deploymentName
aliases:
- name: helmDeploymentName
type: string
description: Defines the name of the deployment.
mandatory: true
scope:
- PARAMETERS
- STAGES
- STEPS
- name: deployTool
type: string
description: Defines the tool which should be used for deployment.
mandatory: true
scope:
- PARAMETERS
- STAGES
- STEPS
default: kubectl
- name: helmDeployWaitSeconds
type: int
description: Number of seconds before helm deploy returns.
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
default: 300
- name: image
aliases:
- name: deployImage
type: string
description: Full name of the image to be deployed.
mandatory: true
scope:
- PARAMETERS
- STAGES
- STEPS
- name: ingressHosts
type: '[]string'
description: List of ingress hosts to be exposed via helm deployment.
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
- name: kubeConfig
type: string
description: Defines the path to the \"kubeconfig\" file.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
- name: kubeContext
type: string
description: Defines the context to use from the \"kubeconfig\" file.
mandatory: false
scope:
- PARAMETERS
- STAGES
- STEPS
- name: kubeToken
type: string
description: Contains the id_token used by kubectl for authentication. Consider using kubeConfig parameter instead.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
- name: namespace
aliases:
- name: helmDeploymentNamespace
- name: k8sDeploymentNamespace
type: string
description: Defines the target Kubernetes namespace for the deployment.
scope:
- PARAMETERS
- STAGES
- STEPS
default: default
- name: tillerNamespace
aliases:
- name: helmTillerNamespace
type: string
description: Defines optional tiller namespace for deployments using helm.
scope:
- PARAMETERS
- STAGES
- STEPS
containers:
- image: dtzar/helm-kubectl:2.12.1
workingDir: /config
conditions:
- conditionRef: strings-equal
params:
- name: deployTool
value: helm
- image: dtzar/helm-kubectl:2.12.1
workingDir: /config
conditions:
- conditionRef: strings-equal
params:
- name: deployTool
value: kubectl

View File

@ -116,6 +116,7 @@ public class CommonStepsTest extends BasePiperTest{
'piperStageWrapper', //intended to be called from within stages
'buildSetResult',
'githubPublishRelease', //implementing new golang pattern without fields
'kubernetesDeploy', //implementing new golang pattern without fields
'xsDeploy', //implementing new golang pattern without fields
]

View File

@ -0,0 +1,96 @@
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import util.*
import static org.hamcrest.Matchers.*
import static org.junit.Assert.assertThat
class KubernetesDeployTest extends BasePiperTest {
private JenkinsReadJsonRule readJsonRule = new JenkinsReadJsonRule(this)
private JenkinsShellCallRule shellCallRule = new JenkinsShellCallRule(this)
private JenkinsStepRule stepRule = new JenkinsStepRule(this)
private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this)
private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this)
private List withEnvArgs = []
private List credentials = []
@Rule
public RuleChain rules = Rules
.getCommonRules(this)
.around(new JenkinsReadYamlRule(this))
.around(readJsonRule)
.around(shellCallRule)
.around(stepRule)
.around(writeFileRule)
.around(dockerExecuteRule)
@Before
void init() {
credentials = []
helper.registerAllowedMethod("withEnv", [List.class, Closure.class], {arguments, closure ->
arguments.each {arg ->
withEnvArgs.add(arg.toString())
}
return closure()
})
helper.registerAllowedMethod('file', [Map], { m -> return m })
helper.registerAllowedMethod('string', [Map], { m -> return m })
helper.registerAllowedMethod('usernamePassword', [Map], { m -> return m })
helper.registerAllowedMethod('withCredentials', [List, Closure], { l, c ->
l.each {m ->
credentials.add(m)
if (m.credentialsId == 'kubeConfig') {
binding.setProperty('PIPER_kubeConfig', 'myKubeConfig')
} else if (m.credentialsId == 'kubeToken') {
binding.setProperty('PIPER_kubeToken','myKubeToken')
} else if (m.credentialsId == 'dockerCredentials') {
binding.setProperty('PIPER_containerRegistryUser', 'registryUser')
binding.setProperty('PIPER_containerRegistryPassword', '********')
}
}
try {
c()
} finally {
binding.setProperty('PIPER_kubeConfig', null)
binding.setProperty('PIPER_kubeToken', null)
binding.setProperty('PIPER_containerRegistryUser', null)
binding.setProperty('PIPER_containerRegistryPassword', null)
}
})
}
@Test
void testKubernetesDeployAllCreds() {
shellCallRule.setReturnValue('./piper getConfig --contextConfig --stepMetadata \'metadata/kubernetesdeploy.yaml\'', '{"kubeConfigFileCredentialsId":"kubeConfig", "kubeTokenCredentialsId":"kubeToken", "dockerCredentialsId":"dockerCredentials", "dockerImage":"my.Registry/K8S:latest"}')
stepRule.step.kubernetesDeploy(
juStabUtils: utils,
testParam: "This is test content",
script: nullScript
)
// asserts
assertThat(writeFileRule.files['metadata/kubernetesdeploy.yaml'], containsString('name: kubernetesDeploy'))
assertThat(withEnvArgs[0], allOf(startsWith('PIPER_parametersJSON'), containsString('"testParam":"This is test content"')))
assertThat(shellCallRule.shell[1], is('./piper kubernetesDeploy'))
assertThat(credentials.size(), is(3))
assertThat(dockerExecuteRule.dockerParams.dockerImage, is('my.Registry/K8S:latest'))
}
@Test
void testKubernetesDeploySomeCreds() {
shellCallRule.setReturnValue('./piper getConfig --contextConfig --stepMetadata \'metadata/kubernetesdeploy.yaml\'', '{"kubeTokenCredentialsId":"kubeToken", "dockerCredentialsId":"dockerCredentials"}')
stepRule.step.kubernetesDeploy(
juStabUtils: utils,
script: nullScript
)
// asserts
assertThat(shellCallRule.shell[1], is('./piper kubernetesDeploy'))
assertThat(credentials.size(), is(2))
}
}

View File

@ -0,0 +1,48 @@
import com.sap.piper.PiperGoUtils
import com.sap.piper.Utils
import groovy.transform.Field
import static com.sap.piper.Prerequisites.checkScript
@Field String STEP_NAME = getClass().getName()
@Field String METADATA_FILE = 'metadata/kubernetesdeploy.yaml'
void call(Map parameters = [:]) {
handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) {
def script = checkScript(this, parameters) ?: this
def utils = parameters.juStabUtils ?: new Utils()
parameters.juStabUtils = null
new PiperGoUtils(this, utils).unstashPiperBin()
utils.unstash('pipelineConfigAndTests')
script.commonPipelineEnvironment.writeToDisk(script)
writeFile(file: METADATA_FILE, text: libraryResource(METADATA_FILE))
withEnv([
"PIPER_parametersJSON=${groovy.json.JsonOutput.toJson(parameters)}",
]) {
// get context configuration
Map config = readJSON (text: sh(returnStdout: true, script: "./piper getConfig --contextConfig --stepMetadata '${METADATA_FILE}'"))
echo "Config: ${config}"
dockerExecute(
script: script,
dockerImage: config.dockerImage,
dockerWorkspace: config.dockerWorkspace,
) {
def creds = []
if (config.kubeConfigFileCredentialsId) creds.add(file(credentialsId: config.kubeConfigFileCredentialsId, variable: 'PIPER_kubeConfig'))
if (config.kubeTokenCredentialsId) creds.add(string(credentialsId: config.kubeTokenCredentialsId, variable: 'PIPER_kubeToken'))
if (config.dockerCredentialsId) creds.add(usernamePassword(credentialsId: config.dockerCredentialsId, passwordVariable: 'PIPER_containerRegistryPassword', usernameVariable: 'PIPER_containerRegistryUser'))
// execute step
withCredentials(creds) {
sh "./piper kubernetesDeploy"
}
}
}
}
}