diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f0dea13..6d8fa19eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Add preserve-digests cli option to skopeo copy command in CopyImageStage ([#1166](https://github.com/opendevstack/ods-jenkins-shared-library/issues/1166)) * Allow registry/image:tag sources in CopyImageStage instead of directly falling back to internal registry ([#1177](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1177)) * Simplify successor management since now issue links are no longer inherited ([#1116](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1116)) +* TIR - remove dynamic pod data, surface helm status and helm report tables reformatting ([#1143](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1143)) ### 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 908cc14fb..1b16d7b40 100644 --- a/src/org/ods/component/HelmDeploymentStrategy.groovy +++ b/src/org/ods/component/HelmDeploymentStrategy.groovy @@ -1,37 +1,34 @@ package org.ods.component -import groovy.json.JsonOutput import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.ods.services.JenkinsService import org.ods.services.OpenShiftService +import org.ods.util.HelmStatus import org.ods.util.ILogger -import org.ods.util.PipelineSteps +import org.ods.util.IPipelineSteps import org.ods.util.PodData class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // Constructor arguments - private final Script script private final IContext context private final OpenShiftService openShift private final JenkinsService jenkins private final ILogger logger - + private IPipelineSteps steps // assigned in constructor - private def steps private final RolloutOpenShiftDeploymentOptions options @SuppressWarnings(['AbcMetric', 'CyclomaticComplexity', 'ParameterCount']) HelmDeploymentStrategy( - def script, + IPipelineSteps steps, IContext context, Map config, OpenShiftService openShift, JenkinsService jenkins, ILogger logger ) { - if (!config.selector) { config.selector = context.selector } @@ -72,10 +69,9 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { if (!config.helmPrivateKeyCredentialsId) { config.helmPrivateKeyCredentialsId = "${context.cdProject}-helm-private-key" } - this.script = script this.context = context this.logger = logger - this.steps = new PipelineSteps(script) + this.steps = steps this.options = new RolloutOpenShiftDeploymentOptions(config) this.openShift = openShift @@ -92,23 +88,19 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // Tag images which have been built in this pipeline from cd project into target project retagImages(context.targetProject, getBuiltImages()) - logger.info "Rolling out ${context.componentId} with HELM, selector: ${options.selector}" + logger.info("Rolling out ${context.componentId} with HELM, selector: ${options.selector}") helmUpgrade(context.targetProject) - - def deploymentResources = openShift.getResourcesForComponent( - context.targetProject, DEPLOYMENT_KINDS, options.selector - ) - logger.info("${this.class.name} -- DEPLOYMENT RESOURCES") - logger.info( - JsonOutput.prettyPrint( - JsonOutput.toJson(deploymentResources))) + HelmStatus helmStatus = openShift.helmStatus(context.targetProject, options.helmReleaseName) + if (logger.debugMode) { + def helmStatusMap = helmStatus.toMap() + logger.debug("${this.class.name} -- HELM STATUS: ${helmStatusMap}") + } // // FIXME: pauseRollouts is non trivial to determine! // // 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) ?: [:] - logger.info(JsonOutput.prettyPrint(JsonOutput.toJson(rolloutData))) + def rolloutData = getRolloutData(helmStatus) return rolloutData } @@ -124,7 +116,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 = [:] mergedHelmValues << options.helmValues // we add the global ones - this allows usage in subcharts @@ -141,13 +133,14 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // deal with dynamic value files - which are env dependent def mergedHelmValuesFiles = [] - mergedHelmValuesFiles.addAll(options.helmValuesFiles) - options.helmEnvBasedValuesFiles.each { envValueFile -> - mergedHelmValuesFiles << envValueFile.replace('.env', - ".${context.environment}") + def envConfigFiles = options.helmEnvBasedValuesFiles.collect {filenamePattern -> + filenamePattern.replace('.env.', ".${context.environment}.") } + mergedHelmValuesFiles.addAll(options.helmValuesFiles) + mergedHelmValuesFiles.addAll(envConfigFiles) + openShift.helmUpgrade( targetProject, options.helmReleaseName, @@ -168,41 +161,59 @@ class HelmDeploymentStrategy extends AbstractDeploymentStrategy { // ] @TypeChecked(TypeCheckingMode.SKIP) private Map> getRolloutData( - Map> deploymentResources) { + HelmStatus helmStatus + ) { + Map> rolloutData = [:] - def rolloutData = [:] - deploymentResources.each { resourceKind, resourceNames -> - resourceNames.each { resourceName -> - def podData = [] + Map> deploymentKinds = helmStatus.resourcesByKind + .findAll { kind, res -> kind in DEPLOYMENT_KINDS } + + deploymentKinds.each { kind, names -> + names.each { name -> + context.addDeploymentToArtifactURIs("${name}-deploymentMean", + [ + type: 'helm', + selector: options.selector, + namespace: context.targetProject, + 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=${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, resourceName) - if (!podData.isEmpty()) { + podData = openShift.checkForPodData(context.targetProject, options.selector, name) + if (podData) { break } - steps.echo("Could not find 'running' pod(s) with label '${options.selector}' - waiting") + steps.echo("${msgPodsNotFound} - waiting") steps.sleep(12) } - 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 + if (!podData) { + throw new RuntimeException(msgPodsNotFound) + } + logger.debug("Helm podData for ${podDataContext.join(', ')}: ${podData}") + + rolloutData["${kind}/${name}"] = podData // TODO: Once the orchestration pipeline can deal with multiple replicas, - // update this to store multiple pod artifacts. + // 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()) + // Deployment and DeploymentConfig resource. + context.addDeploymentToArtifactURIs(name, podData[0]?.toMap()) } } - rolloutData + return rolloutData } } diff --git a/src/org/ods/component/RolloutOpenShiftDeploymentStage.groovy b/src/org/ods/component/RolloutOpenShiftDeploymentStage.groovy index 06e373281..e123e1cca 100644 --- a/src/org/ods/component/RolloutOpenShiftDeploymentStage.groovy +++ b/src/org/ods/component/RolloutOpenShiftDeploymentStage.groovy @@ -2,9 +2,10 @@ package org.ods.component import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode -import org.ods.services.OpenShiftService import org.ods.services.JenkinsService +import org.ods.services.OpenShiftService import org.ods.util.ILogger +import org.ods.util.PipelineSteps @SuppressWarnings('ParameterCount') @TypeChecked @@ -110,26 +111,27 @@ class RolloutOpenShiftDeploymentStage extends Stage { // We have to move everything here, // otherwise Jenkins will complain // about: "hudson.remoting.ProxyException: CpsCallableInvocation{methodName=fileExists, ..." - def isHelmDeployment = steps.fileExists(options.chartDir + '/Chart.yaml') + def isHelmDeployment = this.steps.fileExists(options.chartDir + '/Chart.yaml') logger.info("isHelmDeployment: ${isHelmDeployment}") - def isTailorDeployment = steps.fileExists(options.openshiftDir) + def isTailorDeployment = this.steps.fileExists(options.openshiftDir) if (isTailorDeployment && isHelmDeployment) { - steps.error("Must be either a Tailor based deployment or a Helm based deployment") + this.steps.error("Must be either a Tailor based deployment or a Helm based deployment") throw new IllegalStateException("Must be either a Tailor based deployment or a Helm based deployment") } // Use tailorDeployment in the following cases: // (1) We have an openshiftDir // (2) We do not have an openshiftDir but neither do we have an indication that it is Helm + def steps = new PipelineSteps(script) if (isTailorDeployment || (!isHelmDeployment && !isTailorDeployment)) { - deploymentStrategy = new TailorDeploymentStrategy(script, context, config, openShift, jenkins, logger) + deploymentStrategy = new TailorDeploymentStrategy(steps, context, config, openShift, jenkins, logger) String resourcePath = 'org/ods/component/RolloutOpenShiftDeploymentStage.deprecate-tailor.GString.txt' - def msg =steps.libraryResource(resourcePath) + def msg = this.steps.libraryResource(resourcePath) logger.warn(msg) } if (isHelmDeployment) { - deploymentStrategy = new HelmDeploymentStrategy(script, context, config, openShift, jenkins, logger) + deploymentStrategy = new HelmDeploymentStrategy(steps, context, config, openShift, jenkins, logger) } logger.info("deploymentStrategy: ${deploymentStrategy} -- ${deploymentStrategy.class.name}") return deploymentStrategy.deploy() diff --git a/src/org/ods/component/TailorDeploymentStrategy.groovy b/src/org/ods/component/TailorDeploymentStrategy.groovy index bb0d18175..cfe1d45e8 100644 --- a/src/org/ods/component/TailorDeploymentStrategy.groovy +++ b/src/org/ods/component/TailorDeploymentStrategy.groovy @@ -5,25 +5,24 @@ import groovy.transform.TypeCheckingMode import org.ods.services.JenkinsService import org.ods.services.OpenShiftService import org.ods.util.ILogger -import org.ods.util.PipelineSteps +import org.ods.util.IPipelineSteps import org.ods.util.PodData class TailorDeploymentStrategy extends AbstractDeploymentStrategy { // Constructor arguments - private final Script script private final IContext context private final OpenShiftService openShift private final JenkinsService jenkins private final ILogger logger + private final IPipelineSteps steps // assigned in constructor - private def steps private final RolloutOpenShiftDeploymentOptions options @SuppressWarnings(['AbcMetric', 'CyclomaticComplexity', 'ParameterCount']) TailorDeploymentStrategy( - def script, + IPipelineSteps steps, IContext context, Map config, OpenShiftService openShift, @@ -67,10 +66,9 @@ class TailorDeploymentStrategy extends AbstractDeploymentStrategy { if (!config.containsKey('tailorParams')) { config.tailorParams = [] } - this.script = script this.context = context this.logger = logger - this.steps = new PipelineSteps(script) + this.steps = steps this.options = new RolloutOpenShiftDeploymentOptions(config) this.openShift = openShift diff --git a/src/org/ods/orchestration/phases/DeployOdsComponent.groovy b/src/org/ods/orchestration/phases/DeployOdsComponent.groovy index 260128866..d9a99994a 100644 --- a/src/org/ods/orchestration/phases/DeployOdsComponent.groovy +++ b/src/org/ods/orchestration/phases/DeployOdsComponent.groovy @@ -11,6 +11,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 @@ -31,7 +32,7 @@ class DeployOdsComponent { @TypeChecked(TypeCheckingMode.SKIP) @SuppressWarnings(['AbcMetric']) - public void run(Map repo, String baseDir) { + void run(Map repo, String baseDir) { this.os = ServiceRegistry.instance.get(OpenShiftService) steps.dir(baseDir) { @@ -40,6 +41,7 @@ class DeployOdsComponent { DeploymentDescriptor deploymentDescriptor steps.dir(openShiftDir) { deploymentDescriptor = DeploymentDescriptor.readFromFile(steps) + logger.debug("DeploymentDescriptor '${openShiftDir}': ${deploymentDescriptor.deployments}") } if (!repo.data.openshift.deployments) { repo.data.openshift.deployments = [:] @@ -54,24 +56,31 @@ class DeployOdsComponent { Map deploymentMean = deployment.deploymentMean logger.debug("Helm Config for ${deploymentName} -> ${deploymentMean}") deploymentMean['repoId'] = repo.id + deploymentMean['namespace'] = project.targetProject 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.debug("Helm podData for '${podDataContext.join(', ')}': ${podData}") // TODO: Once the orchestration pipeline can deal with multiple replicas, // update this to deal with multiple pods. @@ -227,6 +236,11 @@ class DeployOdsComponent { deploymentMean.helmDefaultFlags, deploymentMean.helmAdditionalFlags, true) + + def helmStatus = os.helmStatus(project.targetProject, deploymentMean.helmReleaseName) + def helmStatusMap = helmStatus.toMap() + deploymentMean.helmStatus = helmStatusMap + logger.debug("${this.class.name} -- HELM STATUS: ${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 060e8cf14..6ea3e2a0a 100644 --- a/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy +++ b/src/org/ods/orchestration/usecase/LeVADocumentUseCase.groovy @@ -1,7 +1,7 @@ package org.ods.orchestration.usecase -import static groovy.json.JsonOutput.prettyPrint -import static groovy.json.JsonOutput.toJson +import org.apache.commons.lang.StringUtils +import org.ods.orchestration.util.HtmlFormatterUtil import com.cloudbees.groovy.cps.NonCPS import groovy.xml.XmlUtil @@ -22,6 +22,7 @@ import org.ods.services.NexusService import org.ods.services.OpenShiftService import org.ods.util.ILogger import org.ods.util.IPipelineSteps +import org.ods.orchestration.util.MROPipelineUtil.PipelineConfig import java.time.LocalDateTime @@ -83,12 +84,12 @@ class LeVADocumentUseCase extends DocGenUseCase { ] 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' + (PipelineConfig.REPO_TYPE_ODS_SAAS_SERVICE as String) : 'SAAS Component', + (PipelineConfig.REPO_TYPE_ODS_TEST as String) : 'Automated tests', + (PipelineConfig.REPO_TYPE_ODS_SERVICE as String) : '3rd Party Service Component', + (PipelineConfig.REPO_TYPE_ODS_CODE as String) : 'ODS Software Component', + (PipelineConfig.REPO_TYPE_ODS_INFRA as String) : 'Infrastructure as Code Component', + (PipelineConfig.REPO_TYPE_ODS_LIB as String) : 'ODS library component' ] public static String DEVELOPER_PREVIEW_WATERMARK = 'Developer Preview' @@ -138,7 +139,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 -> @@ -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" } @@ -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 @@ -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") @@ -680,11 +681,11 @@ class LeVADocumentUseCase extends DocGenUseCase { def testsOfRepoTypeOdsCode = [] def testsOfRepoTypeOdsService = [] testsGroupedByRepoType.each { repoTypes, tests -> - if (repoTypes.contains(MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE)) { + if (repoTypes.contains(PipelineConfig.REPO_TYPE_ODS_CODE)) { testsOfRepoTypeOdsCode.addAll(tests) } - if (repoTypes.contains(MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SERVICE)) { + if (repoTypes.contains(PipelineConfig.REPO_TYPE_ODS_SERVICE)) { testsOfRepoTypeOdsService.addAll(tests) } } @@ -693,7 +694,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def docHistory = this.getAndStoreDocumentHistory(documentType, keysInDoc) def installedRepos = this.project.repositories.findAll { it -> - MROPipelineUtil.PipelineConfig.INSTALLABLE_REPO_TYPES.contains(it.type) + PipelineConfig.INSTALLABLE_REPO_TYPES.contains(it.type) } def data_ = [ @@ -744,11 +745,11 @@ class LeVADocumentUseCase extends DocGenUseCase { def testsOfRepoTypeOdsService = [] def testsGroupedByRepoType = groupTestsByRepoType(installationTestIssues) testsGroupedByRepoType.each { repoTypes, tests -> - if (repoTypes.contains(MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE)) { + if (repoTypes.contains(PipelineConfig.REPO_TYPE_ODS_CODE)) { testsOfRepoTypeOdsCode.addAll(tests) } - if (repoTypes.contains(MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_SERVICE)) { + if (repoTypes.contains(PipelineConfig.REPO_TYPE_ODS_SERVICE)) { testsOfRepoTypeOdsService.addAll(tests) } } @@ -757,7 +758,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def docHistory = this.getAndStoreDocumentHistory(documentType, keysInDoc) def installedRepos = this.project.repositories.findAll { it -> - MROPipelineUtil.PipelineConfig.INSTALLABLE_REPO_TYPES.contains(it.type) + PipelineConfig.INSTALLABLE_REPO_TYPES.contains(it.type) } def data_ = [ @@ -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 @@ -853,7 +854,7 @@ class LeVADocumentUseCase extends DocGenUseCase { 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, @@ -866,7 +867,7 @@ class LeVADocumentUseCase extends DocGenUseCase { 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, @@ -986,7 +987,7 @@ 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() } + .findAll { it.odsRepoType.toLowerCase() == 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 -> @@ -1036,18 +1037,18 @@ class LeVADocumentUseCase extends DocGenUseCase { def repos = this.project.repositories.collect { Map it -> def clone = it.clone() - clone.printurl = it.url.replaceAll('/+','$0\u200B') + clone.printurl = it.url.replaceAll('/+', '$0\u200B') //Add break space in url in manufacturer def p = ~'https?://\\S*' def m = it.metadata.supplier =~ p - StringBuffer sb = new StringBuffer(); + StringBuffer sb = new StringBuffer() while (m.find()) { - String url = m.group(); + String url = m.group() url = url.replaceAll('/+', '$0\u200B') - m.appendReplacement(sb, url); + m.appendReplacement(sb, url) } - m.appendTail(sb); + m.appendTail(sb) clone.printsupplier = sb.toString() return clone @@ -1071,7 +1072,14 @@ class LeVADocumentUseCase extends DocGenUseCase { @SuppressWarnings('CyclomaticComplexity') String createTIR(Map repo, Map data) { - logger.debug("createTIR - repo:${prettyPrint(toJson(repo))}, data:${prettyPrint(toJson(data))}") + logger.debug("createTIR - repo:${repo}, data:${data}") + + // TODO Determine where to get the target environment in a straightforward way + def targetEnvironment = Project.getConcreteEnvironment( + project.buildParams.targetEnvironment, + project.buildParams.version.toString(), + project.versionedDevEnvsEnabled + ) def documentType = DocumentType.TIR as String @@ -1090,13 +1098,16 @@ class LeVADocumentUseCase extends DocGenUseCase { def keysInDoc = ['Technology-' + repo.id] def docHistory = this.getAndStoreDocumentHistory(documentType + '-' + repo.id, keysInDoc) + Map> deployments = repo.data.openshift.deployments ?: [:] - def data_ = [ + Map deploymentMean = prepareDeploymentMeanInfo(deployments, targetEnvironment) + + def documentData = [ metadata : this.getDocumentMetadata(this.DOCUMENT_TYPE_NAMES[documentType], repo), deployNote : deploynoteData, openShiftData: [ - builds : repo.data.openshift.builds ?: '', - deployments: repo.data.openshift.deployments ?: '' + builds : formatTIRBuilds(repo.data.openshift.builds), + deployments: prepareDeploymentInfo(deployments), ], testResults: [ installation: installationTestData?.testResults @@ -1106,28 +1117,113 @@ class LeVADocumentUseCase extends DocGenUseCase { sections: sections, documentHistory: docHistory?.getDocGenFormat() ?: [], documentHistoryLatestVersionId: docHistory?.latestVersionId ?: 1, - ] + ], + deploymentMean: deploymentMean, + legacy: deploymentMean?.type == 'tailor' ] // Code review report - in the special case of NO jira .. - def codeReviewReport - if (this.project.isAssembleMode && !this.jiraUseCase.jira && - repo.type?.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE.toLowerCase()) { - def currentRepoAsList = [ repo ] - codeReviewReport = obtainCodeReviewReport(currentRepoAsList) + def isOdsCodeRepo = repo.type?.toLowerCase() == PipelineConfig.REPO_TYPE_ODS_CODE.toLowerCase() + + List codeReviewReport = (this.project.isAssembleMode && !this.jiraUseCase.jira && isOdsCodeRepo) + ? obtainCodeReviewReport([repo]) + : null + + def modifier = { byte[] document -> + codeReviewReport + ? this.pdf.merge(this.steps.env.WORKSPACE as String, [document] + codeReviewReport) + : document } - def modifier = { document -> - if (codeReviewReport) { - List documents = [document] - documents += codeReviewReport - // Merge the current document with the code review report - return this.pdf.merge(this.steps.env.WORKSPACE, documents) - } - return document + return this.createDocument(documentType, repo, documentData, [:], modifier, getDocumentTemplateName(documentType, repo), watermarkText) + } + + /* + * Retrieves the deployment mean and fills empty values with proper defaults + */ + protected static Map prepareDeploymentMeanInfo(Map> deployments, String targetEnvironment) { + Map deploymentMean = + deployments.find { it.key.endsWith('-deploymentMean') }.value + + if (deploymentMean.type == 'tailor') { + return formatTIRTailorDeploymentMean(deploymentMean) } - return this.createDocument(documentType, repo, data_, [:], modifier, getDocumentTemplateName(documentType, repo), watermarkText) + return formatTIRHelmDeploymentMean(deploymentMean, targetEnvironment) + } + + /** + * Retrieves all deployments. + * + * The processed map is suited to format the resource information in the TIR. + * This method doesn't return deployment means. + * + * @return A Map with all the deployments. + */ + protected static Map> prepareDeploymentInfo(Map> deployments) { + return deployments + .findAll { ! it.key.endsWith('-deploymentMean') } + .collectEntries { String deploymentName, Map deployment -> + def filteredFields = deployment.findAll { k, v -> k != 'podName' } + return [(deploymentName): filteredFields] + } as Map> + } + + private static Map> formatTIRBuilds(Map> builds) { + if (!builds) { + return [:] + } + + return builds.collectEntries { String buildKey, Map build -> + Map formattedBuild = build + .collectEntries { String key, Object value -> [(StringUtils.capitalize(key)): value] } + return [(buildKey): formattedBuild] + } as Map> + } + + protected static Map formatTIRHelmDeploymentMean(Map mean, String targetEnvironment) { + Map formattedMean = [:] + + def envConfigFiles = mean.helmEnvBasedValuesFiles?.collect{ filenamePattern -> + filenamePattern.replace('.env.', ".${targetEnvironment}.") + } + + // Global config files are those that are not environment specific + def configFiles = mean.helmValuesFiles?.findAll { !envConfigFiles.contains(it) } + + formattedMean.namespace = mean.namespace ?: 'None' + formattedMean.type = mean.type + formattedMean.descriptorPath = mean.chartDir ?: '.' + formattedMean.defaultCmdLineArgs = mean.helmDefaultFlags.join(' ') ?: 'None' + formattedMean.additionalCmdLineArgs = mean.helmAdditionalFlags.join(' ') ?: 'None' + formattedMean.configParams = HtmlFormatterUtil.toUl(mean.helmValues as Map, 'None') + formattedMean.configFiles = HtmlFormatterUtil.toUl(configFiles as List, 'None') + formattedMean.envConfigFiles = HtmlFormatterUtil.toUl(envConfigFiles as List, 'None') + + Map formattedStatus = [:] + + formattedStatus.deployStatus = (mean.helmStatus.status == "deployed") + ? "Successfully deployed" + : mean.helmStatus.status + formattedStatus.resultMessage = mean.helmStatus.description + formattedStatus.lastDeployed = mean.helmStatus.lastDeployed + formattedStatus.resources = HtmlFormatterUtil.toUl(mean.helmStatus.resourcesByKind as Map, 'None') + + formattedMean.deploymentStatus = formattedStatus + + return formattedMean + } + + protected static Map formatTIRTailorDeploymentMean(Map mean) { + Map defaultValues = [ + tailorParamFile: 'None', + tailorParams : 'None', + tailorPreserve : 'No extra resources specified to be preserved' + ].withDefault { 'N/A' } + + return mean.collectEntries { k, v -> + [(k): v ?: defaultValues[k]] + } as Map } String createOverallTIR(Map repo = null, Map data = null) { @@ -1229,7 +1325,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def suffix = "" // compute suffix based on repository type if (repo != null) { - if (repo.type.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_INFRA) { + if (repo.type.toLowerCase() == PipelineConfig.REPO_TYPE_ODS_INFRA) { if (documentType == DocumentType.TIR as String) { suffix += "-infra" } @@ -1256,10 +1352,10 @@ class LeVADocumentUseCase extends DocGenUseCase { String getDocumentTemplatesVersion() { def capability = this.project.getCapability('LeVADocs') - return capability.templatesVersion ? "${capability.templatesVersion}": Project.DEFAULT_TEMPLATE_VERSION + return capability.templatesVersion ? "${capability.templatesVersion}" : Project.DEFAULT_TEMPLATE_VERSION } - boolean shouldCreateArtifact (String documentType, Map repo) { + boolean shouldCreateArtifact(String documentType, Map repo) { List nonArtifactDocTypes = [ DocumentType.TIR as String, DocumentType.DTR as String @@ -1268,11 +1364,11 @@ class LeVADocumentUseCase extends DocGenUseCase { return !(documentType && nonArtifactDocTypes.contains(documentType) && repo) } - Map getFiletypeForDocumentType (String documentType) { + Map getFiletypeForDocumentType(String documentType) { if (!documentType) { - throw new RuntimeException ('Cannot lookup Null docType for storage!') + throw new RuntimeException('Cannot lookup Null docType for storage!') } - Map defaultTypes = [storage: 'zip', content: 'pdf' ] + Map defaultTypes = [storage: 'zip', content: 'pdf'] if (DOCUMENT_TYPE_NAMES.containsKey(documentType)) { return defaultTypes @@ -1378,7 +1474,7 @@ class LeVADocumentUseCase extends DocGenUseCase { def softwareDesignSpecs = testIssue.getResolvedTechnicalSpecifications() .findAll { it.softwareDesignSpec } .collect { it.key } - def riskLevels = testIssue.getResolvedRisks(). collect { + def riskLevels = testIssue.getResolvedRisks().collect { def value = obtainEnum("SeverityOfImpact", it.severityOfImpact) return value ? value.text : "None" } @@ -1401,7 +1497,7 @@ class LeVADocumentUseCase extends DocGenUseCase { } protected List obtainCodeReviewReport(List repos) { - def reports = repos.collect { r -> + def reports = repos.collect { r -> // resurrect? Map resurrectedDocument = resurrectAndStashDocument('SCRR-MD', r, false) this.steps.echo "Resurrected 'SCRR' for ${r.id} -> (${resurrectedDocument.found})" @@ -1453,14 +1549,14 @@ class LeVADocumentUseCase extends DocGenUseCase { def isReleaseManagerComponent = gitUrl.endsWith("${this.project.key}-${normComponentName}.git".toLowerCase()) if (isReleaseManagerComponent) { - return [ : ] + return [:] } def repo_ = this.project.repositories.find { [it.id, it.name, it.metadata.name].contains(normComponentName) } if (!repo_) { - def repoNamesAndIds = this.project.repositories. collect { [id: it.id, name: it.name] } + def repoNamesAndIds = this.project.repositories.collect { [id: it.id, name: it.name] } throw new RuntimeException("Error: unable to create ${documentType}. Could not find a repository " + "configuration with id or name equal to '${normComponentName}' for " + "Jira component '${component.name}' in project '${this.project.key}'. Please check " + @@ -1480,13 +1576,13 @@ class LeVADocumentUseCase extends DocGenUseCase { componentName : component.name, componentId : metadata.id ?: 'N/A - part of this application', componentType : INTERNAL_TO_EXT_COMPONENT_TYPES.get(repo_.type?.toLowerCase()), - doInstall : MROPipelineUtil.PipelineConfig.INSTALLABLE_REPO_TYPES.contains(repo_.type), + doInstall : PipelineConfig.INSTALLABLE_REPO_TYPES.contains(repo_.type), odsRepoType : repo_.type?.toLowerCase(), description : metadata.description, nameOfSoftware : normComponentName ?: metadata.name, references : metadata.references ?: 'N/A', supplier : metadata.supplier, - version : (repo_.type?.toLowerCase() == MROPipelineUtil.PipelineConfig.REPO_TYPE_ODS_CODE) ? + version : (repo_.type?.toLowerCase() == PipelineConfig.REPO_TYPE_ODS_CODE) ? this.project.buildParams.version : metadata.version, requirements : component.getResolvedSystemRequirements(), @@ -1503,7 +1599,7 @@ class LeVADocumentUseCase extends DocGenUseCase { test.getResolvedComponents().collect { [test: test.key, component: it.name] } }.flatten() issueComponentMapping.groupBy { it.component }.collectEntries { c, v -> - [(c.replaceAll("Technology-", "")): v.collect { it.test } ] + [(c.replaceAll("Technology-", "")): v.collect { it.test }] } } @@ -1513,7 +1609,7 @@ class LeVADocumentUseCase extends DocGenUseCase { [ id: it.id, description: it.metadata?.description, - tests: componentTestMapping[it.id]? componentTestMapping[it.id].join(", "): "None defined" + tests: componentTestMapping[it.id] ? componentTestMapping[it.id].join(", ") : "None defined" ] } } @@ -1622,7 +1718,7 @@ class LeVADocumentUseCase extends DocGenUseCase { " this document cannot be considered final.*" } - if (! documentVersionId) { + if (!documentVersionId) { def metadata = this.getDocumentMetadata(documentType) documentVersionId = "${metadata.version}-${metadata.jenkins.buildNumber}" } @@ -1730,7 +1826,7 @@ class LeVADocumentUseCase extends DocGenUseCase { [(sec.section): sec + [content: this.convertImages(sec.content), show: this.project.isIssueToBeShown(sec)]] } - if (!sections || sections.isEmpty() ) { + if (!sections || sections.isEmpty()) { sections = this.levaFiles.getDocumentChapterData(documentType) if (!this.project.data.jira.undoneDocChapters) { this.project.data.jira.undoneDocChapters = [:] @@ -1759,7 +1855,7 @@ class LeVADocumentUseCase extends DocGenUseCase { if (this.project.historyForDocumentExists(document)) { this.project.getHistoryForDocument(document).getVersion() } else { - def trackingIssues = this.getDocumentTrackingIssuesForHistory(document, environments) + def trackingIssues = this.getDocumentTrackingIssuesForHistory(document, environments) this.jiraUseCase.getLatestDocVersionId(trackingIssues) } } @@ -1802,11 +1898,11 @@ class LeVADocumentUseCase extends DocGenUseCase { if (!version) { // The document has not (yet) been generated in this pipeline run. def envs = Environment.values().collect { it.toString() } - def trackingIssues = this.getDocumentTrackingIssuesForHistory(doc, envs) + def trackingIssues = this.getDocumentTrackingIssuesForHistory(doc, envs) version = this.jiraUseCase.getLatestDocVersionId(trackingIssues) if (project.isWorkInProgress || LeVADocumentScheduler.getFirstCreationEnvironment(doc) == - project.buildParams.targetEnvironmentToken ) { + project.buildParams.targetEnvironmentToken) { // Either this is a developer preview or the history is to be updated in this environment. version += 1L } diff --git a/src/org/ods/orchestration/util/HtmlFormatterUtil.groovy b/src/org/ods/orchestration/util/HtmlFormatterUtil.groovy new file mode 100644 index 000000000..659e30e2e --- /dev/null +++ b/src/org/ods/orchestration/util/HtmlFormatterUtil.groovy @@ -0,0 +1,29 @@ +package org.ods.orchestration.util + +class HtmlFormatterUtil { + + static String toUl(Map map, String emptyDefault, String cssClass = 'inner-ul') { + return itemsUl(map, emptyDefault, cssClass) { + def value = it.value in List + ? toUl(it.value as List, emptyDefault, cssClass) + : it.value ?: emptyDefault + + return "
  • ${it.key}: ${value}
  • " + } + } + + static String toUl(List list, String emptyDefault, String cssClass = 'inner-ul') { + return itemsUl(list, emptyDefault, cssClass) { "
  • ${it}
  • " } + } + + private static String itemsUl(items, String emptyDefault, String cssClass, Closure formatItem) { + if (!items) { + return emptyDefault + } + + String body = items.collect { formatItem(it) }.join() + + return "
      ${body}
    " + } + +} diff --git a/src/org/ods/services/OpenShiftService.groovy b/src/org/ods/services/OpenShiftService.groovy index f88881572..5a2253664 100644 --- a/src/org/ods/services/OpenShiftService.groovy +++ b/src/org/ods/services/OpenShiftService.groovy @@ -5,6 +5,7 @@ import groovy.json.JsonOutput import groovy.json.JsonSlurperClassic import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode +import org.ods.util.HelmStatus import org.ods.util.ILogger import org.ods.util.IPipelineSteps import org.ods.util.PodData @@ -155,6 +156,24 @@ class OpenShiftService { } } + HelmStatus helmStatus( + 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 helmStatusMap = new JsonSlurperClassic().parseText(helmStdout) + return HelmStatus.fromJsonObject(helmStatusMap) + } catch (ex) { + throw new RuntimeException("Helm status Failed (${ex.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' : '' diff --git a/src/org/ods/util/HelmStatus.groovy b/src/org/ods/util/HelmStatus.groovy new file mode 100644 index 000000000..e5eb3fdb2 --- /dev/null +++ b/src/org/ods/util/HelmStatus.groovy @@ -0,0 +1,252 @@ +package org.ods.util + +import com.cloudbees.groovy.cps.NonCPS + +class HelmStatus { + + private String name + private String version + private String namespace + private String status + private String description + private String lastDeployed + /** + * Resources names by kind + */ + private Map > resourcesByKind + + @NonCPS + static HelmStatus fromJsonObject(Object object) { + try { + def rootObject = ensureMap(object, "") + def infoObject = ensureMap(rootObject.info, "info") + + // validation of missing keys or types + def missingKeys = [] + def badTypes = [] + def expectedStringAttribsForRoot = collectMissingStringAttributes(rootObject, + ["name", "namespace"]) + def expectedStringAttribsForInfo = collectMissingStringAttributes(infoObject, + ["status", "description", "last_deployed"]) + missingKeys.addAll(expectedStringAttribsForRoot.first) + missingKeys.addAll(expectedStringAttribsForInfo.first) + badTypes.addAll(expectedStringAttribsForRoot.second) + badTypes.addAll(expectedStringAttribsForInfo.second) + def att = "version" + if (!rootObject.containsKey(att)) { + missingKeys << att + } else if (!(rootObject[att] in Integer)) { + badTypes << "${att}: expected Integer, found ${rootObject[att].getClass()}" + } + handleMissingKeysOrBadTypes(missingKeys, badTypes) + 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> resourcesByKind = [:] .withDefault { [] } + for (entry in resourcesObject.entrySet()) { + def key = entry.key as String + def resourceList = ensureList(entry.value, "info.resources.${key}") + resourceList.eachWithIndex { resourceJsonObject, i -> + def resourceContext = "info.resources.${key}.[${i}]" + def resource = extractResource(resourceJsonObject, resourceContext) + if (resource) { + resourcesByKind[resource.first] << resource.second + } + } + } + def hs = new HelmStatus() + + hs.name = rootObject.name + hs.version = rootObject.version as String + hs.namespace = rootObject.namespace + hs.status = infoObject.status + hs.description = infoObject.description + hs.lastDeployed = infoObject["last_deployed"] + + hs.resourcesByKind = resourcesByKind.collectEntries { kind, names -> + [ kind, names.collect() ] + } as Map > + return hs + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException( + "Unexpected helm status information in JSON at 'info': ${ex.message}") + } + } + + @NonCPS + Map> getResources() { + // Return a cloned copy of the map + return resourcesByKind.collectEntries { kind, names -> + [ kind, names.collect() ] + } + } + + @NonCPS + String getName() { + return name + } + + @NonCPS + void setName(String name) { + this.name = name + } + + @NonCPS + String getVersion() { + return version + } + + @NonCPS + void setVersion(String version) { + this.version = version + } + + @NonCPS + String getNamespace() { + return namespace + } + + @NonCPS + void setNamespace(String namespace) { + this.namespace = namespace + } + + @NonCPS + String getStatus() { + return status + } + + @NonCPS + void setStatus(String status) { + this.status = status + } + + @NonCPS + String getDescription() { + return description + } + + @NonCPS + void setDescription(String description) { + this.description = description + } + + @NonCPS + String getLastDeployed() { + return lastDeployed + } + + @NonCPS + void setLastDeployed(String lastDeployed) { + this.lastDeployed = lastDeployed + } + + @NonCPS + Map> getResourcesByKind() { + return resourcesByKind + } + + @NonCPS + void setResourcesByKind(Map> resourcesByKind) { + this.resourcesByKind = resourcesByKind + } + + @NonCPS + Map toMap() { + def result = [ + name: name, + version: version, + namespace: namespace, + status: status, + description: description, + lastDeployed: lastDeployed, + resourcesByKind: resourcesByKind, + ] + return result + } + + @NonCPS + String toString() { + return toMap().toMapString() + } + + @NonCPS + private static Tuple2 extractResource( + resourceJsonObject, String context) { + def resourceObject = ensureMap(resourceJsonObject, context) + Map resource = [:] + if (resourceObject?.kind) { + resource.kind = resourceObject.kind + } + if (resource?.kind == "PodList") { + return null + } + Map metadataObject = ensureMap( + resourceObject.metadata, "${context}.metadata") + if (metadataObject?.name) { + resource["name"] = metadataObject.name + } + + def expected = collectMissingStringAttributes(resource, ["name", "kind"]) + handleMissingKeysOrBadTypes(expected.first, expected.second) + return new Tuple2(resource.kind, resource.name) + } + + @NonCPS + private static Map ensureMap(Object obj, String context) { + if (obj == null) { + return [:] + } + if (!(obj in Map)) { + def msg = context ? + "${context}: expected JSON object, found ${obj.getClass()}" : + "Expected JSON object, found ${obj.getClass()}" + + throw new IllegalArgumentException(msg) + } + return obj as Map + } + + @NonCPS + private static List ensureList(Object obj, String context) { + if (obj == null) { + return [] + } + if (!(obj in List)) { + throw new IllegalArgumentException( + "${context}: expected JSON array, found ${obj.getClass()}") + } + return obj as List + } + + @NonCPS + private static Tuple2, List> collectMissingStringAttributes( + Map jsonObject, List stringAttributes) { + def missingOrEmptyKeys = [] + def badTypes = [] + for (att in stringAttributes) { + if (! (jsonObject.containsKey(att) && jsonObject[att])) { + missingOrEmptyKeys << att + } else if (!(jsonObject[att] in String)) { + badTypes << "${att}: expected String, found ${jsonObject[att].getClass()}" + } + } + return new Tuple2(missingOrEmptyKeys, badTypes) + } + + @NonCPS + private static 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(".")) + } + } + +} diff --git a/src/org/ods/util/PodData.groovy b/src/org/ods/util/PodData.groovy index dd26b8903..d3f05486b 100644 --- a/src/org/ods/util/PodData.groovy +++ b/src/org/ods/util/PodData.groovy @@ -51,10 +51,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..036133f08 100644 --- a/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy +++ b/test/groovy/org/ods/component/HelmDeploymentStrategySpec.groovy @@ -2,85 +2,171 @@ package org.ods.component import org.ods.services.JenkinsService import org.ods.services.OpenShiftService -import org.ods.services.ServiceRegistry -import org.ods.util.Logger +import org.ods.util.HelmStatus +import org.ods.util.ILogger +import org.ods.util.IPipelineSteps import org.ods.util.PodData import spock.lang.Shared +import util.FixtureHelper import vars.test_helper.PipelineSpockTestBase class HelmDeploymentStrategySpec extends PipelineSpockTestBase { - private Logger logger = Mock(Logger) + private ILogger logger = Stub() + @Shared - def contextData = [ - gitUrl: 'https://example.com/scm/foo/bar.git', - gitCommit: 'cd3e9082d7466942e1de86902bb9e663751dae8e', - gitCommitMessage: 'Foo', - gitCommitAuthor: 'John Doe', - gitCommitTime: '2020-03-23 12:27:08 +0100', - gitBranch: 'master', - 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', - artifactUriStore: [builds: [bar: [:]]] + static def contextData = [ + 'artifactUriStore' : [ + 'builds': [ + 'bar': [:], + ], + ], + 'buildTime' : '2020-03-23 12:27:08 +0100', + 'buildUrl' : 'https://jenkins.example.com/job/foo-cd/job/foo-cd-bar-master/11/console', + 'cdProject' : 'myproject-cd', + 'chartDir' : 'chart', + 'clusterRegistryAddress' : 'image-registry.openshift.svc:1000', + 'componentId' : 'core', + 'environment' : 'test', + 'gitBranch' : 'master', + 'gitCommit' : 'cdefab12345', + 'gitCommitAuthor' : 'John Doe', + 'gitCommitMessage' : 'Foo', + 'gitCommitTime' : '2020-03-23 12:27:08 +0100', + 'gitUrl' : 'https://example.com/scm/foo/bar.git', + 'odsSharedLibVersion' : '2.x', + 'openshiftRolloutTimeoutRetries': 5, + 'projectId' : 'myproject', + 'targetProject' : 'myproject-test', ] - def "rollout: check deploymentMean"() { + static Map> expectedRolloutData = [ + 'Deployment/core': [ + new PodData( + [ + 'containers' : [ + 'chart-component-a': "${contextData.clusterRegistryAddress}/myproject-dev/helm-component-a@sha256:12345abcdef", + ], + 'deploymentId' : 'backend-helm-monorepo-chart-component-a-789abcde', + 'podMetaDataCreationTimestamp': '2024-11-11T16:01:04Z', + 'podName' : 'backend-helm-monorepo-chart-component-a-789abcde-asdf', + 'podNamespace' : "${contextData.targetProject}", + 'podStatus' : 'Running', + ]) + ], + 'Deployment/standalone-gateway': [ + new PodData( + [ + 'containers' : [ + 'chart-component-b': "${contextData.clusterRegistryAddress}/myproject-dev/helm-component-b@sha256:98765fedcba", + ], + 'deploymentId' : 'backend-helm-monorepo-chart-component-b-01234abc', + 'podMetaDataCreationTimestamp': '2024-11-11T16:01:04Z', + 'podName' : 'backend-helm-monorepo-chart-component-b-01234abc-qwerty', + 'podNamespace' : "${contextData.targetProject}", + 'podStatus' : 'Running', + ]) + ] + ] + + static def config = [ + 'helmAdditionalFlags' : ['--additional-flag-1', '--additional-flag-2'], + 'helmEnvBasedValuesFiles': ['values.env.yaml', 'secrets.env.yaml'], + 'helmValuesFiles' : ['values.yaml', 'secrets.yaml'], + 'selector' : "app.kubernetes.io/instance=${contextData.componentId}", + ] + + static def corePodData = new PodData([ + 'containers' : [ + 'chart-component-a': "${contextData.clusterRegistryAddress}/myproject-dev/helm-component-a@sha256:12345abcdef", + ], + 'deploymentId' : 'backend-helm-monorepo-chart-component-a-789abcde', + 'podMetaDataCreationTimestamp': '2024-11-11T16:01:04Z', + 'podName' : 'backend-helm-monorepo-chart-component-a-789abcde-asdf', + 'podNamespace' : "${contextData.targetProject}", + 'podStatus' : 'Running', + ]) + + static def standaloneGatewayPodData = new PodData([ + 'containers' : [ + 'chart-component-b': "${contextData.clusterRegistryAddress}/myproject-dev/helm-component-b@sha256:98765fedcba", + ], + 'deploymentId' : 'backend-helm-monorepo-chart-component-b-01234abc', + 'podMetaDataCreationTimestamp': '2024-11-11T16:01:04Z', + 'podName' : 'backend-helm-monorepo-chart-component-b-01234abc-qwerty', + 'podNamespace' : "${contextData.targetProject}", + 'podStatus' : 'Running', + ]) + + def "rollout: check rolloutData"() { given: + def helmStatus = HelmStatus.fromJsonObject(FixtureHelper.createHelmCmdStatusMap()) - 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": [] - ], - "bar":[ - "podName": null, - "podNamespace": null, - "podMetaDataCreationTimestamp": null, - "deploymentId": "bar-124", - "podNode": null, - "podIp": null, - "podStatus": null, - "podStartupTimeStamp": null, - "containers": null, - ] - ] - ] - def config = [:] + OpenShiftService openShift = Mock() + JenkinsService jenkins = Stub() + IPipelineSteps steps = Stub() - def ctxData = contextData + [environment: 'dev', targetProject: 'foo-dev', openshiftRolloutTimeoutRetries: 5, chartDir: 'chart'] - IContext context = new Context(null, ctxData, logger) - OpenShiftService openShiftService = Mock(OpenShiftService.class) - openShiftService.checkForPodData(*_) >> [new PodData([deploymentId: "${contextData.componentId}-124"])] - ServiceRegistry.instance.add(OpenShiftService, openShiftService) + IContext context = Stub { + getTargetProject() >> contextData.targetProject + getEnvironment() >> contextData.environment + getBuildArtifactURIs() >> contextData.artifactUriStore + getComponentId() >> contextData.componentId + getClusterRegistryAddress() >> contextData.clusterRegistryAddress + getCdProject() >> contextData.cdProject + } - JenkinsService jenkinsService = Stub(JenkinsService.class) - jenkinsService.maybeWithPrivateKeyCredentials(*_) >> { args -> args[1]('/tmp/file') } - ServiceRegistry.instance.add(JenkinsService, jenkinsService) + // Invoke the closures passed to the methods, given that those are part + // of HelmDeploymentStrategy and should also be tested + steps.dir(contextData.chartDir, _ as Closure) >> { args -> args[1]() } + jenkins.maybeWithPrivateKeyCredentials("${contextData.cdProject}-helm-private-key", _ as Closure) >> { args -> args[1]() } - HelmDeploymentStrategy strategy = Spy(HelmDeploymentStrategy, constructorArgs: [null, context, config, openShiftService, jenkinsService, logger]) + Map> rolloutData + HelmDeploymentStrategy strategy = new HelmDeploymentStrategy(steps, context, config, openShift, jenkins, logger) when: - def deploymentResources = [Deployment: ['bar']] - def rolloutData = strategy.getRolloutData(deploymentResources) - def actualDeploymentMeans = context.getBuildArtifactURIs() - + rolloutData = strategy.deploy() then: - printCallStack() - assertJobStatusSuccess() + 1 * openShift.helmStatus(contextData.targetProject, contextData.componentId) >> { helmStatus } + + 1 * openShift.helmUpgrade( + contextData.targetProject, + contextData.componentId, + ['values.yaml', 'secrets.yaml', 'values.test.yaml','secrets.test.yaml'], + [ + registry: contextData.clusterRegistryAddress, + componentId: contextData.componentId, + 'global.registry': contextData.clusterRegistryAddress, + 'global.componentId': contextData.componentId, + imageNamespace: contextData.targetProject, + imageTag: '', + 'global.imageNamespace': contextData.targetProject, + 'global.imageTag': '', + ], + ['--install', '--atomic'], + ['--additional-flag-1', '--additional-flag-2'], + true, + ) + + 2 * openShift.checkForPodData(contextData.targetProject, config.selector, _) >> { args -> + switch (args[2]) { + case 'core': + return [corePodData] + case 'standalone-gateway': + return [standaloneGatewayPodData] + default: + return [] + } + } + + assert expectedRolloutData.keySet() == rolloutData.keySet() + + expectedRolloutData.each { key, expectedPodData -> + def actualPodData = rolloutData[key] + + def expectedMaps = expectedPodData*.toMap() + def actualMaps = actualPodData*.toMap() - assert expectedDeploymentMeans == actualDeploymentMeans + assert expectedMaps == actualMaps + } } } diff --git a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy index 0bfdb37ab..18c507e20 100644 --- a/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy +++ b/test/groovy/org/ods/orchestration/usecase/LeVADocumentUseCaseSpec.groovy @@ -22,6 +22,7 @@ import org.ods.util.IPipelineSteps import org.ods.util.Logger import spock.lang.Unroll import util.FixtureHelper +import util.OpenShiftHelper import util.SpecHelper import java.nio.file.Files @@ -109,7 +110,7 @@ class LeVADocumentUseCaseSpec extends SpecHelper { docHistory.load(project.data.jira, []) usecase.getAndStoreDocumentHistory(*_) >> docHistory jenkins.unstashFilesIntoPath(_, _, "SonarQube Report") >> true - steps.getEnv() >> ['RELEASE_PARAM_VERSION': 'WIP'] + steps.getEnv() >> ['RELEASE_PARAM_VERSION': 'WIP', 'BUILD_NUMBER': '10', 'BUILD_URL': 'http://jenkins-project-cd/job/10', 'JOB_NAME': 'project-cd/project-cd-releasemanager-master'] stepsNoWip.getEnv() >> ['RELEASE_PARAM_VERSION': 'CHG00001'] } @@ -1218,22 +1219,6 @@ class LeVADocumentUseCaseSpec extends SpecHelper { def "create TIR"() { given: - // Test Parameters - def repo = project.repositories.first() - def data = [ - openshift: [ - pod: [ - podName: 'N/A', - podNamespace: 'N/A', - podCreationTimestamp: 'N/A', - podEnvironment: 'N/A', - podNode: 'N/A', - podIp: 'N/A', - podStatus: 'N/A' - ] - ] - ] - // Argument Constraints def documentType = TIR as String @@ -1246,36 +1231,27 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase.createTIR(repo, data) then: + // Jira enabled + 2 * jiraUseCase.jira >> Mock(JiraService) 1 * usecase.getDocumentSectionsFileOptional(documentType) >> chapterData 0 * levaFiles.getDocumentChapterData(documentType) 1 * usecase.getWatermarkText(documentType, _) >> watermarkText - - then: 1 * usecase.getDocumentMetadata(LeVADocumentUseCase.DOCUMENT_TYPE_NAMES[documentType], repo) 1 * usecase.getDocumentTemplateName(documentType, repo) >> documentTemplate - 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) + 0 * usecase.obtainCodeReviewReport(_) >> [] + 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) >> { + assert it[2]."deploymentMean" + assert it[2]."legacy" == legacy + } + + where: + legacy | data | repo + false | FixtureHelper.createTIRDataHelm() | FixtureHelper.createTIRRepoHelm() + true | FixtureHelper.createTIRDataTailor() | FixtureHelper.createTIRRepoTailor() } def "create TIR without Jira"() { given: - project.services.jira = null - def data = [ - openshift: [ - pod: [ - podName: 'N/A', - podNamespace: 'N/A', - podCreationTimestamp: 'N/A', - podEnvironment: 'N/A', - podNode: 'N/A', - podIp: 'N/A', - podStatus: 'N/A' - ] - ] - ] - - // Test Parameters - def repo = project.repositories.first() - // Argument Constraints def documentType = TIR as String @@ -1288,15 +1264,170 @@ class LeVADocumentUseCaseSpec extends SpecHelper { usecase.createTIR(repo, data) then: + // No Jira enabled + 2 * jiraUseCase.jira >> null 1 * project.getDocumentChaptersForDocument(documentType) >> [] 1 * usecase.getDocumentSectionsFileOptional(documentType) 1 * levaFiles.getDocumentChapterData(documentType) >> chapterData 1 * usecase.getWatermarkText(documentType, _) >> watermarkText - - then: 1 * usecase.getDocumentMetadata(LeVADocumentUseCase.DOCUMENT_TYPE_NAMES[documentType], repo) 1 * usecase.getDocumentTemplateName(documentType, repo) >> documentTemplate - 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) + 1 * usecase.obtainCodeReviewReport(_) >> [] + 1 * usecase.createDocument(documentType, repo, _, [:], _, documentTemplate, watermarkText) >> { + assert it[2]."deploymentMean" + assert it[2]."legacy" == legacy + } + + where: + legacy | data | repo + false | FixtureHelper.createTIRDataHelm() | FixtureHelper.createTIRRepoHelm() + true | FixtureHelper.createTIRDataTailor() | FixtureHelper.createTIRRepoTailor() + } + + def "assemble deploymentMean and deploymentInfo for TIR with helm"() { + given: + def deployments = FixtureHelper.createTIRRepoHelm().data.openshift.deployments + OpenShiftHelper.isDeploymentKind(*_) >> true + + def expectedMeanInfo = [ + 'namespace': 'myodsproject-dev', + 'type': 'helm', + 'descriptorPath': 'chart', + 'defaultCmdLineArgs': '--install --atomic', + 'additionalCmdLineArgs': '--additional-flag-1 --additional-flag-2', + 'configParams': '''
    • registry: image-registry.openshift.svc:1000
    • componentId: backend-helm-monorepo
    ''', + 'configFiles': '''
    • values.yaml
    ''', + 'envConfigFiles': '''
    • values1.dev.yaml
    • values2.dev.yaml
    ''', + 'deploymentStatus': [ + 'deployStatus': 'Successfully deployed', + 'resultMessage': 'Upgrade complete', + 'lastDeployed': '2024-10-31T11:10:27.478860933Z', + 'resources': '''
    • Deployment:
      • backend-helm-monorepo-chart-component-a
      • backend-helm-monorepo-chart-component-b
    • Service:
      • backend-helm-monorepo-chart
    ''', + ] , + ] + + def expectedDeploymentInfo = [ + 'backend-helm-monorepo-chart-component-b': [ + 'podNamespace': 'myodsproject-dev', + 'podStatus': 'Running', + 'deploymentId': 'backend-helm-monorepo-chart-component-b-567ff4f8f6', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers': [ + 'chart-component-b': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-b@sha256:10002345abcde', + ] , + ] , + 'backend-helm-monorepo-chart-component-a': [ + 'podNamespace': 'myodsproject-dev', + 'podStatus': 'Running', + 'deploymentId': 'backend-helm-monorepo-chart-component-a-5ffd9c7cbd', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers': [ + 'chart-component-a': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-a@sha256:10002345abcde', + ] , + ] , + ] + + when: + def deploymentMeanInfo = usecase.prepareDeploymentMeanInfo(deployments, 'dev') + def deploymentInfo = usecase.prepareDeploymentInfo(deployments) + + then: + deploymentMeanInfo == expectedMeanInfo + deploymentInfo == expectedDeploymentInfo + } + + def "assemble deploymentMean for TIR with helm and default values"() { + given: + def deployments = FixtureHelper.createTIRRepoHelm().data.openshift.deployments + def deploymentMean = deployments.'backend-helm-monorepo-chart-component-b-deploymentMean' + OpenShiftHelper.isDeploymentKind(*_) >> true + + deploymentMean.namespace = '' + deploymentMean.chartDir = '' + deploymentMean.helmDefaultFlags = [] + deploymentMean.helmAdditionalFlags = [] + deploymentMean.helmValues = [:] + deploymentMean.helmValuesFiles = [] + deploymentMean.helmEnvBasedValuesFiles = [] + deploymentMean.helmStatus.status = 'SOME OTHER STATUS' + deploymentMean.helmStatus.resourcesByKind = [:] + + when: + def deploymentMeanInfo = usecase.prepareDeploymentMeanInfo(deployments, 'dev') + + then: + deploymentMeanInfo.namespace == 'None' + deploymentMeanInfo.descriptorPath == '.' + deploymentMeanInfo.defaultCmdLineArgs == 'None' + deploymentMeanInfo.additionalCmdLineArgs == 'None' + deploymentMeanInfo.configParams =='None' + deploymentMeanInfo.configFiles =='None' + deploymentMeanInfo.envConfigFiles == 'None' + deploymentMeanInfo.deploymentStatus.deployStatus == 'SOME OTHER STATUS' + deploymentMeanInfo.deploymentStatus.resources == 'None' + } + + def "assemble deploymentMean and deploymentInfo for TIR with tailor"() { + given: + def deployments = FixtureHelper.createTIRRepoTailor().data.openshift.deployments + OpenShiftHelper.isDeploymentKind(*_) >> true + + def expectedMeanInfo = [ + 'tailorParamFile': 'a-param-file.yaml', + 'tailorParams': [ 'fake-param1', 'fake-param2' ] , + 'selector': 'app=myodsproject-flask-backend', + 'type': 'tailor', + 'tailorSelectors': [ + 'selector': 'app=myodsproject-flask-backend', + 'exclude': 'bc,is', + ] , + 'tailorPreserve': [ 'fake-preserve1', 'fake-preserve2' ] , + 'tailorVerify': true, + ] + + def expectedDeploymentInfo = [ + 'flask-backend': [ + 'podNamespace': 'myodsproject-dev', + 'podStatus': 'Running', + 'deploymentId': 'flask-backend-2', + 'podMetaDataCreationTimestamp': '2024-10-31T11:09:56Z', + 'containers': [ + 'flask-backend': 'image-registry.openshift.svc:1000/myodsproject-dev/flask-backend@sha256:10002345abcde', + ] , + ] , + ] + + when: + def deploymentMeanInfo = usecase.prepareDeploymentMeanInfo(deployments, 'dev') + def deploymentInfo = usecase.prepareDeploymentInfo(deployments) + + then: + deploymentMeanInfo == expectedMeanInfo + deploymentInfo == expectedDeploymentInfo + } + + def "assemble deploymentMean for TIR with tailor and default values"() { + given: + def deployments = FixtureHelper.createTIRRepoTailor().data.openshift.deployments + def deploymentMean = deployments.'flask-backend-deploymentMean' + OpenShiftHelper.isDeploymentKind(*_) >> true + + // These are special cases to be replaced with a custom value when a falsy value is present + deploymentMean.tailorParamFile = '' + deploymentMean.tailorParams = [] + deploymentMean.tailorPreserve = [] + + // This one is the general case + deploymentMean.tailorSelectors = [] + + when: + def deploymentMeanInfo = usecase.prepareDeploymentMeanInfo(deployments, 'dev') + + then: + deploymentMeanInfo.tailorParamFile == 'None' + deploymentMeanInfo.tailorParams == 'None' + deploymentMeanInfo.tailorPreserve == 'No extra resources specified to be preserved' + deploymentMeanInfo.tailorSelectors == 'N/A' } def "create overall DTR"() { @@ -1678,16 +1809,16 @@ 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, diff --git a/test/groovy/org/ods/orchestration/util/HtmlFormatterUtilSpec.groovy b/test/groovy/org/ods/orchestration/util/HtmlFormatterUtilSpec.groovy new file mode 100644 index 000000000..0bc8b97ec --- /dev/null +++ b/test/groovy/org/ods/orchestration/util/HtmlFormatterUtilSpec.groovy @@ -0,0 +1,46 @@ +package org.ods.orchestration.util + +import spock.lang.Specification + +class HtmlFormatterUtilSpec extends Specification { + + def "Maps and Lists converted to HTML
      or to 'EMPTY_DEFAULT' when empty"() { + given: 'String with nonbreakable white space' + + def emptyList = [] + def list = ['v1', 'v2'] + + def emptyMap = [:] + def map = [ + 'k1': '', + 'k2': [], + 'k3': 'v3', + 'k4': ['v4.1', 'v4.2'] + ] + + when: 'We convert an empty list to HTML
        ' + def emptyListHtml = HtmlFormatterUtil.toUl(emptyList as List, 'EMPTY_DEFAULT') + + then: 'We get the String "EMPTY_DEFAULT" as a result for the empty list' + emptyListHtml == 'EMPTY_DEFAULT' + + when: 'We convert an empty map to HTML
          ' + def emptyMapHtml = HtmlFormatterUtil.toUl(emptyMap as Map, 'EMPTY_DEFAULT') + + then: 'We get the String "EMPTY_DEFAULT" as a result for the empty map' + emptyMapHtml == 'EMPTY_DEFAULT' + + when: "We convert a list to HTML
            with class 'some-ul-class'" + def listHtml = HtmlFormatterUtil.toUl(list as List, 'EMPTY_DEFAULT', 'some-ul-class') + + then: "We get the HTML String for a HTML
              with the list items as
            • sub-elements" + listHtml == "
              • v1
              • v2
              " + + when: "We convert a map to HTML
                with class 'some-ul-class'" + def mapHtml = HtmlFormatterUtil.toUl(map as Map, 'EMPTY_DEFAULT', 'some-ul-class') + + then: "We get the HTML String for a HTML
                  with the map items as
                • sub-elements" + mapHtml == "
                  • k1: EMPTY_DEFAULT
                  • k2: EMPTY_DEFAULT
                  • k3: v3
                  • k4:
                    • v4.1
                    • v4.2
                  " + + } +} diff --git a/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy b/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy index efbabf285..b8b3e803c 100644 --- a/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy +++ b/test/groovy/org/ods/services/OpenShiftServiceSpec.groovy @@ -200,25 +200,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 +242,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.helmStatus('myproject-dev', 'backend-helm-monorepo') + + then: + 1 * steps.sh( + script: 'helm -n myproject-dev status backend-helm-monorepo --show-resources -o json', + label: 'Gather Helm status for release backend-helm-monorepo in myproject-dev', + returnStdout: true, + ) >> helmJsonText + helmStatusData.name == 'backend-helm-monorepo' + helmStatusData.namespace == 'myproject-dev' + } + + 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": "backend-helm-monorepo", + "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: + service.helmStatus('myproject-dev ', 'backend-helm-monorepo') +// OpenShiftService.DEPLOYMENT_KIND, OpenShiftService.DEPLOYMENTCONFIG_KIND,]) + then: + 1 * steps.sh( + script: 'helm -n myproject-dev status backend-helm-monorepo --show-resources -o json', + label: 'Gather Helm status for release backend-helm-monorepo in myproject-dev ', + returnStdout: true, + ) >> helmJsonText + thrown RuntimeException + } + def "helm upgrade"() { given: def steps = Spy(util.PipelineSteps) @@ -752,7 +789,7 @@ class OpenShiftServiceSpec extends SpecHelper { when: def result = service.getConsoleUrl(steps) - then: + then: result == routeUrl } diff --git a/test/groovy/util/FixtureHelper.groovy b/test/groovy/util/FixtureHelper.groovy index 0a109ab56..caa5568b4 100644 --- a/test/groovy/util/FixtureHelper.groovy +++ b/test/groovy/util/FixtureHelper.groovy @@ -2,13 +2,12 @@ package util import groovy.json.JsonSlurperClassic import groovy.transform.InheritConstructors - -import org.ods.services.GitService import org.apache.http.client.utils.URIBuilder import org.junit.contrib.java.lang.system.EnvironmentVariables -import org.ods.orchestration.parser.* -import org.ods.orchestration.usecase.* -import org.ods.orchestration.util.* +import org.ods.orchestration.parser.JUnitParser +import org.ods.orchestration.usecase.JiraUseCase +import org.ods.orchestration.util.Project +import org.ods.services.GitService import org.ods.util.IPipelineSteps import org.ods.util.Logger import org.yaml.snakeyaml.Yaml @@ -601,6 +600,1573 @@ class FixtureHelper { ] } + static Map createTIRDataHelm() { + [ + 'git' : [ + 'previousSucessfulCommit': 'b00012345bcdef', + 'baseTag' : '', + 'commit' : 'a00012345bcdef', + 'previousCommit' : 'b00012345bcdef', + 'targetTag' : '', + 'branch' : 'master', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-backend-helm-monorepo.git', + ], + 'previousSucessfulCommit': 'c00012345bcdef', + 'documents' : [ + ], + 'openshift' : [ + 'testResults' : 1, + 'deployments' : [ + 'backend-helm-monorepo-chart-component-deploymentMean': [ + 'chartDir' : 'chart', + 'helmAdditionalFlags' : ['--additional-flag-1', '--additional-flag-2'], + 'helmEnvBasedValuesFiles': ['values1.env.yaml', 'values2.env.yaml'], + 'helmValues' : [ + 'registry' : 'image-registry.openshift.svc:1000', + 'componentId': 'backend-helm-monorepo', + ], + 'helmDefaultFlags' : ['--install', '--atomic'], + 'namespace' : 'myodsproject-dev', + 'helmReleaseName' : 'backend-helm-monorepo', + 'selector' : 'app.kubernetes.io/instance=backend-helm-monorepo', + 'helmValuesFiles' : ['values.yaml'], + 'type' : 'helm', + 'helmStatus' : [ + 'name' : 'backend-helm-monorepo', + 'namespace' : 'myodsproject-dev', + 'description' : 'Upgrade complete', + 'resourcesByKind': [ + 'Deployment': ['backend-helm-monorepo-chart-component-a', 'backend-helm-monorepo-chart-component-b'], + 'Service' : ['backend-helm-monorepo-chart'], + ], + 'version' : '14', + 'status' : 'deployed', + 'lastDeployed' : '2024-10-31T11:10:27.478860933Z', + ], + ], + 'backend-helm-monorepo-chart-component-b' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'backend-helm-monorepo-chart-component-b-567ff4f8f6', + 'podName' : 'backend-helm-monorepo-chart-component-b-567ff4f8f6-kvmqm', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers' : [ + 'chart-component-b': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-b@sha256:10002345abcde', + ], + ], + 'backend-helm-monorepo-chart-component-a' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'backend-helm-monorepo-chart-component-a-5ffd9c7cbd', + 'podName' : 'backend-helm-monorepo-chart-component-a-5ffd9c7cbd-h4wsb', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers' : [ + 'chart-component-a': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-a@sha256:10002345abcde', + ], + ], + ], + 'testResultsFolder' : 'build/test-results/test', + 'xunitTestResultsStashPath': 'test-reports-junit-xml-backend-helm-monorepo-19', + 'SCRR' : 'SCRR-myodsproject-backend-helm-monorepo.docx', + 'SCRR-MD' : 'SCRR-myodsproject-backend-helm-monorepo.md', + 'builds' : [ + 'backend-helm-monorepo-component-b': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/backend-helm-monorepo-component-b@sha256:10002345abcde', + 'buildId': 'backend-helm-monorepo-component-b-26', + ], + 'backend-helm-monorepo-component-a': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/backend-helm-monorepo-component-a@sha256:10002345abcde', + 'buildId': 'backend-helm-monorepo-component-a-26', + ], + ], + 'CREATED_BY_BUILD' : 'WIP/19', + 'sonarqubeScanStashPath' : 'scrr-report-backend-helm-monorepo-19', + ], + ] + } + + static Map createTIRRepoHelm() { + [ + 'include' : true, + 'metadata' : [ + 'supplier' : 'IT INF IAS', + 'name' : 'PostgreSQL', + 'description': 'A fully functional PostgreSQL Cluster with Patroni', + 'type' : 'ods', + 'version' : '4.x', + ], + 'data' : [ + 'git' : [ + 'previousSucessfulCommit': 'b00012345bcdef', + 'baseTag' : '', + 'commit' : 'a00012345bcdef', + 'previousCommit' : 'b00012345bcdef', + 'targetTag' : '', + 'branch' : 'master', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-backend-helm-monorepo.git', + ], + 'previousSucessfulCommit': 'c00012345bcdef', + 'documents' : [ + ], + 'openshift' : [ + 'testResults' : 1, + 'deployments' : [ + 'backend-helm-monorepo-chart-component-b-deploymentMean': [ + 'chartDir' : 'chart', + 'helmAdditionalFlags' : ['--additional-flag-1', '--additional-flag-2'], + 'helmEnvBasedValuesFiles': ['values1.env.yaml', 'values2.env.yaml'], + 'helmValues' : [ + 'registry' : 'image-registry.openshift.svc:1000', + 'componentId': 'backend-helm-monorepo', + ], + 'helmDefaultFlags' : ['--install', '--atomic'], + 'namespace' : 'myodsproject-dev', + 'helmReleaseName' : 'backend-helm-monorepo', + 'selector' : 'app.kubernetes.io/instance=backend-helm-monorepo', + 'helmValuesFiles' : ['values.yaml'], + 'type' : 'helm', + 'helmStatus' : [ + 'name' : 'backend-helm-monorepo', + 'namespace' : 'myodsproject-dev', + 'description' : 'Upgrade complete', + 'resourcesByKind': [ + 'Deployment': ['backend-helm-monorepo-chart-component-a', 'backend-helm-monorepo-chart-component-b'], + 'Service' : ['backend-helm-monorepo-chart'], + ], + 'version' : '14', + 'status' : 'deployed', + 'lastDeployed' : '2024-10-31T11:10:27.478860933Z', + ], + ], + 'backend-helm-monorepo-chart-component-b' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'backend-helm-monorepo-chart-component-b-567ff4f8f6', + 'podName' : 'backend-helm-monorepo-chart-component-b-567ff4f8f6-kvmqm', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers' : [ + 'chart-component-b': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-b@sha256:10002345abcde', + ], + ], + 'backend-helm-monorepo-chart-component-a' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'backend-helm-monorepo-chart-component-a-5ffd9c7cbd', + 'podName' : 'backend-helm-monorepo-chart-component-a-5ffd9c7cbd-h4wsb', + 'podMetaDataCreationTimestamp': '2024-10-31T11:10:28Z', + 'containers' : [ + 'chart-component-a': 'image-registry.openshift.svc:1000/myodsproject-dev/backend-helm-monorepo-component-a@sha256:10002345abcde', + ], + ], + ], + 'testResultsFolder' : 'build/test-results/test', + 'xunitTestResultsStashPath': 'test-reports-junit-xml-backend-helm-monorepo-19', + 'SCRR' : 'SCRR-myodsproject-backend-helm-monorepo.docx', + 'SCRR-MD' : 'SCRR-myodsproject-backend-helm-monorepo.md', + 'builds' : [ + 'backend-helm-monorepo-component-b': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/backend-helm-monorepo-component-b@sha256:10002345abcde', + 'buildId': 'backend-helm-monorepo-component-b-26', + ], + 'backend-helm-monorepo-component-a': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/backend-helm-monorepo-component-a@sha256:10002345abcde', + 'buildId': 'backend-helm-monorepo-component-a-26', + ], + ], + 'CREATED_BY_BUILD' : 'WIP/19', + 'sonarqubeScanStashPath' : 'scrr-report-backend-helm-monorepo-19', + ], + ], + 'doInstall' : true, + 'pipelineConfig': [ + 'dependencies': [], + ], + 'defaultBranch' : 'master', + 'id' : 'backend-helm-monorepo', + 'type' : 'ods', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-backend-helm-monorepo.git', + ] + } + + static Map createTIRDataTailor() { + [ + 'git' : [ + 'previousSucessfulCommit': null, + 'baseTag' : '', + 'commit' : 'a00012345bcdef', + 'previousCommit' : null, + 'targetTag' : '', + 'branch' : 'master', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-flask-backend.git', + ], + 'previousSucessfulCommit': 'c00012345bcdef', + 'documents' : [ + ], + 'openshift' : [ + 'testResults' : 1, + 'deployments' : [ + 'flask-backend' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'flask-backend-2', + 'podName' : 'flask-backend-2-plcgr', + 'podMetaDataCreationTimestamp': '2024-10-31T11:09:56Z', + 'containers' : [ + 'flask-backend': 'image-registry.openshift.svc:1000/myodsproject-dev/flask-backend@sha256:10002345abcde', + ], + ], + 'flask-backend-deploymentMean': [ + 'tailorParamFile': '', + 'tailorParams' : [], + 'selector' : 'app=myodsproject-flask-backend', + 'type' : 'tailor', + 'tailorSelectors': [ + 'selector': 'app=myodsproject-flask-backend', + 'exclude' : 'bc,is', + ], + 'tailorPreserve' : [], + 'tailorVerify' : true, + ], + ], + 'testResultsFolder' : 'build/test-results/test', + 'xunitTestResultsStashPath': 'test-reports-junit-xml-flask-backend-19', + 'SCRR' : 'SCRR-myodsproject-flask-backend.docx', + 'SCRR-MD' : 'SCRR-myodsproject-flask-backend.md', + 'builds' : [ + 'flask-backend': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/flask-backend@sha256:10002345abcde', + 'buildId': 'flask-backend-2', + ], + ], + 'CREATED_BY_BUILD' : 'WIP/19', + 'sonarqubeScanStashPath' : 'scrr-report-flask-backend-19', + ], + ] + } + + static Map createTIRRepoTailor() { + [ + 'include' : true, + 'metadata' : [ + 'supplier' : 'https://www.palletsprojects.com/p/flask/', + 'name' : 'Flask', + 'description': 'Flask is a micro web framework written in Python. Technologies: Flask 3.0.0, Python 3.11', + 'type' : 'ods', + 'version' : '4.x', + ], + 'data' : [ + 'git' : [ + 'previousSucessfulCommit': null, + 'baseTag' : '', + 'commit' : 'a00012345bcdef', + 'previousCommit' : null, + 'targetTag' : '', + 'branch' : 'master', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-flask-backend.git', + ], + 'previousSucessfulCommit': 'c00012345bcdef', + 'documents' : [ + ], + 'openshift' : [ + 'testResults' : 1, + 'deployments' : [ + 'flask-backend' : [ + 'podNamespace' : 'myodsproject-dev', + 'podStatus' : 'Running', + 'deploymentId' : 'flask-backend-2', + 'podName' : 'flask-backend-2-plcgr', + 'podMetaDataCreationTimestamp': '2024-10-31T11:09:56Z', + 'containers' : [ + 'flask-backend': 'image-registry.openshift.svc:1000/myodsproject-dev/flask-backend@sha256:10002345abcde', + ], + ], + 'flask-backend-deploymentMean': [ + 'tailorParamFile': 'a-param-file.yaml', + 'tailorParams' : ['fake-param1', 'fake-param2'], + 'selector' : 'app=myodsproject-flask-backend', + 'type' : 'tailor', + 'tailorSelectors': [ + 'selector': 'app=myodsproject-flask-backend', + 'exclude' : 'bc,is', + ], + 'tailorPreserve' : ['fake-preserve1', 'fake-preserve2'], + 'tailorVerify' : true, + ], + ], + 'testResultsFolder' : 'build/test-results/test', + 'xunitTestResultsStashPath': 'test-reports-junit-xml-flask-backend-19', + 'SCRR' : 'SCRR-myodsproject-flask-backend.docx', + 'SCRR-MD' : 'SCRR-myodsproject-flask-backend.md', + 'builds' : [ + 'flask-backend': [ + 'image' : 'image-registry.openshift.svc:1000/myodsproject-cd/flask-backend@sha256:10002345abcde', + 'buildId': 'flask-backend-2', + ], + ], + 'CREATED_BY_BUILD' : 'WIP/19', + 'sonarqubeScanStashPath' : 'scrr-report-flask-backend-19', + ], + ], + 'doInstall' : true, + 'pipelineConfig': [ + 'dependencies': [], + ], + 'defaultBranch' : 'master', + 'id' : 'flask-backend', + 'type' : 'ods', + 'url' : 'https://bitbucket-myodsproject-cd.ocp.mycompany.com/scm/myodsproject/myodsproject-flask-backend.git', + ] + } + + static Map createHelmCmdStatusMap() { + [ + 'info' : [ + 'deleted' : '', + 'description' : 'Upgrade complete', + 'first_deployed': '2022-12-19T09:44:32.164490076Z', + 'last_deployed' : '2024-03-04T15:21:09.34520527Z', + 'resources' : [ + 'v1/Cluster' : [[ + 'apiVersion': 'postgresql.k8s.k8db.io/v1', + 'kind' : 'Cluster', + 'metadata' : [ + 'annotations' : [ + 'meta.helm.sh/release-name' : 'standalone-app', + 'meta.helm.sh/release-namespace': 'myproject-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' : 'some-cluster', + 'app.kubernetes.io/version' : 'aaaabbbbcccc', + 'helm.sh/chart' : 'some-cluster-0.1.0_aaaabbbbcccc', + ], + 'name' : 'some-cluster', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2880969905', + 'uid' : '12345678-1234-1234-1234-200000000abcde', + ], + 'spec' : [ + 'affinity' : [ + 'podAntiAffinityType': 'preferred', + ], + 'bootstrap' : [ + 'initdb': [ + 'database' : 'app', + 'encoding' : 'UTF8', + 'localeCType' : 'C', + 'localeCollate': 'C', + 'owner' : 'app', + ], + ], + 'enableSuperuserAccess': true, + 'failoverDelay' : 0, + 'imageName' : 'quay.io/k8db/postgresql:x.x', + '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' : 'TLSvx.x', + 'ssl_min_protocol_version' : 'TLSvx.x', + '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' : 'some-cluster-ca', + 'expirations' : [ + 'some-cluster-ca' : '2024-08-29 14:02:22 +0000 UTC', + 'some-cluster-replication': '2024-08-29 14:02:22 +0000 UTC', + 'some-cluster-server' : '2024-08-29 14:02:22 +0000 UTC', + ], + 'replicationTLSSecret': 'some-cluster-replication', + 'serverAltDNSNames' : ['some-cluster-rw', 'some-cluster-rw.myproject-test', 'some-cluster-rw.myproject-test.svc', 'some-cluster-r', 'some-cluster-r.myproject-test', 'some-cluster-r.myproject-test.svc', 'some-cluster-ro', 'some-cluster-ro.myproject-test', 'some-cluster-ro.myproject-test.svc'], + 'serverCASecret' : 'some-cluster-ca', + 'serverTLSSecret' : 'some-cluster-server', + ], + 'cloudNativePostgresqlCommitHash' : '900010000', + 'cloudNativePostgresqlOperatorHash': '12345abcdef', + 'conditions' : [[ + 'lastTransitionTime': '2024-05-25T14:42:08Z', + 'message' : 'Cluster is Ready', + 'reason' : 'ClusterIsReady', + 'status' : 'True', + 'type' : 'Ready', + ], [ + 'lastTransitionTime': '2023-07-04T13:19:38Z', + 'message' : 'vlr addon is disabled', + 'reason' : 'Disabled', + 'status' : 'False', + 'type' : 'k8s.k8db.io/vlr', + ], [ + 'lastTransitionTime': '2023-07-04T13:19:38Z', + 'message' : 'external-backup-adapter addon is disabled', + 'reason' : 'Disabled', + 'status' : 'False', + 'type' : 'k8s.k8db.io/extBackpAdapt', + ], [ + 'lastTransitionTime': '2023-07-04T13:19:38Z', + 'message' : 'external-backup-adapter-cluster addon is disabled', + 'reason' : 'Disabled', + 'status' : 'False', + 'type' : 'k8s.k8db.io/extBackpAdaptCluster', + ], [ + 'lastTransitionTime': '2023-07-04T13:19:40Z', + 'message' : 'kstn addon is disabled', + 'reason' : 'Disabled', + 'status' : 'False', + 'type' : 'k8s.k8db.io/kstn', + ], [ + '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' : 'some-cluster-1', + 'currentPrimaryTimestamp' : '2023-07-04T13:19:27.039619Z', + 'healthyPVC' : ['some-cluster-1'], + 'instanceNames' : ['some-cluster-1'], + 'instances' : 1, + 'instancesReportedState' : [ + 'some-cluster-1': [ + 'isPrimary' : true, + 'timeLineID': 1, + ], + ], + 'instancesStatus' : [ + 'healthy': ['some-cluster-1'], + ], + 'latestGeneratedNode' : 1, + 'licenseStatus' : [ + 'licenseExpiration': '2999-12-31T00:00:00Z', + 'licenseStatus' : 'Valid license (My Company (my_company))', + 'repositoryAccess' : false, + 'valid' : true, + ], + 'managedRolesStatus' : [], + 'phase' : 'Cluster in healthy state', + 'poolerIntegrations' : [ + 'pgBouncerIntegration': [], + ], + 'pvcCount' : 1, + 'readService' : 'some-cluster-r', + 'readyInstances' : 1, + 'secretsResourceVersion' : [ + 'applicationSecretVersion': '2880969810', + 'clientCaSecretVersion' : '2880969811', + 'replicationSecretVersion': '2880969813', + 'serverCaSecretVersion' : '2880969811', + 'serverSecretVersion' : '2880969815', + 'superuserSecretVersion' : '2880969816', + ], + 'targetPrimary' : 'some-cluster-1', + 'targetPrimaryTimestamp' : '2023-07-04T13:18:29.516149Z', + 'timelineID' : 1, + 'topology' : [ + 'instances' : [ + 'some-cluster-1': [], + ], + 'nodesUsed' : 1, + 'successfullyExtracted': true, + ], + 'writeService' : 'some-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': 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core-appconfig-configmap', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2880955101', + 'uid' : '12345678-1234-1234-1234-600000000abcde', + ], + ]], + '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' : 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2865328801', + 'uid' : '12345678-1234-1234-1234-100000000abcde', + ], + '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' : 'cf012345cf', + 'checksum/rsa-key-secret' : '57a57a57a57a', + 'checksum/security-exandradev-secret': 'abcdef12345', + 'checksum/security-unify-secret' : '1a2b3c4d', + ], + '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': 'some-cluster-app', + ], + ], + ], [ + 'name' : 'DB_PASSWORD', + 'valueFrom': [ + 'secretKeyRef': [ + 'key' : 'password', + 'name': 'some-cluster-app', + ], + ], + ]], + 'image' : 'image-registry.openshift.svc:1000/myproject-test/core-standalone:ea01234567', + '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-f7f7f7f7\" 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' : 'myproject-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' : 'myproject-test', + 'resourceVersion' : '2865332166', + 'uid' : '12345678-1234-1234-1234-220000000abcde', + ], + '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.svc:1000/myproject-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-500000000c\" 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' : 'cf012345cf', + 'checksum/rsa-key-secret' : '57a57a57a57a', + 'checksum/security-exandradev-secret' : 'abcdef12345', + 'checksum/security-unify-secret' : '1a2b3c4d', + 'k8s.ovn.org/pod-networks' : '{\"default\":{\"ip_addresses\":[\"10.200.10.50/24\"],\"mac_address\":\"0a:00:00:00:00:0a\",\"gateway_ips\":[\"10.200.10.1\"],\"routes\":[{\"dest\":\"10.200.0.0/16\",\"nextHop\":\"10.200.10.1\"},{\"dest\":\"170.30.0.0/16\",\"nextHop\":\"10.200.10.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.200.10.1\"}],\"ip_address\":\"10.200.10.50/24\",\"gateway_ip\":\"10.200.10.1\"}}', + 'k8s.v1.cni.cncf.io/network-status' : '[{\n \"name\": \"ovn-kubernetes\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.200.10.50\"\n ],\n \"mac\": \"0a:00:00:00:00:0a\",\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-f7f7f7f7-', + 'labels' : [ + 'app.kubernetes.io/instance': 'standalone-app', + 'app.kubernetes.io/name' : 'core', + 'pod-template-hash' : 'f7f7f7f7', + ], + 'name' : 'core-f7f7f7f7-8abcx', + 'namespace' : 'myproject-test', + 'ownerReferences' : [[ + 'apiVersion' : 'apps/v1', + 'blockOwnerDeletion': true, + 'controller' : true, + 'kind' : 'ReplicaSet', + 'name' : 'core-f7f7f7f7', + 'uid' : '12345678-1234-1234-1234-900000000abcde', + ]], + 'resourceVersion' : '2865328796', + 'uid' : '12345678-1234-1234-1234-400000000abcde', + ], + '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': 'some-cluster-app', + ], + ], + ], [ + 'name' : 'DB_PASSWORD', + 'valueFrom': [ + 'secretKeyRef': [ + 'key' : 'password', + 'name': 'some-cluster-app', + ], + ], + ]], + 'image' : 'image-registry.openshift.svc:1000/myproject-test/core-standalone:ea01234567', + '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/secretaccount', + 'name' : 'kube-api-access-lkjhg', + 'readOnly' : true, + ]], + ]], + 'dnsPolicy' : 'ClusterFirst', + 'enableServiceLinks' : true, + 'imagePullSecrets' : [[ + 'name': 'default-dockercfg-xasdf', + ]], + 'nodeName' : 'ip-10.8.30.200.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-lkjhg', + 'projected': [ + 'defaultMode': 420, + 'sources' : [[ + 'serviceAccountToken': [ + 'expirationSeconds': 3607, + 'path' : 'token', + ], + ], [ + 'configMap': [ + 'items': [[ + 'key' : 'ca.crt', + 'path': 'ca.crt', + ]], + 'name' : 'kube-some-ca.crt', + ], + ], [ + 'downwardAPI': [ + 'items': [[ + 'fieldRef': [ + 'apiVersion': 'v1', + 'fieldPath' : 'metadata.namespace', + ], + 'path' : 'namespace', + ]], + ], + ], [ + 'configMap': [ + 'items': [[ + 'key' : 'service-ca.crt', + 'path': 'service-ca.crt', + ]], + 'name' : 'openshift-some-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://475000574', + 'image' : 'image-registry.openshift.svc:1000/myproject-test/core-standalone:ea01234567', + 'imageID' : 'image-registry.openshift.svc:1000/myproject-test/core-standalone@sha256:6a000a6', + 'lastState' : [], + 'name' : 'core', + 'ready' : true, + 'restartCount': 0, + 'started' : true, + 'state' : [ + 'running': [ + 'startedAt': '2024-05-25T13:42:52Z', + ], + ], + ]], + 'hostIP' : '10.8.30.200', + 'phase' : 'Running', + 'podIP' : '10.200.10.50', + 'podIPs' : [[ + 'ip': '10.200.10.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\":\"0c:00:00:00:00:0c\",\"gateway_ips\":[\"10.200.10.1\"],\"routes\":[{\"dest\":\"10.200.0.0/16\",\"nextHop\":\"10.200.10.1\"},{\"dest\":\"170.30.0.0/16\",\"nextHop\":\"10.200.10.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.200.10.1\"}],\"ip_address\":\"10.251.18.51/24\",\"gateway_ip\":\"10.200.10.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\": \"0c:00:00:00:00:0c\",\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-500000000c-', + 'labels' : [ + 'app.kubernetes.io/instance': 'standalone-app', + 'app.kubernetes.io/name' : 'standalone-gateway', + 'pod-template-hash' : '500000000c', + ], + 'name' : 'standalone-gateway-500000000c-6h0h6', + 'namespace' : 'myproject-test', + 'ownerReferences' : [[ + 'apiVersion' : 'apps/v1', + 'blockOwnerDeletion': true, + 'controller' : true, + 'kind' : 'ReplicaSet', + 'name' : 'standalone-gateway-500000000c', + 'uid' : '12345678-1234-1234-1234-700000000abcde', + ]], + 'resourceVersion' : '2865332161', + 'uid' : '12345678-1234-1234-1234-110000000abcde', + ], + 'spec' : [ + 'containers' : [[ + 'image' : 'image-registry.openshift.svc:1000/myproject-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/secretaccount', + 'name' : 'kube-api-access-zxcvb', + 'readOnly' : true, + ]], + ]], + 'dnsPolicy' : 'ClusterFirst', + 'enableServiceLinks' : true, + 'imagePullSecrets' : [[ + 'name': 'default-dockercfg-xasdf', + ]], + 'nodeName' : 'ip-10.8.30.200.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-zxcvb', + 'projected': [ + 'defaultMode': 420, + 'sources' : [[ + 'serviceAccountToken': [ + 'expirationSeconds': 3607, + 'path' : 'token', + ], + ], [ + 'configMap': [ + 'items': [[ + 'key' : 'ca.crt', + 'path': 'ca.crt', + ]], + 'name' : 'kube-some-ca.crt', + ], + ], [ + 'downwardAPI': [ + 'items': [[ + 'fieldRef': [ + 'apiVersion': 'v1', + 'fieldPath' : 'metadata.namespace', + ], + 'path' : 'namespace', + ]], + ], + ], [ + 'configMap': [ + 'items': [[ + 'key' : 'service-ca.crt', + 'path': 'service-ca.crt', + ]], + 'name' : 'openshift-some-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://14b000b41', + 'image' : 'image-registry.openshift.svc:1000/myproject-test/standalone-gateway:7b5e50e13fd78502967881f4970484ae08b76dc4', + 'imageID' : 'image-registry.openshift.svc:1000/myproject-test/standalone-gateway@sha256:c30003c', + 'lastState' : [], + 'name' : 'standalone-gateway', + 'ready' : true, + 'restartCount': 0, + 'started' : true, + 'state' : [ + 'running': [ + 'startedAt': '2024-05-25T13:43:50Z', + ], + ], + ]], + 'hostIP' : '10.8.30.200', + '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': 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core-rsa-key-secret', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2880969794', + 'uid' : '12345678-1234-1234-1234-300000000abcde', + ], + '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': 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core-security-exandradev-secret', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2880969795', + 'uid' : '12345678-1234-1234-1234-500000000abcde', + ], + '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': 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core-security-unify-secret', + 'namespace' : 'myproject-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': 'myproject-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' : 'ea01234567', + 'helm.sh/chart' : 'core-0.1.0_ea01234567', + ], + 'name' : 'core', + 'namespace' : 'myproject-test', + 'resourceVersion' : '2687980260', + 'uid' : '12345678-1234-1234-1234-123456789abcde', + ], + 'spec' : [ + 'clusterIP' : '100.30.20.100', + 'clusterIPs' : ['100.30.20.100'], + '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': 'myproject-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' : 'myproject-test', + 'resourceVersion' : '2497441712', + 'uid' : '12345678-1234-1234-1234-800000000abcde', + ], + 'spec' : [ + 'clusterIP' : '100.30.100.70', + 'clusterIPs' : ['100.30.100.70'], + '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': [], + ], + ]], + ], + 'status' : 'deployed', + ], + 'manifest' : 'REDACTED\n', + 'name' : 'standalone-app', + 'namespace': 'myproject-test', + 'version' : 43, + ] + } + static Map createProjectMetadata() { def file = new FixtureHelper().getResource("project-metadata.yml") return new Yaml().load(file.text) diff --git a/test/groovy/util/HelmStatusSpec.groovy b/test/groovy/util/HelmStatusSpec.groovy new file mode 100644 index 000000000..de80f5de5 --- /dev/null +++ b/test/groovy/util/HelmStatusSpec.groovy @@ -0,0 +1,43 @@ +package org.ods.util + + +import org.ods.services.OpenShiftService +import util.FixtureHelper +import util.SpecHelper + +class HelmStatusSpec extends SpecHelper { + def "helm status parsing"() { + given: + def helmStatusJsonObj = FixtureHelper.createHelmCmdStatusMap() + + when: + def helmStatus = HelmStatus.fromJsonObject(helmStatusJsonObj) + def simpleStatusMap = helmStatus.toMap() + def simpleStatusNoResources = simpleStatusMap.findAll { k,v -> k != "resourcesByKind"} + def helmStatusResources = helmStatus.getResources() + def deploymentResources = helmStatusResources.subMap([ + OpenShiftService.DEPLOYMENT_KIND, OpenShiftService.DEPLOYMENTCONFIG_KIND]) + + then: + simpleStatusNoResources == [ + name: 'standalone-app', + version: '43', + namespace: 'myproject-test', + status: 'deployed', + description: 'Upgrade complete', + lastDeployed: '2024-03-04T15:21:09.34520527Z' + ] + + simpleStatusMap.resourcesByKind == [ + 'Cluster': ['some-cluster'], + 'ConfigMap': ['core-appconfig-configmap'], + 'Deployment': ['core', 'standalone-gateway'], + 'Secret': ['core-rsa-key-secret', 'core-security-exandradev-secret', 'core-security-unify-secret'], + 'Service': ['core', 'standalone-gateway'], + ] + + deploymentResources == [ + Deployment: [ 'core', 'standalone-gateway'] + ] + } +} diff --git a/test/groovy/util/OpenShiftHelper.groovy b/test/groovy/util/OpenShiftHelper.groovy index 748acb3db..a468835bf 100644 --- a/test/groovy/util/OpenShiftHelper.groovy +++ b/test/groovy/util/OpenShiftHelper.groovy @@ -2,7 +2,15 @@ package util import groovy.json.JsonSlurper +import static org.ods.services.OpenShiftService.DEPLOYMENTCONFIG_KIND +import static org.ods.services.OpenShiftService.DEPLOYMENT_KIND + class OpenShiftHelper { + + static isDeploymentKind(String kind) { + return kind in [DEPLOYMENTCONFIG_KIND, DEPLOYMENT_KIND] + } + static def validateResourceParams(script, project, resources) { if (project) { assert script =~ /\s-n\s+\Q${project}\E(?:\s|$)/ diff --git a/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy b/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy index 9bffdc955..d3a39bed2 100644 --- a/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy +++ b/test/groovy/vars/OdsComponentStageRolloutOpenShiftDeploymentSpec.groovy @@ -1,16 +1,20 @@ package vars + 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.OpenShiftService import org.ods.services.ServiceRegistry +import org.ods.util.HelmStatus import org.ods.util.Logger import org.ods.util.PodData +import spock.lang.Shared +import spock.lang.Unroll +import util.FixtureHelper import util.PipelineSteps import vars.test_helper.PipelineSpockTestBase -import spock.lang.* class OdsComponentStageRolloutOpenShiftDeploymentSpec extends PipelineSpockTestBase { @@ -149,15 +153,21 @@ 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: 'myproject', + componentId: 'core', + environment: 'dev', + targetProject: 'myproject-dev', + openshiftRolloutTimeoutRetries: 5, + chartDir: 'chart'] + 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('myproject-dev', 'backend-helm-monorepo') >> HelmStatus.fromJsonObject(FixtureHelper.createHelmCmdStatusMap()) + // 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 +202,21 @@ class OdsComponentStageRolloutOpenShiftDeploymentSpec extends PipelineSpockTestB return metadata } } - def deploymentInfo = script.call(context) + def deploymentInfo = script.call(context, [ + helmReleaseName: "backend-helm-monorepo", + ]) 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('myproject-dev', 'backend-helm-monorepo', ['values.yaml'], ['registry':null, 'componentId':'core', 'global.registry':null, 'global.componentId':'core', 'imageNamespace':'myproject-dev', 'imageTag':'cd3e9082', 'global.imageNamespace':'myproject-dev', 'global.imageTag':'cd3e9082'], ['--install', '--atomic'], [], true) } @Unroll diff --git a/test/resources/helmstatus.json b/test/resources/helmstatus.json new file mode 100644 index 000000000..132a64c81 --- /dev/null +++ b/test/resources/helmstatus.json @@ -0,0 +1,1450 @@ +{ + "name": "backend-helm-monorepo", + "info": { + "first_deployed": "2024-11-08T14:37:41.509186505Z", + "last_deployed": "2024-11-11T16:01:03.800460171Z", + "deleted": "", + "description": "Upgrade complete", + "status": "deployed", + "notes": "1. Get the application URL by running these commands:\n export POD_NAME=$(kubectl get pods --namespace myproject-dev -l \"app.kubernetes.io/name=chart,app.kubernetes.io/instance=backend-helm-monorepo\" -o jsonpath=\"{.items[0].metadata.name}\")\n export CONTAINER_PORT=$(kubectl get pod --namespace myproject-dev $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n echo \"Visit http://127.0.0.1:8080 to use your application\"\n kubectl --namespace myproject-dev port-forward $POD_NAME 8080:$CONTAINER_PORT\n", + "resources": { + "v1/Deployment": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "3", + "meta.helm.sh/release-name": "backend-helm-monorepo", + "meta.helm.sh/release-namespace": "myproject-dev" + }, + "creationTimestamp": "2024-11-08T14:37:41Z", + "generation": 3, + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "chart", + "app.kubernetes.io/version": "1.16.0", + "example.com/component": "a", + "helm.sh/chart": "chart-0.1.0" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:meta.helm.sh/release-name": {}, + "f:meta.helm.sh/release-namespace": {} + }, + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/managed-by": {}, + "f:app.kubernetes.io/name": {}, + "f:app.kubernetes.io/version": {}, + "f:example.com/component": {}, + "f:helm.sh/chart": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": {}, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/name": {}, + "f:example.com/component": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"chart-component-a\"}": { + ".": {}, + "f:env": { + ".": {}, + "k:{\"name\":\"APP_LISTEN_PORT\"}": { + ".": {}, + "f:name": {}, + "f:value": {} + } + }, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:name": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:securityContext": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:serviceAccount": {}, + "f:serviceAccountName": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "helm", + "operation": "Update", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:06Z" + } + ], + "name": "backend-helm-monorepo-chart-component-a", + "namespace": "myproject-dev", + "resourceVersion": "4223533956", + "uid": "5a3aa4dc-e990-4ef8-81f4-947262af7617" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "a" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "a" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "APP_LISTEN_PORT", + "value": "8080" + } + ], + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-a:f6db0fd2", + "imagePullPolicy": "IfNotPresent", + "name": "chart-component-a", + "ports": [ + { + "containerPort": 80, + "name": "http", + "protocol": "TCP" + } + ], + "resources": {}, + "securityContext": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2024-11-08T14:37:44Z", + "lastUpdateTime": "2024-11-08T14:37:44Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + }, + { + "lastTransitionTime": "2024-11-08T14:37:41Z", + "lastUpdateTime": "2024-11-11T16:01:06Z", + "message": "ReplicaSet \"backend-helm-monorepo-chart-component-a-6467d6c55d\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + } + ], + "observedGeneration": 3, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "3", + "meta.helm.sh/release-name": "backend-helm-monorepo", + "meta.helm.sh/release-namespace": "myproject-dev" + }, + "creationTimestamp": "2024-11-08T14:37:41Z", + "generation": 3, + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "chart", + "app.kubernetes.io/version": "1.16.0", + "example.com/component": "b", + "helm.sh/chart": "chart-0.1.0" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:meta.helm.sh/release-name": {}, + "f:meta.helm.sh/release-namespace": {} + }, + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/managed-by": {}, + "f:app.kubernetes.io/name": {}, + "f:app.kubernetes.io/version": {}, + "f:example.com/component": {}, + "f:helm.sh/chart": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": {}, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/name": {}, + "f:example.com/component": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"chart-component-b\"}": { + ".": {}, + "f:env": { + ".": {}, + "k:{\"name\":\"APP_LISTEN_PORT\"}": { + ".": {}, + "f:name": {}, + "f:value": {} + } + }, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:name": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:securityContext": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:serviceAccount": {}, + "f:serviceAccountName": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "helm", + "operation": "Update", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:05Z" + } + ], + "name": "backend-helm-monorepo-chart-component-b", + "namespace": "myproject-dev", + "resourceVersion": "4223533904", + "uid": "76db28c3-dbb5-4626-9e1d-30d496662220" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "b" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "b" + } + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "APP_LISTEN_PORT", + "value": "8081" + } + ], + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-b:f6db0fd2", + "imagePullPolicy": "IfNotPresent", + "name": "chart-component-b", + "ports": [ + { + "containerPort": 80, + "name": "http", + "protocol": "TCP" + } + ], + "resources": {}, + "securityContext": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2024-11-08T14:37:43Z", + "lastUpdateTime": "2024-11-08T14:37:43Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + }, + { + "lastTransitionTime": "2024-11-08T14:37:41Z", + "lastUpdateTime": "2024-11-11T16:01:05Z", + "message": "ReplicaSet \"backend-helm-monorepo-chart-component-b-7d9d7456b\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + } + ], + "observedGeneration": 3, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } + } + ], + "v1/Pod(related)": [ + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "k8s.ovn.org/pod-networks": "{\"default\":{\"ip_addresses\":[\"10.251.44.130/24\"],\"mac_address\":\"0a:58:0a:fb:2c:82\",\"gateway_ips\":[\"10.251.44.1\"],\"routes\":[{\"dest\":\"10.200.0.0/16\",\"nextHop\":\"10.251.44.1\"},{\"dest\":\"170.30.0.0/16\",\"nextHop\":\"10.251.44.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.251.44.1\"}],\"ip_address\":\"10.251.44.130/24\",\"gateway_ip\":\"10.251.44.1\"}}", + "k8s.v1.cni.cncf.io/network-status": "[{\n \"name\": \"ovn-kubernetes\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.251.44.130\"\n ],\n \"mac\": \"0a:58:0a:fb:2c:82\",\n \"default\": true,\n \"dns\": {}\n}]", + "kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request for container chart-component-a; cpu, memory limit for container chart-component-a", + "openshift.io/scc": "restricted-v2", + "seccomp.security.alpha.kubernetes.io/pod": "runtime/default" + }, + "creationTimestamp": "2024-11-11T16:01:04Z", + "generateName": "backend-helm-monorepo-chart-component-a-6467d6c55d-", + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "a", + "pod-template-hash": "6467d6c55d" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:k8s.ovn.org/pod-networks": {} + } + } + }, + "manager": "ip-10-8-32-56", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/name": {}, + "f:example.com/component": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"458e87be-1cd1-4522-bd68-4b92e0ff261d\"}": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"chart-component-a\"}": { + ".": {}, + "f:env": { + ".": {}, + "k:{\"name\":\"APP_LISTEN_PORT\"}": { + ".": {}, + "f:name": {}, + "f:value": {} + } + }, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:name": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:securityContext": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:serviceAccount": {}, + "f:serviceAccountName": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:k8s.v1.cni.cncf.io/network-status": {} + } + } + }, + "manager": "multus-daemon", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:05Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"10.251.44.130\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:06Z" + } + ], + "name": "backend-helm-monorepo-chart-component-a-6467d6c55d-nvdmb", + "namespace": "myproject-dev", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "backend-helm-monorepo-chart-component-a-6467d6c55d", + "uid": "458e87be-1cd1-4522-bd68-4b92e0ff261d" + } + ], + "resourceVersion": "4223533939", + "uid": "0a376dc6-6949-4bca-b568-2c0d5033c687" + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "APP_LISTEN_PORT", + "value": "8080" + } + ], + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-a:f6db0fd2", + "imagePullPolicy": "IfNotPresent", + "name": "chart-component-a", + "ports": [ + { + "containerPort": 80, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi" + }, + "requests": { + "cpu": "10m", + "memory": "10Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "runAsNonRoot": true, + "runAsUser": 1005790000 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/secretaccount", + "name": "kube-api-access-4xzkq", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-zxnp9" + } + ], + "nodeName": "ip-10-8-32-56.aws.mycompany.com", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1005790000, + "seLinuxOptions": { + "level": "s0:c76,c45" + }, + "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-4xzkq", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-some-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + }, + { + "configMap": { + "items": [ + { + "key": "service-ca.crt", + "path": "service-ca.crt" + } + ], + "name": "openshift-some-ca.crt" + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:04Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:06Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:06Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:04Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "cri-o://c1aa57c3c70ecea0feb2b56aa529a89b136819cee1f9eff69feb0861af729f08", + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-a:f6db0fd2", + "imageID": "image-registry.openshift.svc:1000/myproject-dev/helm-component-a@sha256:4cb63016037873c1a7c03482512caebf6567180aaa18b92670ce527b32ac4e5b", + "lastState": {}, + "name": "chart-component-a", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-11-11T16:01:05Z" + } + } + } + ], + "hostIP": "10.8.32.56", + "phase": "Running", + "podIP": "10.251.44.130", + "podIPs": [ + { + "ip": "10.251.44.130" + } + ], + "qosClass": "Burstable", + "startTime": "2024-11-11T16:01:04Z" + } + } + ], + "kind": "PodList", + "metadata": { + "resourceVersion": "4223534074" + } + }, + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "k8s.ovn.org/pod-networks": "{\"default\":{\"ip_addresses\":[\"10.251.46.148/24\"],\"mac_address\":\"0a:58:0a:fb:2e:94\",\"gateway_ips\":[\"10.251.46.1\"],\"routes\":[{\"dest\":\"10.200.0.0/16\",\"nextHop\":\"10.251.46.1\"},{\"dest\":\"170.30.0.0/16\",\"nextHop\":\"10.251.46.1\"},{\"dest\":\"100.64.0.0/16\",\"nextHop\":\"10.251.46.1\"}],\"ip_address\":\"10.251.46.148/24\",\"gateway_ip\":\"10.251.46.1\"}}", + "k8s.v1.cni.cncf.io/network-status": "[{\n \"name\": \"ovn-kubernetes\",\n \"interface\": \"eth0\",\n \"ips\": [\n \"10.251.46.148\"\n ],\n \"mac\": \"0a:58:0a:fb:2e:94\",\n \"default\": true,\n \"dns\": {}\n}]", + "kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request for container chart-component-b; cpu, memory limit for container chart-component-b", + "openshift.io/scc": "restricted-v2", + "seccomp.security.alpha.kubernetes.io/pod": "runtime/default" + }, + "creationTimestamp": "2024-11-11T16:01:04Z", + "generateName": "backend-helm-monorepo-chart-component-b-7d9d7456b-", + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart", + "example.com/component": "b", + "pod-template-hash": "7d9d7456b" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:k8s.ovn.org/pod-networks": {} + } + } + }, + "manager": "ip-10-8-34-157", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/name": {}, + "f:example.com/component": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"130fca64-a8b2-4ec1-8c3f-b2e98435813d\"}": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"chart-component-b\"}": { + ".": {}, + "f:env": { + ".": {}, + "k:{\"name\":\"APP_LISTEN_PORT\"}": { + ".": {}, + "f:name": {}, + "f:value": {} + } + }, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:name": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:securityContext": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:serviceAccount": {}, + "f:serviceAccountName": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2024-11-11T16:01:04Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"10.251.46.148\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:05Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:k8s.v1.cni.cncf.io/network-status": {} + } + } + }, + "manager": "multus-daemon", + "operation": "Update", + "subresource": "status", + "time": "2024-11-11T16:01:05Z" + } + ], + "name": "backend-helm-monorepo-chart-component-b-7d9d7456b-bfqh8", + "namespace": "myproject-dev", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "backend-helm-monorepo-chart-component-b-7d9d7456b", + "uid": "130fca64-a8b2-4ec1-8c3f-b2e98435813d" + } + ], + "resourceVersion": "4223533889", + "uid": "fe85cef7-2553-4db9-8ce8-2f36c0243e6f" + }, + "spec": { + "containers": [ + { + "env": [ + { + "name": "APP_LISTEN_PORT", + "value": "8081" + } + ], + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-b:f6db0fd2", + "imagePullPolicy": "IfNotPresent", + "name": "chart-component-b", + "ports": [ + { + "containerPort": 80, + "name": "http", + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "1", + "memory": "1Gi" + }, + "requests": { + "cpu": "10m", + "memory": "10Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": [ + "ALL" + ] + }, + "runAsNonRoot": true, + "runAsUser": 1005790000 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/secretaccount", + "name": "kube-api-access-2nkvg", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "imagePullSecrets": [ + { + "name": "default-dockercfg-zxnp9" + } + ], + "nodeName": "ip-10-8-34-157.aws.mycompany.com", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 1005790000, + "seLinuxOptions": { + "level": "s0:c76,c45" + }, + "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-2nkvg", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-some-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + }, + { + "configMap": { + "items": [ + { + "key": "service-ca.crt", + "path": "service-ca.crt" + } + ], + "name": "openshift-some-ca.crt" + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:04Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:05Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:05Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-11-11T16:01:04Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "cri-o://1c4c3c777865130a3d5f6f73d9f8e09de0dcbb2d320895e7caa219cd16674895", + "image": "image-registry.openshift.svc:1000/myproject-dev/helm-component-b:f6db0fd2", + "imageID": "image-registry.openshift.svc:1000/myproject-dev/helm-component-b@sha256:90847a3958af6a7fef1763130f196418704a8e7b8ca44f83527291989111acbc", + "lastState": {}, + "name": "chart-component-b", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-11-11T16:01:05Z" + } + } + } + ], + "hostIP": "10.8.34.157", + "phase": "Running", + "podIP": "10.251.46.148", + "podIPs": [ + { + "ip": "10.251.46.148" + } + ], + "qosClass": "Burstable", + "startTime": "2024-11-11T16:01:04Z" + } + } + ], + "kind": "PodList", + "metadata": { + "resourceVersion": "4223534077" + } + } + ], + "v1/Service": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "meta.helm.sh/release-name": "backend-helm-monorepo", + "meta.helm.sh/release-namespace": "myproject-dev" + }, + "creationTimestamp": "2024-11-08T14:37:41Z", + "labels": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/managed-by": "Helm", + "app.kubernetes.io/name": "chart", + "app.kubernetes.io/version": "1.16.0", + "helm.sh/chart": "chart-0.1.0" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:meta.helm.sh/release-name": {}, + "f:meta.helm.sh/release-namespace": {} + }, + "f:labels": { + ".": {}, + "f:app.kubernetes.io/instance": {}, + "f:app.kubernetes.io/managed-by": {}, + "f:app.kubernetes.io/name": {}, + "f:app.kubernetes.io/version": {}, + "f:helm.sh/chart": {} + } + }, + "f:spec": { + "f:internalTrafficPolicy": {}, + "f:ports": { + ".": {}, + "k:{\"port\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:name": {}, + "f:port": {}, + "f:protocol": {}, + "f:targetPort": {} + } + }, + "f:selector": {}, + "f:sessionAffinity": {}, + "f:type": {} + } + }, + "manager": "helm", + "operation": "Update", + "time": "2024-11-08T14:37:41Z" + } + ], + "name": "backend-helm-monorepo-chart", + "namespace": "myproject-dev", + "resourceVersion": "4213346120", + "uid": "db6377ce-870f-44c4-a96d-93a608e3e4c4" + }, + "spec": { + "clusterIP": "172.30.109.108", + "clusterIPs": [ + "172.30.109.108" + ], + "internalTrafficPolicy": "Cluster", + "ipFamilies": [ + "IPv4" + ], + "ipFamilyPolicy": "SingleStack", + "ports": [ + { + "name": "http", + "port": 80, + "protocol": "TCP", + "targetPort": "http" + } + ], + "selector": { + "app.kubernetes.io/instance": "backend-helm-monorepo", + "app.kubernetes.io/name": "chart" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + } + ] + } + }, + "config": { + "affinity": {}, + "autoscaling": { + "enabled": false, + "maxReplicas": 100, + "minReplicas": 1, + "targetCPUUtilizationPercentage": 80 + }, + "componentId": "backend-helm-monorepo", + "fullnameOverride": "", + "global": { + "componentId": "backend-helm-monorepo", + "imageNamespace": "myproject-dev", + "imageTag": "f6db0fd2", + "registry": "image-registry.openshift.svc:1000" + }, + "imageNamespace": "myproject-dev", + "imagePullPolicy": "IfNotPresent", + "imagePullSecrets": [], + "imageTag": "f6db0fd2", + "ingress": { + "annotations": {}, + "className": "", + "enabled": false, + "hosts": [ + { + "host": "chart-example.local", + "paths": [ + { + "path": "/", + "pathType": "ImplementationSpecific" + } + ] + } + ], + "tls": [] + }, + "livenessProbe": { + "enabled": false + }, + "nameOverride": "", + "nodeSelector": {}, + "podAnnotations": {}, + "podSecurityContext": {}, + "readinessProbe": { + "enabled": false + }, + "registry": "image-registry.openshift.svc:1000", + "replicaCount": 1, + "resources": {}, + "securityContext": {}, + "service": { + "port": 80, + "type": "ClusterIP" + }, + "serviceAccount": { + "annotations": {}, + "create": false, + "name": "" + }, + "tolerations": [] + }, + "manifest": "---\n# Source: chart/templates/service.yaml\napiVersion: v1\nkind: Service\nmetadata:\n name: backend-helm-monorepo-chart\n labels:\n helm.sh/chart: chart-0.1.0\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n app.kubernetes.io/version: \"1.16.0\"\n app.kubernetes.io/managed-by: Helm\nspec:\n type: ClusterIP\n ports:\n - port: 80\n targetPort: http\n protocol: TCP\n name: http\n selector:\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n---\n# Source: chart/templates/deployment_component-a.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: \"backend-helm-monorepo-chart-component-a\"\n labels:\n helm.sh/chart: chart-0.1.0\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n app.kubernetes.io/version: \"1.16.0\"\n app.kubernetes.io/managed-by: Helm\n example.com/component: a\nspec:\n replicas: 1\n selector:\n matchLabels:\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n example.com/component: a\n template:\n metadata:\n labels:\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n example.com/component: a\n spec:\n serviceAccountName: default\n securityContext:\n {}\n containers:\n - name: \"chart-component-a\"\n securityContext:\n {}\n image: \"image-registry.openshift.svc:1000/myproject-dev/helm-component-a:f6db0fd2\"\n imagePullPolicy: IfNotPresent\n env:\n - name: APP_LISTEN_PORT\n value: \"8080\"\n ports:\n - name: http\n containerPort: 80\n protocol: TCP\n resources:\n {}\n---\n# Source: chart/templates/deployment_component-b.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: \"backend-helm-monorepo-chart-component-b\"\n labels:\n helm.sh/chart: chart-0.1.0\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n app.kubernetes.io/version: \"1.16.0\"\n app.kubernetes.io/managed-by: Helm\n example.com/component: b\nspec:\n replicas: 1\n selector:\n matchLabels:\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n example.com/component: b\n template:\n metadata:\n labels:\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n example.com/component: b\n spec:\n serviceAccountName: default\n securityContext:\n {}\n containers:\n - name: \"chart-component-b\"\n securityContext:\n {}\n image: \"image-registry.openshift.svc:1000/myproject-dev/helm-component-b:f6db0fd2\"\n imagePullPolicy: IfNotPresent\n env:\n - name: APP_LISTEN_PORT\n value: \"8081\"\n ports:\n - name: http\n containerPort: 80\n protocol: TCP\n resources:\n {}\n", + "hooks": [ + { + "name": "backend-helm-monorepo-chart-test-connection", + "kind": "Pod", + "path": "chart/templates/tests/test-connection.yaml", + "manifest": "apiVersion: v1\nkind: Pod\nmetadata:\n name: \"backend-helm-monorepo-chart-test-connection\"\n labels:\n helm.sh/chart: chart-0.1.0\n app.kubernetes.io/name: chart\n app.kubernetes.io/instance: backend-helm-monorepo\n app.kubernetes.io/version: \"1.16.0\"\n app.kubernetes.io/managed-by: Helm\n annotations:\n \"helm.sh/hook\": test\nspec:\n containers:\n - name: wget\n image: busybox\n command: ['wget']\n args: ['backend-helm-monorepo-chart:80']\n restartPolicy: Never", + "events": [ + "test" + ], + "last_run": { + "started_at": "", + "completed_at": "", + "phase": "" + } + } + ], + "version": 4, + "namespace": "myproject-dev" +}