mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2025-07-20 14:55:50 +00:00
Implements gitops flow
Signed-off-by: Philipp Markiewka <philipp.markiewka@cloudogu.com>
This commit is contained in:
parent
9b835de50d
commit
f8c219e32a
6 changed files with 263 additions and 24 deletions
208
Jenkinsfile
vendored
208
Jenkinsfile
vendored
|
@ -3,22 +3,10 @@
|
||||||
// "Constants"
|
// "Constants"
|
||||||
String getCesBuildLibVersion() { '1.44.3' }
|
String getCesBuildLibVersion() { '1.44.3' }
|
||||||
String getCesBuildLibRepo() { 'https://github.com/cloudogu/ces-build-lib/' }
|
String getCesBuildLibRepo() { 'https://github.com/cloudogu/ces-build-lib/' }
|
||||||
String getAppDockerRegistry() { 'eu.gcr.io/cloudogu-backend' }
|
String getScmManagerCredentials() { 'scmm-user' }
|
||||||
String getCloudoguDockerRegistry() { 'ecosystem.cloudogu.com' }
|
String getConfigRepositoryUrl() { "http://scmm-scm-manager:9091/scm/repo/cluster/gitops" }
|
||||||
String getRegistryCredentials() { 'jenkins' }
|
String getConfigRepositoryPRUrl() { 'http://scmm-scm-manager:9091/scm/api/v2/pull-requests/cluster/gitops' }
|
||||||
String getScmManagerCredentials() { 'jenkins' }
|
String getDockerRegistryBaseUrl() { "localhost:9092" }
|
||||||
String getKubectlImage() { 'lachlanevenson/k8s-kubectl:v1.18.2' }
|
|
||||||
String getJdkImage() { 'bellsoft/liberica-openjdk-alpine' }
|
|
||||||
String getHelmImage() { "${cloudoguDockerRegistry}/build-images/helm-kubeval:gcl308.0.0-alpine-helmv3.3.1" }
|
|
||||||
String getKubevalImage() { 'garethr/kubeval:0.15.0' }
|
|
||||||
String getGitVersion() { '2.24.3' }
|
|
||||||
String getConfigRepositoryUrl() { "https://ecosystem.cloudogu.com/scm/repo/backend/k8s-gitops" }
|
|
||||||
String getConfigRepositoryPRUrl() { 'ecosystem.cloudogu.com/scm/api/v2/pull-requests/backend/k8s-gitops' }
|
|
||||||
String getDockerRegistryBaseUrl() { "https://console.cloud.google.com/gcr/images/cloudogu-backend/eu/${application}.cloudogu.com" }
|
|
||||||
String getHelmReleaseChartSourceUrl() { "ssh://k8s-git-ops@ecosystem.cloudogu.com:2222/repo/backend/myCloudogu-helm-chart" }
|
|
||||||
// Redundant definition of helm repo, because Flux connects via SSH and Jenkins via HTTPS. Best would be to use SSH on Jenkins -> No redundancy
|
|
||||||
String getHelmChartRepoSourceUrl() { "https://ecosystem.cloudogu.com/scm/repo/backend/myCloudogu-helm-chart" }
|
|
||||||
String getHelmChartTag() { "1.0.4" }
|
|
||||||
|
|
||||||
properties([
|
properties([
|
||||||
// Don't run concurrent builds, because the ITs use the same port causing random failures on concurrent builds.
|
// Don't run concurrent builds, because the ITs use the same port causing random failures on concurrent builds.
|
||||||
|
@ -34,19 +22,23 @@ node {
|
||||||
|
|
||||||
mvn = cesBuilbLib.MavenWrapper.new(this)
|
mvn = cesBuilbLib.MavenWrapper.new(this)
|
||||||
|
|
||||||
|
application = "spring-petclinic-plain"
|
||||||
|
|
||||||
catchError {
|
catchError {
|
||||||
|
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
checkout scm
|
checkout scm
|
||||||
|
git.clean('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
mvn 'clean package -DskipTests'
|
mvn 'clean package -DskipTests'
|
||||||
|
|
||||||
archiveArtifacts artifacts: '**/target/*.jar'
|
archiveArtifacts artifacts: '**/target/*.jar'
|
||||||
}
|
}
|
||||||
|
|
||||||
String jacoco = "org.jacoco:jacoco-maven-plugin:0.8.1"
|
String jacoco = "org.jacoco:jacoco-maven-plugin:0.8.5"
|
||||||
|
|
||||||
stage('Test') {
|
stage('Test') {
|
||||||
mvn "${jacoco}:prepare-agent test ${jacoco}:report"
|
mvn "${jacoco}:prepare-agent test ${jacoco}:report"
|
||||||
|
@ -60,24 +52,42 @@ node {
|
||||||
|
|
||||||
stage('Static Code Analysis') {
|
stage('Static Code Analysis') {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String imageName = ""
|
||||||
stage('Docker') {
|
stage('Docker') {
|
||||||
|
String imageTag = createImageTag()
|
||||||
|
imageName = "${dockerRegistryBaseUrl}/${application}:${imageTag}"
|
||||||
|
mvn "spring-boot:build-image -DskipTests -Dspring-boot.build-image.imageName=${imageName}"
|
||||||
|
|
||||||
if (isBuildSuccessful()) {
|
if (isBuildSuccessful()) {
|
||||||
def docker = cesBuilbLib.Docker.new(this)
|
def docker = cesBuilbLib.Docker.new(this)
|
||||||
|
// The docker daemon cant use the k8s service name, because it is not running inside the cluster
|
||||||
String imageTag = createImageTag()
|
docker.withRegistry("http://${dockerRegistryBaseUrl}") {
|
||||||
imageName = "docker-registry:9092/petclinic-plain:${imageTag}"
|
def image = docker.image(imageName)
|
||||||
def dockerImage = docker.build(imageName)
|
image.push()
|
||||||
dockerImage.push()
|
}
|
||||||
} else {
|
} else {
|
||||||
echo 'Skipping docker push, because build not successful or neither on develop branch nor "forceDeployStaging" param set'
|
echo 'Skipping docker push, because build not successful'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Deploy') {
|
stage('Deploy') {
|
||||||
|
if (isBuildSuccessful()) {
|
||||||
|
|
||||||
|
def gitopsConfig = [
|
||||||
|
scmmCredentialsId: scmManagerCredentials,
|
||||||
|
scmmConfigRepoUrl: configRepositoryUrl,
|
||||||
|
scmmPullRequestUrl: configRepositoryPRUrl,
|
||||||
|
updateImages: [
|
||||||
|
[ deploymentFilename: "deployment.yaml",
|
||||||
|
containerName: "spring-petclinic-plain",
|
||||||
|
imageName: imageName ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
pushToConfigRepo(gitopsConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +95,98 @@ node {
|
||||||
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml'
|
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String pushToConfigRepo(Map gitopsConfig) {
|
||||||
|
|
||||||
|
def git = cesBuilbLib.Git.new(this, scmManagerCredentials)
|
||||||
|
def applicationRepo = GitRepo.create(git)
|
||||||
|
def changesOnGitOpsRepo = ''
|
||||||
|
|
||||||
|
git.committerName = 'Jenkins'
|
||||||
|
git.committerEmail = 'jenkins@cloudogu.com'
|
||||||
|
|
||||||
|
def configRepoTempDir = '.configRepoTempDir'
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
dir(configRepoTempDir) {
|
||||||
|
|
||||||
|
git url: gitopsConfig.scmmConfigRepoUrl, branch: 'master', changelog: false, poll: false
|
||||||
|
git.fetch()
|
||||||
|
|
||||||
|
def repoChanges = new HashSet<String>()
|
||||||
|
repoChanges += createApplicationForStageAndPushToBranch 'staging', 'master', applicationRepo, git, gitopsConfig
|
||||||
|
|
||||||
|
git.checkoutOrCreate(application)
|
||||||
|
repoChanges += createApplicationForStageAndPushToBranch 'production', application, applicationRepo, git, gitopsConfig
|
||||||
|
|
||||||
|
changesOnGitOpsRepo = aggregateChangesOnGitOpsRepo(repoChanges)
|
||||||
|
|
||||||
|
if (changesOnGitOpsRepo) {
|
||||||
|
createPullRequest(gitopsConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sh "rm -rf ${configRepoTempDir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (changesOnGitOpsRepo) {
|
||||||
|
// with GitOps we can only add a deployment marker for staging yet
|
||||||
|
// addDeploymentAnnotationToGrafana("staging")
|
||||||
|
}
|
||||||
|
|
||||||
|
return changesOnGitOpsRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
private String aggregateChangesOnGitOpsRepo(changes) {
|
||||||
|
// Remove empty
|
||||||
|
(changes - '')
|
||||||
|
// and concat into string
|
||||||
|
.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
String createApplicationForStageAndPushToBranch(String stage, String branch, GitRepo applicationRepo, def git, Map gitopsConfig) {
|
||||||
|
|
||||||
|
String commitPrefix = stage == 'staging' ? '[S] ' : ''
|
||||||
|
|
||||||
|
sh "mkdir -p ${stage}/${application}"
|
||||||
|
// copy extra resources like sealed secrets
|
||||||
|
echo "Copying k8s payload from application repo to gitOps Repo: 'k8s/${stage}/*' to '${stage}/${application}'"
|
||||||
|
sh "cp ${env.WORKSPACE}/k8s/${stage}/* ${stage}/${application}/ || true"
|
||||||
|
|
||||||
|
gitopsConfig.updateImages.each {
|
||||||
|
updateImageVersion("${stage}/${application}/${it['deploymentFilename']}", it['containerName'], it['imageName'])
|
||||||
|
}
|
||||||
|
|
||||||
|
git.add('.')
|
||||||
|
if (git.areChangesStagedForCommit()) {
|
||||||
|
git.commit(commitPrefix + createApplicationCommitMessage(git, applicationRepo), applicationRepo.authorName, applicationRepo.authorEmail)
|
||||||
|
|
||||||
|
// If some else pushes between the pull above and this push, the build will fail.
|
||||||
|
// So we pull if push fails and try again
|
||||||
|
git.pushAndPullOnFailure("origin ${branch}")
|
||||||
|
return "${stage} (${git.commitHashShort})"
|
||||||
|
} else {
|
||||||
|
echo "No changes on gitOps repo for ${stage} (branch: ${branch}). Not committing or pushing."
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflect build parameters in commit message used for GitOps.
|
||||||
|
* This is meant to bring more transparency into GitOps repo.
|
||||||
|
*/
|
||||||
|
String createApplicationCommitMessage(def git, def applicationRepo) {
|
||||||
|
String issueIds = (applicationRepo.commitMessage =~ /#\d*/).collect { "${it} " }.join('')
|
||||||
|
|
||||||
|
String[] urlSplit = applicationRepo.repositoryUrl.split('/')
|
||||||
|
def repoNamespace = urlSplit[-2]
|
||||||
|
def repoName = urlSplit[-1]
|
||||||
|
String message = "${issueIds}${repoNamespace}/${repoName}@${applicationRepo.commitHash}"
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reflect build parameters version name.
|
* Reflect build parameters version name.
|
||||||
|
@ -101,3 +203,61 @@ String createImageTag() {
|
||||||
|
|
||||||
return "${new Date().format('yyyyMMddHHmm')}-${git.commitHashShort}${branchSuffix}"
|
return "${new Date().format('yyyyMMddHHmm')}-${git.commitHashShort}${branchSuffix}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateImageVersion(String deploymentFilePath, String containerName, String newImageTag) {
|
||||||
|
def data = readYaml file: deploymentFilePath
|
||||||
|
def containers = data.spec.template.spec.containers
|
||||||
|
def updateContainer = containers.find {it.name == containerName}
|
||||||
|
updateContainer.image = newImageTag
|
||||||
|
writeYaml file: deploymentFilePath, data: data, overwrite: true
|
||||||
|
}
|
||||||
|
|
||||||
|
void createPullRequest(Map gitopsConfig) {
|
||||||
|
|
||||||
|
withCredentials([usernamePassword(credentialsId: gitopsConfig.scmmCredentialsId, passwordVariable: 'GIT_PASSWORD', usernameVariable: 'GIT_USER')]) {
|
||||||
|
|
||||||
|
String script =
|
||||||
|
'curl -s -o /dev/null -w "%{http_code}" ' +
|
||||||
|
"-u ${GIT_USER}:${GIT_PASSWORD} " +
|
||||||
|
'-H "Content-Type: application/vnd.scmm-pullRequest+json;v=2" ' +
|
||||||
|
'--data \'{"title": "created by service ' + application + '", "source": "' + application + '", "target": "master"}\' ' +
|
||||||
|
gitopsConfig.scmmPullRequestUrl
|
||||||
|
|
||||||
|
// For debugging the quotation of the shell script, just do: echo script
|
||||||
|
String http_code = sh returnStdout: true, script: script
|
||||||
|
|
||||||
|
// At this point we could write a mail to the last committer that his commit triggered a new or updated GitOps PR
|
||||||
|
|
||||||
|
echo "http_code: ${http_code}"
|
||||||
|
// PR exists if we get 409
|
||||||
|
if (http_code != "201" && http_code != "409") {
|
||||||
|
unstable 'Could not create pull request'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String application
|
||||||
|
def cesBuilbLib
|
||||||
|
|
||||||
|
class GitRepo {
|
||||||
|
|
||||||
|
static GitRepo create(git) {
|
||||||
|
// Constructors can't be used in Jenkins pipelines due to CPS
|
||||||
|
// https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/#constructors
|
||||||
|
return new GitRepo(git.commitAuthorName, git.commitAuthorEmail ,git.commitHashShort, git.commitMessage, git.repositoryUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
GitRepo(String authorName, String authorEmail, String commitHash, String commitMessage, String repositoryUrl) {
|
||||||
|
this.authorName = authorName
|
||||||
|
this.authorEmail = authorEmail
|
||||||
|
this.commitHash = commitHash
|
||||||
|
this.commitMessage = commitMessage
|
||||||
|
this.repositoryUrl = repositoryUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
final String authorName
|
||||||
|
final String authorEmail
|
||||||
|
final String commitHash
|
||||||
|
final String commitMessage
|
||||||
|
final String repositoryUrl
|
||||||
|
}
|
||||||
|
|
23
k8s/production/deployment.yaml
Normal file
23
k8s/production/deployment.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: spring-petclinic-plain
|
||||||
|
namespace: production
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: spring-petclinic-plain
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: spring-petclinic-plain
|
||||||
|
image: localhost:9092/petclinic-plain:1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: SOME_ENV
|
||||||
|
value: "Some Value 12345"
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: spring-petclinic-plain
|
16
k8s/production/service.yaml
Normal file
16
k8s/production/service.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
namespace: production
|
||||||
|
name: spring-petclinic-plain
|
||||||
|
labels:
|
||||||
|
app: spring-petclinic-plain
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- port: 9094
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: spring-petclinic-plain
|
23
k8s/staging/deployment.yaml
Normal file
23
k8s/staging/deployment.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: spring-petclinic-plain
|
||||||
|
namespace: staging
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: spring-petclinic-plain
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: spring-petclinic-plain
|
||||||
|
image: localhost:9092/petclinic-plain:1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: SOME_ENV
|
||||||
|
value: "Some Value 12345"
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: spring-petclinic-plain
|
16
k8s/staging/service.yaml
Normal file
16
k8s/staging/service.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
namespace: staging
|
||||||
|
name: spring-petclinic-plain
|
||||||
|
labels:
|
||||||
|
app: spring-petclinic-plain
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
ports:
|
||||||
|
- port: 9093
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: spring-petclinic-plain
|
1
pom.xml
1
pom.xml
|
@ -241,6 +241,7 @@
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
<verbose>true</verbose>
|
<verbose>true</verbose>
|
||||||
<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
|
<dateFormat>yyyy-MM-dd'T'HH:mm:ssZ</dateFormat>
|
||||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||||
|
|
Loading…
Reference in a new issue