diff --git a/.pipeline/build.js b/.pipeline/build.js index 3ac899f8..c20aa94c 100644 --- a/.pipeline/build.js +++ b/.pipeline/build.js @@ -2,4 +2,7 @@ const task = require('./lib/build.js') const settings = require('./lib/config.js') -task(Object.assign(settings, { phase: 'build'})) +Promise.resolve(task(Object.assign(settings, { phase: 'build' }))).catch((error) => { + console.error(error); + process.exit(1); +}) diff --git a/.pipeline/deploy.js b/.pipeline/deploy.js index 59550945..37992788 100644 --- a/.pipeline/deploy.js +++ b/.pipeline/deploy.js @@ -2,4 +2,7 @@ const settings = require('./lib/config.js') const task = require('./lib/deploy.js') -task(Object.assign(settings, { phase: settings.options.env})); +Promise.resolve(task(Object.assign(settings, { phase: settings.options.env }))).catch((error) => { + console.error(error); + process.exit(1); +}) diff --git a/.pipeline/lib/build.js b/.pipeline/lib/build.js index 7141f95b..c3d20046 100644 --- a/.pipeline/lib/build.js +++ b/.pipeline/lib/build.js @@ -1,11 +1,11 @@ "use strict"; -const { OpenShiftClientX } = require("@bcgov/pipeline-cli"); const path = require("path"); +const { HmcrOpenShiftClientX } = require("./openshift-client"); module.exports = (settings) => { const phases = settings.phases; const options = settings.options; - const oc = new OpenShiftClientX( + const oc = new HmcrOpenShiftClientX( Object.assign({ namespace: phases.build.namespace }, options) ); const phase = "build"; @@ -70,5 +70,5 @@ module.exports = (settings) => { phases[phase].changeId, phases[phase].instance ); - oc.applyAndBuild(objects); + return oc.applyAndBuild(objects); }; diff --git a/.pipeline/lib/deploy.js b/.pipeline/lib/deploy.js index a80cc0b3..4ba9679f 100644 --- a/.pipeline/lib/deploy.js +++ b/.pipeline/lib/deploy.js @@ -1,10 +1,49 @@ "use strict"; -const { OpenShiftClientX } = require("@bcgov/pipeline-cli"); const path = require("path"); +const buildTask = require("./build"); +const { HmcrOpenShiftClientX } = require("./openshift-client"); const util = require("./util"); -module.exports = (settings) => { +const imageNames = ["hmcr-api", "hmcr-client", "hmcr-hangfire"]; + +function missingBuildImages(oc, buildNamespace, version) { + return imageNames.filter((imageName) => { + try { + oc.raw( + "get", + ["istag", `${imageName}:${version}`, "-o", "name"], + { namespace: buildNamespace } + ); + return false; + } catch (error) { + return true; + } + }); +} + +async function ensureBuildImages(settings, oc, version) { + const buildNamespace = settings.phases.build.namespace; + const missingImages = missingBuildImages(oc, buildNamespace, version); + + if (missingImages.length === 0) { + return; + } + + console.log( + `⚠️ Missing build image tags for ${version}: ${missingImages.join(", ")}. Rebuilding before deploy.` + ); + await buildTask(settings); + + const remainingMissingImages = missingBuildImages(oc, buildNamespace, version); + if (remainingMissingImages.length > 0) { + throw new Error( + `Build images are still missing after rebuild for ${version}: ${remainingMissingImages.join(", ")}` + ); + } +} + +module.exports = async (settings) => { const phases = settings.phases; const options = settings.options; const phase = options.env; @@ -13,7 +52,7 @@ module.exports = (settings) => { const version = options.version || `v1.0.${githubRunNumber}`; console.log(`🚀 Using version: ${version}`); - const oc = new OpenShiftClientX( + const oc = new HmcrOpenShiftClientX( Object.assign({ namespace: phases[phase].namespace }, options) ); @@ -21,12 +60,24 @@ module.exports = (settings) => { path.resolve(__dirname, "../../openshift") ); var objects = []; + const logDbClaimName = `${phases[phase].name}-logdb${phases[phase].suffix}`; const logDbSecret = util.getSecret( oc, phases[phase].namespace, - `${phases[phase].name}-logdb${phases[phase].suffix}` + logDbClaimName + ); + const logDbPersistentVolumeSize = util.getPersistentVolumeClaimSize( + oc, + phases[phase].namespace, + logDbClaimName ); + if (logDbPersistentVolumeSize) { + console.log( + `Reusing existing logDb PVC size for ${logDbClaimName}: ${logDbPersistentVolumeSize}` + ); + } + objects.push( ...oc.processDeploymentTemplate( `${templatesLocalBaseUrl}/client-deploy-config.yaml`, @@ -70,6 +121,9 @@ module.exports = (settings) => { SUFFIX: phases[phase].suffix, VERSION: version, ENV: phases[phase].phase, + ...(logDbPersistentVolumeSize + ? { PERSISTENT_VOLUME_SIZE: logDbPersistentVolumeSize } + : {}), }, } ) @@ -137,6 +191,7 @@ module.exports = (settings) => { `${changeId}`, phases[phase].instance ); + await ensureBuildImages(settings, oc, version); oc.importImageStreams( objects, phases[phase].tag, @@ -144,30 +199,10 @@ module.exports = (settings) => { version ); - // Ensure image streams are imported before proceeding - const imageNames = ["hmcr-api", "hmcr-client", "hmcr-hangfire"]; - imageNames.forEach((imageName) => { - try { - console.log(`🔄 Importing image stream for ${imageName}`); - oc.raw("import-image", [ - `${imageName}:${phases.build.tag}`, - `--from=d3d940-tools/${imageName}:${phases.build.tag}`, - "--confirm", - "-n", - phases.build.namespace, - ]); - console.log(`✅ Successfully imported image stream for ${imageName}`); - } catch (error) { - console.error(`❌ Failed to import image stream for ${imageName}: ${error.message}`); - } - }); - let imageExists = false; - oc.applyAndDeploy(objects, phases[phase].instance) + return oc.applyAndDeploy(objects, phases[phase].instance) .then(() => { - const imageNames = ["hmcr-api", "hmcr-client", "hmcr-hangfire"]; - imageExists = imageNames.every((imageName) => { try { const imageSha = oc.raw("get", [ @@ -209,8 +244,6 @@ module.exports = (settings) => { console.log("❌ Skipping final tagging because image does not exist."); return; } - const imageNames = ["hmcr-api", "hmcr-client", "hmcr-hangfire"]; - imageNames.forEach((imageName) => { const sourceImage = `d3d940-tools/${imageName}:latest`; const targetImage = `${phases[phase].namespace}/${imageName}`; @@ -227,4 +260,4 @@ module.exports = (settings) => { ); }); }); -}; \ No newline at end of file +}; diff --git a/.pipeline/lib/openshift-client.js b/.pipeline/lib/openshift-client.js new file mode 100644 index 00000000..a8dd440d --- /dev/null +++ b/.pipeline/lib/openshift-client.js @@ -0,0 +1,32 @@ +"use strict"; + +const { OpenShiftClientX } = require("@bcgov/pipeline-cli"); + +function normalizeImageStream(resource) { + if ( + resource && + resource.kind === "ImageStream" && + resource.status && + Array.isArray(resource.status.tags) + ) { + resource.status.tags.forEach((tag) => { + if (!Array.isArray(tag.items)) { + tag.items = []; + } + }); + } + + return resource; +} + +class HmcrOpenShiftClientX extends OpenShiftClientX { + object(name, args) { + return normalizeImageStream(super.object(name, args)); + } + + objects(names, args) { + return super.objects(names, args).map(normalizeImageStream); + } +} + +module.exports = { HmcrOpenShiftClientX }; diff --git a/.pipeline/lib/util.js b/.pipeline/lib/util.js index 652ea76e..605cc992 100644 --- a/.pipeline/lib/util.js +++ b/.pipeline/lib/util.js @@ -23,6 +23,25 @@ function getSecret(oc, namespace, secretId) { return secret; } +function getPersistentVolumeClaimSize(oc, namespace, claimName) { + try { + const raw = oc.raw("get", [ + "-n", + namespace, + "pvc", + claimName, + "-o", + "json", + ]); + const claim = JSON.parse(raw.stdout); + + return claim?.spec?.resources?.requests?.storage || null; + } catch (error) { + return null; + } +} + module.exports = { getSecret, + getPersistentVolumeClaimSize, }; diff --git a/client/src/js/components/fragments/Header.js b/client/src/js/components/fragments/Header.js index 11bb5e39..1b5a80da 100644 --- a/client/src/js/components/fragments/Header.js +++ b/client/src/js/components/fragments/Header.js @@ -29,6 +29,7 @@ const Header = ({ currentUser }) => { const location = useLocation(); const [collapsed, setCollapsed] = useState(true); const [version, setVersion] = useState(null); + const environmentClass = version || 'unknown'; useEffect(() => { api.getVersion().then((response) => setVersion(response.data.environment.toLowerCase())); @@ -48,7 +49,7 @@ const Header = ({ currentUser }) => { return (
- + { - +