Skip to content

Commit 331edab

Browse files
committed
implemented gitops process
Signed-off-by: Marek Markiewka <[email protected]>
1 parent 506b41e commit 331edab

File tree

1 file changed

+210
-3
lines changed

1 file changed

+210
-3
lines changed

src/com/cloudogu/gitops/gitopsbuildlib/GitOps.groovy

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,223 @@ package com.cloudogu.gitops.gitopsbuildlib
22

33
class GitOps implements Serializable{
44
private script
5-
private cesBuidLib
5+
private cesBuildLib
6+
private scmManagerCredentials
7+
private application
68

7-
GitOps(script, cesBuildLibRepo, cesBuildLibVersion) {
9+
GitOps(script, application, cesBuildLibRepo, cesBuildLibVersion, scmManagerCredentials) {
810
this.script = script
11+
this.application = application
912
initCesBuildLib(cesBuildLibRepo, cesBuildLibVersion)
13+
this.scmManagerCredentials = scmManagerCredentials
1014
}
1115

1216
private initCesBuildLib(cesBuildLibRepo, cesBuildLibVersion) {
13-
this.cesBuidLib = script.library(identifier: "ces-build-lib@${cesBuildLibVersion}",
17+
this.cesBuildLib = script.library(identifier: "ces-build-lib@${cesBuildLibVersion}",
1418
retriever: script.modernSCM([$class: 'GitSCMSource', remote: cesBuildLibRepo])
1519
).com.cloudogu.ces.cesbuildlib
1620
}
21+
22+
String createBuildDescription(String pushedChanges, String imageName) {
23+
String description = ''
24+
25+
description += "GitOps commits: "
26+
27+
if (pushedChanges) {
28+
description += pushedChanges
29+
} else {
30+
description += 'No changes'
31+
}
32+
33+
description += "\nImage: ${imageName}"
34+
35+
return description
36+
}
37+
38+
String createImageTag() {
39+
def git = cesBuildLib.Git.new(this)
40+
String branch = git.simpleBranchName
41+
String branchSuffix = ""
42+
43+
if (!"develop".equals(branch)) {
44+
branchSuffix = "-${branch}"
45+
}
46+
47+
return "${new Date().format('yyyyMMddHHmm')}-${git.commitHashShort}${branchSuffix}"
48+
}
49+
50+
String pushToConfigRepo(Map gitopsConfig) {
51+
52+
def git = cesBuildLib.Git.new(this, scmManagerCredentials)
53+
def changesOnGitOpsRepo = ''
54+
55+
// Query and store info about application repo before cloning into gitops repo
56+
def applicationRepo = GitRepo.create(git)
57+
58+
// Display that Jenkins made the GitOps commits not the application repo author
59+
git.committerName = 'Jenkins'
60+
git.committerEmail = '[email protected]'
61+
62+
def configRepoTempDir = '.configRepoTempDir'
63+
64+
try {
65+
66+
dir(configRepoTempDir) {
67+
68+
git url: gitopsConfig.scmmConfigRepoUrl, branch: gitopsConfig.mainBranch, changelog: false, poll: false
69+
git.fetch()
70+
71+
def allRepoChanges = new HashSet<String>()
72+
73+
gitopsConfig.stages.each{ stage, config ->
74+
//checkout the main_branch before creating a new stage_branch. so it won't be branched off of an already checked out stage_branch
75+
git.checkoutOrCreate(mainBranch)
76+
77+
if(config.deployDirectly) {
78+
allRepoChanges += createApplicationForStageAndPushToBranch stage as String, mainBranch, applicationRepo, git, gitopsConfig
79+
} else {
80+
String stageBranch = "${stage}_${application}"
81+
git.checkoutOrCreate(stageBranch)
82+
String repoChanges = createApplicationForStageAndPushToBranch stage as String, stageBranch, applicationRepo, git, gitopsConfig
83+
if(repoChanges) {
84+
createPullRequest(gitopsConfig, stage as String, stageBranch)
85+
allRepoChanges += repoChanges
86+
}
87+
}
88+
}
89+
changesOnGitOpsRepo = aggregateChangesOnGitOpsRepo(allRepoChanges)
90+
}
91+
} finally {
92+
sh "rm -rf ${configRepoTempDir}"
93+
}
94+
95+
return changesOnGitOpsRepo
96+
}
97+
98+
private String createApplicationForStageAndPushToBranch(String stage, String branch, GitRepo applicationRepo, def git, Map gitopsConfig) {
99+
100+
String commitPrefix = "[${stage}] "
101+
102+
sh "mkdir -p ${stage}/${application}/"
103+
sh "mkdir -p ${configDir}/"
104+
// copy extra resources like sealed secrets
105+
echo "Copying k8s payload from application repo to gitOps Repo: 'k8s/${stage}/*' to '${stage}/${application}'"
106+
sh "cp ${env.WORKSPACE}/k8s/${stage}/* ${stage}/${application}/ || true"
107+
sh "cp ${env.WORKSPACE}/*.yamllint.yaml ${configDir}/ || true"
108+
109+
validateK8sRessources("${stage}/${application}/", k8sVersion)
110+
validateYamlResources("${configDir}/config.yamllint.yaml", "${stage}/${application}/")
111+
112+
gitopsConfig.updateImages.each {
113+
updateImageVersion("${stage}/${application}/${it['deploymentFilename']}", it['containerName'], it['imageName'])
114+
}
115+
116+
git.add('.')
117+
if (git.areChangesStagedForCommit()) {
118+
git.commit(commitPrefix + createApplicationCommitMessage(git, applicationRepo), applicationRepo.authorName, applicationRepo.authorEmail)
119+
120+
// If some else pushes between the pull above and this push, the build will fail.
121+
// So we pull if push fails and try again
122+
git.pushAndPullOnFailure("origin ${branch}")
123+
return "${stage} (${git.commitHashShort})"
124+
} else {
125+
echo "No changes on gitOps repo for ${stage} (branch: ${branch}). Not committing or pushing."
126+
return ''
127+
}
128+
}
129+
130+
private String aggregateChangesOnGitOpsRepo(changes) {
131+
// Remove empty
132+
(changes - '')
133+
// and concat into string
134+
.join('; ')
135+
}
136+
137+
private String createApplicationCommitMessage(def git, def applicationRepo) {
138+
String issueIds = (applicationRepo.commitMessage =~ /#\d*/).collect { "${it} " }.join('')
139+
140+
String[] urlSplit = applicationRepo.repositoryUrl.split('/')
141+
def repoNamespace = urlSplit[-2]
142+
def repoName = urlSplit[-1]
143+
String message = "${issueIds}${repoNamespace}/${repoName}@${applicationRepo.commitHash}"
144+
145+
return message
146+
}
147+
148+
private void createPullRequest(Map gitopsConfig, String stage, String sourceBranch) {
149+
150+
withCredentials([usernamePassword(credentialsId: gitopsConfig.scmmCredentialsId, passwordVariable: 'GIT_PASSWORD', usernameVariable: 'GIT_USER')]) {
151+
152+
String script =
153+
'curl -s -o /dev/null -w "%{http_code}" ' +
154+
"-u ${GIT_USER}:${GIT_PASSWORD} " +
155+
'-H "Content-Type: application/vnd.scmm-pullRequest+json;v=2" ' +
156+
'--data \'{"title": "created by service ' + application + ' for stage ' + stage + '", "source": "' + sourceBranch + '", "target": "' + mainBranch + '"}\' ' +
157+
gitopsConfig.scmmPullRequestUrl
158+
159+
// For debugging the quotation of the shell script, just do: echo script
160+
String http_code = sh returnStdout: true, script: script
161+
162+
// At this point we could write a mail to the last committer that his commit triggered a new or updated GitOps PR
163+
164+
echo "http_code: ${http_code}"
165+
// PR exists if we get 409
166+
if (http_code != "201" && http_code != "409") {
167+
unstable 'Could not create pull request'
168+
}
169+
}
170+
}
171+
172+
private void updateImageVersion(String deploymentFilePath, String containerName, String newImageTag) {
173+
def data = readYaml file: deploymentFilePath
174+
def containers = data.spec.template.spec.containers
175+
def updateContainer = containers.find {it.name == containerName}
176+
updateContainer.image = newImageTag
177+
writeYaml file: deploymentFilePath, data: data, overwrite: true
178+
}
179+
180+
// Validates all yaml-resources within the target-directory against the specs of the given k8s version
181+
private void validateK8sRessources(String targetDirectory, String k8sVersion) {
182+
withDockerImage(helmImage) {
183+
result = sh returnStdout: true, script: "kubeval -d ${targetDirectory} -v ${k8sVersion} --strict"
184+
}
185+
}
186+
187+
private void validateYamlResources(String configFile, String targetDirectory) {
188+
withDockerImage(yamlLintImage) {
189+
sh returnStdout: true, script: "yamllint -c ${configFile} ${targetDirectory}"
190+
}
191+
}
192+
193+
private void withDockerImage(String image, Closure body) {
194+
cesBuildLib.Docker.new(this).image(image)
195+
// Allow accessing WORKSPACE even when we are in a child dir (using "dir() {}")
196+
.inside("${pwd().equals(env.WORKSPACE) ? '' : "-v ${env.WORKSPACE}:${env.WORKSPACE}"}") {
197+
body()
198+
}
199+
}
200+
201+
/** Queries and stores info about current repo and HEAD commit */
202+
class GitRepo {
203+
204+
static GitRepo create(git) {
205+
// Constructors can't be used in Jenkins pipelines due to CPS
206+
// https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/#constructors
207+
return new GitRepo(git.commitAuthorName, git.commitAuthorEmail ,git.commitHashShort, git.commitMessage, git.repositoryUrl)
208+
}
209+
210+
GitRepo(String authorName, String authorEmail, String commitHash, String commitMessage, String repositoryUrl) {
211+
this.authorName = authorName
212+
this.authorEmail = authorEmail
213+
this.commitHash = commitHash
214+
this.commitMessage = commitMessage
215+
this.repositoryUrl = repositoryUrl
216+
}
217+
218+
final String authorName
219+
final String authorEmail
220+
final String commitHash
221+
final String commitMessage
222+
final String repositoryUrl
223+
}
17224
}

0 commit comments

Comments
 (0)