diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000000000000000000000000000000000000..53861d6861aef7b56661aefe6ee7a783a919fd90 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,292 @@ +pipeline { + agent { + node { + label 'ozgcloud-jenkins-build-agent-jdk21' + } + } + + environment { + RELEASE_REGEX = /\d+.\d+.\d+/ + SNAPSHOT_REGEX = /\d+.\d+.\d+-SNAPSHOT/ + FAILED_STAGE = "" + SH_SUCCESS_STATUS_CODE = 0 + IMAGE_TAG = generateImageTag() + HELM_CHART_VERSION = generateHelmChartVersion() + BUILD_PROFILE = getBuildProfile() + } + + options { + timeout(time: 1, unit: 'HOURS') + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '5')) + } + + stages { + stage('Check Version') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + def version = getPomVersion() + + if(isReleaseBranch()){ + if ( !(version ==~ RELEASE_REGEX) ) { + error("Keine Release Version für Branch ${env.BRANCH_NAME}.") + } + } else { + if ( !(version ==~ SNAPSHOT_REGEX) ) { + error("Keine Snapshot Version für Branch ${env.BRANCH_NAME}.") + } + } + } + } + } + + stage('Build Administration') { + steps { + script { + FAILED_STAGE=env.STAGE_NAME + } + + configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { + sh "mvn -s $MAVEN_SETTINGS clean install -Dmaven.wagon.http.retryHandler.count=3 -DelasticTests.disabled=true -Dbuild.number=$BUILD_NUMBER" + } + } + } + + stage('Deploy to Nexus'){ + when { + anyOf { + branch 'master' + branch 'release' + } + } + steps { + script { + FAILED_STAGE = env.STAGE_NAME + } + configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { + sh 'mvn -s $MAVEN_SETTINGS -DskipTests deploy -Dmaven.wagon.http.retryHandler.count=3' + } + } + } + + stage('Build and publish Docker image') { + steps { + script { + FAILED_STAGE=env.STAGE_NAME + } + + configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { + withCredentials([usernamePassword(credentialsId: 'jenkins-nexus-login', usernameVariable: 'USER', passwordVariable: 'PASSWORD')]) { + sh 'mvn -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3 $BUILD_PROFILE -Ddocker.publishRegistry.username=${USER} -Ddocker.publishRegistry.password=${PASSWORD}' + } + } + } + } + + stage('Sonar Checks') { + when { + branch 'master' + } + steps { + script { + FAILED_STAGE=env.STAGE_NAME + + configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { + try { + withSonarQubeEnv('sonarqube-ozg-sh'){ + sh 'mvn -s $MAVEN_SETTINGS sonar:sonar' + } + } catch (Exception e) { + unstable("SonarQube failed") + } + } + } + } + } + + stage ('OWASP Dependency-Check Vulnerabilities') { + steps { + dependencyCheck additionalArguments: ''' + -o "./" + -s "./" + -f "ALL" + -d /dependency-check-data + --suppression dependency-check-supressions.xml + --disableKnownExploited + --noupdate + --disableArchive + --prettyPrint''', odcInstallation: 'dependency-check-owasp' + + dependencyCheckPublisher pattern: 'dependency-check-report.xml' + } + } + + stage('Trigger Dev rollout') { + when { + branch 'master' + } + steps { + script { + FAILED_STAGE = env.STAGE_NAME + + cloneGitopsRepo() + + setNewDevAdministrationVersion() + + pushDevGitopsRepo() + } + } + } + } + + post { + always{ + junit testResults: '**/target/surefire-reports/*.xml', skipPublishingChecks: true + } + failure { + script { + if (isMasterBranch() || isReleaseBranch()) { + sendFailureMessage() + } + } + } + } +} + +String generateImageTag() { + return "${env.BRANCH_NAME}-${getPomVersion()}" +} + +String getPomVersion() { + def pom = readMavenPom file: 'pom.xml' + + return pom.version +} + +String getBuildProfile() { + if (isMasterBranch()) { + return "-P dev" + } else if (isReleaseBranch()) { + return "-P release" + } else { + return "" + } +} + + +Void sendFailureMessage() { + def room = getRoom() + def data = getFailureData() + + sh "curl -XPOST -H 'authorization: Bearer ${getElementAccessToken()}' -d '${data}' https://matrix.ozg-sh.de/_matrix/client/v3/rooms/$room/send/m.room.message" +} + +String getElementAccessToken() { + withCredentials([string(credentialsId: 'element-login-json', variable: 'LOGIN_JSON')]) { + return readJSON ( text: sh (script: '''curl -XPOST -d \"$LOGIN_JSON\" https://matrix.ozg-sh.de/_matrix/client/v3/login''', returnStdout: true)).access_token + } +} + +String getFailureData() { + return """{"msgtype":"m.text", \ + "body":"Administration: Build Failed. Stage: ${FAILED_STAGE} Build-ID: ${env.BUILD_NUMBER}", \ + "format": "org.matrix.custom.html", \ + "formatted_body":"Administration: Build Failed. Stage: ${FAILED_STAGE} Build-ID: ${env.BUILD_NUMBER}"}""" +} + +String getRoom() { + if (isReleaseBranch()) { + return "!oWZpUGTFsxkJIYNfYg:matrix.ozg-sh.de" + } else { + return "!iQPAvQIiRwRpNOszjw:matrix.ozg-sh.de" + } +} + +Void configureGit() { + def email = "jenkins@ozg-sh.de" + def name = "jenkins" + + dir("gitops") { + sh "git config user.email '${email}'" + sh "git config user.name '${name}'" + } +} + +Void cloneGitopsRepo() { + withCredentials([usernamePassword(credentialsId: 'jenkins-gitea-access-token', passwordVariable: 'TOKEN', usernameVariable: 'USER')]) { + sh 'git clone https://${USER}:${TOKEN}@git.ozg-sh.de/ozgcloud-devops/gitops.git' + } + + configureGit() +} + +Void setNewDevAdministrationVersion() { + setNewAdministrationGitopsVersion("dev") +} + +Void setNewTestAdministrationVersion() { + setNewAdministrationGitopsVersion("test") +} + +Void setNewAdministrationGitopsVersion(String environment) { + dir("gitops") { + def envFile = "${environment}/application/values/administration-values.yaml" + + def envVersions = readYaml file: envFile + + envVersions.administration.image.tag = IMAGE_TAG + envVersions.administration.helm.version = HELM_CHART_VERSION + + writeYaml file: envFile, data: envVersions, overwrite: true + } +} + + +Void pushDevGitopsRepo() { + pushNewGitopsVersion('dev') +} + +Void pushTestGitopsRepo() { + pushNewGitopsVersion('test') +} + +Void pushNewGitopsVersion(String environment) { + dir('gitops') { + if (!hasAdministrationValuesFileChanged(environment)) { + return + } + + withCredentials([usernamePassword(credentialsId: 'jenkins-gitea-access-token', passwordVariable: 'TOKEN', usernameVariable: 'USER')]) { + sh "git add ${environment}/application/values/administration-values.yaml" + + sh "git commit -m 'jenkins rollout ${environment} administration version ${IMAGE_TAG}'" + sh 'git push https://${USER}:${TOKEN}@git.ozg-sh.de/ozgcloud-devops/gitops.git' + } + } +} + +Boolean hasAdministrationValuesFileChanged(String environment) { + return sh (script: "git status | grep '${environment}/application/values/administration-values.yaml'", returnStatus: true) == env.SH_SUCCESS_STATUS_CODE as Integer +} + +Boolean isMasterBranch() { + return env.BRANCH_NAME == 'master' +} + +Boolean isReleaseBranch() { + return env.BRANCH_NAME == 'release' +} + +String generateHelmChartVersion() { + def chartVersion = getPomVersion() + + if (isMasterBranch()) { + chartVersion += "-${env.GIT_COMMIT.take(7)}" + } + else if (!isReleaseBranch()) { + chartVersion += "-${env.BRANCH_NAME}" + } + + return chartVersion.replaceAll("_", "-") +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7ba9924b2d6e6630ab00418626a16a7421a8daf8..c6dd5bf74155ef462e9651f936f630f4036dbcb9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,16 +12,16 @@ <groupId>de.ozgcloud</groupId> <artifactId>administration</artifactId> <version>1.0.0-SNAPSHOT</version> - <name>administration</name> + <name>Administration</name> <description>Administration Backend Project</description> + <properties> - <java.version>21</java.version> - <!--<maven.compiler.source>21</maven.compiler.source> - <maven.compiler.target>21</maven.compiler.target>--> - <build.number>x</build.number> - <build.url>no-url</build.url> + <imageName>docker.ozg-sh.de/administration</imageName> + <build.number>SET_BY_JENKINS</build.number> </properties> + <dependencies> + <!-- Spring --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> @@ -35,6 +35,7 @@ <artifactId>spring-boot-starter-web</artifactId> </dependency> + <!-- Dev --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> @@ -46,25 +47,74 @@ <artifactId>lombok</artifactId> <optional>true</optional> </dependency> + + <!-- Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> - + <profiles> + <profile> + <id>dev</id> + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <image> + <tags>${imageName}:snapshot-latest</tags> + </image> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>release</id> + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <configuration> + <image> + <tags>${imageName}:latest</tags> + </image> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> <build> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> - <excludes> - <exclude> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - </exclude> - </excludes> + <image> + <name>${imageName}:${env.BRANCH_NAME}-${project.version}</name> + <publish>true</publish> + </image> + <docker> + <publishRegistry> + <username>${docker.publishRegistry.username}</username> + <password>${docker.publishRegistry.password}</password> + </publishRegistry> + </docker> + <excludes> + <exclude> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + </exclude> + </excludes> </configuration> <executions> <execution> @@ -75,7 +125,6 @@ <configuration> <additionalProperties> <number>${build.number}</number> - <url>${build.url}</url> </additionalProperties> </configuration> </execution> diff --git a/src/main/java/de/ozgcloud/admin/Root.java b/src/main/java/de/ozgcloud/admin/Root.java new file mode 100644 index 0000000000000000000000000000000000000000..b5658c3ff6a812571e2dcb10e2ba4fcc6e524740 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/Root.java @@ -0,0 +1,15 @@ +package de.ozgcloud.admin; + +import java.time.Instant; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class Root { + private String javaVersion; + private String buildVersion; + private Instant buildTime; + private String buildNumber; +} diff --git a/src/main/java/de/ozgcloud/admin/RootController.java b/src/main/java/de/ozgcloud/admin/RootController.java new file mode 100644 index 0000000000000000000000000000000000000000..bf17388b3274c557be524e5dc1d48e8dc347a7b2 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/RootController.java @@ -0,0 +1,32 @@ +package de.ozgcloud.admin; + +import org.springframework.boot.info.BuildProperties; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping(RootController.PATH) +public class RootController { + static final String PATH = "/api"; // NOSONAR + + private final BuildProperties buildProperties; + + @GetMapping + public Root getRoot() { + return buildRoot(); + } + + private Root buildRoot() { + return Root.builder() + .javaVersion(System.getProperty("java.version")) + .buildTime(buildProperties.getTime()) + .buildVersion(buildProperties.getVersion()) + .buildNumber(buildProperties.get("number")) + .build(); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/AdministrationApplicationTest.java b/src/test/java/de/ozgcloud/admin/AdministrationApplicationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ae71e12d72f6210d5afa910fd074a9ecff35654f --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/AdministrationApplicationTest.java @@ -0,0 +1,12 @@ +package de.ozgcloud.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AdministrationApplicationTest { + + @Test + void shouldSpringContextLoad() { // NOSONAR + } +} diff --git a/src/test/java/de/ozgcloud/admin/RootControllerTest.java b/src/test/java/de/ozgcloud/admin/RootControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f2e35aae3631b6179a8c2941c3524296fcfa6b8e --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/RootControllerTest.java @@ -0,0 +1,91 @@ +package de.ozgcloud.admin; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.info.BuildProperties; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import lombok.SneakyThrows; + +@ExtendWith(MockitoExtension.class) +class RootControllerTest { + + @Spy + @InjectMocks + private RootController rootController; + + @Mock + private BuildProperties buildProperties; + + private MockMvc mockMvc; + + @BeforeEach + void mock() { + mockMvc = MockMvcBuilders.standaloneSetup(rootController).build(); + } + + @DisplayName("Root") + @Nested + class TestInfo { + + @Test + @SneakyThrows + void shouldHaveJavaVersion() { + System.setProperty("java.version", RootTestFactory.JAVA_VERSION); + + ResultActions result = doRequest(); + + result.andExpect(jsonPath("$.javaVersion").value(RootTestFactory.JAVA_VERSION)); + } + + @Test + @SneakyThrows + void shouldHaveVersion() { + when(buildProperties.getVersion()).thenReturn(RootTestFactory.BUILD_VERSION); + + ResultActions result = doRequest(); + + result.andExpect(jsonPath("$.buildVersion").value(RootTestFactory.BUILD_VERSION)); + } + + @Test + @SneakyThrows + void shouldHaveBuildTime() { + when(buildProperties.getTime()).thenReturn(RootTestFactory.BUILD_TIME); + + ResultActions result = doRequest(); + + result.andExpect(jsonPath("$.buildTime").value(RootTestFactory.BUILD_TIME.getEpochSecond())); + } + + @Test + @SneakyThrows + void shouldHaveBuildNumber() { + when(buildProperties.get("number")).thenReturn(RootTestFactory.BUILD_NUMBER); + + ResultActions result = doRequest(); + + result.andExpect(jsonPath("$.buildNumber").value(RootTestFactory.BUILD_NUMBER)); + } + + @SneakyThrows + private ResultActions doRequest() { + return mockMvc.perform(get(RootController.PATH)).andExpect(status().isOk()); + } + + } + +} diff --git a/src/test/java/de/ozgcloud/admin/RootTestFactory.java b/src/test/java/de/ozgcloud/admin/RootTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e0879c7530926dbbe52514acab93f2bf3cf63820 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/RootTestFactory.java @@ -0,0 +1,14 @@ +package de.ozgcloud.admin; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +public class RootTestFactory { + + public static final Instant BUILD_TIME = LocalDateTime.parse("2021-04-01T10:30").toInstant(ZoneOffset.UTC); + public static final String JAVA_VERSION = "1"; + public static final String BUILD_VERSION = "2"; + public static final String BUILD_NUMBER = "3"; + +} diff --git a/src/test/java/de/ozgcloud/admin/web/controller/BasicAppInfoControllerTest.java b/src/test/java/de/ozgcloud/admin/web/controller/BasicAppInfoControllerTest.java index dd9f156119a65fe1081f63cc39d880649d2c5b43..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/test/java/de/ozgcloud/admin/web/controller/BasicAppInfoControllerTest.java +++ b/src/test/java/de/ozgcloud/admin/web/controller/BasicAppInfoControllerTest.java @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur - * Zentrales IT-Management - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -package de.ozgcloud.admin.web.controller; - -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import de.ozgcloud.admin.service.BasicAppInfoService; - -@SpringBootTest -class BasicAppInfoControllerTest { - final Instant sampleTime = LocalDateTime.parse("2021-04-01T10:30").toInstant(ZoneOffset.UTC); - final String javaVersion = "1"; - final String buildVersion = "2"; - final String buildNumber = "3"; - final String buildUrl = "url://test"; - - @Spy - @InjectMocks - private BasicAppInfoController basicAppInfoController; - - @Mock - private BasicAppInfoService basicAppInfoService; - - private MockMvc mockMvc; - - @BeforeEach - void mock() { - mockMvc = MockMvcBuilders.standaloneSetup(basicAppInfoController).build(); - } - - @DisplayName("Basic info") - @Nested - class TestInfo { - - @Test - void shouldHaveJavaVersion() throws Exception { - when(basicAppInfoService.getJavaVersion()).thenReturn(javaVersion); - - doRequest() - .andExpect(jsonPath("$.javaVersion").value(javaVersion)); - } - - @Test - void shouldHaveVersion() throws Exception { - when(basicAppInfoService.getVersion()).thenReturn(buildVersion); - - doRequest() - .andExpect(jsonPath("$.buildVersion").value(buildVersion)); - } - - @Test - void shouldHaveBuildTime() throws Exception { - when(basicAppInfoService.getBuildTime()).thenReturn(sampleTime); - - doRequest() - .andExpect(jsonPath("$.buildTime").value(sampleTime.getEpochSecond())); - } - - @Test - void shouldHaveBuildNumber() throws Exception { - when(basicAppInfoService.getBuildNumber()).thenReturn(buildNumber); - - doRequest() - .andExpect(jsonPath("$.buildNumber").value(buildNumber)); - } - - @Test - void shouldHaveBuildUrl() throws Exception { - when(basicAppInfoService.getBuildUrl()).thenReturn(buildUrl); - - doRequest() - .andExpect(jsonPath("$.buildUrl").value(buildUrl)); - } - - private ResultActions doRequest() throws Exception { - return mockMvc.perform(get(BasicAppInfoController.PATH)).andExpect(status().isOk()); - } - } - -}