mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-11-24 08:32:32 +02:00
feat(Gitops): new step to update deployment (#2178)
* kanikoExecute: improve user experience * ensure proper tags * update permissions in case a container runs with a different user we need to make sure that the orchestrator user can work on the file * update permissions * ensure availablility of directories on Jenkins * (fix) clean up tmp dir in test * add resilience for incorrect step yaml * incorporate PR feedback * Adds piper step to update deployment configuration in external git repository. https://github.wdf.sap.corp/ContinuousDelivery/piper-ita/issues/21 * Adds handling of branchName as an optional parameter * Update resources/metadata/gitopsUpdateDeployment.yaml Feedback about description Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> * Adapt to interface guide * Refactors to GitopsExecRunner * Refactors to GitopsExecRunner in test * Removes unnecessary mocked methods * Adds tests for git utils * Adds new step to CommonStepsTest.groovy * Updates description from yaml * Restricts visibility of methods and interfaces Adds comments where necessary * Updates comments * Fixes URL name * updates description * updates generated file * Fixes compile issue in CommonStepsTest.groovy * Updates long description * Updates test to run green on all kind of OS * Removes global variables from tests * Default branch: master Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> * Typo in Hierarchy Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> * Refactors test to allow parallel execution * Renames utility variable in gitopsUpdateDeployment.go * Renames error variables in gitopsUpdateDeployment.go * simplified parameters for kubectl * Refactors util classes to use parameters rather than global variables * makes username and password mandatory * remove unnecessary mandatory flag * remove new methods from mock that are not necessary * replaces with EqualError * replaces with NoError * update generated file * refactor tests * refactor tests * make tests parallel executable * parallel execution of tests * Refactors interfaces to stop exposing interfaces * Feedback from PR * Simplifies failing mocks * Renames variables and interfaces * Fixes error messages * shorten variable names * Renames unused parameters in tests * Cleanup nil parameters * Typo * Wrap errors and remove unnecessary logs * Remove containername and filePath from GENERAL scope * correct generated file * corrects expected error messages Co-authored-by: OliverNocon <oliver.nocon@sap.com> Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com> Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
This commit is contained in:
parent
5003ac09ae
commit
586044192c
175
cmd/gitopsUpdateDeployment.go
Normal file
175
cmd/gitopsUpdateDeployment.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/command"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/docker"
|
||||||
|
gitUtil "github.com/SAP/jenkins-library/pkg/git"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/log"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/telemetry"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type iGitopsUpdateDeploymentGitUtils interface {
|
||||||
|
CommitSingleFile(filePath, commitMessage string) (plumbing.Hash, error)
|
||||||
|
PushChangesToRepository(username, password string) error
|
||||||
|
PlainClone(username, password, serverURL, directory string) error
|
||||||
|
ChangeBranch(branchName string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitopsUpdateDeploymentFileUtils interface {
|
||||||
|
TempDir(dir, pattern string) (name string, err error)
|
||||||
|
RemoveAll(path string) error
|
||||||
|
FileWrite(path string, content []byte, perm os.FileMode) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitopsUpdateDeploymentExecRunner interface {
|
||||||
|
RunExecutable(executable string, params ...string) error
|
||||||
|
Stdout(out io.Writer)
|
||||||
|
Stderr(err io.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitopsUpdateDeploymentGitUtils struct {
|
||||||
|
worktree *git.Worktree
|
||||||
|
repository *git.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitopsUpdateDeploymentGitUtils) CommitSingleFile(filePath, commitMessage string) (plumbing.Hash, error) {
|
||||||
|
return gitUtil.CommitSingleFile(filePath, commitMessage, g.worktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitopsUpdateDeploymentGitUtils) PushChangesToRepository(username, password string) error {
|
||||||
|
return gitUtil.PushChangesToRepository(username, password, g.repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitopsUpdateDeploymentGitUtils) PlainClone(username, password, serverURL, directory string) error {
|
||||||
|
var err error
|
||||||
|
g.repository, err = gitUtil.PlainClone(username, password, serverURL, directory)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "plain clone failed")
|
||||||
|
}
|
||||||
|
g.worktree, err = g.repository.Worktree()
|
||||||
|
return errors.Wrap(err, "failed to retrieve worktree")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gitopsUpdateDeploymentGitUtils) ChangeBranch(branchName string) error {
|
||||||
|
return gitUtil.ChangeBranch(branchName, g.worktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitopsUpdateDeployment(config gitopsUpdateDeploymentOptions, telemetryData *telemetry.CustomData) {
|
||||||
|
// for command execution use Command
|
||||||
|
var c gitopsUpdateDeploymentExecRunner = &command.Command{}
|
||||||
|
// reroute command output to logging framework
|
||||||
|
c.Stdout(log.Writer())
|
||||||
|
c.Stderr(log.Writer())
|
||||||
|
|
||||||
|
// for http calls import piperhttp "github.com/SAP/jenkins-library/pkg/http"
|
||||||
|
// and use a &piperhttp.Client{} in a custom system
|
||||||
|
// Example: step checkmarxExecuteScan.go
|
||||||
|
|
||||||
|
// error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end
|
||||||
|
err := runGitopsUpdateDeployment(&config, c, &gitopsUpdateDeploymentGitUtils{}, piperutils.Files{})
|
||||||
|
if err != nil {
|
||||||
|
log.Entry().WithError(err).Fatal("step execution failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGitopsUpdateDeployment(config *gitopsUpdateDeploymentOptions, command gitopsUpdateDeploymentExecRunner, gitUtils iGitopsUpdateDeploymentGitUtils, fileUtils gitopsUpdateDeploymentFileUtils) error {
|
||||||
|
temporaryFolder, err := fileUtils.TempDir(".", "temp-")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create temporary directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer fileUtils.RemoveAll(temporaryFolder)
|
||||||
|
|
||||||
|
err = gitUtils.PlainClone(config.Username, config.Password, config.ServerURL, temporaryFolder)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to plain clone repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gitUtils.ChangeBranch(config.BranchName)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to change branch")
|
||||||
|
}
|
||||||
|
|
||||||
|
registryImage, err := buildRegistryPlusImage(config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply kubectl command")
|
||||||
|
}
|
||||||
|
patchString := "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"" + config.ContainerName + "\",\"image\":\"" + registryImage + "\"}]}}}}"
|
||||||
|
|
||||||
|
filePath := filepath.Join(temporaryFolder, config.FilePath)
|
||||||
|
|
||||||
|
kubectlOutputBytes, err := runKubeCtlCommand(command, patchString, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply kubectl command")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fileUtils.FileWrite(filePath, kubectlOutputBytes, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write file")
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := commitAndPushChanges(config, gitUtils)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to commit and push changes")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Entry().Infof("Changes committed with %s", commit.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKubeCtlCommand(command gitopsUpdateDeploymentExecRunner, patchString string, filePath string) ([]byte, error) {
|
||||||
|
var kubectlOutput = bytes.Buffer{}
|
||||||
|
command.Stdout(&kubectlOutput)
|
||||||
|
|
||||||
|
kubeParams := []string{
|
||||||
|
"patch",
|
||||||
|
"--local",
|
||||||
|
"--output=yaml",
|
||||||
|
"--patch=" + patchString,
|
||||||
|
"--filename=" + filePath,
|
||||||
|
}
|
||||||
|
err := command.RunExecutable("kubectl", kubeParams...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to apply kubectl command")
|
||||||
|
}
|
||||||
|
return kubectlOutput.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildRegistryPlusImage(config *gitopsUpdateDeploymentOptions) (string, error) {
|
||||||
|
registryURL := config.ContainerRegistryURL
|
||||||
|
if registryURL == "" {
|
||||||
|
return config.ContainerImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := docker.ContainerRegistryFromURL(registryURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "registry URL could not be extracted")
|
||||||
|
}
|
||||||
|
if url != "" {
|
||||||
|
url = url + "/"
|
||||||
|
}
|
||||||
|
return url + config.ContainerImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commitAndPushChanges(config *gitopsUpdateDeploymentOptions, gitUtils iGitopsUpdateDeploymentGitUtils) (plumbing.Hash, error) {
|
||||||
|
commit, err := gitUtils.CommitSingleFile(config.FilePath, config.CommitMessage)
|
||||||
|
if err != nil {
|
||||||
|
return [20]byte{}, errors.Wrap(err, "committing changes failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gitUtils.PushChangesToRepository(config.Username, config.Password)
|
||||||
|
if err != nil {
|
||||||
|
return [20]byte{}, errors.Wrap(err, "pushing changes failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
}
|
218
cmd/gitopsUpdateDeployment_generated.go
Normal file
218
cmd/gitopsUpdateDeployment_generated.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by piper's step-generator. DO NOT EDIT.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SAP/jenkins-library/pkg/config"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/log"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/telemetry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gitopsUpdateDeploymentOptions struct {
|
||||||
|
BranchName string `json:"branchName,omitempty"`
|
||||||
|
CommitMessage string `json:"commitMessage,omitempty"`
|
||||||
|
ServerURL string `json:"serverUrl,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
FilePath string `json:"filePath,omitempty"`
|
||||||
|
ContainerName string `json:"containerName,omitempty"`
|
||||||
|
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
|
||||||
|
ContainerImage string `json:"containerImage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitopsUpdateDeploymentCommand Updates Kubernetes Deployment Manifest in an Infrastructure Git Repository
|
||||||
|
func GitopsUpdateDeploymentCommand() *cobra.Command {
|
||||||
|
const STEP_NAME = "gitopsUpdateDeployment"
|
||||||
|
|
||||||
|
metadata := gitopsUpdateDeploymentMetadata()
|
||||||
|
var stepConfig gitopsUpdateDeploymentOptions
|
||||||
|
var startTime time.Time
|
||||||
|
|
||||||
|
var createGitopsUpdateDeploymentCmd = &cobra.Command{
|
||||||
|
Use: STEP_NAME,
|
||||||
|
Short: "Updates Kubernetes Deployment Manifest in an Infrastructure Git Repository",
|
||||||
|
Long: `This step allows you to update the deployment manifest for Kubernetes in a git repository.
|
||||||
|
|
||||||
|
It can for example be used for GitOps scenarios where the update of the manifests triggers an update of the corresponding deployment in Kubernetes.
|
||||||
|
|
||||||
|
As of today, it supports the update of deployment yaml files via kubectl patch. The container inside the yaml must be described within the following hierarchy: {"spec":{"template":{"spec":{"containers":[{...}]}}}}`,
|
||||||
|
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
startTime = time.Now()
|
||||||
|
log.SetStepName(STEP_NAME)
|
||||||
|
log.SetVerbose(GeneralConfig.Verbose)
|
||||||
|
|
||||||
|
path, _ := os.Getwd()
|
||||||
|
fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
|
||||||
|
log.RegisterHook(fatalHook)
|
||||||
|
|
||||||
|
err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
|
||||||
|
if err != nil {
|
||||||
|
log.SetErrorCategory(log.ErrorConfiguration)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.RegisterSecret(stepConfig.Username)
|
||||||
|
log.RegisterSecret(stepConfig.Password)
|
||||||
|
|
||||||
|
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
|
||||||
|
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
|
||||||
|
log.RegisterHook(&sentryHook)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
telemetryData := telemetry.CustomData{}
|
||||||
|
telemetryData.ErrorCode = "1"
|
||||||
|
handler := func() {
|
||||||
|
telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
|
||||||
|
telemetryData.ErrorCategory = log.GetErrorCategory().String()
|
||||||
|
telemetry.Send(&telemetryData)
|
||||||
|
}
|
||||||
|
log.DeferExitHandler(handler)
|
||||||
|
defer handler()
|
||||||
|
telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME)
|
||||||
|
gitopsUpdateDeployment(stepConfig, &telemetryData)
|
||||||
|
telemetryData.ErrorCode = "0"
|
||||||
|
log.Entry().Info("SUCCESS")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
addGitopsUpdateDeploymentFlags(createGitopsUpdateDeploymentCmd, &stepConfig)
|
||||||
|
return createGitopsUpdateDeploymentCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func addGitopsUpdateDeploymentFlags(cmd *cobra.Command, stepConfig *gitopsUpdateDeploymentOptions) {
|
||||||
|
cmd.Flags().StringVar(&stepConfig.BranchName, "branchName", `master`, "The name of the branch where the changes should get pushed into.")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.CommitMessage, "commitMessage", `Updated {{containerName}} to version {{containerImage}}`, "The commit message of the commit that will be done to do the changes.")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.ServerURL, "serverUrl", `https://github.com`, "GitHub server url to the repository.")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User name for git authentication")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password/token for git authentication.")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.FilePath, "filePath", os.Getenv("PIPER_filePath"), "Relative path in the git repository to the deployment descriptor file that shall be updated")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.ContainerName, "containerName", os.Getenv("PIPER_containerName"), "The name of the container to update")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "http(s) url of the Container registry where the image is located")
|
||||||
|
cmd.Flags().StringVar(&stepConfig.ContainerImage, "containerImage", os.Getenv("PIPER_containerImage"), "Container image name with version tag to annotate in the deployment configuration.")
|
||||||
|
|
||||||
|
cmd.MarkFlagRequired("commitMessage")
|
||||||
|
cmd.MarkFlagRequired("serverUrl")
|
||||||
|
cmd.MarkFlagRequired("username")
|
||||||
|
cmd.MarkFlagRequired("password")
|
||||||
|
cmd.MarkFlagRequired("filePath")
|
||||||
|
cmd.MarkFlagRequired("containerName")
|
||||||
|
cmd.MarkFlagRequired("containerImage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve step metadata
|
||||||
|
func gitopsUpdateDeploymentMetadata() config.StepData {
|
||||||
|
var theMetaData = config.StepData{
|
||||||
|
Metadata: config.StepMetadata{
|
||||||
|
Name: "gitopsUpdateDeployment",
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
Spec: config.StepSpec{
|
||||||
|
Inputs: config.StepInputs{
|
||||||
|
Parameters: []config.StepParameters{
|
||||||
|
{
|
||||||
|
Name: "branchName",
|
||||||
|
ResourceRef: []config.ResourceReference{},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: false,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "commitMessage",
|
||||||
|
ResourceRef: []config.ResourceReference{},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "serverUrl",
|
||||||
|
ResourceRef: []config.ResourceReference{},
|
||||||
|
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{{Name: "githubServerUrl"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "username",
|
||||||
|
ResourceRef: []config.ResourceReference{
|
||||||
|
{
|
||||||
|
Name: "gitHttpsCredentialsId",
|
||||||
|
Param: "username",
|
||||||
|
Type: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "password",
|
||||||
|
ResourceRef: []config.ResourceReference{
|
||||||
|
{
|
||||||
|
Name: "gitHttpsCredentialsId",
|
||||||
|
Param: "password",
|
||||||
|
Type: "secret",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "filePath",
|
||||||
|
ResourceRef: []config.ResourceReference{},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "containerName",
|
||||||
|
ResourceRef: []config.ResourceReference{},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "containerRegistryUrl",
|
||||||
|
ResourceRef: []config.ResourceReference{
|
||||||
|
{
|
||||||
|
Name: "commonPipelineEnvironment",
|
||||||
|
Param: "container/registryUrl",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: false,
|
||||||
|
Aliases: []config.Alias{{Name: "dockerRegistryUrl"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "containerImage",
|
||||||
|
ResourceRef: []config.ResourceReference{
|
||||||
|
{
|
||||||
|
Name: "commonPipelineEnvironment",
|
||||||
|
Param: "container/imageNameTag",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
|
||||||
|
Type: "string",
|
||||||
|
Mandatory: true,
|
||||||
|
Aliases: []config.Alias{{Name: "image"}, {Name: "containerImageNameTag"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return theMetaData
|
||||||
|
}
|
16
cmd/gitopsUpdateDeployment_generated_test.go
Normal file
16
cmd/gitopsUpdateDeployment_generated_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGitopsUpdateDeploymentCommand(t *testing.T) {
|
||||||
|
|
||||||
|
testCmd := GitopsUpdateDeploymentCommand()
|
||||||
|
|
||||||
|
// only high level testing performed - details are tested in step generation procedure
|
||||||
|
assert.Equal(t, "gitopsUpdateDeployment", testCmd.Use, "command name incorrect")
|
||||||
|
|
||||||
|
}
|
227
cmd/gitopsUpdateDeployment_test.go
Normal file
227
cmd/gitopsUpdateDeployment_test.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildRegistryPlusImage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("build full image", func(t *testing.T) {
|
||||||
|
registryImage, err := buildRegistryPlusImage(&gitopsUpdateDeploymentOptions{
|
||||||
|
ContainerRegistryURL: "https://myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "myregistry.com/myFancyContainer:1337", registryImage)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("without registry", func(t *testing.T) {
|
||||||
|
registryImage, err := buildRegistryPlusImage(&gitopsUpdateDeploymentOptions{
|
||||||
|
ContainerRegistryURL: "",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "myFancyContainer:1337", registryImage)
|
||||||
|
})
|
||||||
|
t.Run("without faulty URL", func(t *testing.T) {
|
||||||
|
_, err := buildRegistryPlusImage(&gitopsUpdateDeploymentOptions{
|
||||||
|
ContainerRegistryURL: "//myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "registry URL could not be extracted: invalid registry url")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunGitopsUpdateDeployment(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("successful run", func(t *testing.T) {
|
||||||
|
var configuration = &gitopsUpdateDeploymentOptions{
|
||||||
|
BranchName: "main",
|
||||||
|
CommitMessage: "This is the commit message",
|
||||||
|
ServerURL: "https://github.com",
|
||||||
|
Username: "admin3",
|
||||||
|
Password: "validAccessToken",
|
||||||
|
FilePath: "dir1/dir2/depl.yaml",
|
||||||
|
ContainerName: "myContainer",
|
||||||
|
ContainerRegistryURL: "https://myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
}
|
||||||
|
|
||||||
|
gitUtilsMock := &validGitUtilsMock{}
|
||||||
|
|
||||||
|
runnerMock := gitOpsExecRunnerMock{}
|
||||||
|
var c gitopsUpdateDeploymentExecRunner = &runnerMock
|
||||||
|
|
||||||
|
err := runGitopsUpdateDeployment(configuration, c, gitUtilsMock, piperutils.Files{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, configuration.BranchName, gitUtilsMock.changedBranch)
|
||||||
|
assert.Equal(t, expectedYaml, gitUtilsMock.savedFile)
|
||||||
|
assert.Equal(t, "kubectl", runnerMock.executable)
|
||||||
|
assert.Equal(t, "patch", runnerMock.kubectlParams[0])
|
||||||
|
assert.Equal(t, "--local", runnerMock.kubectlParams[1])
|
||||||
|
assert.Equal(t, "--output=yaml", runnerMock.kubectlParams[2])
|
||||||
|
assert.Equal(t, `--patch={"spec":{"template":{"spec":{"containers":[{"name":"myContainer","image":"myregistry.com/myFancyContainer:1337"}]}}}}`, runnerMock.kubectlParams[3])
|
||||||
|
assert.True(t, strings.Contains(runnerMock.kubectlParams[4], filepath.Join("dir1/dir2/depl.yaml")))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid URL", func(t *testing.T) {
|
||||||
|
var configuration = &gitopsUpdateDeploymentOptions{
|
||||||
|
BranchName: "main",
|
||||||
|
CommitMessage: "This is the commit message",
|
||||||
|
ServerURL: "https://github.com",
|
||||||
|
Username: "admin3",
|
||||||
|
Password: "validAccessToken",
|
||||||
|
FilePath: "dir1/dir2/depl.yaml",
|
||||||
|
ContainerName: "myContainer",
|
||||||
|
ContainerRegistryURL: "//myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
}
|
||||||
|
|
||||||
|
gitUtilsMock := &validGitUtilsMock{}
|
||||||
|
|
||||||
|
err := runGitopsUpdateDeployment(configuration, nil, gitUtilsMock, piperutils.Files{})
|
||||||
|
assert.EqualError(t, err, "failed to apply kubectl command: registry URL could not be extracted: invalid registry url")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on plane clone", func(t *testing.T) {
|
||||||
|
var configuration = &gitopsUpdateDeploymentOptions{
|
||||||
|
BranchName: "main",
|
||||||
|
CommitMessage: "This is the commit message",
|
||||||
|
ServerURL: "https://github.com",
|
||||||
|
Username: "admin3",
|
||||||
|
Password: "validAccessToken",
|
||||||
|
FilePath: "dir1/dir2/depl.yaml",
|
||||||
|
ContainerName: "myContainer",
|
||||||
|
ContainerRegistryURL: "https://myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := runGitopsUpdateDeployment(configuration, nil, &gitUtilsMockErrorClone{}, piperutils.Files{})
|
||||||
|
assert.EqualError(t, err, "failed to plain clone repository: error on clone")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on temp dir creation", func(t *testing.T) {
|
||||||
|
var configuration = &gitopsUpdateDeploymentOptions{
|
||||||
|
BranchName: "main",
|
||||||
|
CommitMessage: "This is the commit message",
|
||||||
|
ServerURL: "https://github.com",
|
||||||
|
Username: "admin3",
|
||||||
|
Password: "validAccessToken",
|
||||||
|
FilePath: "dir1/dir2/depl.yaml",
|
||||||
|
ContainerName: "myContainer",
|
||||||
|
ContainerRegistryURL: "https://myregistry.com/registry/containers",
|
||||||
|
ContainerImage: "myFancyContainer:1337",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := runGitopsUpdateDeployment(configuration, nil, &gitopsUpdateDeploymentGitUtils{}, filesMockErrorTempDirCreation{})
|
||||||
|
assert.EqualError(t, err, "failed to create temporary directory: error appeared")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitOpsExecRunnerMock struct {
|
||||||
|
out io.Writer
|
||||||
|
kubectlParams []string
|
||||||
|
executable string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *gitOpsExecRunnerMock) Stdout(out io.Writer) {
|
||||||
|
e.out = out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gitOpsExecRunnerMock) Stderr(io.Writer) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *gitOpsExecRunnerMock) RunExecutable(executable string, params ...string) error {
|
||||||
|
e.executable = executable
|
||||||
|
e.kubectlParams = params
|
||||||
|
_, err := e.out.Write([]byte(expectedYaml))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type filesMockErrorTempDirCreation struct{}
|
||||||
|
|
||||||
|
func (c filesMockErrorTempDirCreation) FileWrite(string, []byte, os.FileMode) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filesMockErrorTempDirCreation) TempDir(string, string) (name string, err error) {
|
||||||
|
return "", errors.New("error appeared")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filesMockErrorTempDirCreation) RemoveAll(string) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitUtilsMockErrorClone struct{}
|
||||||
|
|
||||||
|
func (gitUtilsMockErrorClone) CommitSingleFile(string, string) (plumbing.Hash, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gitUtilsMockErrorClone) PushChangesToRepository(string, string) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gitUtilsMockErrorClone) PlainClone(string, string, string, string) error {
|
||||||
|
return errors.New("error on clone")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gitUtilsMockErrorClone) ChangeBranch(string) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gitUtilsMockErrorClone) GetWorktree() (*git.Worktree, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
type validGitUtilsMock struct {
|
||||||
|
savedFile string
|
||||||
|
changedBranch string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (validGitUtilsMock) GetWorktree() (*git.Worktree, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validGitUtilsMock) ChangeBranch(branchName string) error {
|
||||||
|
v.changedBranch = branchName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validGitUtilsMock) CommitSingleFile(string, string) (plumbing.Hash, error) {
|
||||||
|
matches, _ := piperutils.Files{}.Glob("*/dir1/dir2/depl.yaml")
|
||||||
|
fileRead, _ := piperutils.Files{}.FileRead(matches[0])
|
||||||
|
v.savedFile = string(fileRead)
|
||||||
|
return [20]byte{123}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (validGitUtilsMock) PushChangesToRepository(string, string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (validGitUtilsMock) PlainClone(_, _, _, directory string) error {
|
||||||
|
filePath := filepath.Join(directory, "dir1/dir2/depl.yaml")
|
||||||
|
err := piperutils.Files{}.MkdirAll(filepath.Join(directory, "dir1/dir2"), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = piperutils.Files{}.FileWrite(filePath, []byte(existingYaml), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingYaml = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myFancyApp\n labels:\n tier: application\nspec:\n replicas: 4\n selector:\n matchLabels:\n run: myContainer\n template:\n metadata:\n labels:\n run: myContainer\n spec:\n containers:\n - image: myregistry.com/myFancyContainer:1336\n name: myContainer"
|
||||||
|
var expectedYaml = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: myFancyApp\n labels:\n tier: application\nspec:\n replicas: 4\n selector:\n matchLabels:\n run: myContainer\n template:\n metadata:\n labels:\n run: myContainer\n spec:\n containers:\n - image: myregistry.com/myFancyContainer:1337\n name: myContainer"
|
@ -76,6 +76,7 @@ func Execute() {
|
|||||||
rootCmd.AddCommand(GithubCreatePullRequestCommand())
|
rootCmd.AddCommand(GithubCreatePullRequestCommand())
|
||||||
rootCmd.AddCommand(GithubPublishReleaseCommand())
|
rootCmd.AddCommand(GithubPublishReleaseCommand())
|
||||||
rootCmd.AddCommand(GithubSetCommitStatusCommand())
|
rootCmd.AddCommand(GithubSetCommitStatusCommand())
|
||||||
|
rootCmd.AddCommand(GitopsUpdateDeploymentCommand())
|
||||||
rootCmd.AddCommand(CloudFoundryDeleteServiceCommand())
|
rootCmd.AddCommand(CloudFoundryDeleteServiceCommand())
|
||||||
rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand())
|
rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand())
|
||||||
rootCmd.AddCommand(AbapEnvironmentCloneGitRepoCommand())
|
rootCmd.AddCommand(AbapEnvironmentCloneGitRepoCommand())
|
||||||
|
114
pkg/git/git.go
Normal file
114
pkg/git/git.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// utilsWorkTree interface abstraction of git.Worktree to enable tests
|
||||||
|
type utilsWorkTree interface {
|
||||||
|
Add(path string) (plumbing.Hash, error)
|
||||||
|
Commit(msg string, opts *git.CommitOptions) (plumbing.Hash, error)
|
||||||
|
Checkout(opts *git.CheckoutOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilsRepository interface abstraction of git.Repository to enable tests
|
||||||
|
type utilsRepository interface {
|
||||||
|
Worktree() (*git.Worktree, error)
|
||||||
|
Push(o *git.PushOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilsGit interface abstraction of git to enable tests
|
||||||
|
type utilsGit interface {
|
||||||
|
plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitSingleFile Commits the file located in the relative file path with the commitMessage to the given worktree.
|
||||||
|
// In case of errors, the error is returned. In the successful case the commit is provided.
|
||||||
|
func CommitSingleFile(filePath, commitMessage string, worktree *git.Worktree) (plumbing.Hash, error) {
|
||||||
|
return commitSingleFile(filePath, commitMessage, worktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commitSingleFile(filePath, commitMessage string, worktree utilsWorkTree) (plumbing.Hash, error) {
|
||||||
|
_, err := worktree.Add(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return [20]byte{}, errors.Wrap(err, "failed to add file to git")
|
||||||
|
}
|
||||||
|
|
||||||
|
commit, err := worktree.Commit(commitMessage, &git.CommitOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return [20]byte{}, errors.Wrap(err, "failed to commit file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushChangesToRepository Pushes all committed changes in the repository to the remote repository
|
||||||
|
func PushChangesToRepository(username, password string, repository *git.Repository) error {
|
||||||
|
return pushChangesToRepository(username, password, repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushChangesToRepository(username, password string, repository utilsRepository) error {
|
||||||
|
pushOptions := &git.PushOptions{
|
||||||
|
Auth: &http.BasicAuth{Username: username, Password: password},
|
||||||
|
}
|
||||||
|
err := repository.Push(pushOptions)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to push commit")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlainClone Clones a non-bare repository to the provided directory
|
||||||
|
func PlainClone(username, password, serverURL, directory string) (*git.Repository, error) {
|
||||||
|
abstractedGit := &abstractionGit{}
|
||||||
|
return plainClone(username, password, serverURL, directory, abstractedGit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainClone(username, password, serverURL, directory string, abstractionGit utilsGit) (*git.Repository, error) {
|
||||||
|
gitCloneOptions := git.CloneOptions{
|
||||||
|
Auth: &http.BasicAuth{Username: username, Password: password},
|
||||||
|
URL: serverURL,
|
||||||
|
}
|
||||||
|
repository, err := abstractionGit.plainClone(directory, false, &gitCloneOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to clone git")
|
||||||
|
}
|
||||||
|
return repository, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeBranch checkout the provided branch.
|
||||||
|
// It will create a new branch if the branch does not exist yet.
|
||||||
|
// It will checkout "master" if no branch name if provided
|
||||||
|
func ChangeBranch(branchName string, worktree *git.Worktree) error {
|
||||||
|
return changeBranch(branchName, worktree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeBranch(branchName string, worktree utilsWorkTree) error {
|
||||||
|
if branchName == "" {
|
||||||
|
branchName = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkoutOptions = &git.CheckoutOptions{}
|
||||||
|
checkoutOptions.Branch = plumbing.NewBranchReferenceName(branchName)
|
||||||
|
checkoutOptions.Create = false
|
||||||
|
err := worktree.Checkout(checkoutOptions)
|
||||||
|
if err != nil {
|
||||||
|
// branch might not exist, try to create branch
|
||||||
|
checkoutOptions.Create = true
|
||||||
|
err = worktree.Checkout(checkoutOptions)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to checkout branch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type abstractionGit struct{}
|
||||||
|
|
||||||
|
func (abstractionGit) plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error) {
|
||||||
|
return git.PlainClone(path, isBare, o)
|
||||||
|
}
|
214
pkg/git/git_test.go
Normal file
214
pkg/git/git_test.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("successful run", func(t *testing.T) {
|
||||||
|
hash, err := commitSingleFile(".", "message", &WorktreeMock{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, plumbing.Hash([20]byte{4, 5, 6}), hash)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error adding file", func(t *testing.T) {
|
||||||
|
_, err := commitSingleFile(".", "message", WorktreeMockFailing{
|
||||||
|
failingAdd: true,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "failed to add file to git: failed to add file")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error committing file", func(t *testing.T) {
|
||||||
|
_, err := commitSingleFile(".", "message", WorktreeMockFailing{
|
||||||
|
failingCommit: true,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "failed to commit file: failed to commit file")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushChangesToRepository(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("successful push", func(t *testing.T) {
|
||||||
|
err := pushChangesToRepository("user", "password", RepositoryMock{
|
||||||
|
test: t,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error pushing", func(t *testing.T) {
|
||||||
|
err := pushChangesToRepository("user", "password", RepositoryMockError{})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "failed to push commit: error on push commits")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlainClone(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("successful clone", func(t *testing.T) {
|
||||||
|
abstractedGit := &UtilsGitMock{}
|
||||||
|
_, err := plainClone("user", "password", "URL", "directory", abstractedGit)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "directory", abstractedGit.path)
|
||||||
|
assert.False(t, abstractedGit.isBare)
|
||||||
|
assert.Equal(t, "http-basic-auth - user:*******", abstractedGit.authString)
|
||||||
|
assert.Equal(t, "URL", abstractedGit.URL)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on cloning", func(t *testing.T) {
|
||||||
|
abstractedGit := UtilsGitMockError{}
|
||||||
|
_, err := plainClone("user", "password", "URL", "directory", abstractedGit)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "failed to clone git: error during clone")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangeBranch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("checkout existing branch", func(t *testing.T) {
|
||||||
|
worktreeMock := &WorktreeMock{}
|
||||||
|
err := changeBranch("otherBranch", worktreeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(plumbing.NewBranchReferenceName("otherBranch")), worktreeMock.checkedOutBranch)
|
||||||
|
assert.False(t, worktreeMock.create)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty branch defaulted to master", func(t *testing.T) {
|
||||||
|
worktreeMock := &WorktreeMock{}
|
||||||
|
err := changeBranch("", worktreeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(plumbing.NewBranchReferenceName("master")), worktreeMock.checkedOutBranch)
|
||||||
|
assert.False(t, worktreeMock.create)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("create new branch", func(t *testing.T) {
|
||||||
|
err := changeBranch("otherBranch", WorktreeUtilsNewBranch{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on new branch", func(t *testing.T) {
|
||||||
|
err := changeBranch("otherBranch", WorktreeMockFailing{
|
||||||
|
failingCheckout: true,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "failed to checkout branch: failed to checkout branch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoryMock struct {
|
||||||
|
worktree *git.Worktree
|
||||||
|
test *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RepositoryMock) Worktree() (*git.Worktree, error) {
|
||||||
|
if r.worktree != nil {
|
||||||
|
return r.worktree, nil
|
||||||
|
}
|
||||||
|
return &git.Worktree{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RepositoryMock) Push(o *git.PushOptions) error {
|
||||||
|
assert.Equal(r.test, "http-basic-auth - user:*******", o.Auth.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoryMockError struct{}
|
||||||
|
|
||||||
|
func (RepositoryMockError) Worktree() (*git.Worktree, error) {
|
||||||
|
return nil, errors.New("error getting worktree")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RepositoryMockError) Push(*git.PushOptions) error {
|
||||||
|
return errors.New("error on push commits")
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeMockFailing struct {
|
||||||
|
failingAdd bool
|
||||||
|
failingCommit bool
|
||||||
|
failingCheckout bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WorktreeMockFailing) Add(string) (plumbing.Hash, error) {
|
||||||
|
if w.failingAdd {
|
||||||
|
return [20]byte{}, errors.New("failed to add file")
|
||||||
|
}
|
||||||
|
return [20]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WorktreeMockFailing) Commit(string, *git.CommitOptions) (plumbing.Hash, error) {
|
||||||
|
if w.failingCommit {
|
||||||
|
return [20]byte{}, errors.New("failed to commit file")
|
||||||
|
}
|
||||||
|
return [20]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WorktreeMockFailing) Checkout(*git.CheckoutOptions) error {
|
||||||
|
if w.failingCheckout {
|
||||||
|
return errors.New("failed to checkout branch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeMock struct {
|
||||||
|
expectedBranchName string
|
||||||
|
checkedOutBranch string
|
||||||
|
create bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WorktreeMock) Add(string) (plumbing.Hash, error) {
|
||||||
|
return [20]byte{1, 2, 3}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WorktreeMock) Commit(string, *git.CommitOptions) (plumbing.Hash, error) {
|
||||||
|
return [20]byte{4, 5, 6}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WorktreeMock) Checkout(opts *git.CheckoutOptions) error {
|
||||||
|
w.checkedOutBranch = string(opts.Branch)
|
||||||
|
w.create = opts.Create
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorktreeUtilsNewBranch struct{}
|
||||||
|
|
||||||
|
func (WorktreeUtilsNewBranch) Add(string) (plumbing.Hash, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WorktreeUtilsNewBranch) Commit(string, *git.CommitOptions) (plumbing.Hash, error) {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WorktreeUtilsNewBranch) Checkout(opts *git.CheckoutOptions) error {
|
||||||
|
if opts.Create {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("branch already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
type UtilsGitMock struct {
|
||||||
|
path string
|
||||||
|
isBare bool
|
||||||
|
authString string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UtilsGitMock) plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error) {
|
||||||
|
u.path = path
|
||||||
|
u.isBare = isBare
|
||||||
|
u.authString = o.Auth.String()
|
||||||
|
u.URL = o.URL
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type UtilsGitMockError struct{}
|
||||||
|
|
||||||
|
func (UtilsGitMockError) plainClone(string, bool, *git.CloneOptions) (*git.Repository, error) {
|
||||||
|
return nil, errors.New("error during clone")
|
||||||
|
}
|
@ -28,6 +28,11 @@ type FileUtils interface {
|
|||||||
type Files struct {
|
type Files struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TempDir creates a temporary directory
|
||||||
|
func (f Files) TempDir(dir, pattern string) (name string, err error) {
|
||||||
|
return ioutil.TempDir(dir, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
// FileExists returns true if the file system entry for the given path exists and is not a directory.
|
// FileExists returns true if the file system entry for the given path exists and is not a directory.
|
||||||
func (f Files) FileExists(filename string) (bool, error) {
|
func (f Files) FileExists(filename string) (bool, error) {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
|
115
resources/metadata/gitopsUpdateDeployment.yaml
Normal file
115
resources/metadata/gitopsUpdateDeployment.yaml
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
metadata:
|
||||||
|
name: gitopsUpdateDeployment
|
||||||
|
description: Updates Kubernetes Deployment Manifest in an Infrastructure Git Repository
|
||||||
|
longDescription: |
|
||||||
|
This step allows you to update the deployment manifest for Kubernetes in a git repository.
|
||||||
|
|
||||||
|
It can for example be used for GitOps scenarios where the update of the manifests triggers an update of the corresponding deployment in Kubernetes.
|
||||||
|
|
||||||
|
As of today, it supports the update of deployment yaml files via kubectl patch. The container inside the yaml must be described within the following hierarchy: {"spec":{"template":{"spec":{"containers":[{...}]}}}}
|
||||||
|
spec:
|
||||||
|
inputs:
|
||||||
|
secrets:
|
||||||
|
- name: gitHttpsCredentialsId
|
||||||
|
description: Jenkins 'Username with password' credentials ID containing username/password for http access to your git repository.
|
||||||
|
type: jenkins
|
||||||
|
params:
|
||||||
|
- name: branchName
|
||||||
|
description: The name of the branch where the changes should get pushed into.
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
type: string
|
||||||
|
default: master
|
||||||
|
- name: commitMessage
|
||||||
|
description: The commit message of the commit that will be done to do the changes.
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
type: string
|
||||||
|
mandatory: true
|
||||||
|
default: Updated {{containerName}} to version {{containerImage}}
|
||||||
|
- name: serverUrl
|
||||||
|
aliases:
|
||||||
|
- name: githubServerUrl
|
||||||
|
description: GitHub server url to the repository.
|
||||||
|
scope:
|
||||||
|
- GENERAL
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
type: string
|
||||||
|
default: https://github.com
|
||||||
|
mandatory: true
|
||||||
|
- name: username
|
||||||
|
type: string
|
||||||
|
description: User name for git authentication
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
mandatory: true
|
||||||
|
secret: true
|
||||||
|
resourceRef:
|
||||||
|
- name: gitHttpsCredentialsId
|
||||||
|
type: secret
|
||||||
|
param: username
|
||||||
|
- name: password
|
||||||
|
type: string
|
||||||
|
description: Password/token for git authentication.
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
mandatory: true
|
||||||
|
secret: true
|
||||||
|
resourceRef:
|
||||||
|
- name: gitHttpsCredentialsId
|
||||||
|
type: secret
|
||||||
|
param: password
|
||||||
|
- name: filePath
|
||||||
|
description: Relative path in the git repository to the deployment descriptor file that shall be updated
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
type: string
|
||||||
|
mandatory: true
|
||||||
|
- name: containerName
|
||||||
|
description: The name of the container to update
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
type: string
|
||||||
|
mandatory: true
|
||||||
|
- name: containerRegistryUrl
|
||||||
|
aliases:
|
||||||
|
- name: dockerRegistryUrl
|
||||||
|
type: string
|
||||||
|
description: http(s) url of the Container registry where the image is located
|
||||||
|
scope:
|
||||||
|
- GENERAL
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
||||||
|
resourceRef:
|
||||||
|
- name: commonPipelineEnvironment
|
||||||
|
param: container/registryUrl
|
||||||
|
- name: containerImage
|
||||||
|
aliases:
|
||||||
|
- name: image
|
||||||
|
deprecated: true
|
||||||
|
- name: containerImageNameTag
|
||||||
|
type: string
|
||||||
|
description: Container image name with version tag to annotate in the deployment configuration.
|
||||||
|
resourceRef:
|
||||||
|
- name: commonPipelineEnvironment
|
||||||
|
param: container/imageNameTag
|
||||||
|
mandatory: true
|
||||||
|
scope:
|
||||||
|
- PARAMETERS
|
||||||
|
- STAGES
|
||||||
|
- STEPS
|
@ -164,7 +164,8 @@ public class CommonStepsTest extends BasePiperTest{
|
|||||||
'gctsDeploy', //implementing new golang pattern without fields
|
'gctsDeploy', //implementing new golang pattern without fields
|
||||||
'containerSaveImage', //implementing new golang pattern without fields
|
'containerSaveImage', //implementing new golang pattern without fields
|
||||||
'detectExecuteScan', //implementing new golang pattern without fields
|
'detectExecuteScan', //implementing new golang pattern without fields
|
||||||
'kanikoExecute' //implementing new golang pattern without fields
|
'kanikoExecute', //implementing new golang pattern without fields
|
||||||
|
'gitopsUpdateDeployment' //implementing new golang pattern without fields
|
||||||
]
|
]
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
11
vars/gitopsUpdateDeployment.groovy
Normal file
11
vars/gitopsUpdateDeployment.groovy
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import groovy.transform.Field
|
||||||
|
|
||||||
|
@Field String STEP_NAME = getClass().getName()
|
||||||
|
@Field String METADATA_FILE = 'metadata/gitopsUpdateDeployment.yaml'
|
||||||
|
|
||||||
|
void call(Map parameters = [:]) {
|
||||||
|
List credentials = [
|
||||||
|
[type: 'usernamePassword', id: 'gitHttpsCredentialsId', env: ['PIPER_username', 'PIPER_password']],
|
||||||
|
]
|
||||||
|
piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user