@@ -2,16 +2,223 @@ package com.cloudogu.gitops.gitopsbuildlib
2
2
3
3
class GitOps implements Serializable {
4
4
private script
5
- private cesBuidLib
5
+ private cesBuildLib
6
+ private scmManagerCredentials
7
+ private application
6
8
7
- GitOps (script , cesBuildLibRepo , cesBuildLibVersion ) {
9
+ GitOps (script , application , cesBuildLibRepo , cesBuildLibVersion , scmManagerCredentials ) {
8
10
this . script = script
11
+ this . application = application
9
12
initCesBuildLib(cesBuildLibRepo, cesBuildLibVersion)
13
+ this . scmManagerCredentials = scmManagerCredentials
10
14
}
11
15
12
16
private initCesBuildLib (cesBuildLibRepo , cesBuildLibVersion ) {
13
- this . cesBuidLib = script. library(identifier : " ces-build-lib@${ cesBuildLibVersion} " ,
17
+ this . cesBuildLib = script. library(identifier : " ces-build-lib@${ cesBuildLibVersion} " ,
14
18
retriever : script. modernSCM([$class : ' GitSCMSource' , remote : cesBuildLibRepo])
15
19
). com. cloudogu. ces. cesbuildlib
16
20
}
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 + = " \n Image: ${ 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
+ }
17
224
}
0 commit comments