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)) 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..db473ddee 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,119 @@ 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 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(), + handleEmptyValues(withoutHelmStatus)) + helmReleasesCovered << (releaseName) + } else { + deploymentsForTir.put(deploymentName, handleEmptyValues(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 + } + + 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/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, ] } 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..f0f539905 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,74 @@ class LeVADocumentUseCaseSpec extends SpecHelper { 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) } + def "assemble deploymentInfo for TIR with helm"() { + given: + def file = new FixtureHelper().getResource("deployments-data-helm.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: "None", + helmEnvBasedValuesFiles :"None", + 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 "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 @@ -1657,25 +1728,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-helm.json b/test/resources/deployments-data-helm.json new file mode 100644 index 000000000..9a9f26b68 --- /dev/null +++ b/test/resources/deployments-data-helm.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/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 + } + } +} 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" +}