From 97463eebc986f1558c37d5ff5a84ec188f9e92d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrich=20Kr=C3=A4mer?= Date: Mon, 1 Jul 2024 11:45:03 +0000 Subject: [PATCH 1/5] TIR - remove dynamic pod data and surface helm status If helm is used for deployment, the components deployed by helm are no longer listed as toplevel elements in the TIR. Instead the helm release is represented by its status so that users will see tables under: Deployment Resource: -deploymentMean Deployment Resource: -deploymentStatus The DeploymentStatus has rows surfacing the releaseName, releaseRevision, namespace, deployStatus, deployDescription, lastDeployed and resources. Here an example from the resources: "Deployment: backend-helm-monorepo-chart-component-a, backend- helm-monorepo-chart-component-b, Service: backend-helm-monorepo- chart" Non-Helm deployments are surfaced as before. Further change notes: - To avoid CPS issues in Jenkins all types were using their own file and the Immutable annotation was avoided. - Type HelmStatusData captures helm status with high fidelity whereas HelmStatusSimpleData captures shallower information in particular omitting the details of resources except their names and kinds. This approach simplifies the json Helm status json parsing code while keeping the consuming code relatively lean. In the deployment artifact the simplified helmStatus is kept under key 'helmStatus' in the deploymentMean information. Which is also written to `ods-deployments.json` by DeploymentDescriptor. - PodData fields capturing dynamic data where removed, except for the podName which has a prefix useful for correlating dynamic data with pods. At the moment this is still needed for both tailor and helm deployments. The code which finds this named pods did require retry logic even for helm installs using --atomic. --- .../AbstractDeploymentStrategy.groovy | 3 + .../component/HelmDeploymentStrategy.groovy | 100 ++++--- .../phases/DeployOdsComponent.groovy | 27 +- .../usecase/LeVADocumentUseCase.groovy | 257 ++++++++++++------ .../util/DeploymentDescriptor.groovy | 13 + src/org/ods/services/OpenShiftService.groovy | 49 +++- .../util/HelmStatusContainerStatusData.groovy | 32 +++ src/org/ods/util/HelmStatusData.groovy | 181 ++++++++++++ src/org/ods/util/HelmStatusInfoData.groovy | 48 ++++ src/org/ods/util/HelmStatusResource.groovy | 25 ++ .../ods/util/HelmStatusResourceData.groovy | 39 +++ src/org/ods/util/HelmStatusSimpleData.groovy | 70 +++++ src/org/ods/util/PodData.groovy | 15 - 13 files changed, 712 insertions(+), 147 deletions(-) create mode 100644 src/org/ods/util/HelmStatusContainerStatusData.groovy create mode 100644 src/org/ods/util/HelmStatusData.groovy create mode 100644 src/org/ods/util/HelmStatusInfoData.groovy create mode 100644 src/org/ods/util/HelmStatusResource.groovy create mode 100644 src/org/ods/util/HelmStatusResourceData.groovy create mode 100644 src/org/ods/util/HelmStatusSimpleData.groovy diff --git a/src/org/ods/component/AbstractDeploymentStrategy.groovy b/src/org/ods/component/AbstractDeploymentStrategy.groovy index 359541c9a..32213c70b 100644 --- a/src/org/ods/component/AbstractDeploymentStrategy.groovy +++ b/src/org/ods/component/AbstractDeploymentStrategy.groovy @@ -14,6 +14,9 @@ abstract class AbstractDeploymentStrategy implements IDeploymentStrategy { @Override abstract Map> deploy() + // Fetches original kubernetes revisions of deployment resources. + // + // returns a two level map with keys resourceKind -> resourceName -> revision (int) protected Map> fetchOriginalVersions(Map> deploymentResources) { def originalVersions = [:] deploymentResources.each { resourceKind, resourceNames -> diff --git a/src/org/ods/component/HelmDeploymentStrategy.groovy b/src/org/ods/component/HelmDeploymentStrategy.groovy index 0bd9360c9..c706c24ee 100644 --- a/src/org/ods/component/HelmDeploymentStrategy.groovy +++ b/src/org/ods/component/HelmDeploymentStrategy.groovy @@ -5,6 +5,7 @@ import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.ods.services.JenkinsService import org.ods.services.OpenShiftService +import org.ods.util.HelmStatusSimpleData import org.ods.util.ILogger import org.ods.util.PipelineSteps import org.ods.util.PodData @@ -94,10 +95,16 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { logger.info "Rolling out ${context.componentId} with HELM, selector: ${options.selector}" helmUpgrade(context.targetProject) + HelmStatusSimpleData helmStatus = openShift.helmStatus(context.targetProject, options.helmReleaseName) + def helmStatusMap = helmStatus.toMap() + def deploymentResources = getDeploymentResources(helmStatus) - def deploymentResources = openShift.getResourcesForComponent( - context.targetProject, DEPLOYMENT_KINDS, options.selector - ) + logger.info("${this.class.name} -- HELM STATUS") + logger.info( + JsonOutput.prettyPrint( + JsonOutput.toJson(helmStatusMap))) + + // not sure if we need both HELM STATUS and DEPLOYMENT RESOURCES" logger.info("${this.class.name} -- DEPLOYMENT RESOURCES") logger.info( JsonOutput.prettyPrint( @@ -107,11 +114,12 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // // we assume that Helm does "Deployment" that should work for most // // cases since they don't have triggers. // metadataSvc.updateMetadata(false, deploymentResources) - def rolloutData = getRolloutData(deploymentResources) ?: [:] + def rolloutData = getRolloutData(helmStatus) logger.info(JsonOutput.prettyPrint(JsonOutput.toJson(rolloutData))) return rolloutData } + @SuppressWarnings(['UnnecessaryCast']) // otherwise IDE marked up helmUpgrade call private void helmUpgrade(String targetProject) { steps.dir(options.chartDir) { jenkins.maybeWithPrivateKeyCredentials(options.helmPrivateKeyCredentialsId) { String pkeyFile -> @@ -124,7 +132,7 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { options.helmValues['componentId'] = context.componentId // we persist the original ones set from outside - here we just add ours - Map mergedHelmValues = [:] + Map mergedHelmValues = [:] as Map mergedHelmValues << options.helmValues // we add the global ones - this allows usage in subcharts @@ -140,7 +148,7 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { mergedHelmValues['global.imageTag'] = options.imageTag // deal with dynamic value files - which are env dependent - def mergedHelmValuesFiles = [] + def mergedHelmValuesFiles = [] as List mergedHelmValuesFiles.addAll(options.helmValuesFiles) options.helmEnvBasedValuesFiles.each { envValueFile -> @@ -161,6 +169,10 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { } } + private Map> getDeploymentResources(HelmStatusSimpleData helmStatus) { + helmStatus.getResourcesByKind(DEPLOYMENT_KINDS) + } + // rollout returns a map like this: // [ // 'DeploymentConfig/foo': [[podName: 'foo-a', ...], [podName: 'foo-b', ...]], @@ -168,39 +180,55 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // ] @TypeChecked(TypeCheckingMode.SKIP) private Map> getRolloutData( - Map> deploymentResources) { - + HelmStatusSimpleData helmStatus + ) { def rolloutData = [:] - deploymentResources.each { resourceKind, resourceNames -> - resourceNames.each { resourceName -> - def podData = [] - for (def i = 0; i < options.deployTimeoutRetries; i++) { - podData = openShift.checkForPodData(context.targetProject, options.selector, resourceName) - if (!podData.isEmpty()) { - break - } - steps.echo("Could not find 'running' pod(s) with label '${options.selector}' - waiting") - steps.sleep(12) + helmStatus.resources.each { resource -> + if (! (resource.kind in DEPLOYMENT_KINDS)) { + return // continues with next + } + context.addDeploymentToArtifactURIs("${resource.name}-deploymentMean", + [ + 'type': 'helm', + 'selector': options.selector, + 'chartDir': options.chartDir, + 'helmReleaseName': options.helmReleaseName, + 'helmEnvBasedValuesFiles': options.helmEnvBasedValuesFiles, + 'helmValuesFiles': options.helmValuesFiles, + 'helmValues': options.helmValues, + 'helmDefaultFlags': options.helmDefaultFlags, + 'helmAdditionalFlags': options.helmAdditionalFlags, + 'helmStatus': helmStatus.toMap(), + ] + ) + def podDataContext = [ + "targetProject=${context.targetProject}", + "selector=${options.selector}", + "name=${resource.name}", + ] + def msgPodsNotFound = "Could not find 'running' pod(s) for '${podDataContext.join(', ')}'" + List podData = null + for (def i = 0; i < options.deployTimeoutRetries; i++) { + podData = openShift.checkForPodData(context.targetProject, options.selector, resource.name) + if (podData) { + break } - context.addDeploymentToArtifactURIs("${resourceName}-deploymentMean", - [ - 'type': 'helm', - 'selector': options.selector, - 'chartDir': options.chartDir, - 'helmReleaseName': options.helmReleaseName, - 'helmEnvBasedValuesFiles': options.helmEnvBasedValuesFiles, - 'helmValuesFiles': options.helmValuesFiles, - 'helmValues': options.helmValues, - 'helmDefaultFlags': options.helmDefaultFlags, - 'helmAdditionalFlags': options.helmAdditionalFlags - ]) - rolloutData["${resourceKind}/${resourceName}"] = podData - // TODO: Once the orchestration pipeline can deal with multiple replicas, - // update this to store multiple pod artifacts. - // TODO: Potential conflict if resourceName is duplicated between - // Deployment and DeploymentConfig resource. - context.addDeploymentToArtifactURIs(resourceName, podData[0]?.toMap()) + steps.echo("${msgPodsNotFound} - waiting") + steps.sleep(12) + } + if (!podData) { + throw new RuntimeException(msgPodsNotFound) } + + logger.debug("Helm podData for ${podDataContext.join(', ')}: " + + "${JsonOutput.prettyPrint(JsonOutput.toJson(podData))}") + + rolloutData["${resource.kind}/${resource.name}"] = podData + // TODO: Once the orchestration pipeline can deal with multiple replicas, + // update this to store multiple pod artifacts. + // TODO: Potential conflict if resourceName is duplicated between + // Deployment and DeploymentConfig resource. + context.addDeploymentToArtifactURIs(resource.name, podData[0]?.toMap()) } rolloutData } diff --git a/src/org/ods/orchestration/phases/DeployOdsComponent.groovy b/src/org/ods/orchestration/phases/DeployOdsComponent.groovy index 260128866..4c697bb26 100644 --- a/src/org/ods/orchestration/phases/DeployOdsComponent.groovy +++ b/src/org/ods/orchestration/phases/DeployOdsComponent.groovy @@ -1,5 +1,6 @@ package org.ods.orchestration.phases +import groovy.json.JsonOutput import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.ods.util.IPipelineSteps @@ -11,6 +12,7 @@ import org.ods.services.GitService import org.ods.orchestration.util.DeploymentDescriptor import org.ods.orchestration.util.MROPipelineUtil import org.ods.orchestration.util.Project +import org.ods.util.PodData // Deploy ODS comnponent (code or service) to 'qa' or 'prod'. @TypeChecked @@ -58,21 +60,29 @@ class DeployOdsComponent { applyTemplates(openShiftDir, deploymentMean) def retries = project.environmentConfig?.openshiftRolloutTimeoutRetries ?: 10 - def podData = null + def podDataContext = [ + "targetProject=${project.targetProject}", + "selector=${deploymentMean.selector}", + "name=${deploymentName}", + ] + def msgPodsNotFound = "Could not find 'running' pod(s) for '${podDataContext.join(', ')}'" + List podData = null for (def i = 0; i < retries; i++) { podData = os.checkForPodData(project.targetProject, deploymentMean.selector, deploymentName) if (podData) { break } - steps.echo("Could not find 'running' pod(s) with label '${deploymentMean.selector}' - waiting") + steps.echo("${msgPodsNotFound} - waiting") steps.sleep(12) } if (!podData) { - throw new RuntimeException("Could not find 'running' pod(s) with label " + - "'${deploymentMean.selector}'") + throw new RuntimeException(msgPodsNotFound) } + logger.info("Helm podData for '${podDataContext.join(', ')}': " + + "${JsonOutput.prettyPrint(JsonOutput.toJson(podData))}") + // TODO: Once the orchestration pipeline can deal with multiple replicas, // update this to deal with multiple pods. def pod = podData[0].toMap() @@ -227,6 +237,15 @@ class DeployOdsComponent { deploymentMean.helmDefaultFlags, deploymentMean.helmAdditionalFlags, true) + + def helmStatus = os. helmStatus(project.targetProject, deploymentMean.helmReleaseName) + def helmStatusMap = helmStatus.toMap() + deploymentMean.helmStatus = helmStatusMap + logger.info("${this.class.name} -- HELM STATUS") + logger.info( + JsonOutput.prettyPrint( + JsonOutput.toJson(helmStatusMap))) + } } jenkins.maybeWithPrivateKeyCredentials(secretName) { String pkeyFile -> diff --git a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy index ec02672c0..f143889e0 100644 --- a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy +++ b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy @@ -79,16 +79,16 @@ class LeVADocumentUseCase extends DocGenUseCase { ] static Map DOCUMENT_TYPE_FILESTORAGE_EXCEPTIONS = [ - 'SCRR-MD' : [storage: 'pdf', content: 'pdf' ] + 'SCRR-MD': [storage: 'pdf', content: 'pdf'] ] static Map INTERNAL_TO_EXT_COMPONENT_TYPES = [ - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SAAS_SERVICE as String) : 'SAAS Component', - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_TEST as String) : 'Automated tests', - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SERVICE as String) : '3rd Party Service Component', - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE as String) : 'ODS Software Component', - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_INFRA as String) : 'Infrastructure as Code Component', - (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_LIB as String) : 'ODS library component' + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SAAS_SERVICE as String): 'SAAS Component', + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_TEST as String) : 'Automated tests', + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SERVICE as String) : '3rd Party Service Component', + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE as String) : 'ODS Software Component', + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_INFRA as String) : 'Infrastructure as Code Component', + (MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_LIB as String) : 'ODS library component' ] public static String DEVELOPER_PREVIEW_WATERMARK = 'Developer Preview' @@ -138,7 +138,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def requirements = this.project.getSystemRequirements() def reqsWithNoGampTopic = getReqsWithNoGampTopic(requirements) def reqsGroupedByGampTopic = getReqsGroupedByGampTopic(requirements) - reqsGroupedByGampTopic << ['uncategorized': reqsWithNoGampTopic ] + reqsGroupedByGampTopic << ['uncategorized': reqsWithNoGampTopic] def requirementsForDocument = reqsGroupedByGampTopic.collectEntries { gampTopic, reqs -> def updatedReqs = reqs.collect { req -> @@ -146,18 +146,18 @@ class LeVADocumentUseCase extends DocGenUseCase { def epic = epics.isEmpty() ? null : epics.first() return [ - key : req.key, - applicability : 'Mandatory', - ursName : req.name, - ursDescription : this.convertImages(req.description ?: ''), - csName : req.configSpec.name ?: 'N/A', - csDescription : this.convertImages(req.configSpec.description ?: ''), - fsName : req.funcSpec.name ?: 'N/A', - fsDescription : this.convertImages(req.funcSpec.description ?: ''), - epic : epic?.key, - epicName : epic?.epicName, - epicTitle : epic?.title, - epicDescription : this.convertImages(epic?.description), + key : req.key, + applicability : 'Mandatory', + ursName : req.name, + ursDescription : this.convertImages(req.description ?: ''), + csName : req.configSpec.name ?: 'N/A', + csDescription : this.convertImages(req.configSpec.description ?: ''), + fsName : req.funcSpec.name ?: 'N/A', + fsDescription : this.convertImages(req.funcSpec.description ?: ''), + epic : epic?.key, + epicName : epic?.epicName, + epicTitle : epic?.title, + epicDescription: this.convertImages(epic?.description), ] } @@ -193,17 +193,18 @@ class LeVADocumentUseCase extends DocGenUseCase { protected Map sortByEpicAndRequirementKeys(List updatedReqs) { def sortedUpdatedReqs = SortUtil.sortIssuesByKey(updatedReqs) def reqsGroupByEpic = sortedUpdatedReqs.findAll { - it.epic != null }.groupBy { it.epic }.sort() + it.epic != null + }.groupBy { it.epic }.sort() def reqsGroupByEpicUpdated = reqsGroupByEpic.values().indexed(1).collect { index, epicStories -> def aStory = epicStories.first() [ - epicName : aStory.epicName, - epicTitle : aStory.epicTitle, - epicDescription : this.convertImages(aStory.epicDescription ?: ''), - key : aStory.epic, - epicIndex : index, - stories : epicStories, + epicName : aStory.epicName, + epicTitle : aStory.epicTitle, + epicDescription: this.convertImages(aStory.epicDescription ?: ''), + key : aStory.epic, + epicIndex : index, + stories : epicStories, ] } def output = [ @@ -216,14 +217,14 @@ class LeVADocumentUseCase extends DocGenUseCase { @NonCPS private def computeKeysInDocForCSD(def data) { - return data.collect { it.subMap(['key', 'epics']).values() } + return data.collect { it.subMap(['key', 'epics']).values() } .flatten().unique() } @NonCPS private def computeKeysInDocForDTP(def data, def tests) { return data.collect { 'Technology-' + it.id } + tests - .collect { [it.testKey, it.systemRequirement.split(', '), it.softwareDesignSpec.split(', ')] } + .collect { [it.testKey, it.systemRequirement.split(', '), it.softwareDesignSpec.split(', ')] } .flatten() } @@ -286,7 +287,7 @@ class LeVADocumentUseCase extends DocGenUseCase { description += testIssue.name } - def riskLevels = testIssue.getResolvedRisks(). collect { + def riskLevels = testIssue.getResolvedRisks().collect { def value = obtainEnum("SeverityOfImpact", it.severityOfImpact) return value ? value.text : "None" } @@ -312,19 +313,19 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(this.DOCUMENT_TYPE_NAMES[documentType], repo), data : [ - repo : repo, - sections : sections, - tests : tests, - numAdditionalTests: junit.getNumberOfTestCases(unitTestData.testResults) - testIssues.count { !it.isUnexecuted }, - testFiles : SortUtil.sortIssuesByProperties(unitTestData.testReportFiles.collect { file -> + repo : repo, + sections : sections, + tests : tests, + numAdditionalTests : junit.getNumberOfTestCases(unitTestData.testResults) - testIssues.count { !it.isUnexecuted }, + testFiles : SortUtil.sortIssuesByProperties(unitTestData.testReportFiles.collect { file -> [name: file.name, path: file.path, text: XmlUtil.serialize(file.text)] } ?: [], ["name"]), - discrepancies : discrepancies.discrepancies, - conclusion : [ + discrepancies : discrepancies.discrepancies, + conclusion : [ summary : discrepancies.conclusion.summary, statement: discrepancies.conclusion.statement ], - documentHistory: docHistory?.getDocGenFormat() ?: [], + documentHistory : docHistory?.getDocGenFormat() ?: [], documentHistoryLatestVersionId: docHistory?.latestVersionId ?: 1, ] ] @@ -398,7 +399,7 @@ class LeVADocumentUseCase extends DocGenUseCase { //Discrepancy ID -> BUG Issue ID discrepancyID : bug.key, //Test Case No. -> JIRA (Test Case Key) - testcaseID : bug.tests. collect { it.key }.join(", "), + testcaseID : bug.tests.collect { it.key }.join(", "), //- Level of Test Case = Unit / Integration / Acceptance / Installation level : "Integration", //Description of Failure or Discrepancy -> Bug Issue Summary @@ -421,7 +422,7 @@ class LeVADocumentUseCase extends DocGenUseCase { //Discrepancy ID -> BUG Issue ID discrepancyID : bug.key, //Test Case No. -> JIRA (Test Case Key) - testcaseID : bug.tests. collect { it.key }.join(", "), + testcaseID : bug.tests.collect { it.key }.join(", "), //- Level of Test Case = Unit / Integration / Acceptance / Installation level : "Acceptance", //Description of Failure or Discrepancy -> Bug Issue Summary @@ -455,8 +456,8 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(this.DOCUMENT_TYPE_NAMES[documentType]), data : [ - sections : sections, - documentHistory: docHistory?.getDocGenFormat() ?: [], + sections : sections, + documentHistory : docHistory?.getDocGenFormat() ?: [], documentHistoryLatestVersionId: docHistory?.latestVersionId ?: 1, ] ] @@ -497,14 +498,14 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(this.DOCUMENT_TYPE_NAMES[documentType]), data : [ - sections : sections, - numAdditionalAcceptanceTests : junit.getNumberOfTestCases(acceptanceTestData.testResults) - acceptanceTestIssues.count { !it.isUnexecuted }, - numAdditionalIntegrationTests: junit.getNumberOfTestCases(integrationTestData.testResults) - integrationTestIssues.count { !it.isUnexecuted }, - conclusion : [ + sections : sections, + numAdditionalAcceptanceTests : junit.getNumberOfTestCases(acceptanceTestData.testResults) - acceptanceTestIssues.count { !it.isUnexecuted }, + numAdditionalIntegrationTests : junit.getNumberOfTestCases(integrationTestData.testResults) - integrationTestIssues.count { !it.isUnexecuted }, + conclusion : [ summary : discrepancies.conclusion.summary, statement: discrepancies.conclusion.statement ], - documentHistory: docHistory?.getDocGenFormat() ?: [], + documentHistory : docHistory?.getDocGenFormat() ?: [], documentHistoryLatestVersionId: docHistory?.latestVersionId ?: 1, ] ] @@ -554,7 +555,7 @@ class LeVADocumentUseCase extends DocGenUseCase { @NonCPS private def computeKeysInDocForRA(def data) { return data - .collect { it.subMap(['key', 'requirements', 'techSpecs', 'mitigations', 'tests']).values() } + .collect { it.subMap(['key', 'requirements', 'techSpecs', 'mitigations', 'tests']).values() } .flatten() } @@ -569,7 +570,7 @@ class LeVADocumentUseCase extends DocGenUseCase { } def risks = this.project.getRisks() - .findAll { it != null } + .findAll { it != null } .collect { r -> def mitigationsText = this.replaceDashToNonBreakableUnicode(r.mitigations ? r.mitigations.join(", ") : "None") def testsText = this.replaceDashToNonBreakableUnicode(r.tests ? r.tests.join(", ") : "None") @@ -588,11 +589,11 @@ class LeVADocumentUseCase extends DocGenUseCase { requirement: requirement, gxpRelevance: gxpRelevance ? gxpRelevance."short" : "None", probabilityOfOccurrence: probabilityOfOccurrence ? probabilityOfOccurrence."short" : "None", - severityOfImpact: severityOfImpact ? severityOfImpact."short" : "None", - probabilityOfDetection: probabilityOfDetection ? probabilityOfDetection."short" : "None", - riskPriority: riskPriority ? riskPriority."short" : "None", - riskPriorityNumber: (r.riskPriorityNumber != null) ? r.riskPriorityNumber : "N/A", - riskComment: r.riskComment ? r.riskComment : "N/A", + severityOfImpact : severityOfImpact ? severityOfImpact."short" : "None", + probabilityOfDetection : probabilityOfDetection ? probabilityOfDetection."short" : "None", + riskPriority : riskPriority ? riskPriority."short" : "None", + riskPriorityNumber : (r.riskPriorityNumber != null) ? r.riskPriorityNumber : "N/A", + riskComment : r.riskComment ? r.riskComment : "N/A", ] } @@ -663,7 +664,7 @@ class LeVADocumentUseCase extends DocGenUseCase { @NonCPS private def computeKeysInDocForIPV(def data) { return data - .collect { it.subMap(['key', 'components', 'techSpecs']).values() } + .collect { it.subMap(['key', 'components', 'techSpecs']).values() } .flatten() } @@ -699,9 +700,9 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(DOCUMENT_TYPE_NAMES[documentType]), data : [ - repositories : installedRepos.collect { [id: it.id, type: it.type, doInstall: it.doInstall, data: [git: [url: it.data.git == null ? null : it.data.git.url]]] }, - sections : sections, - tests : SortUtil.sortIssuesByKey(installationTestIssues.collect { testIssue -> + repositories : installedRepos.collect { [id: it.id, type: it.type, doInstall: it.doInstall, data: [git: [url: it.data.git == null ? null : it.data.git.url]]] }, + sections : sections, + tests : SortUtil.sortIssuesByKey(installationTestIssues.collect { testIssue -> [ key : testIssue.key, summary : testIssue.name, @@ -723,7 +724,7 @@ class LeVADocumentUseCase extends DocGenUseCase { @NonCPS private def computeKeysInDocForIVR(def data) { return data - .collect { it.subMap(['key', 'components', 'techSpecs']).values() } + .collect { it.subMap(['key', 'components', 'techSpecs']).values() } .flatten() } @@ -753,7 +754,7 @@ class LeVADocumentUseCase extends DocGenUseCase { } } - def keysInDoc = this.computeKeysInDocForIVR(installationTestIssues) + def keysInDoc = this.computeKeysInDocForIVR(installationTestIssues) def docHistory = this.getAndStoreDocumentHistory(documentType, keysInDoc) def installedRepos = this.project.repositories.findAll { it -> @@ -763,9 +764,9 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(this.DOCUMENT_TYPE_NAMES[documentType]), data : [ - repositories : installedRepos.collect { [id: it.id, type: it.type, doInstall: it.doInstall, data: [git: [url: it.data.git == null ? null : it.data.git.url]]] }, - sections : sections, - tests : SortUtil.sortIssuesByKey(installationTestIssues.collect { testIssue -> + repositories : installedRepos.collect { [id: it.id, type: it.type, doInstall: it.doInstall, data: [git: [url: it.data.git == null ? null : it.data.git.url]]] }, + sections : sections, + tests : SortUtil.sortIssuesByKey(installationTestIssues.collect { testIssue -> [ key : testIssue.key, description: this.convertImages(testIssue.description ?: ''), @@ -775,12 +776,12 @@ class LeVADocumentUseCase extends DocGenUseCase { techSpec : testIssue.techSpecs.join(", ") ?: "N/A" ] }), - numAdditionalTests: junit.getNumberOfTestCases(installationTestData.testResults) - installationTestIssues.count { !it.isUnexecuted }, - testFiles : SortUtil.sortIssuesByProperties(installationTestData.testReportFiles.collect { file -> + numAdditionalTests : junit.getNumberOfTestCases(installationTestData.testResults) - installationTestIssues.count { !it.isUnexecuted }, + testFiles : SortUtil.sortIssuesByProperties(installationTestData.testReportFiles.collect { file -> [name: file.name, path: file.path, text: file.text] } ?: [], ["name"]), - discrepancies : discrepancies.discrepancies, - conclusion : [ + discrepancies : discrepancies.discrepancies, + conclusion : [ summary : discrepancies.conclusion.summary, statement: discrepancies.conclusion.statement ], @@ -818,7 +819,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def matchedHandler = { result -> result.each { testIssue, testCase -> testIssue.isSuccess = !(testCase.error || testCase.failure || testCase.skipped - || !testIssue.getResolvedBugs(). findAll { bug -> bug.status?.toLowerCase() != "done" }.isEmpty() + || !testIssue.getResolvedBugs().findAll { bug -> bug.status?.toLowerCase() != "done" }.isEmpty() || testIssue.isUnexecuted) testIssue.comment = testIssue.isUnexecuted ? "This Test Case has not been executed" : "" testIssue.timestamp = testIssue.isUnexecuted ? "N/A" : testCase.timestamp @@ -846,37 +847,37 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(DOCUMENT_TYPE_NAMES[documentType]), data : [ - sections : sections, - integrationTests : SortUtil.sortIssuesByKey(integrationTestIssues.collect { testIssue -> + sections : sections, + integrationTests : SortUtil.sortIssuesByKey(integrationTestIssues.collect { testIssue -> [ key : testIssue.key, description : this.convertImages(getTestDescription(testIssue)), requirements: testIssue.requirements ? testIssue.requirements.join(", ") : "N/A", isSuccess : testIssue.isSuccess, - bugs : testIssue.bugs ? testIssue.bugs.join(", ") : (testIssue.comment ? "": "N/A"), + bugs : testIssue.bugs ? testIssue.bugs.join(", ") : (testIssue.comment ? "" : "N/A"), steps : sortTestSteps(testIssue.steps), timestamp : testIssue.timestamp ? testIssue.timestamp.replaceAll("T", " ") : "N/A", comment : testIssue.comment, actualResult: testIssue.actualResult ] }), - acceptanceTests : SortUtil.sortIssuesByKey(acceptanceTestIssues.collect { testIssue -> + acceptanceTests : SortUtil.sortIssuesByKey(acceptanceTestIssues.collect { testIssue -> [ key : testIssue.key, description : this.convertImages(getTestDescription(testIssue)), requirements: testIssue.requirements ? testIssue.requirements.join(", ") : "N/A", isSuccess : testIssue.isSuccess, - bugs : testIssue.bugs ? testIssue.bugs.join(", ") : (testIssue.comment ? "": "N/A"), + bugs : testIssue.bugs ? testIssue.bugs.join(", ") : (testIssue.comment ? "" : "N/A"), steps : sortTestSteps(testIssue.steps), timestamp : testIssue.timestamp ? testIssue.timestamp.replaceAll("T", " ") : "N/A", comment : testIssue.comment, actualResult: testIssue.actualResult ] }), - integrationTestFiles: SortUtil.sortIssuesByProperties(integrationTestData.testReportFiles.collect { file -> + integrationTestFiles : SortUtil.sortIssuesByProperties(integrationTestData.testReportFiles.collect { file -> [name: file.name, path: file.path, text: file.text] } ?: [], ["name"]), - acceptanceTestFiles : SortUtil.sortIssuesByProperties(acceptanceTestData.testReportFiles.collect { file -> + acceptanceTestFiles : SortUtil.sortIssuesByProperties(acceptanceTestData.testReportFiles.collect { file -> [name: file.name, path: file.path, text: file.text] } ?: [], ["name"]), documentHistory: docHistory?.getDocGenFormat() ?: [], @@ -904,8 +905,8 @@ class LeVADocumentUseCase extends DocGenUseCase { def data_ = [ metadata: this.getDocumentMetadata(DOCUMENT_TYPE_NAMES[documentType]), data : [ - sections : sections, - integrationTests: SortUtil.sortIssuesByKey(integrationTestIssues.collect { testIssue -> + sections : sections, + integrationTests : SortUtil.sortIssuesByKey(integrationTestIssues.collect { testIssue -> [ key : testIssue.key, description : this.convertImages(testIssue.description ?: testIssue.name), @@ -914,7 +915,7 @@ class LeVADocumentUseCase extends DocGenUseCase { steps : sortTestSteps(testIssue.steps) ] }), - acceptanceTests : SortUtil.sortIssuesByKey(acceptanceTestIssues.collect { testIssue -> + acceptanceTests : SortUtil.sortIssuesByKey(acceptanceTestIssues.collect { testIssue -> [ key : testIssue.key, description : this.convertImages(testIssue.description ?: testIssue.name), @@ -986,17 +987,17 @@ class LeVADocumentUseCase extends DocGenUseCase { // Get the components that we consider modules in SSDS (the ones you have to code) def modules = componentsMetadata - .findAll { it.odsRepoType.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE.toLowerCase() } - .collect { component -> + .findAll { it.odsRepoType.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE.toLowerCase() } + .collect { component -> // We will set-up a double loop in the template. For moustache limitations we need to have lists component.requirements = component.requirements.findAll { it != null }.collect { r -> - [key: r.key, name: r.name, + [key : r.key, name: r.name, reqDescription: this.convertImages(r.description), gampTopic: r.gampTopic ?: "uncategorized"] }.groupBy { it.gampTopic.toLowerCase() } .collect { k, v -> [gampTopic: k, requirementsofTopic: v] } return component - } + } if (!sections."sec10") sections."sec10" = [:] sections."sec10".modules = modules @@ -1096,9 +1097,9 @@ class LeVADocumentUseCase extends DocGenUseCase { deployNote : deploynoteData, openShiftData: [ builds : repo.data.openshift.builds ?: '', - deployments: repo.data.openshift.deployments ?: '' + deployments: assembleDeployments(repo.data.openshift.deployments ?: [:]), ], - testResults: [ + testResults : [ installation: installationTestData?.testResults ], data: [ @@ -1113,7 +1114,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def codeReviewReport if (this.project.isAssembleMode && !this.jiraUseCase.jira && repo.type?.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE.toLowerCase()) { - def currentRepoAsList = [ repo ] + def currentRepoAsList = [repo] codeReviewReport = obtainCodeReviewReport(currentRepoAsList) } @@ -1130,6 +1131,92 @@ class LeVADocumentUseCase extends DocGenUseCase { return this.createDocument(documentType, repo, data_, [:], modifier, getDocumentTemplateName(documentType, repo), watermarkText) } + /** + * Helm releases become top level elements, tailor deployments are left alone. + */ + @SuppressWarnings('CyclomaticComplexity') + Map assembleDeployments(Map> deployments) { + // collect helm releases + Map> deploymentsMeansHelm = deployments.findAll { + it.key.endsWith('-deploymentMean') && it.value.type == "helm" + } + Map> deploymentsHelmByRelease = [:] + deploymentsMeansHelm.each { String deploymentName, Map deploymentMeanHelm -> + deploymentsHelmByRelease << [(deploymentMeanHelm.helmReleaseName): deploymentMeanHelm] + } + Set componentsCoveredByHelm = [] + deploymentsHelmByRelease.each { String release, Map deploymentMeanHelm -> + List> resources = deploymentMeanHelm?.helmStatus?.resources ?: [] + resources.each { + if (!it?.kind) { + logger.debug("skipping resource - no kind defined: ${it}") + return + } + if (!it?.name) { + logger.debug("skipping resource - no name defined: ${it}") + return + } + if (!os.isDeploymentKind(it.kind)) { + return + } + componentsCoveredByHelm << it.name + } + } + + Set helmReleasesCovered = [] + Map deploymentsForTir = [:] + deployments.each { String deploymentName, Map deployment -> + if (deploymentName.endsWith('-deploymentMean')) { + if (deployment.type == "helm") { + String releaseName = deployment?.helmReleaseName + if (!releaseName) { + logger.warn("No helmReleaseName name in ${deploymentName}: skipping") + return + } + if (releaseName in helmReleasesCovered) { + return + } + def withoutHelmStatus = deployment.findAll { k, v -> k != 'helmStatus' } + deploymentsForTir.put("${releaseName}-deploymentMean".toString(), withoutHelmStatus) + def helmStatus = assembleHelmStatus(deployment?.helmStatus ?: [:]) + deploymentsForTir.put("${releaseName}-deploymentStatus".toString(), helmStatus) + helmReleasesCovered << (releaseName) + } else { + deploymentsForTir.put(deploymentName, deployment) + } + } else { + if (deploymentName in componentsCoveredByHelm) { + return + } else { + deploymentsForTir.put(deploymentName, deployment.findAll { k, v -> k != 'podName' }) + } + } + } + logger.debug("createTIR - assembled deployments data:${prettyPrint(toJson(deploymentsForTir))}") + deploymentsForTir + } + + @SuppressWarnings('UnnecessaryCast') + Map assembleHelmStatus(Map helmStatus) { + def resources = helmStatus?.resources ?: [] as List > + def properResources = resources.findAll { + it?.kind && it?.name + } + def byKind = properResources.groupBy { it.kind } + String formattedResourcesByKind = byKind.collect { kind, resourcesOfKind -> + "${kind}: " + resourcesOfKind.collect { it.name }.join(', ') + }.sort().join(', ') + + def assembledHelmStatus = helmStatus.collectEntries { k, v -> + if (k == 'resources') { + [(k): formattedResourcesByKind ] + } else { + [(k): v] + } + } + return assembledHelmStatus + } + String createOverallTIR(Map repo = null, Map data = null) { def documentTypeName = DOCUMENT_TYPE_NAMES[DocumentType.OVERALL_TIR as String] def metadata = this.getDocumentMetadata(documentTypeName) diff --git a/src/org/ods/orchestration/util/DeploymentDescriptor.groovy b/src/org/ods/orchestration/util/DeploymentDescriptor.groovy index 76608a5ba..32170204d 100644 --- a/src/org/ods/orchestration/util/DeploymentDescriptor.groovy +++ b/src/org/ods/orchestration/util/DeploymentDescriptor.groovy @@ -17,6 +17,19 @@ class DeploymentDescriptor { this.createdByBuild = createdByBuild ?: '' } + /** + * This function takes a map of deployments and returns a stripped down version of it. + * + * deploymentMean information is moved from a key of the form '${resource-name}-deploymentMean' into the Map for + * key '${resource-name}' under key 'deploymentMean'. + * + * For each deployment, it processes its containers and modifies the image name based on certain conditions. + * If the first part of the image name is in the EXCLUDE_NAMESPACES_FROM_IMPORT list, it keeps the full image name. + * Otherwise, it only keeps the last part of the image name. + * + * @param deployments The original deployments map whose values are Maps themselves + * @return A new stripped down deployments map. + */ static Map stripDeployments(Map deployments) { def strippedDownDeployments = [:] def deploymentMeanPostfix = 'deploymentMean' diff --git a/src/org/ods/services/OpenShiftService.groovy b/src/org/ods/services/OpenShiftService.groovy index cf0cf7f39..2c1430640 100644 --- a/src/org/ods/services/OpenShiftService.groovy +++ b/src/org/ods/services/OpenShiftService.groovy @@ -5,6 +5,8 @@ import groovy.json.JsonOutput import groovy.json.JsonSlurperClassic import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode +import org.ods.util.HelmStatusData +import org.ods.util.HelmStatusSimpleData import org.ods.util.ILogger import org.ods.util.IPipelineSteps import org.ods.util.PodData @@ -155,6 +157,33 @@ class OpenShiftService { } } + HelmStatusSimpleData helmStatus( + String project, + String release + ) { + def helmStatusData = retrieveHelmStatus(project, release) + HelmStatusSimpleData.from(helmStatusData) + } + + HelmStatusData retrieveHelmStatus( + String project, + String release + ) { + try { + def helmStdout = steps.sh( + script: "helm -n ${project} status ${release} --show-resources -o json", + label: "Gather Helm status for release ${release} in ${project}", + returnStdout: true + ).toString().trim() + def object = new JsonSlurperClassic().parseText(helmStdout) + def helmStatusData = HelmStatusData.fromJsonObject(object) + helmStatusData + } catch (Exception e) { + throw new RuntimeException("Helm status Failed (${e.message})!" + + "Helm could not gather status of ${release} in ${project}") + } + } + @SuppressWarnings(['LineLength', 'ParameterCount']) void tailorApply(String project, Map target, String paramFile, List params, List preserve, String tailorPrivateKeyFile, boolean verify) { def verifyFlag = verify ? '--verify' : '' @@ -429,7 +458,12 @@ class OpenShiftService { ) } - // Returns data about the pods (replicas) of the deployment. + boolean isDeploymentKind(String kind) { + boolean b = kind in [DEPLOYMENTCONFIG_KIND, DEPLOYMENT_KIND] + b + } + + // Returns data about the pods (replicas) of the deployment. // If not all pods are running until the retries are exhausted, // an exception is thrown. List getPodDataForDeployment(String project, String kind, String podManagerName, int retries) { @@ -449,7 +483,7 @@ class OpenShiftService { throw new RuntimeException("Could not find 'running' pod(s) with label '${label}'") } - // getResourcesForComponent returns a map in which each kind is mapped to a list of resources. + // getResourcesForComponent returns a map in which each kind is mapped to a list of resources names. Map> getResourcesForComponent(String project, List kinds, String selector) { def items = steps.sh( script: """oc -n ${project} get ${kinds.join(',')} \ @@ -1256,7 +1290,7 @@ class OpenShiftService { ) } - @SuppressWarnings(['CyclomaticComplexity', 'AbcMetric']) + @SuppressWarnings(['CyclomaticComplexity', 'AbcMetric', 'LineLength']) @TypeChecked(TypeCheckingMode.SKIP) private List extractPodData(Map podJson) { List pods = [] @@ -1270,16 +1304,17 @@ class OpenShiftService { if (podOCData.metadata?.generateName) { pod.deploymentId = podOCData.metadata.generateName - ~/-$/ // Trim dash suffix } - pod.podNode = podOCData.spec?.nodeName ?: 'N/A' - pod.podIp = podOCData.status?.podIP ?: 'N/A' pod.podStatus = podOCData.status?.phase ?: 'N/A' - pod.podStartupTimeStamp = podOCData.status?.startTime ?: 'N/A' pod.containers = [:] // We need to get the image SHA from the containerStatuses, and not // from the pod spec because the pod spec image field is optional // and may not contain an image SHA, but e.g. a tag, depending on // the pod manager (e.g. ReplicationController, ReplicaSet). See - // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#container-v1-core. + // Comment above from version 1.19 no longer available online + // Cluster currently run on 1.27 + // docs for 1.27: https://v1-27.docs.kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/ + // latest: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container + // example of an imageID: "image-registry.openshift-image-registry.svc:5000/guardians-test/core-standalone@sha256:6a2290e522133866cafc4864c30ea6ba591de4238a865936e3e4f953d60c173a" podOCData.spec?.containers?.each { container -> podOCData.status?.containerStatuses?.each { containerStatus -> if (containerStatus.name == container.name) { diff --git a/src/org/ods/util/HelmStatusContainerStatusData.groovy b/src/org/ods/util/HelmStatusContainerStatusData.groovy new file mode 100644 index 000000000..e3cb46c75 --- /dev/null +++ b/src/org/ods/util/HelmStatusContainerStatusData.groovy @@ -0,0 +1,32 @@ +package org.ods.util + +import groovy.transform.TypeChecked + +@TypeChecked +class HelmStatusContainerStatusData { + + String name + String image + String imageID + + @SuppressWarnings(['Instanceof']) + HelmStatusContainerStatusData(Map map) { + def missingKeys = [] + def badTypes = [] + + def stringTypes = [ "name", "image", "imageID"] + for (att in stringTypes) { + if (!map.containsKey(att)) { + missingKeys << att + } else if (!(map[att] instanceof String)) { + badTypes << "${att}: expected String, found ${map[att].getClass()}" + } + } + HelmStatusData.handleMissingKeysOrBadTypes(missingKeys, badTypes) + + this.name = map['name'] as String + this.image = map['image'] as String + this.imageID = map['imageID'] as String + } + +} diff --git a/src/org/ods/util/HelmStatusData.groovy b/src/org/ods/util/HelmStatusData.groovy new file mode 100644 index 000000000..f1127f46a --- /dev/null +++ b/src/org/ods/util/HelmStatusData.groovy @@ -0,0 +1,181 @@ +package org.ods.util + +import com.cloudbees.groovy.cps.NonCPS +import groovy.transform.PackageScope +import groovy.transform.TypeChecked + +// Relevant data returned by helm status --show-resources -o json +@TypeChecked +class HelmStatusData { + + // release name + String name + String namespace + + HelmStatusInfoData info + + // version is an integer but we map it to a string. + String version + + @SuppressWarnings(['IfStatementBraces']) + @NonCPS + static HelmStatusData fromJsonObject(Object object) { + try { + def jsonObject = ensureMap(object, "") + + Map status = [:] + + // Constructors of classes receiving the data catches missing keys or unexpected types and + // report them in summary as IllegalArgumentException. + if (jsonObject.name) status.name = jsonObject.name + if (jsonObject.version) status.version = jsonObject.version + if (jsonObject.namespace) status.namespace = jsonObject.namespace + + def infoObject = ensureMap(jsonObject.info, "info") + Map info = [:] + + if (infoObject.status) info.status = infoObject.status + if (infoObject.description) info.description = infoObject.description + if (infoObject['last_deployed']) info.lastDeployed = infoObject['last_deployed'] + + def resourcesObject = ensureMap(infoObject.resources, "info.resources") + // All resources are in an json object which organize resources by keys + // Examples are "v1/Cluster", "v1/ConfigMap" "v1/Deployment", "v1/Pod(related)" + // For these keys the map contains list of resources. + Map> resourcesMap = [:] + for (entry in resourcesObject.entrySet()) { + def key = entry.key as String + def resourceList = ensureList(entry.value, "info.resources.${key}") + def resources = [] + resourceList.eachWithIndex { resourceJsonObject, i -> + def resourceData = fromJsonObjectHelmStatusResource( + resourceJsonObject, + "info.resources.${key}.[${i}]") + if (resourceData != null) { + resources << resourceData + } + } + resourcesMap.put(key, resources) + } + info.resources = resourcesMap + status.info = new HelmStatusInfoData(info) + new HelmStatusData(status) + } catch (Exception e) { + throw new IllegalArgumentException( + "Unexpected helm status information in JSON at 'info': ${e.getMessage()}") + } + } + + @SuppressWarnings(['Instanceof']) + @NonCPS + private static Map ensureMap(Object obj, String context) { + if (obj == null) { + return [:] + } + if (!(obj instanceof Map)) { + def msg = context ? + "${context}: expected JSON object, found ${obj.getClass()}" : + "Expected JSON object, found ${obj.getClass()}" + + throw new IllegalArgumentException(msg) + } + obj as Map + } + + @SuppressWarnings(['Instanceof']) + @NonCPS + private static List ensureList(Object obj, String context) { + if (obj == null) { + return [] + } + if (!(obj instanceof List)) { + throw new IllegalArgumentException( + "${context}: expected JSON array, found ${obj.getClass()}") + } + obj as List + } + + @SuppressWarnings(['IfStatementBraces', 'Instanceof']) + @NonCPS + private static HelmStatusResourceData fromJsonObjectHelmStatusResource( + resourceJsonObject, String context) { + def resourceObject = ensureMap(resourceJsonObject, context) + Map resource = [:] + if (resourceObject.apiVersion) resource.apiVersion = resourceObject.apiVersion + if (resourceObject.kind) resource.kind = resourceObject.kind + Map metadataObject = ensureMap( + resourceObject.metadata, "${context}.metadata") + if (metadataObject.name) resource.metadataName = metadataObject.name + if (resource.kind == "PodList") { + return null + } + if (resource.kind == "Pod") { + def statusObject = ensureMap(resourceObject.status, + "${context}.status") + List containerStatuses = [] + def containerStatusesJsonArray = ensureList(statusObject.containerStatuses, + "${context}.status.containerStatuses") + containerStatusesJsonArray.eachWithIndex { cs, int csi -> + def cso = ensureMap(cs, "${context}.status.containerStatuses[${csi}]") + Map containerStatus = [:] + if (cso.name) containerStatus.name = cso.name + if (cso.image) containerStatus.image = cso.image + if (cso.imageID) containerStatus.imageID = cso.imageID + containerStatuses << new HelmStatusContainerStatusData(cso) + } + resource.containerStatuses = containerStatuses + } + try { + new HelmStatusResourceData(resource) + } catch (Exception e) { + throw new IllegalArgumentException( + "Unexpected helm status JSON at '${context}': ${e.getMessage()}") + } + } + + @SuppressWarnings(['PublicMethodsBeforeNonPublicMethods']) + @NonCPS + static @PackageScope void handleMissingKeysOrBadTypes(List missingKeys, List badTypes) { + if (missingKeys || badTypes) { + def msgs = [] + if (missingKeys) { + msgs << "Missing keys: ${missingKeys.join(', ')}" + } + if (badTypes) { + msgs << "Bad types: ${badTypes.join(', ')}" + } + throw new IllegalArgumentException(msgs.join(".")) + } + } + + @SuppressWarnings(['Instanceof']) + HelmStatusData(Map map) { + def missingKeys = [] + def badTypes = [] + + def stringTypes = [ "name", "namespace"] + for (att in stringTypes) { + if (!map.containsKey(att)) { + missingKeys << att + } else if (!(map[att] instanceof String)) { + badTypes << "${att}: expected String, found ${map[att].getClass()}" + } + } + if (!map.containsKey('info')) { + missingKeys << 'info' + } else if (!(map['info'] instanceof HelmStatusInfoData)) { + badTypes << "info: expected HelmStatusInfoData, found ${map['info'].getClass()}" + } + + if (!map.containsKey('version')) { + missingKeys << 'version' + } + handleMissingKeysOrBadTypes(missingKeys, badTypes) + + this.name = map['name'] as String + this.namespace = map['namespace'] as String + this.info = map['info'] as HelmStatusInfoData + this.version = map['version'].toString() + } + +} diff --git a/src/org/ods/util/HelmStatusInfoData.groovy b/src/org/ods/util/HelmStatusInfoData.groovy new file mode 100644 index 000000000..e0525595e --- /dev/null +++ b/src/org/ods/util/HelmStatusInfoData.groovy @@ -0,0 +1,48 @@ +package org.ods.util + +import groovy.transform.TypeChecked + +//Relevant helm status info data we want to capture +@TypeChecked +class HelmStatusInfoData { + + // deployment status - see `helm status --help` for full list + // Example: "deployed" + String status + // description of the release (can be completion message or error message) + // Example: "Upgrade complete" + String description + // last-deployed field. + // Example: "2024-03-04T15:21:09.34520527Z" + String lastDeployed + + Map> resources + + @SuppressWarnings(['Instanceof']) + HelmStatusInfoData(Map map) { + def missingKeys = [] + def badTypes = [] + + def stringTypes = [ "status", "description", "lastDeployed"] + for (att in stringTypes) { + if (!map.containsKey(att)) { + missingKeys << att + } else if (!(map[att] instanceof String)) { + badTypes << "${att}: expected String, found ${map[att].getClass()}" + } + } + if (!map.containsKey('resources')) { + missingKeys << 'resources' + } else if (!(map['resources'] instanceof Map)) { + badTypes << "resources: expected Map, found ${map['resources'].getClass()}" + } + + HelmStatusData.handleMissingKeysOrBadTypes(missingKeys, badTypes) + + this.status = map['status'] as String + this.description = map['description'] as String + this.lastDeployed = map['lastDeployed'] as String + this.resources = map['resources'] as Map + } + +} diff --git a/src/org/ods/util/HelmStatusResource.groovy b/src/org/ods/util/HelmStatusResource.groovy new file mode 100644 index 000000000..7069d22ff --- /dev/null +++ b/src/org/ods/util/HelmStatusResource.groovy @@ -0,0 +1,25 @@ +package org.ods.util + +import com.cloudbees.groovy.cps.NonCPS +import groovy.transform.TypeChecked + +@TypeChecked +class HelmStatusResource { + + String kind + String name + + @NonCPS + Map toMap() { + [ + kind: kind, + name: name, + ] + } + + @NonCPS + String toString() { + toMap().toMapString() + } + +} diff --git a/src/org/ods/util/HelmStatusResourceData.groovy b/src/org/ods/util/HelmStatusResourceData.groovy new file mode 100644 index 000000000..d5a8c7492 --- /dev/null +++ b/src/org/ods/util/HelmStatusResourceData.groovy @@ -0,0 +1,39 @@ +package org.ods.util + +import groovy.transform.TypeChecked + +//Relevant helm status resource data we want to capture. This is inside the info data. +@TypeChecked +class HelmStatusResourceData { + + String apiVersion + String kind + String metadataName + // for kind "Pod" containerStatusData may be present + List containerStatuses = [] + + @SuppressWarnings(['Instanceof']) + HelmStatusResourceData(Map map) { + def missingKeys = [] + def badTypes = [] + + def stringTypes = [ "apiVersion", "kind", "metadataName"] + for (att in stringTypes) { + if (!map.containsKey(att)) { + missingKeys << att + } else if (!(map[att] instanceof String)) { + badTypes << "${att}: expected String, found ${map[att].getClass()}" + } + } + + HelmStatusData.handleMissingKeysOrBadTypes(missingKeys, badTypes) + + this.apiVersion = map['apiVersion'] as String + this.kind = map['kind'] as String + this.metadataName = map['metadataName'] as String + if (map.containsKey('containerStatuses')) { + this.containerStatuses = (map['containerStatuses'] as List) + } + } + +} diff --git a/src/org/ods/util/HelmStatusSimpleData.groovy b/src/org/ods/util/HelmStatusSimpleData.groovy new file mode 100644 index 000000000..12a70a660 --- /dev/null +++ b/src/org/ods/util/HelmStatusSimpleData.groovy @@ -0,0 +1,70 @@ +package org.ods.util + +import com.cloudbees.groovy.cps.NonCPS +import groovy.transform.TypeChecked + +@TypeChecked +class HelmStatusSimpleData { + + String releaseName + String releaseRevision + String namespace + String deployStatus + String deployDescription + String lastDeployed + List resources + + static HelmStatusSimpleData fromJsonObject(Object jsonObject) { + from(HelmStatusData.fromJsonObject(jsonObject)) + } + + @SuppressWarnings(['NestedForLoop']) + static HelmStatusSimpleData from(HelmStatusData status) { + def simpleResources = [] + for (resourceList in status.info.resources.values()) { + for (hsr in resourceList) { + simpleResources << new HelmStatusResource(kind: hsr.kind, name: hsr.metadataName) + } + } + new HelmStatusSimpleData( + releaseName: status.name, + releaseRevision: status.version, + namespace: status.namespace, + deployStatus: status.info.status, + deployDescription: status.info.description, + lastDeployed: status.info.lastDeployed, + resources: simpleResources,) + } + + Map> getResourcesByKind(List kinds) { + def deploymentResources = resources.findAll { it.kind in kinds } + Map> resourcesByKind = [:] + deploymentResources.each { + if (!resourcesByKind.containsKey(it.kind)) { + resourcesByKind[it.kind] = [] + } + resourcesByKind[it.kind] << it.name + } + resourcesByKind + } + + @NonCPS + Map toMap() { + def result = [ + releaseName: releaseName, + releaseRevision: releaseRevision, + namespace: namespace, + deployStatus: deployStatus, + deployDescription: deployDescription, + lastDeployed: lastDeployed, + resources: resources.collect { [kind: it.kind, name: it.name] } + ] + result + } + + @NonCPS + String toString() { + toMap().toMapString() + } + +} diff --git a/src/org/ods/util/PodData.groovy b/src/org/ods/util/PodData.groovy index dd26b8903..87edd4c0a 100644 --- a/src/org/ods/util/PodData.groovy +++ b/src/org/ods/util/PodData.groovy @@ -24,22 +24,10 @@ class PodData { // Example: foo-3 String deploymentId - // podNode is the node name on which of the pod, equal to .spec.nodeName. - // Example: ip-172-32-53-123.eu-west-1.compute.internal - String podNode - - // podIp is the IP of the pod, equal to .status.podIP. - // Example: 10.132.16.73 - String podIp - // podStatus is the status phase of the pod, equal to .status.phase // Example: Running String podStatus - // podStartupTimeStamp is the start time of the pod, equal to .status.startTime. - // Example: 2020-11-02T10:57:35Z - String podStartupTimeStamp - // containers is a map of container names to their image. // Example: [bar: '172.30.21.193:5000/foo/bar@sha256:a828...4389'] Map containers @@ -51,10 +39,7 @@ class PodData { podNamespace: podNamespace, podMetaDataCreationTimestamp: podMetaDataCreationTimestamp, deploymentId: deploymentId, - podNode: podNode, - podIp: podIp, podStatus: podStatus, - podStartupTimeStamp: podStartupTimeStamp, containers: containers, ] } From 449dd9e75ec8e41c730eef3010ca29ddda41d699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrich=20Kr=C3=A4mer?= Date: Mon, 1 Jul 2024 13:10:17 +0000 Subject: [PATCH 2/5] Tests were previously not pushed. --- .../HelmDeploymentStrategySpec.groovy | 90 +- .../usecase/LeVADocumentUseCaseSpec.groovy | 86 +- .../ods/services/OpenShiftServiceSpec.groovy | 66 +- test/groovy/util/HelmStatusSpec.groovy | 45 + ...StageRolloutOpenShiftDeploymentSpec.groovy | 36 +- test/resources/deployments-data.json | 112 ++ test/resources/helmstatus.json | 1422 +++++++++++++++++ 7 files changed, 1782 insertions(+), 75 deletions(-) create mode 100644 test/groovy/util/HelmStatusSpec.groovy create mode 100644 test/resources/deployments-data.json create mode 100644 test/resources/helmstatus.json diff --git a/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy b/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy index 81edfd3b5..f4f7e73d3 100644 --- a/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy +++ b/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy @@ -1,11 +1,14 @@ package org.ods.component +import groovy.json.JsonSlurperClassic import org.ods.services.JenkinsService import org.ods.services.OpenShiftService import org.ods.services.ServiceRegistry +import org.ods.util.HelmStatusSimpleData import org.ods.util.Logger import org.ods.util.PodData import spock.lang.Shared +import util.FixtureHelper import vars.test_helper.PipelineSpockTestBase class HelmDeploymentStrategySpec extends PipelineSpockTestBase { @@ -21,45 +24,75 @@ class HelmDeploymentStrategySpec extends PipelineSpockTestBase { buildUrl: 'https://jenkins.example.com/job/foo-cd/job/foo-cd-bar-master/11/console', buildTime: '2020-03-23 12:27:08 +0100', odsSharedLibVersion: '2.x', - projectId: 'foo', - componentId: 'bar', - cdProject: 'foo-cd', + projectId: 'guardians', + componentId: 'core', + cdProject: 'guardians-cd', artifactUriStore: [builds: [bar: [:]]] ] def "rollout: check deploymentMean"() { given: + def expectedDeploymentMean = [ + type : "helm", + selector : "app=guardians-core", + chartDir : "chart", + helmReleaseName : "core", + helmEnvBasedValuesFiles: [], + helmValuesFiles : ["values.yaml"], + helmValues : [:], + helmDefaultFlags : ["--install", "--atomic"], + helmAdditionalFlags : [], + helmStatus : [ + releaseName : "standalone-app", + releaseRevision : "43", + namespace : "guardians-test", + deployStatus : "deployed", + deployDescription: "Upgrade complete", + lastDeployed : "2024-03-04T15:21:09.34520527Z", + resources : [ + [kind: "ConfigMap", name: "core-appconfig-configmap"], + [kind: "Deployment", name: "core"], + [kind: "Deployment", name: "standalone-gateway"], + [kind: "Service", name: "core"], + [kind: "Service", name: "standalone-gateway"], + [kind: "Cluster", name: "edb-cluster"], + [kind: "Secret", name: "core-rsa-key-secret"], + [kind: "Secret", name: "core-security-exandradev-secret"], + [kind: "Secret", name: "core-security-unify-secret"] + ] + ] + ] def expectedDeploymentMeans = [ - "builds": [:], - "deployments": [ - "bar-deploymentMean": [ - "type": "helm", - "selector": "app=foo-bar", - "chartDir": "chart", - "helmReleaseName": "bar", - "helmEnvBasedValuesFiles": [], - "helmValuesFiles": ["values.yaml"], - "helmValues": [:], - "helmDefaultFlags": ["--install", "--atomic"], - "helmAdditionalFlags": [] + builds : [:], + deployments: [ + "core-deploymentMean" : expectedDeploymentMean, + "core" : [ + podName : null, + podNamespace : null, + podMetaDataCreationTimestamp: null, + deploymentId : "core-124", + podStatus : null, + containers : null + ], + "standalone-gateway-deploymentMean": expectedDeploymentMean, + "standalone-gateway" : [ + podName : null, + podNamespace : null, + podMetaDataCreationTimestamp: null, + deploymentId : "core-124", + podStatus : null, + containers : null, ], - "bar":[ - "podName": null, - "podNamespace": null, - "podMetaDataCreationTimestamp": null, - "deploymentId": "bar-124", - "podNode": null, - "podIp": null, - "podStatus": null, - "podStartupTimeStamp": null, - "containers": null, - ] ] ] + def config = [:] - def ctxData = contextData + [environment: 'dev', targetProject: 'foo-dev', openshiftRolloutTimeoutRetries: 5, chartDir: 'chart'] + def helmStatusFile = new FixtureHelper().getResource("helmstatus.json") + def helmStatus = HelmStatusSimpleData.fromJsonObject(new JsonSlurperClassic().parseText(helmStatusFile.text)) + + def ctxData = contextData + [environment: 'test', targetProject: 'guardians-test', openshiftRolloutTimeoutRetries: 5, chartDir: 'chart'] IContext context = new Context(null, ctxData, logger) OpenShiftService openShiftService = Mock(OpenShiftService.class) openShiftService.checkForPodData(*_) >> [new PodData([deploymentId: "${contextData.componentId}-124"])] @@ -72,8 +105,7 @@ class HelmDeploymentStrategySpec extends PipelineSpockTestBase { HelmDeploymentStrategy strategy = Spy(HelmDeploymentStrategy, constructorArgs: [null, context, config, openShiftService, jenkinsService, logger]) when: - def deploymentResources = [Deployment: ['bar']] - def rolloutData = strategy.getRolloutData(deploymentResources) + strategy.getRolloutData(helmStatus) def actualDeploymentMeans = context.getBuildArtifactURIs() diff --git a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy index 799c04509..b643ed9b2 100644 --- a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy +++ b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy @@ -1,6 +1,7 @@ package org.ods.orchestration.usecase import groovy.json.JsonSlurper +import groovy.json.JsonSlurperClassic import groovy.util.logging.Log import groovy.util.logging.Slf4j import org.apache.commons.io.FileUtils @@ -8,6 +9,7 @@ import org.junit.Rule import org.junit.rules.TemporaryFolder import org.ods.util.ILogger import org.ods.services.ServiceRegistry +import org.ods.util.PodData import spock.lang.Unroll import org.ods.services.JenkinsService @@ -890,6 +892,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { 1 * usecase.updateJiraDocumentationTrackingIssue(documentType, uri, "${docHistory.getVersion()}") } + def "create IVR"() { given: jiraUseCase = Spy(new JiraUseCase(project, steps, util, Mock(JiraService), logger)) @@ -960,10 +963,10 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def version = (odsRepoType == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE) ? 'WIP' : '1.0' def expectedSpecifications = systemDesignSpec - ? ["key":"NET-128", - "req_key":"NET-125", - "description":systemDesignSpec] - : null + ? ["key":"NET-128", + "req_key":"NET-125", + "description":systemDesignSpec] + : null def expectedComponents = ["key":"Technology-demo-app-catalogue", "nameOfSoftware":"demo-app-catalogue", "componentType":componentTypeLong, @@ -1073,7 +1076,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { "risks": ["NET-126"], "tests": ["NET-127"] }''', - ''' + ''' { "key": "NET-128", "id": "128", @@ -1087,7 +1090,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { "risks": ["NET-126"], "tests": ["NET-127"] }''', - ''' + ''' { "key": "NET-128", "id": "128", @@ -1278,6 +1281,43 @@ class LeVADocumentUseCaseSpec extends SpecHelper { 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) } + def "assemble deploymentInfo for TIR"() { + given: + def file = new FixtureHelper().getResource("deployments-data.json") + os.isDeploymentKind(*_) >> true + + when: + def deploymentsData = new JsonSlurperClassic().parseText(file.text) + def assembledData = usecase.assembleDeployments(deploymentsData.deployments) + + then: + + assembledData["backend-helm-monorepo-deploymentMean"] == [ + chartDir : "chart", + repoId : "backend-helm-monorepo", + helmAdditionalFlags: [], + helmEnvBasedValuesFiles :[], + helmValues :[ + registry :"image-registry.openshift-image-registry.svc:5000", + componentId :"backend-helm-monorepo" + ], + helmDefaultFlags :["--install", "--atomic"], + helmReleaseName :"backend-helm-monorepo", + selector :"app.kubernetes.io/instance=backend-helm-monorepo", + helmValuesFiles :["values.yaml"], + type :"helm", ] + + assembledData["backend-helm-monorepo-deploymentStatus"] == [ + releaseRevision :"2", + releaseName :"backend-helm-monorepo", + namespace: "kraemerh-dev", + deployDescription :"Upgrade complete", + resources : "Deployment: backend-helm-monorepo-chart-component-a, backend-helm-monorepo-chart-component-b, Service: backend-helm-monorepo-chart", + deployStatus :"deployed", + lastDeployed :"2024-06-26T12:59:51.270713404Z" + ] + } + def "create overall DTR"() { given: // Argument Constraints @@ -1657,25 +1697,25 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def "order steps"() { given: def testIssue = [ key: "JIRA-1" , - steps: [ - [ - orderId: 2, - data: "N/A" - ], - [ - orderId: 1, - data: "N/A" - ] - ]] + steps: [ + [ + orderId: 2, + data: "N/A" + ], + [ + orderId: 1, + data: "N/A" + ] + ]] - when: - LeVADocumentUseCase leVADocumentUseCase = new LeVADocumentUseCase(null, null, null, - null, null, null, null, null, null, null, - null, null, null, null) - def ordered = leVADocumentUseCase.sortTestSteps(testIssue.steps) + when: + LeVADocumentUseCase leVADocumentUseCase = new LeVADocumentUseCase(null, null, null, + null, null, null, null, null, null, null, + null, null, null, null) + def ordered = leVADocumentUseCase.sortTestSteps(testIssue.steps) - then: - ordered.get(0).orderId == 1 + then: + ordered.get(0).orderId == 1 } def "referenced documents version"() { diff --git a/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy b/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy index efbabf285..8a96b37cf 100644 --- a/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy +++ b/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy @@ -2,6 +2,7 @@ package org.ods.services import groovy.json.JsonSlurperClassic +import org.ods.util.HelmStatusSimpleData import org.ods.util.ILogger import org.ods.util.IPipelineSteps import org.ods.util.Logger @@ -189,6 +190,10 @@ class OpenShiftServiceSpec extends SpecHelper { then: results.size() == expected.size() for (int i = 0; i < results.size(); i++) { + // Note: podIp, podNode and podStartupTimeStamp are no longer in type PodData. + // Each result is only a subset of the corresponding expected data. + // While it can surpise that the == assertion below would not catch this, + // it actually comes in handy so we can leave the original data in place. results[i].toMap() == expected[i] } } @@ -200,25 +205,17 @@ class OpenShiftServiceSpec extends SpecHelper { def file = new FixtureHelper().getResource("pods.json") List expected = [ [ - podName : 'example-be-token-6fcb4d85d6-7jr2r', podNamespace : 'proj-dev', podMetaDataCreationTimestamp: '2023-07-24T11:58:29Z', deploymentId : 'example-be-token-6fcb4d85d6', - podNode : 'ip-10-32-10-30.eu-west-1.compute.internal', - podIp : '192.0.2.172', podStatus : 'Running', - podStartupTimeStamp : '2023-07-24T11:58:29Z', containers : ['be-token': 'image-registry.openshift-image-registry.svc:5000/proj-dev/example-be-token@sha256:cc5e57f98ee789429384e8df2832a89fbf1092b724aa8f3faff2708e227cb39e'] ], [ - podName : 'example-be-token-6fcb4d85d6-ndp8x', podNamespace : 'proj-dev', podMetaDataCreationTimestamp: '2023-07-24T11:58:29Z', deploymentId : 'example-be-token-6fcb4d85d6', - podNode : 'ip-10-32-9-69.eu-west-1.compute.internal', - podIp : '192.0.2.171', podStatus : 'Running', - podStartupTimeStamp : '2023-07-24T11:58:29Z', containers : ['be-token': 'image-registry.openshift-image-registry.svc:5000/proj-dev/example-be-token@sha256:cc5e57f98ee789429384e8df2832a89fbf1092b724aa8f3faff2708e227cb39e'] ] ] @@ -250,16 +247,61 @@ class OpenShiftServiceSpec extends SpecHelper { podNamespace: 'foo-dev', podMetaDataCreationTimestamp: '2020-05-18T10:43:56Z', deploymentId: 'bar-164', - podNode: 'ip-172-31-61-82.eu-central-1.compute.internal', - podIp: '10.128.17.92', podStatus: 'Running', - podStartupTimeStamp: '2020-05-18T10:43:56Z', containers: [ bar: '172.30.21.196:5000/foo-dev/bar@sha256:07ba1778e7003335e6f6e0f809ce7025e5a8914dc5767f2faedd495918bee58a' ] ] } + def "helm status data extraction"() { + given: + def steps = Spy(util.PipelineSteps) + def service = new OpenShiftService(steps, new Logger(steps, false)) + def helmJsonText = new FixtureHelper().getResource("helmstatus.json").text + + when: + def helmStatusData = service.retrieveHelmStatus('guardians-test', 'standalone-app') +// OpenShiftService.DEPLOYMENT_KIND, OpenShiftService.DEPLOYMENTCONFIG_KIND,]) + then: + 1 * steps.sh( + script: 'helm -n guardians-test status standalone-app --show-resources -o json', + label: 'Gather Helm status for release standalone-app in guardians-test', + returnStdout: true, + ) >> helmJsonText + helmStatusData.name == 'standalone-app' + helmStatusData.namespace == 'guardians-test' + } + + def "helm status data extraction bad content"() { + given: + def steps = Spy(util.PipelineSteps) + def service = new OpenShiftService(steps, new Logger(steps, false)) + def helmJsonText = """ +{ + "name": "standalone-app", + "info": { + "first_deployed": "2022-12-19T09:44:32.164490076Z", + "last_deployed": "2024-03-04T15:21:09.34520527Z", + "deleted": "", + "description": "Upgrade complete", + "status": "deployed", + "resources" : {} + } +} + """ + when: + def helmStatusData = service.helmStatus('guardians-test', 'standalone-app') +// OpenShiftService.DEPLOYMENT_KIND, OpenShiftService.DEPLOYMENTCONFIG_KIND,]) + then: + 1 * steps.sh( + script: 'helm -n guardians-test status standalone-app --show-resources -o json', + label: 'Gather Helm status for release standalone-app in guardians-test', + returnStdout: true, + ) + thrown RuntimeException + } + def "helm upgrade"() { given: def steps = Spy(util.PipelineSteps) @@ -752,7 +794,7 @@ class OpenShiftServiceSpec extends SpecHelper { when: def result = service.getConsoleUrl(steps) - then: + then: result == routeUrl } diff --git a/test/groovy/util/HelmStatusSpec.groovy b/test/groovy/util/HelmStatusSpec.groovy new file mode 100644 index 000000000..de63030a9 --- /dev/null +++ b/test/groovy/util/HelmStatusSpec.groovy @@ -0,0 +1,45 @@ +package org.ods.util + +import groovy.json.JsonSlurperClassic +import org.ods.services.OpenShiftService +import util.FixtureHelper +import util.SpecHelper + +class HelmStatusSpec extends SpecHelper { + def "helm status parsing"() { + given: + def file = new FixtureHelper().getResource("helmstatus.json") + + when: + def helmParsedStatus = HelmStatusData.fromJsonObject(new JsonSlurperClassic().parseText(file.text)) + def helmStatus = HelmStatusSimpleData.from(helmParsedStatus) + def simpleStatusMap = helmStatus.toMap() + def simpleStatusNoResources = simpleStatusMap.findAll { k,v -> k != "resources"} + def deploymentResources = helmStatus.getResourcesByKind([ + OpenShiftService.DEPLOYMENT_KIND, OpenShiftService.DEPLOYMENTCONFIG_KIND,]) + then: + simpleStatusNoResources == [ + releaseName: 'standalone-app', + releaseRevision: '43', + namespace: 'guardians-test', + deployStatus: 'deployed', + deployDescription: 'Upgrade complete', + lastDeployed: '2024-03-04T15:21:09.34520527Z' + ] + simpleStatusMap.resources == [ + [kind: 'ConfigMap', name:'core-appconfig-configmap'], + [kind: 'Deployment', name:'core'], + [kind: 'Deployment', name:'standalone-gateway'], + [kind: 'Service', name:'core'], + [kind: 'Service', name:'standalone-gateway'], + [kind: 'Cluster', name:'edb-cluster'], + [kind: 'Secret', name:'core-rsa-key-secret'], + [kind: 'Secret', name:'core-security-exandradev-secret'], + [kind: 'Secret', name:'core-security-unify-secret'] + ] + deploymentResources == [ + Deployment: [ 'core', 'standalone-gateway'] + ] + + } +} diff --git a/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy b/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy index 9bffdc955..e47edde1c 100644 --- a/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy +++ b/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy @@ -1,13 +1,18 @@ package vars + +import groovy.json.JsonSlurperClassic import org.codehaus.groovy.runtime.typehandling.GroovyCastException import org.ods.component.Context import org.ods.component.IContext import org.ods.services.OpenShiftService import org.ods.services.JenkinsService import org.ods.services.ServiceRegistry +import org.ods.util.HelmStatusData +import org.ods.util.HelmStatusSimpleData import org.ods.util.Logger import org.ods.util.PodData +import util.FixtureHelper import util.PipelineSteps import vars.test_helper.PipelineSpockTestBase import spock.lang.* @@ -149,15 +154,22 @@ class OdsComponentStageRolloutOpenShiftDeploymentSpec extends PipelineSpockTestB def "run successfully with Helm"() { given: - def c = config + [environment: 'dev',targetProject: 'foo-dev',openshiftRolloutTimeoutRetries: 5,chartDir: 'chart'] + def c = config + [ + projectId: 'guardians', + componentId: 'core', + environment: 'test', + targetProject: 'guardians-test', + openshiftRolloutTimeoutRetries: 5, + chartDir: 'chart'] + def helmJsonText = new FixtureHelper().getResource("helmstatus.json").text + IContext context = new Context(null, c, logger) OpenShiftService openShiftService = Mock(OpenShiftService.class) - openShiftService.getResourcesForComponent('foo-dev', ['Deployment', 'DeploymentConfig'], 'app=foo-bar') >> [Deployment: ['bar']] - openShiftService.getRevision(*_) >> 123 - openShiftService.rollout(*_) >> "${config.componentId}-124" - openShiftService.getPodDataForDeployment(*_) >> [new PodData([ deploymentId: "${config.componentId}-124" ])] - openShiftService.getImagesOfDeployment(*_) >> [[ repository: 'foo', name: 'bar' ]] - openShiftService.checkForPodData(*_) >> [new PodData([deploymentId: "${config.componentId}-124"])] + openShiftService.helmStatus('guardians-test', 'standalone-app') >> HelmStatusSimpleData.from(HelmStatusData.fromJsonObject(new JsonSlurperClassic().parseText(helmJsonText))) + // todo: verify that we did not want to ensure that build images are tagged here. + // - the org.ods.component.Context.artifactUriStore is not initialized with c when created above! + // - as a consequence the build artifacts are empty so no retagging happens here. + openShiftService.checkForPodData(*_) >> [new PodData([deploymentId: "${c.componentId}-124"])] ServiceRegistry.instance.add(OpenShiftService, openShiftService) JenkinsService jenkinsService = Stub(JenkinsService.class) jenkinsService.maybeWithPrivateKeyCredentials(*_) >> { args -> args[1]('/tmp/file') } @@ -192,19 +204,21 @@ class OdsComponentStageRolloutOpenShiftDeploymentSpec extends PipelineSpockTestB return metadata } } - def deploymentInfo = script.call(context) + def deploymentInfo = script.call(context, [ + helmReleaseName: "standalone-app", + ]) then: printCallStack() assertJobStatusSuccess() - deploymentInfo['Deployment/bar'][0].deploymentId == "bar-124" + deploymentInfo['Deployment/core'][0].deploymentId == "core-124" // test artifact URIS def buildArtifacts = context.getBuildArtifactURIs() buildArtifacts.size() > 0 - buildArtifacts.deployments['bar-deploymentMean']['type'] == 'helm' + buildArtifacts.deployments['core-deploymentMean']['type'] == 'helm' - 1 * openShiftService.helmUpgrade('foo-dev', 'bar', ['values.yaml'], ['registry':null, 'componentId':'bar', 'global.registry':null, 'global.componentId':'bar', 'imageNamespace':'foo-dev', 'imageTag':'cd3e9082', 'global.imageNamespace':'foo-dev', 'global.imageTag':'cd3e9082'], ['--install', '--atomic'], [], true) + 1 * openShiftService.helmUpgrade('guardians-test', 'standalone-app', ['values.yaml'], ['registry':null, 'componentId':'core', 'global.registry':null, 'global.componentId':'core', 'imageNamespace':'guardians-test', 'imageTag':'cd3e9082', 'global.imageNamespace':'guardians-test', 'global.imageTag':'cd3e9082'], ['--install', '--atomic'], [], true) } @Unroll diff --git a/test/resources/deployments-data.json b/test/resources/deployments-data.json new file mode 100644 index 000000000..9a9f26b68 --- /dev/null +++ b/test/resources/deployments-data.json @@ -0,0 +1,112 @@ +{ + "deployments": { + "backend-helm-monorepo-chart-component-a": { + "podName": "backend-helm-monorepo-chart-component-a-7d6659884-vhl2z", + "podNamespace": "kraemerh-test", + "podMetaDataCreationTimestamp": "2024-06-26T13:05:33Z", + "deploymentId": "backend-helm-monorepo-chart-component-a-7d6659884", + "podStatus": "Running", + "containers": { + "chart-component-a": "image-registry.openshift-image-registry.svc:5000/kraemerh-test/backend-helm-monorepo-component-a@sha256:5c6440e6179138842d75a9b4a0eb9dd283097839931119e79ee0da43656c8870" + } + }, + "backend-helm-monorepo-chart-component-a-deploymentMean": { + "type": "helm", + "selector": "app.kubernetes.io/instance=backend-helm-monorepo", + "chartDir": "chart", + "helmReleaseName": "backend-helm-monorepo", + "helmEnvBasedValuesFiles": [ + ], + "helmValuesFiles": [ + "values.yaml" + ], + "helmValues": { + "registry": "image-registry.openshift-image-registry.svc:5000", + "componentId": "backend-helm-monorepo" + }, + "helmDefaultFlags": [ + "--install", + "--atomic" + ], + "helmAdditionalFlags": [ + ], + "helmStatus": { + "releaseName": "backend-helm-monorepo", + "releaseRevision": "2", + "namespace": "kraemerh-dev", + "deployStatus": "deployed", + "deployDescription": "Upgrade complete", + "lastDeployed": "2024-06-26T12:59:51.270713404Z", + "resources": [ + { + "kind": "Deployment", + "name": "backend-helm-monorepo-chart-component-a" + }, + { + "kind": "Deployment", + "name": "backend-helm-monorepo-chart-component-b" + }, + { + "kind": "Service", + "name": "backend-helm-monorepo-chart" + } + ] + }, + "repoId": "backend-helm-monorepo" + }, + "backend-helm-monorepo-chart-component-b": { + "podName": "backend-helm-monorepo-chart-component-b-87c7f548d-6hhcz", + "podNamespace": "kraemerh-test", + "podMetaDataCreationTimestamp": "2024-06-26T13:05:33Z", + "deploymentId": "backend-helm-monorepo-chart-component-b-87c7f548d", + "podStatus": "Running", + "containers": { + "chart-component-b": "image-registry.openshift-image-registry.svc:5000/kraemerh-test/backend-helm-monorepo-component-b@sha256:5e9ed6ba8458a9501a9d973398ff27e6e50411d3745cec0dac761e07378185a2" + } + }, + "backend-helm-monorepo-chart-component-b-deploymentMean": { + "type": "helm", + "selector": "app.kubernetes.io/instance=backend-helm-monorepo", + "chartDir": "chart", + "helmReleaseName": "backend-helm-monorepo", + "helmEnvBasedValuesFiles": [ + ], + "helmValuesFiles": [ + "values.yaml" + ], + "helmValues": { + "registry": "image-registry.openshift-image-registry.svc:5000", + "componentId": "backend-helm-monorepo" + }, + "helmDefaultFlags": [ + "--install", + "--atomic" + ], + "helmAdditionalFlags": [ + ], + "helmStatus": { + "releaseName": "backend-helm-monorepo", + "releaseRevision": "2", + "namespace": "kraemerh-dev", + "deployStatus": "deployed", + "deployDescription": "Upgrade complete", + "lastDeployed": "2024-06-26T12:59:51.270713404Z", + "resources": [ + { + "kind": "Deployment", + "name": "backend-helm-monorepo-chart-component-a" + }, + { + "kind": "Deployment", + "name": "backend-helm-monorepo-chart-component-b" + }, + { + "kind": "Service", + "name": "backend-helm-monorepo-chart" + } + ] + }, + "repoId": "backend-helm-monorepo" + } + } +} diff --git a/test/resources/helmstatus.json b/test/resources/helmstatus.json new file mode 100644 index 000000000..e81088a83 --- /dev/null +++ b/test/resources/helmstatus.json @@ -0,0 +1,1422 @@ +{ + "name": "standalone-app", + "info": { + "first_deployed": "2022-12-19T09:44:32.164490076Z", + "last_deployed": "2024-03-04T15:21:09.34520527Z", + "deleted": "", + "description": "Upgrade complete", + "status": "deployed", + "resources": { + "v1/Cluster": [ + { + "apiVersion": "postgresql.k8s.enterprisedb.io/v1", + "kind": "Cluster", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-07-04T13:18:28Z", + "generation": 3, + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "edb-cluster", + "app.kubernetes.io/version": "2f70aaae0e4facb06e6981c542b235e89fe156c3", + "helm.sh/chart": "edb-cluster-0.1.0_2f70aaae0e4facb06e6981c542b235e89fe156c3" + }, + "name": "edb-cluster", + "namespace": "guardians-test", + "resourceVersion": "2880969905", + "uid": "3ee355b9-0ebb-4997-9351-47b96def618f" + }, + "spec": { + "affinity": { + "podAntiAffinityType": "preferred" + }, + "bootstrap": { + "initdb": { + "database": "app", + "encoding": "UTF8", + "localeCType": "C", + "localeCollate": "C", + "owner": "app" + } + }, + "enableSuperuserAccess": true, + "failoverDelay": 0, + "imageName": "quay.io/enterprisedb/postgresql:15.3", + "instances": 1, + "logLevel": "info", + "maxSyncReplicas": 0, + "minSyncReplicas": 0, + "monitoring": { + "customQueriesConfigMap": [ + { + "key": "queries", + "name": "postgresql-operator-default-monitoring" + } + ], + "disableDefaultQueries": false, + "enablePodMonitor": false + }, + "postgresGID": 26, + "postgresUID": 26, + "postgresql": { + "parameters": { + "archive_mode": "on", + "archive_timeout": "5min", + "dynamic_shared_memory_type": "posix", + "log_destination": "csvlog", + "log_directory": "/controller/log", + "log_filename": "postgres", + "log_rotation_age": "0", + "log_rotation_size": "0", + "log_truncate_on_rotation": "false", + "logging_collector": "on", + "max_parallel_workers": "32", + "max_replication_slots": "32", + "max_worker_processes": "32", + "shared_memory_type": "mmap", + "shared_preload_libraries": "", + "ssl_max_protocol_version": "TLSv1.3", + "ssl_min_protocol_version": "TLSv1.3", + "wal_keep_size": "512MB", + "wal_receiver_timeout": "5s", + "wal_sender_timeout": "5s" + }, + "syncReplicaElectionConstraint": { + "enabled": false + } + }, + "primaryUpdateMethod": "restart", + "primaryUpdateStrategy": "unsupervised", + "replicationSlots": { + "highAvailability": { + "enabled": true, + "slotPrefix": "_cnp_" + }, + "updateInterval": 30 + }, + "resources": {}, + "smartShutdownTimeout": 180, + "startDelay": 30, + "stopDelay": 30, + "storage": { + "resizeInUseVolumes": true, + "size": "20Gi" + }, + "switchoverDelay": 40000000 + }, + "status": { + "certificates": { + "clientCASecret": "edb-cluster-ca", + "expirations": { + "edb-cluster-ca": "2024-08-29 14:02:22 +0000 UTC", + "edb-cluster-replication": "2024-08-29 14:02:22 +0000 UTC", + "edb-cluster-server": "2024-08-29 14:02:22 +0000 UTC" + }, + "replicationTLSSecret": "edb-cluster-replication", + "serverAltDNSNames": [ + "edb-cluster-rw", + "edb-cluster-rw.guardians-test", + "edb-cluster-rw.guardians-test.svc", + "edb-cluster-r", + "edb-cluster-r.guardians-test", + "edb-cluster-r.guardians-test.svc", + "edb-cluster-ro", + "edb-cluster-ro.guardians-test", + "edb-cluster-ro.guardians-test.svc" + ], + "serverCASecret": "edb-cluster-ca", + "serverTLSSecret": "edb-cluster-server" + }, + "cloudNativePostgresqlCommitHash": "949626034", + "cloudNativePostgresqlOperatorHash": "0737af0747dd2ac7040c7b21655bd8e5fa4e01028fe9d833891be071390e8785", + "conditions": [ + { + "lastTransitionTime": "2024-05-25T14:42:08Z", + "message": "Cluster is Ready", + "reason": "ClusterIsReady", + "status": "True", + "type": "Ready" + }, + { + "lastTransitionTime": "2023-07-04T13:19:38Z", + "message": "velero addon is disabled", + "reason": "Disabled", + "status": "False", + "type": "k8s.enterprisedb.io/velero" + }, + { + "lastTransitionTime": "2023-07-04T13:19:38Z", + "message": "external-backup-adapter addon is disabled", + "reason": "Disabled", + "status": "False", + "type": "k8s.enterprisedb.io/externalBackupAdapter" + }, + { + "lastTransitionTime": "2023-07-04T13:19:38Z", + "message": "external-backup-adapter-cluster addon is disabled", + "reason": "Disabled", + "status": "False", + "type": "k8s.enterprisedb.io/externalBackupAdapterCluster" + }, + { + "lastTransitionTime": "2023-07-04T13:19:40Z", + "message": "kasten addon is disabled", + "reason": "Disabled", + "status": "False", + "type": "k8s.enterprisedb.io/kasten" + }, + { + "lastTransitionTime": "2023-11-30T15:26:14Z", + "message": "Continuous archiving is working", + "reason": "ContinuousArchivingSuccess", + "status": "True", + "type": "ContinuousArchiving" + } + ], + "configMapResourceVersion": { + "metrics": { + "postgresql-operator-default-monitoring": "2880955105" + } + }, + "currentPrimary": "edb-cluster-1", + "currentPrimaryTimestamp": "2023-07-04T13:19:27.039619Z", + "healthyPVC": [ + "edb-cluster-1" + ], + "instanceNames": [ + "edb-cluster-1" + ], + "instances": 1, + "instancesReportedState": { + "edb-cluster-1": { + "isPrimary": true, + "timeLineID": 1 + } + }, + "instancesStatus": { + "healthy": [ + "edb-cluster-1" + ] + }, + "latestGeneratedNode": 1, + "licenseStatus": { + "licenseExpiration": "2999-12-31T00:00:00Z", + "licenseStatus": "Valid license (Boehringer Ingelheim (boehringer_ingelheim))", + "repositoryAccess": false, + "valid": true + }, + "managedRolesStatus": {}, + "phase": "Cluster in healthy state", + "poolerIntegrations": { + "pgBouncerIntegration": {} + }, + "pvcCount": 1, + "readService": "edb-cluster-r", + "readyInstances": 1, + "secretsResourceVersion": { + "applicationSecretVersion": "2880969810", + "clientCaSecretVersion": "2880969811", + "replicationSecretVersion": "2880969813", + "serverCaSecretVersion": "2880969811", + "serverSecretVersion": "2880969815", + "superuserSecretVersion": "2880969816" + }, + "targetPrimary": "edb-cluster-1", + "targetPrimaryTimestamp": "2023-07-04T13:18:29.516149Z", + "timelineID": 1, + "topology": { + "instances": { + "edb-cluster-1": {} + }, + "nodesUsed": 1, + "successfullyExtracted": true + }, + "writeService": "edb-cluster-rw" + } + } + ], + "v1/ConfigMap": [ + { + "apiVersion": "v1", + "data": { + "application.yaml": "REDACTED\n" + }, + "kind": "ConfigMap", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-05-16T15:41:54Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core-appconfig-configmap", + "namespace": "guardians-test", + "resourceVersion": "2880955101", + "uid": "612ad220-26de-44b6-bbf6-31ba57e456cb" + } + } + ], + "v1/Deployment": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "36", + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2022-12-19T09:44:33Z", + "generation": 42, + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core", + "namespace": "guardians-test", + "resourceVersion": "2865328801", + "uid": "30d8bb5a-06ff-4705-97d4-51f7737a9bfe" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "core" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "annotations": { + "checksum/appconfig-configmap": "cf3b985c902671383801409e8a965ad17ab4ec10e5b6237b7486bd5d16dcec67", + "checksum/rsa-key-secret": "57a5e7abec60d7d4084c36a3d59aca39df32371cff531337d060401b9655d3e5", + "checksum/security-exandradev-secret": "07f38b38833d3701cfcd128a3429ba4defe5272e99164fbba4352a40ed94f99a", + "checksum/security-unify-secret": "2705b192feffaf260f2d5d524d6dfdefe6348d6faa95662f1485fbfd63af2a95" + }, + "creationTimestamp": null, + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "core" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "EXANDRADEV_CLIENT_ID", + "valueFrom": { + "secretKeyRef": { + "key": "clientId", + "name": "core-security-exandradev-secret" + } + } + }, + { + "name": "EXANDRADEV_CLIENT_SECRET", + "valueFrom": { + "secretKeyRef": { + "key": "clientSecret", + "name": "core-security-exandradev-secret" + } + } + }, + { + "name": "UNIFY_CLIENT_ID", + "valueFrom": { + "secretKeyRef": { + "key": "clientId", + "name": "core-security-unify-secret" + } + } + }, + { + "name": "UNIFY_CLIENT_SECRET", + "valueFrom": { + "secretKeyRef": { + "key": "clientSecret", + "name": "core-security-unify-secret" + } + } + }, + { + "name": "DB_USERNAME", + "valueFrom": { + "secretKeyRef": { + "key": "username", + "name": "edb-cluster-app" + } + } + }, + { + "name": "DB_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "key": "password", + "name": "edb-cluster-app" + } + } + } + ], + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/core-standalone:ea80478c0052a5d76c505d7e5f56556ac63ea982", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/q/health/live", + "port": "http", + "scheme": "HTTP" + }, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "core", + "ports": [ + { + "containerPort": 8081, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "512Mi" + }, + "requests": { + "cpu": "1", + "memory": "512Mi" + } + }, + "securityContext": {}, + "startupProbe": { + "failureThreshold": 20, + "httpGet": { + "path": "/q/health/started", + "port": "http", + "scheme": "HTTP" + }, + "periodSeconds": 3, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/deployments/core/rsa", + "name": "exandra-rsa-key-volume", + "readOnly": true + }, + { + "mountPath": "/deployments/config", + "name": "exandra-config-volume", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30, + "volumes": [ + { + "name": "exandra-rsa-key-volume", + "secret": { + "defaultMode": 420, + "items": [ + { + "key": "rsaKey", + "path": "jwk.json" + } + ], + "secretName": "core-rsa-key-secret" + } + }, + { + "configMap": { + "defaultMode": 420, + "name": "core-appconfig-configmap" + }, + "name": "exandra-config-volume" + } + ] + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2023-05-16T15:53:18Z", + "lastUpdateTime": "2024-03-04T15:21:26Z", + "message": "ReplicaSet \"core-75c8f865f7\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2024-05-25T13:43:04Z", + "lastUpdateTime": "2024-05-25T13:43:04Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 42, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "18", + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-05-08T09:40:33Z", + "generation": 18, + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "standalone-gateway", + "app.kubernetes.io/version": "7b5e50e13fd78502967881f4970484ae08b76dc4", + "helm.sh/chart": "standalone-gateway-0.1.0_7b5e50e13fd78502967881f4970484ae08b76d" + }, + "name": "standalone-gateway", + "namespace": "guardians-test", + "resourceVersion": "2865332166", + "uid": "e4d081ee-0e07-48f7-873a-50a167513b09" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "standalone-gateway" + } + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "standalone-gateway" + } + }, + "spec": { + "containers": [ + { + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/standalone-gateway:7b5e50e13fd78502967881f4970484ae08b76dc4", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/ready", + "port": 9901, + "scheme": "HTTP" + }, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "standalone-gateway", + "ports": [ + { + "containerPort": 8000, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "256Mi" + } + }, + "securityContext": {}, + "startupProbe": { + "failureThreshold": 30, + "httpGet": { + "path": "/ready", + "port": 9901, + "scheme": "HTTP" + }, + "initialDelaySeconds": 1, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2023-05-08T09:40:33Z", + "lastUpdateTime": "2023-12-20T16:48:17Z", + "message": "ReplicaSet \"standalone-gateway-5466b58d7c\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2024-05-25T13:43:54Z", + "lastUpdateTime": "2024-05-25T13:43:54Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 18, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } + } + ], + "v1/Pod(related)": [ + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "checksum/appconfig-configmap": "cf3b985c902671383801409e8a965ad17ab4ec10e5b6237b7486bd5d16dcec67", + "checksum/rsa-key-secret": "57a5e7abec60d7d4084c36a3d59aca39df32371cff531337d060401b9655d3e5", + "checksum/security-exandradev-secret": "07f38b38833d3701cfcd128a3429ba4defe5272e99164fbba4352a40ed94f99a", + "checksum/security-unify-secret": "2705b192feffaf260f2d5d524d6dfdefe6348d6faa95662f1485fbfd63af2a95", + "k8s.ovn.org/pod-networks": "{\"default\":{\"ip_addresses\":[\"10.251.18.50/24\"],\"mac_address\":\"0a:58:0a:fb:12:32\",\"gateway_ips\":[\"10.251.18.1\"],\"routes\":[{\"dest\":\"10.251.0.0/16\",\"nextHop\":\"10.251.18.1\"},{\"dest\":\"172.30.0.0/16\",\"nextHop\":\"10.251.18.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.251.18.1\"}],\"ip_address\":\"10.251.18.50/24\",\"gateway_ip\":\"10.251.18.1\"}}", + "k8s.v1.cni.cncf.io/network-status": "[{\n \"name\": \"ovn-kubernetes\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.251.18.50\"\n ],\n \"mac\": \"0a:58:0a:fb:12:32\",\n \"default\": true,\n \"dns\": {}\n}]", + "openshift.io/scc": "restricted-v2", + "seccomp.security.alpha.kubernetes.io/pod": "runtime/default" + }, + "creationTimestamp": "2024-05-25T13:41:18Z", + "generateName": "core-75c8f865f7-", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "core", + "pod-template-hash": "75c8f865f7" + }, + "name": "core-75c8f865f7-8tbcw", + "namespace": "guardians-test", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "core-75c8f865f7", + "uid": "f98ac8f8-8e79-4511-9124-7514fe409761" + } + ], + "resourceVersion": "2865328796", + "uid": "47adcd3a-b1f4-409a-9647-4e00e3c453cb" + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "EXANDRADEV_CLIENT_ID", + "valueFrom": { + "secretKeyRef": { + "key": "clientId", + "name": "core-security-exandradev-secret" + } + } + }, + { + "name": "EXANDRADEV_CLIENT_SECRET", + "valueFrom": { + "secretKeyRef": { + "key": "clientSecret", + "name": "core-security-exandradev-secret" + } + } + }, + { + "name": "UNIFY_CLIENT_ID", + "valueFrom": { + "secretKeyRef": { + "key": "clientId", + "name": "core-security-unify-secret" + } + } + }, + { + "name": "UNIFY_CLIENT_SECRET", + "valueFrom": { + "secretKeyRef": { + "key": "clientSecret", + "name": "core-security-unify-secret" + } + } + }, + { + "name": "DB_USERNAME", + "valueFrom": { + "secretKeyRef": { + "key": "username", + "name": "edb-cluster-app" + } + } + }, + { + "name": "DB_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "key": "password", + "name": "edb-cluster-app" + } + } + } + ], + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/core-standalone:ea80478c0052a5d76c505d7e5f56556ac63ea982", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/q/health/live", + "port": "http", + "scheme": "HTTP" + }, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "core", + "ports": [ + { + "containerPort": 8081, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "512Mi" + }, + "requests": { + "cpu": "1", + "memory": "512Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "runAsNonRoot": true, + "runAsUser": 1001270000 + }, + "startupProbe": { + "failureThreshold": 20, + "httpGet": { + "path": "/q/health/started", + "port": "http", + "scheme": "HTTP" + }, + "periodSeconds": 3, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/deployments/core/rsa", + "name": "exandra-rsa-key-volume", + "readOnly": true + }, + { + "mountPath": "/deployments/config", + "name": "exandra-config-volume", + "readOnly": true + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-wdbhx", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-kn5ds" + } + ], + "nodeName": "ip-10-8-33-221.ec2.internal", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1001270000, + "seLinuxOptions": { + "level": "s0:c36,c5" + }, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "exandra-rsa-key-volume", + "secret": { + "defaultMode": 420, + "items": [ + { + "key": "rsaKey", + "path": "jwk.json" + } + ], + "secretName": "core-rsa-key-secret" + } + }, + { + "configMap": { + "defaultMode": 420, + "name": "core-appconfig-configmap" + }, + "name": "exandra-config-volume" + }, + { + "name": "kube-api-access-wdbhx", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + }, + { + "configMap": { + "items": [ + { + "key": "service-ca.crt", + "path": "service-ca.crt" + } + ], + "name": "openshift-service-ca.crt" + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:41:18Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:43:03Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:43:03Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:41:18Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "cri-o://475026e4b427c773eb5355446e4e9da989d91d7864b0e4f623c3f942885a11a2", + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/core-standalone:ea80478c0052a5d76c505d7e5f56556ac63ea982", + "imageID": "image-registry.openshift-image-registry.svc:5000/guardians-test/core-standalone@sha256:6a2290e522133866cafc4864c30ea6ba591de4238a865936e3e4f953d60c173a", + "lastState": {}, + "name": "core", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-05-25T13:42:52Z" + } + } + } + ], + "hostIP": "10.8.33.221", + "phase": "Running", + "podIP": "10.251.18.50", + "podIPs": [ + { + "ip": "10.251.18.50" + } + ], + "qosClass": "Guaranteed", + "startTime": "2024-05-25T13:41:18Z" + } + } + ], + "kind": "PodList", + "metadata": { + "resourceVersion": "2886974735" + } + }, + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "k8s.ovn.org/pod-networks": "{\"default\":{\"ip_addresses\":[\"10.251.18.51/24\"],\"mac_address\":\"0a:58:0a:fb:12:33\",\"gateway_ips\":[\"10.251.18.1\"],\"routes\":[{\"dest\":\"10.251.0.0/16\",\"nextHop\":\"10.251.18.1\"},{\"dest\":\"172.30.0.0/16\",\"nextHop\":\"10.251.18.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.251.18.1\"}],\"ip_address\":\"10.251.18.51/24\",\"gateway_ip\":\"10.251.18.1\"}}", + "k8s.v1.cni.cncf.io/network-status": "[{\n \"name\": \"ovn-kubernetes\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.251.18.51\"\n ],\n \"mac\": \"0a:58:0a:fb:12:33\",\n \"default\": true,\n \"dns\": {}\n}]", + "openshift.io/scc": "restricted-v2", + "seccomp.security.alpha.kubernetes.io/pod": "runtime/default" + }, + "creationTimestamp": "2024-05-25T13:41:18Z", + "generateName": "standalone-gateway-5466b58d7c-", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "standalone-gateway", + "pod-template-hash": "5466b58d7c" + }, + "name": "standalone-gateway-5466b58d7c-6h87c", + "namespace": "guardians-test", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "standalone-gateway-5466b58d7c", + "uid": "86bc0197-c804-47fe-97ce-34655e72347e" + } + ], + "resourceVersion": "2865332161", + "uid": "ad9109db-bb3e-4302-8b51-66e8a647b06d" + }, + "spec": { + "containers": [ + { + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/standalone-gateway:7b5e50e13fd78502967881f4970484ae08b76dc4", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/ready", + "port": 9901, + "scheme": "HTTP" + }, + "periodSeconds": 5, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "standalone-gateway", + "ports": [ + { + "containerPort": 8000, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "512Mi" + }, + "requests": { + "cpu": "100m", + "memory": "256Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "runAsNonRoot": true, + "runAsUser": 1001270000 + }, + "startupProbe": { + "failureThreshold": 30, + "httpGet": { + "path": "/ready", + "port": 9901, + "scheme": "HTTP" + }, + "initialDelaySeconds": 1, + "periodSeconds": 1, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-6tc2p", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-kn5ds" + } + ], + "nodeName": "ip-10-8-33-221.ec2.internal", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1001270000, + "seLinuxOptions": { + "level": "s0:c36,c5" + }, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + } + ], + "volumes": [ + { + "name": "kube-api-access-6tc2p", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + }, + { + "configMap": { + "items": [ + { + "key": "service-ca.crt", + "path": "service-ca.crt" + } + ], + "name": "openshift-service-ca.crt" + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:41:18Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:43:54Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:43:54Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-05-25T13:41:18Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "cri-o://14b477cac3d4639fd0dbb9c024ac5274c506f827034783ac5934df6d06da784c", + "image": "image-registry.openshift-image-registry.svc:5000/guardians-test/standalone-gateway:7b5e50e13fd78502967881f4970484ae08b76dc4", + "imageID": "image-registry.openshift-image-registry.svc:5000/guardians-test/standalone-gateway@sha256:c347bebe2497e1e7701bc57b34778e62ac072d223d121e438958e3ffdae4df1a", + "lastState": {}, + "name": "standalone-gateway", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-05-25T13:43:50Z" + } + } + } + ], + "hostIP": "10.8.33.221", + "phase": "Running", + "podIP": "10.251.18.51", + "podIPs": [ + { + "ip": "10.251.18.51" + } + ], + "qosClass": "Burstable", + "startTime": "2024-05-25T13:41:18Z" + } + } + ], + "kind": "PodList", + "metadata": { + "resourceVersion": "2886974735" + } + } + ], + "v1/Secret": [ + { + "apiVersion": "v1", + "data": { + "rsaKey": "REDACTED" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-08-25T08:54:46Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core-rsa-key-secret", + "namespace": "guardians-test", + "resourceVersion": "2880969794", + "uid": "45e94955-0ee4-41c6-ab53-582ceabd3274" + }, + "type": "Opaque" + }, + { + "apiVersion": "v1", + "data": { + "clientId": "REDACTED", + "clientSecret": "REDACTED" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-08-25T08:54:46Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core-security-exandradev-secret", + "namespace": "guardians-test", + "resourceVersion": "2880969795", + "uid": "4db28b76-f90f-4431-a19d-73ef7e5d5ae7" + }, + "type": "Opaque" + }, + { + "apiVersion": "v1", + "data": { + "clientId": "REDACTED", + "clientSecret": "REDACTED" + }, + "kind": "Secret", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-05-16T15:41:54Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core-security-unify-secret", + "namespace": "guardians-test", + "resourceVersion": "2880969797", + "uid": "536ceb38-0457-4186-bd09-efe234b5fca1" + }, + "type": "Opaque" + } + ], + "v1/Service": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2022-12-19T09:44:33Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "core", + "app.kubernetes.io/version": "ea80478c0052a5d76c505d7e5f56556ac63ea982", + "helm.sh/chart": "core-0.1.0_ea80478c0052a5d76c505d7e5f56556ac63ea982" + }, + "name": "core", + "namespace": "guardians-test", + "resourceVersion": "2687980260", + "uid": "287bf074-73da-4546-a43d-6d1f23c82365" + }, + "spec": { + "clusterIP": "172.30.21.158", + "clusterIPs": [ + "172.30.21.158" + ], + "internalTrafficPolicy": "Cluster", + "ipFamilies": [ + "IPv4" + ], + "ipFamilyPolicy": "SingleStack", + "ports": [ + { + "name": "http", + "port": 8081, + "protocol": "TCP", + "targetPort": 8081 + } + ], + "selector": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "core" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "standalone-app", + "meta.helm.sh/release-namespace": "guardians-test" + }, + "creationTimestamp": "2023-05-08T09:40:33Z", + "labels": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "standalone-gateway", + "app.kubernetes.io/version": "7b5e50e13fd78502967881f4970484ae08b76dc4", + "helm.sh/chart": "standalone-gateway-0.1.0_7b5e50e13fd78502967881f4970484ae08b76d" + }, + "name": "standalone-gateway", + "namespace": "guardians-test", + "resourceVersion": "2497441712", + "uid": "87924b81-ff69-4676-b03c-31cb84fcea1e" + }, + "spec": { + "clusterIP": "172.30.187.73", + "clusterIPs": [ + "172.30.187.73" + ], + "internalTrafficPolicy": "Cluster", + "ipFamilies": [ + "IPv4" + ], + "ipFamilyPolicy": "SingleStack", + "ports": [ + { + "name": "http", + "port": 80, + "protocol": "TCP", + "targetPort": 8000 + } + ], + "selector": { + "app.kubernetes.io/instance": "standalone-app", + "app.kubernetes.io/name": "standalone-gateway" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + } + ] + } + }, + "manifest": "REDACTED\n", + "version": 43, + "namespace": "guardians-test" +} From 5a0a021d33e395a241e585a94ea22fd4c69e84a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrich=20Kra=CC=88mer?= Date: Tue, 2 Jul 2024 13:46:44 +0200 Subject: [PATCH 3/5] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e78f77b..341e42d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Changed * Enhance SSDS Document Generation Performance using New Atlassian APIs ([#1084](https://github.com/opendevstack/ods-jenkins-shared-library/issues/1084)) +* TIR - remove dynamic pod data and surface helm status ([#1135](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1135)) ### Fixed * Fix Tailor deployment drifts for D, Q envs ([#1055](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1055)) From 0e23c03a40e52955c884f087bbc19c941deca7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrich=20Kr=C3=A4mer?= Date: Tue, 2 Jul 2024 12:02:55 +0000 Subject: [PATCH 4/5] Report HelmStatus first in TIR under release-deploymentStatus --- src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy index f143889e0..55a39f38f 100644 --- a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy +++ b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy @@ -1176,10 +1176,10 @@ class LeVADocumentUseCase extends DocGenUseCase { if (releaseName in helmReleasesCovered) { return } - def withoutHelmStatus = deployment.findAll { k, v -> k != 'helmStatus' } - deploymentsForTir.put("${releaseName}-deploymentMean".toString(), withoutHelmStatus) def helmStatus = assembleHelmStatus(deployment?.helmStatus ?: [:]) deploymentsForTir.put("${releaseName}-deploymentStatus".toString(), helmStatus) + def withoutHelmStatus = deployment.findAll { k, v -> k != 'helmStatus' } + deploymentsForTir.put("${releaseName}-deploymentMean".toString(), withoutHelmStatus) helmReleasesCovered << (releaseName) } else { deploymentsForTir.put(deploymentName, deployment) From 555745c4f6289ee61e781153433a0accf5468844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrich=20Kr=C3=A4mer?= Date: Thu, 4 Jul 2024 11:54:06 +0000 Subject: [PATCH 5/5] TIR: render empty helm/tailor values as None or similar --- .../usecase/LeVADocumentUseCase.groovy | 37 +++++++++++++++--- .../usecase/LeVADocumentUseCaseSpec.groovy | 39 +++++++++++++++++-- ...s-data.json => deployments-data-helm.json} | 0 test/resources/deployments-data-tailor.json | 21 ++++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) rename test/resources/{deployments-data.json => deployments-data-helm.json} (100%) create mode 100644 test/resources/deployments-data-tailor.json diff --git a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy index 55a39f38f..db473ddee 100644 --- a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy +++ b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy @@ -1164,8 +1164,8 @@ class LeVADocumentUseCase extends DocGenUseCase { } Set helmReleasesCovered = [] - Map deploymentsForTir = [:] - deployments.each { String deploymentName, Map deployment -> + Map > deploymentsForTir = [:] + deployments.each { String deploymentName, Map deployment -> if (deploymentName.endsWith('-deploymentMean')) { if (deployment.type == "helm") { String releaseName = deployment?.helmReleaseName @@ -1176,13 +1176,14 @@ class LeVADocumentUseCase extends DocGenUseCase { if (releaseName in helmReleasesCovered) { return } - def helmStatus = assembleHelmStatus(deployment?.helmStatus ?: [:]) + def helmStatus = assembleHelmStatus( (deployment?.helmStatus ?: [:] ) as Map) deploymentsForTir.put("${releaseName}-deploymentStatus".toString(), helmStatus) def withoutHelmStatus = deployment.findAll { k, v -> k != 'helmStatus' } - deploymentsForTir.put("${releaseName}-deploymentMean".toString(), withoutHelmStatus) + deploymentsForTir.put("${releaseName}-deploymentMean".toString(), + handleEmptyValues(withoutHelmStatus)) helmReleasesCovered << (releaseName) } else { - deploymentsForTir.put(deploymentName, deployment) + deploymentsForTir.put(deploymentName, handleEmptyValues(deployment)) } } else { if (deploymentName in componentsCoveredByHelm) { @@ -1217,6 +1218,32 @@ class LeVADocumentUseCase extends DocGenUseCase { return assembledHelmStatus } + Map handleEmptyValues(Map deployment) { + if (deployment?.type == 'tailor') { + def tailorEmptyValues = [ + tailorParamFile: 'None', + tailorParams: 'None', + tailorPreserve: 'No extra resources specified to be preserved' + ] + return deployment.collectEntries { k, v -> + def newValue = (tailorEmptyValues.containsKey(k) && !v) ? tailorEmptyValues[k] : v + [(k): newValue] + } + } + if (deployment?.type == 'helm') { + def helmEmptyValues = [ + helmAdditionalFlags: 'None', + helmEnvBasedValuesFiles: 'None', + helmValues: 'None', + ] + return deployment.collectEntries { k, v -> + def newValue = (helmEmptyValues.containsKey(k) && !v) ? helmEmptyValues[k] : v + [(k): newValue] + } + } + return deployment + } + String createOverallTIR(Map repo = null, Map data = null) { def documentTypeName = DOCUMENT_TYPE_NAMES[DocumentType.OVERALL_TIR as String] def metadata = this.getDocumentMetadata(documentTypeName) diff --git a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy index b643ed9b2..f0f539905 100644 --- a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy +++ b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy @@ -1281,9 +1281,9 @@ class LeVADocumentUseCaseSpec extends SpecHelper { 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) } - def "assemble deploymentInfo for TIR"() { + def "assemble deploymentInfo for TIR with helm"() { given: - def file = new FixtureHelper().getResource("deployments-data.json") + def file = new FixtureHelper().getResource("deployments-data-helm.json") os.isDeploymentKind(*_) >> true when: @@ -1295,8 +1295,8 @@ class LeVADocumentUseCaseSpec extends SpecHelper { assembledData["backend-helm-monorepo-deploymentMean"] == [ chartDir : "chart", repoId : "backend-helm-monorepo", - helmAdditionalFlags: [], - helmEnvBasedValuesFiles :[], + helmAdditionalFlags: "None", + helmEnvBasedValuesFiles :"None", helmValues :[ registry :"image-registry.openshift-image-registry.svc:5000", componentId :"backend-helm-monorepo" @@ -1318,6 +1318,37 @@ class LeVADocumentUseCaseSpec extends SpecHelper { ] } + def "assemble deploymentInfo for TIR with tailor"() { + given: + def file = new FixtureHelper().getResource("deployments-data-tailor.json") + os.isDeploymentKind(*_) >> true + + when: + def deploymentsData = new JsonSlurperClassic().parseText(file.text) + def assembledData = usecase.assembleDeployments(deploymentsData.deployments) + + then: + + assembledData["backend-first-deploymentMean"] == [ + type: "tailor", + selector: "app=kraemerh-backend-first", + tailorSelectors: [ + selector: "app=kraemerh-backend-first", + exclude: "bc,is", + ], + tailorParamFile: "None", + tailorParams: "None", + tailorPreserve: "No extra resources specified to be preserved", + tailorVerify: true + ] + + assembledData["backend-first"] == [ + containers: [ + "backend-first": "backend-first@sha256:fc5fb63f4ac45e207a4a1ceba37534814489c16e82306cf46aca76627c0f5e1e" + ] + ] + } + def "create overall DTR"() { given: // Argument Constraints diff --git a/test/resources/deployments-data.json b/test/resources/deployments-data-helm.json similarity index 100% rename from test/resources/deployments-data.json rename to test/resources/deployments-data-helm.json diff --git a/test/resources/deployments-data-tailor.json b/test/resources/deployments-data-tailor.json new file mode 100644 index 000000000..3a511365b --- /dev/null +++ b/test/resources/deployments-data-tailor.json @@ -0,0 +1,21 @@ +{ + "deployments": { + "backend-first": { + "containers": { + "backend-first": "backend-first@sha256:fc5fb63f4ac45e207a4a1ceba37534814489c16e82306cf46aca76627c0f5e1e" + } + }, + "backend-first-deploymentMean": { + "type": "tailor", + "selector": "app=kraemerh-backend-first", + "tailorSelectors": { + "selector": "app=kraemerh-backend-first", + "exclude": "bc,is" + }, + "tailorParamFile": "", + "tailorParams": [], + "tailorPreserve": [], + "tailorVerify": true + } + } +}