benjamin 7395c391d9
Added error check for canary promote actions (#432)
* Added checkForErrors so canary promote action fails when there is an error

* Added tests for checkForErrors

* Probable integration error fix

* Probable integration error fix

* Revert changes back

* Added checkForErrors unit tests

* Fixed multiple tests issue

---------

Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
2025-07-15 15:00:09 -06:00

302 lines
8.9 KiB
TypeScript

import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import {DeployResult} from '../../types/deployResult'
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {
isDeploymentEntity,
isIngressEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes'
import * as fileHelper from '../../utilities/fileUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
import {
UnsetClusterSpecificDetails,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils'
export const GREEN_LABEL_VALUE = 'green'
export const NONE_LABEL_VALUE = 'None'
export const BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color'
export const GREEN_SUFFIX = '-green'
export const STABLE_SUFFIX = '-stable'
export async function deleteGreenObjects(
kubectl: Kubectl,
toDelete: K8sObject[],
timeout?: string
): Promise<K8sDeleteObject[]> {
// const resourcesToDelete: K8sDeleteObject[] = []
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
return {
name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX),
kind: obj.kind,
namespace: obj.metadata.namespace
}
})
core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`)
await deleteObjects(kubectl, resourcesToDelete, timeout)
return resourcesToDelete
}
export async function deleteObjects(
kubectl: Kubectl,
deleteList: K8sDeleteObject[],
timeout?: string
) {
// delete services and deployments
for (const delObject of deleteList) {
try {
const result = await kubectl.delete(
[delObject.kind, delObject.name],
delObject.namespace,
timeout
)
checkForErrors([result])
} catch (ex) {
core.debug(`failed to delete object ${delObject.name}: ${ex}`)
}
}
}
// other common functions
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
const deploymentEntityList: K8sObject[] = []
const serviceEntityList: K8sObject[] = []
const routedServiceEntityList: K8sObject[] = []
const unroutedServiceEntityList: K8sObject[] = []
const ingressEntityList: K8sObject[] = []
const otherEntitiesList: K8sObject[] = []
const serviceNameMap = new Map<string, string>()
// Manifest objects per type. All resources should be parsed and
// organized before we can check if services are “routed” or not.
filePaths.forEach((filePath: string) => {
try {
const fileContents = fs.readFileSync(filePath).toString()
yaml.loadAll(fileContents, (inputObject: any) => {
if (!!inputObject) {
const kind = inputObject.kind
if (isDeploymentEntity(kind)) {
deploymentEntityList.push(inputObject)
} else if (isServiceEntity(kind)) {
serviceEntityList.push(inputObject)
} else if (isIngressEntity(kind)) {
ingressEntityList.push(inputObject)
} else {
otherEntitiesList.push(inputObject)
}
}
})
} catch (error) {
core.error(`Error processing file ${filePath}: ${error.message}`)
throw error
}
})
serviceEntityList.forEach((inputObject: any) => {
if (isServiceRouted(inputObject, deploymentEntityList)) {
const name = inputObject.metadata.name
routedServiceEntityList.push(inputObject)
serviceNameMap.set(name, getBlueGreenResourceName(name, GREEN_SUFFIX))
} else {
unroutedServiceEntityList.push(inputObject)
}
})
return {
serviceEntityList: routedServiceEntityList,
serviceNameMap: serviceNameMap,
unroutedServiceEntityList: unroutedServiceEntityList,
deploymentEntityList: deploymentEntityList,
ingressEntityList: ingressEntityList,
otherObjects: otherEntitiesList
}
}
export function isServiceRouted(
serviceObject: any[],
deploymentEntityList: any[]
): boolean {
const serviceSelector: any = getServiceSelector(serviceObject)
return (
serviceSelector &&
deploymentEntityList.some((depObject) => {
// finding if there is a deployment in the given manifests the service targets
const matchLabels: any = getDeploymentMatchLabels(depObject)
return (
matchLabels &&
isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)
)
})
)
}
export async function deployWithLabel(
kubectl: Kubectl,
deploymentObjectList: any[],
nextLabel: string,
timeout?: string
): Promise<BlueGreenDeployment> {
const newObjectsList = deploymentObjectList.map((inputObject) =>
getNewBlueGreenObject(inputObject, nextLabel)
)
core.debug(
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
)
const deployResult = await deployObjects(kubectl, newObjectsList, timeout)
return {deployResult, objects: newObjectsList}
}
export function getNewBlueGreenObject(
inputObject: any,
labelValue: string
): K8sObject {
const newObject = JSON.parse(JSON.stringify(inputObject))
// Updating name only if label is green label is given
if (labelValue === GREEN_LABEL_VALUE) {
newObject.metadata.name = getBlueGreenResourceName(
inputObject.metadata.name,
GREEN_SUFFIX
)
}
// Adding labels and annotations
addBlueGreenLabelsAndAnnotations(newObject, labelValue)
return newObject
}
export function addBlueGreenLabelsAndAnnotations(
inputObject: any,
labelValue: string
) {
//creating the k8s.deploy.color label
const newLabels = new Map<string, string>()
newLabels[BLUE_GREEN_VERSION_LABEL] = labelValue
// updating object labels and selector labels
updateObjectLabels(inputObject, newLabels, false)
updateSelectorLabels(inputObject, newLabels, false)
// updating spec labels if it is not a service
if (!isServiceEntity(inputObject.kind)) {
updateSpecLabels(inputObject, newLabels, false)
}
}
export function getBlueGreenResourceName(name: string, suffix: string) {
return `${name}${suffix}`
}
export function getDeploymentMatchLabels(deploymentObject: any): any {
if (
deploymentObject?.kind?.toUpperCase() ==
KubernetesWorkload.POD.toUpperCase() &&
deploymentObject?.metadata?.labels
) {
return deploymentObject.metadata.labels
} else if (deploymentObject?.spec?.selector?.matchLabels) {
return deploymentObject.spec.selector.matchLabels
}
}
export function getServiceSelector(serviceObject: any): any {
if (serviceObject?.spec?.selector) {
return serviceObject.spec.selector
}
}
export function isServiceSelectorSubsetOfMatchLabel(
serviceSelector: any,
matchLabels: any
): boolean {
const serviceSelectorMap = new Map()
const matchLabelsMap = new Map()
JSON.parse(JSON.stringify(serviceSelector), (key, value) => {
serviceSelectorMap.set(key, value)
})
JSON.parse(JSON.stringify(matchLabels), (key, value) => {
matchLabelsMap.set(key, value)
})
let isMatch = true
serviceSelectorMap.forEach((value, key) => {
if (
!!key &&
(!matchLabelsMap.has(key) || matchLabelsMap.get(key)) != value
)
isMatch = false
})
return isMatch
}
export async function fetchResource(
kubectl: Kubectl,
kind: string,
name: string,
namespace?: string
): Promise<K8sObject> {
const result = await kubectl.getResource(kind, name, false, namespace)
if (result == null || !!result.stderr) {
return null
}
if (!!result.stdout) {
const resource = JSON.parse(result.stdout) as K8sObject
try {
UnsetClusterSpecificDetails(resource)
return resource
} catch (ex) {
core.debug(
`Exception occurred while Parsing ${resource} in Json object: ${ex}`
)
}
}
}
export async function deployObjects(
kubectl: Kubectl,
objectsList: any[],
timeout?: string
): Promise<DeployResult> {
// Handle empty objects list gracefully to prevent "Configuration paths must exist" error
if (!objectsList || objectsList.length === 0) {
core.debug('No objects to deploy, skipping kubectl apply')
return {
execResult: {exitCode: 0, stdout: '', stderr: ''},
manifestFiles: []
}
}
const manifestFiles = fileHelper.writeObjectsToFile(objectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const execResult = await kubectl.apply(
manifestFiles,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([execResult])
return {execResult, manifestFiles}
}