1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00
sap-jenkins-library/cmd/kubernetesDeploy.go
Arthur Lenz b335387eac
feat(k8s): Add basic support for Helm 3 in kubernetesDeploy step (#1438)
* Extends kubernetesDeploy step to support Helm 3

Currently, the kubernetesDeploy step has no support to Helm 3 due to the fact that:
- the initialization command used works only for Helm 2
- the image used when running the helm CLI is based on Helm 2

The need for Helm 3 support comes from the fact that Helm 3 introduces major architectural changes,
more specifically, the removal of its server-side agent called Tiller - thus, being incompatible with
one another.

This commit adds this support by introducing a new configuration field (helmVersion).
By default, its values is set to 2 (Helm 2) to avoid breaking any existing functionalities.

* Use deployTool field to decide between Helm 2 or 3

* Remove helm init and replace wait for atomic in v3

* Update cmd/kubernetesDeploy.go

Nice catch!

Co-Authored-By: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>

* Add documentation for kubernetesDeploy step

* Add helm3 example for kubernetesDeploy step using mandatory fields

* Add new line at the end of kubernetesDeploy documentation

* Link kubernetesDeploy step with docs generator

* Add possible values for deployTool in kubernetesDeploy

* dummy change

* Revert "dummy change"

Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
2020-04-24 09:37:11 +02:00

238 lines
8.5 KiB
Go

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"
"github.com/SAP/jenkins-library/pkg/telemetry"
)
func kubernetesDeploy(config kubernetesDeployOptions, telemetryData *telemetry.CustomData) {
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())
}
func runKubernetesDeploy(config kubernetesDeployOptions, command execRunner, stdout io.Writer) {
if config.DeployTool == "helm" || config.DeployTool == "helm3" {
runHelmDeploy(config, command, stdout)
} else {
runKubectlDeploy(config, command)
}
}
func runHelmDeploy(config kubernetesDeployOptions, command execRunner, 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 config.DeployTool == "helm" && len(config.TillerNamespace) > 0 {
helmEnv = append(helmEnv, fmt.Sprintf("TILLER_NAMESPACE=%v", config.TillerNamespace))
}
log.Entry().Debugf("Helm SetEnv: %v", helmEnv)
command.SetEnv(helmEnv)
command.Stdout(stdout)
if config.DeployTool == "helm" {
initParams := []string{"init", "--client-only"}
if err := command.RunExecutable("helm", initParams...); err != nil {
log.Entry().WithError(err).Fatal("Helm init call 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,
"--set",
fmt.Sprintf("image.repository=%v/%v,image.tag=%v,secret.dockerconfigjson=%v%v", containerRegistry, containerImageName, containerImageTag, dockerRegistrySecretData.Data.DockerConfJSON, ingressHosts),
}
if config.DeployTool == "helm" {
upgradeParams = append(upgradeParams, "--wait", "--timeout", strconv.Itoa(config.HelmDeployWaitSeconds))
}
if config.DeployTool == "helm3" {
upgradeParams = append(upgradeParams, "--atomic", "--timeout", fmt.Sprintf("%vs", config.HelmDeployWaitSeconds))
}
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 execRunner) {
_, 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.SetEnv(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)
}