Add timeout to the rollout status (#425)

* Added timeout to the rollout status and tests for it

* Fixed integration test errors

* Fix for blue green integration test

* Probable fix for integration errors

* No jobs run error fixed

* Changed timeout to file level constant

* Added parsing logic for timeout

* Made tests more concise

* implemented timeout validation check in an extracted utils mod

* Changed function name to parseDuration

* Removed timeout parameter from getResource

---------

Co-authored-by: David Gamero <david340804@gmail.com>
Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
This commit is contained in:
benjamin
2025-07-09 13:22:21 -04:00
committed by GitHub
parent e207ec429b
commit ac0b58c9a5
28 changed files with 1677 additions and 209 deletions
@@ -19,6 +19,19 @@ import {ExecOutput} from '@actions/exec'
jest.mock('../../types/kubectl')
const kubectl = new Kubectl('')
const TEST_TIMEOUT = '60s'
// Test constants to follow DRY principle
const EXPECTED_GREEN_OBJECTS = [
{name: 'nginx-service-green', kind: 'Service'},
{name: 'nginx-deployment-green', kind: 'Deployment'}
]
const MOCK_EXEC_OUTPUT = {
exitCode: 0,
stderr: '',
stdout: ''
} as ExecOutput
describe('bluegreenhelper functions', () => {
let testObjects
@@ -40,28 +53,56 @@ describe('bluegreenhelper functions', () => {
[].concat(
testObjects.deploymentEntityList,
testObjects.serviceEntityList
)
),
TEST_TIMEOUT
)
expect(value).toHaveLength(2)
expect(value).toContainEqual({
name: 'nginx-service-green',
kind: 'Service'
expect(value).toHaveLength(EXPECTED_GREEN_OBJECTS.length)
EXPECTED_GREEN_OBJECTS.forEach((expectedObject) => {
expect(value).toContainEqual(expectedObject)
})
expect(value).toContainEqual({
name: 'nginx-deployment-green',
kind: 'Deployment'
})
test('handles timeout when deleting objects', async () => {
// Mock deleteObjects to prevent actual execution
const deleteSpy = jest
.spyOn(kubectl, 'delete')
.mockResolvedValue(MOCK_EXEC_OUTPUT)
await bgHelper.deleteObjects(
kubectl,
EXPECTED_GREEN_OBJECTS,
TEST_TIMEOUT
)
// Verify kubectl.delete is called with timeout for each object in deleteList
expect(deleteSpy).toHaveBeenCalledTimes(EXPECTED_GREEN_OBJECTS.length)
EXPECTED_GREEN_OBJECTS.forEach(({name, kind}) => {
expect(deleteSpy).toHaveBeenCalledWith(
[kind, name],
undefined,
TEST_TIMEOUT
)
})
})
test('parses objects correctly from one file (getManifestObjects)', () => {
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
const expectedTypes = [
{
list: testObjects.deploymentEntityList,
kind: 'Deployment',
selectorApp: 'nginx'
},
{list: testObjects.serviceEntityList, kind: 'Service'},
{list: testObjects.ingressEntityList, kind: 'Ingress'}
]
expect(
testObjects.deploymentEntityList[0].spec.selector.matchLabels.app
).toBe('nginx')
expectedTypes.forEach(({list, kind, selectorApp}) => {
expect(list[0].kind).toBe(kind)
if (selectorApp) {
expect(list[0].spec.selector.matchLabels.app).toBe(selectorApp)
}
})
})
test('parses other kinds of objects (getManifestObjects)', () => {
@@ -102,23 +143,24 @@ describe('bluegreenhelper functions', () => {
})
test('correctly makes new blue green object (getNewBlueGreenObject and addBlueGreenLabelsAndAnnotations)', () => {
const modifiedDeployment = getNewBlueGreenObject(
testObjects.deploymentEntityList[0],
GREEN_LABEL_VALUE
)
const testCases = [
{
object: testObjects.deploymentEntityList[0],
expectedName: 'nginx-deployment-green'
},
{
object: testObjects.serviceEntityList[0],
expectedName: 'nginx-service-green'
}
]
expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green')
expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe(
'green'
)
const modifiedSvc = getNewBlueGreenObject(
testObjects.serviceEntityList[0],
GREEN_LABEL_VALUE
)
expect(modifiedSvc.metadata.name).toBe('nginx-service-green')
expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green')
testCases.forEach(({object, expectedName}) => {
const modifiedObject = getNewBlueGreenObject(object, GREEN_LABEL_VALUE)
expect(modifiedObject.metadata.name).toBe(expectedName)
expect(modifiedObject.metadata.labels['k8s.deploy.color']).toBe(
'green'
)
})
})
test('correctly fetches k8s objects', async () => {
@@ -140,24 +182,41 @@ describe('bluegreenhelper functions', () => {
})
test('exits when fails to fetch k8s objects', async () => {
const mockExecOutput = {
stdout: 'this should not matter',
exitCode: 0,
stderr: 'this is a fake error'
} as ExecOutput
jest
.spyOn(kubectl, 'getResource')
.mockImplementation(() => Promise.resolve(mockExecOutput))
let fetched = await fetchResource(
kubectl,
'nginx-deployment',
'Deployment'
)
expect(fetched).toBe(null)
const errorTestCases = [
{
description: 'with stderr error',
mockOutput: {
stdout: 'this should not matter',
exitCode: 0,
stderr: 'this is a fake error'
} as ExecOutput,
mockImplementation: () => Promise.resolve
},
{
description: 'with undefined implementation',
mockOutput: null,
mockImplementation: () => undefined
}
]
jest.spyOn(kubectl, 'getResource').mockImplementation()
fetched = await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
expect(fetched).toBe(null)
for (const testCase of errorTestCases) {
const spy = jest.spyOn(kubectl, 'getResource')
if (testCase.mockOutput) {
spy.mockImplementation(() => Promise.resolve(testCase.mockOutput))
} else {
spy.mockImplementation()
}
const fetched = await fetchResource(
kubectl,
'nginx-deployment',
'Deployment'
)
expect(fetched).toBe(null)
spy.mockRestore()
}
})
test('returns null when fetch fails to unset k8s objects', async () => {
@@ -32,7 +32,8 @@ export const STABLE_SUFFIX = '-stable'
export async function deleteGreenObjects(
kubectl: Kubectl,
toDelete: K8sObject[]
toDelete: K8sObject[],
timeout?: string
): Promise<K8sDeleteObject[]> {
// const resourcesToDelete: K8sDeleteObject[] = []
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
@@ -45,18 +46,23 @@ export async function deleteGreenObjects(
core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`)
await deleteObjects(kubectl, resourcesToDelete)
await deleteObjects(kubectl, resourcesToDelete, timeout)
return resourcesToDelete
}
export async function deleteObjects(
kubectl: Kubectl,
deleteList: K8sDeleteObject[]
deleteList: K8sDeleteObject[],
timeout?: string
) {
// delete services and deployments
for (const delObject of deleteList) {
try {
const result = await kubectl.delete([delObject.kind, delObject.name])
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}`)
@@ -141,7 +147,8 @@ export function isServiceRouted(
export async function deployWithLabel(
kubectl: Kubectl,
deploymentObjectList: any[],
nextLabel: string
nextLabel: string,
timeout?: string
): Promise<BlueGreenDeployment> {
const newObjectsList = deploymentObjectList.map((inputObject) =>
getNewBlueGreenObject(inputObject, nextLabel)
@@ -150,7 +157,7 @@ export async function deployWithLabel(
core.debug(
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
)
const deployResult = await deployObjects(kubectl, newObjectsList)
const deployResult = await deployObjects(kubectl, newObjectsList, timeout)
return {deployResult, objects: newObjectsList}
}
@@ -267,15 +274,26 @@ export async function fetchResource(
export async function deployObjects(
kubectl: Kubectl,
objectsList: any[]
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
serverSideApply,
timeout
)
return {execResult, manifestFiles}
+264 -1
View File
@@ -1,10 +1,17 @@
import {getManifestObjects} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {deployBlueGreen, deployBlueGreenIngress} from './deploy'
import {
deployBlueGreen,
deployBlueGreenIngress,
deployBlueGreenService,
deployBlueGreenSMI
} from './deploy'
import * as routeTester from './route'
import {Kubectl} from '../../types/kubectl'
import {RouteStrategy} from '../../types/routeStrategy'
import * as TSutils from '../../utilities/trafficSplitUtils'
import * as bgHelper from './blueGreenHelper'
import * as smiHelper from './smiBlueGreenHelper'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
@@ -73,3 +80,259 @@ describe('deploy tests', () => {
})
})
})
// Timeout tests
describe('deploy timeout tests', () => {
let testObjects
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
test('deployBlueGreen with timeout passes to strategy functions', async () => {
const kubectl = new Kubectl('')
const timeout = '300s'
const mockBgDeployment: BlueGreenDeployment = {
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
}
// Mock the helper functions that are actually called
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
const setupSMISpy = jest
.spyOn(smiHelper, 'setupSMI')
.mockResolvedValue(mockBgDeployment)
const routeSpy = jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockResolvedValue(mockBgDeployment)
// Test INGRESS strategy
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.INGRESS,
timeout
)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Test SERVICE strategy
deployWithLabelSpy.mockClear()
deployObjectsSpy.mockClear()
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.SERVICE,
timeout
)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Test SMI strategy
deployWithLabelSpy.mockClear()
setupSMISpy.mockClear()
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.SMI,
timeout
)
expect(setupSMISpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
setupSMISpy.mockRestore()
routeSpy.mockRestore()
})
test('deployBlueGreenIngress with timeout', async () => {
const kubectl = new Kubectl('')
const timeout = '240s'
// Mock the dependencies
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue({
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
})
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
await deployBlueGreenIngress(kubectl, ingressFilepath, timeout)
// Verify deployWithLabel was called with timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
test('deployBlueGreenService with timeout', async () => {
const kubectl = new Kubectl('')
const timeout = '180s'
// Mock the dependencies
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue({
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
})
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
await deployBlueGreenService(kubectl, ingressFilepath, timeout)
// Verify deployWithLabel was called with timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
test('deployBlueGreenSMI with timeout', async () => {
const kubectl = new Kubectl('')
const timeout = '360s'
// Mock the dependencies
const setupSMISpy = jest.spyOn(smiHelper, 'setupSMI').mockResolvedValue({
objects: [],
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
}
})
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue({
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
})
await deployBlueGreenSMI(kubectl, ingressFilepath, timeout)
// Verify setupSMI was called with timeout
expect(setupSMISpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
setupSMISpy.mockRestore()
deployObjectsSpy.mockRestore()
deployWithLabelSpy.mockRestore()
})
test('deploy functions without timeout should pass undefined', async () => {
const kubectl = new Kubectl('')
const deployWithLabelSpy = jest
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue({
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
})
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
await deployBlueGreenIngress(kubectl, ingressFilepath)
// Verify deployWithLabel was called with undefined timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
undefined
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
})
+25 -15
View File
@@ -22,16 +22,17 @@ import {DeployResult} from '../../types/deployResult'
export async function deployBlueGreen(
kubectl: Kubectl,
files: string[],
routeStrategy: RouteStrategy
routeStrategy: RouteStrategy,
timeout?: string
): Promise<BlueGreenDeployment> {
const blueGreenDeployment = await (async () => {
switch (routeStrategy) {
case RouteStrategy.INGRESS:
return await deployBlueGreenIngress(kubectl, files)
return await deployBlueGreenIngress(kubectl, files, timeout)
case RouteStrategy.SMI:
return await deployBlueGreenSMI(kubectl, files)
return await deployBlueGreenSMI(kubectl, files, timeout)
default:
return await deployBlueGreenService(kubectl, files)
return await deployBlueGreenService(kubectl, files, timeout)
}
})()
@@ -39,7 +40,8 @@ export async function deployBlueGreen(
const routeDeployment = await routeBlueGreenForDeploy(
kubectl,
files,
routeStrategy
routeStrategy,
timeout
)
core.endGroup()
@@ -52,7 +54,8 @@ export async function deployBlueGreen(
export async function deployBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -67,20 +70,23 @@ export async function deployBlueGreenSMI(
const otherObjDeployment: DeployResult = await deployObjects(
kubectl,
newObjectsList
newObjectsList,
timeout
)
// make extraservices and trafficsplit
const smiAndSvcDeployment = await setupSMI(
kubectl,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// create new deloyments
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
blueGreenDeployment.objects.push(...newObjectsList)
@@ -98,7 +104,8 @@ export async function deployBlueGreenSMI(
export async function deployBlueGreenIngress(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -111,14 +118,15 @@ export async function deployBlueGreenIngress(
const workloadDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
servicesAndDeployments,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
const otherObjects = [].concat(
manifestObjects.otherObjects,
manifestObjects.unroutedServiceEntityList
)
await deployObjects(kubectl, otherObjects)
await deployObjects(kubectl, otherObjects, timeout)
core.debug(
`new objects after processing services and other objects: \n
${JSON.stringify(servicesAndDeployments)}`
@@ -132,7 +140,8 @@ export async function deployBlueGreenIngress(
export async function deployBlueGreenService(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -140,7 +149,8 @@ export async function deployBlueGreenService(
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
// create other non deployment and non service entities
@@ -150,7 +160,7 @@ export async function deployBlueGreenService(
manifestObjects.unroutedServiceEntityList
)
await deployObjects(kubectl, newObjectsList)
await deployObjects(kubectl, newObjectsList, timeout)
// returning deployment details to check for rollout stability
return {
deployResult: blueGreenDeployment.deployResult,
@@ -156,3 +156,135 @@ describe('promote tests', () => {
expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrow()
})
})
// Timeout tests
describe('promote timeout tests', () => {
beforeEach(() => {
// @ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
const mockDeployWithLabel = () =>
jest.spyOn(bgHelper, 'deployWithLabel').mockResolvedValue({
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
})
const setupFetchResource = (
kind: string,
name: string,
labelValue: string
) => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = labelValue
jest.spyOn(bgHelper, 'fetchResource').mockResolvedValue({
kind,
spec: {},
metadata: {labels: mockLabels, name}
})
}
test.each([
{
name: 'promoteBlueGreenIngress with timeout',
fn: promoteBlueGreenIngress,
kind: 'Ingress',
resourceName: 'nginx-ingress-green',
timeout: '300s',
setup: () =>
setupFetchResource(
'Ingress',
'nginx-ingress-green',
bgHelper.GREEN_LABEL_VALUE
)
},
{
name: 'promoteBlueGreenService with timeout',
fn: promoteBlueGreenService,
kind: 'Service',
resourceName: 'nginx-service-green',
timeout: '240s',
setup: () => {
setupFetchResource(
'Service',
'nginx-service-green',
bgHelper.GREEN_LABEL_VALUE
)
jest
.spyOn(servicesTester, 'validateServicesState')
.mockResolvedValue(true)
}
},
{
name: 'promoteBlueGreenSMI with timeout',
fn: promoteBlueGreenSMI,
kind: 'TrafficSplit',
resourceName: 'nginx-service-trafficsplit',
timeout: '180s',
setup: () => {
const mockTsObject: TrafficSplitObject = {
apiVersion: 'v1alpha3',
kind: TRAFFIC_SPLIT_OBJECT,
metadata: {
name: 'nginx-service-trafficsplit',
labels: new Map<string, string>(),
annotations: new Map<string, string>()
},
spec: {
service: 'nginx-service',
backends: [
{service: 'nginx-service-stable', weight: MIN_VAL},
{service: 'nginx-service-green', weight: MAX_VAL}
]
}
}
jest
.spyOn(bgHelper, 'fetchResource')
.mockResolvedValue(mockTsObject)
jest
.spyOn(smiTester, 'validateTrafficSplitsState')
.mockResolvedValue(true)
}
}
])('$name', async ({fn, timeout, setup}) => {
setup()
const deployWithLabelSpy = mockDeployWithLabel()
await fn(kubectl, testObjects, timeout)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
bgHelper.NONE_LABEL_VALUE,
timeout
)
deployWithLabelSpy.mockRestore()
})
test('promote functions without timeout should pass undefined', async () => {
setupFetchResource(
'Ingress',
'nginx-ingress-green',
bgHelper.GREEN_LABEL_VALUE
)
const deployWithLabelSpy = mockDeployWithLabel()
await promoteBlueGreenIngress(kubectl, testObjects)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
bgHelper.NONE_LABEL_VALUE,
undefined
)
deployWithLabelSpy.mockRestore()
})
})
+12 -6
View File
@@ -11,7 +11,8 @@ import {validateTrafficSplitsState} from './smiBlueGreenHelper'
export async function promoteBlueGreenIngress(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
//checking if anything to promote
const {areValid, invalidIngresses} = await validateIngresses(
@@ -32,7 +33,8 @@ export async function promoteBlueGreenIngress(
manifestObjects.deploymentEntityList,
manifestObjects.serviceEntityList
),
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
// create stable services with new configuration
@@ -41,7 +43,8 @@ export async function promoteBlueGreenIngress(
export async function promoteBlueGreenService(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
// checking if services are in the right state ie. targeting green deployments
if (
@@ -54,13 +57,15 @@ export async function promoteBlueGreenService(
return await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
}
export async function promoteBlueGreenSMI(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
// checking if there is something to promote
if (
@@ -76,6 +81,7 @@ export async function promoteBlueGreenSMI(
return await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
}
+132 -3
View File
@@ -8,9 +8,13 @@ import {
rejectBlueGreenService,
rejectBlueGreenSMI
} from './reject'
import * as bgHelper from './blueGreenHelper'
import * as routeHelper from './route'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kubectl = new Kubectl('')
const TEST_TIMEOUT_SHORT = '60s'
const TEST_TIMEOUT_LONG = '120s'
jest.mock('../../types/kubectl')
@@ -43,15 +47,140 @@ describe('reject tests', () => {
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
})
test('reject blue/green service', async () => {
const value = await rejectBlueGreenService(kubectl, testObjects)
test('reject blue/green ingress with timeout', async () => {
// Mock routeBlueGreenIngressUnchanged and deleteGreenObjects
jest
.spyOn(routeHelper, 'routeBlueGreenIngressUnchanged')
.mockResolvedValue({
deployResult: {
execResult: {stdout: '', stderr: '', exitCode: 0},
manifestFiles: []
},
objects: [
{
kind: 'Ingress',
metadata: {
name: 'nginx-ingress',
labels: new Map<string, string>()
},
spec: {}
}
]
})
jest.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue([
{name: 'nginx-service-green', kind: 'Service'},
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
const value = await rejectBlueGreenIngress(
kubectl,
testObjects,
TEST_TIMEOUT_LONG
)
const bgDeployment = value.routeResult
const deleteResult = value.deleteResult
expect(deleteResult).toHaveLength(2)
for (const obj of deleteResult) {
if (obj.kind === 'Service') {
expect(obj.name).toBe('nginx-service-green')
}
if (obj.kind === 'Deployment') {
expect(obj.name).toBe('nginx-deployment-green')
}
}
expect(bgDeployment.objects).toHaveLength(1)
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
// Verify deleteGreenObjects is called with timeout
expect(bgHelper.deleteGreenObjects).toHaveBeenCalledWith(
kubectl,
[].concat(
testObjects.deploymentEntityList,
testObjects.serviceEntityList
),
TEST_TIMEOUT_LONG
)
expect(routeHelper.routeBlueGreenIngressUnchanged).toHaveBeenCalledWith(
kubectl,
testObjects.serviceNameMap,
testObjects.ingressEntityList,
TEST_TIMEOUT_LONG
)
})
test('reject blue/green service', async () => {
jest.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue([
{name: 'nginx-service-green', kind: 'Service'},
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
const value = await rejectBlueGreenService(
kubectl,
testObjects,
TEST_TIMEOUT_SHORT
)
const deleteResult = value.deleteResult
expect(deleteResult).toHaveLength(2)
expect(deleteResult).toContainEqual({
name: 'nginx-service-green',
kind: 'Service'
})
expect(deleteResult).toContainEqual({
name: 'nginx-deployment-green',
kind: 'Deployment'
})
})
test('reject blue/green service with timeout', async () => {
// Mock routeBlueGreenService and deleteGreenObjects
jest.spyOn(routeHelper, 'routeBlueGreenService').mockResolvedValue({
deployResult: {
execResult: {stdout: '', stderr: '', exitCode: 0},
manifestFiles: []
},
objects: [
{
kind: 'Service',
metadata: {
name: 'nginx-service',
labels: new Map<string, string>()
},
spec: {}
}
]
})
jest
.spyOn(bgHelper, 'deleteGreenObjects')
.mockResolvedValue([
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
const value = await rejectBlueGreenService(
kubectl,
testObjects,
TEST_TIMEOUT_LONG
)
const bgDeployment = value.routeResult
const deleteResult = value.deleteResult
// Verify deleteGreenObjects is called with timeout
expect(bgHelper.deleteGreenObjects).toHaveBeenCalledWith(
kubectl,
testObjects.deploymentEntityList,
TEST_TIMEOUT_LONG
)
// Assertions for routeResult and deleteResult
expect(deleteResult).toHaveLength(1)
expect(deleteResult[0].name).toBe('nginx-deployment-green')
expect(bgDeployment.objects).toHaveLength(1)
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-service')
})
+20 -10
View File
@@ -12,14 +12,16 @@ import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route'
export async function rejectBlueGreenIngress(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// get all kubernetes objects defined in manifest files
// route ingress to stables services
const routeResult = await routeBlueGreenIngressUnchanged(
kubectl,
manifestObjects.serviceNameMap,
manifestObjects.ingressEntityList
manifestObjects.ingressEntityList,
timeout
)
// delete green services and deployments
@@ -28,7 +30,8 @@ export async function rejectBlueGreenIngress(
[].concat(
manifestObjects.deploymentEntityList,
manifestObjects.serviceEntityList
)
),
timeout
)
return {routeResult, deleteResult}
@@ -36,19 +39,22 @@ export async function rejectBlueGreenIngress(
export async function rejectBlueGreenService(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// route to stable objects
const routeResult = await routeBlueGreenService(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// delete new deployments with green suffix
const deleteResult = await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList
manifestObjects.deploymentEntityList,
timeout
)
return {routeResult, deleteResult}
@@ -56,25 +62,29 @@ export async function rejectBlueGreenService(
export async function rejectBlueGreenSMI(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// route trafficsplit to stable deployments
const routeResult = await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// delete rejected new bluegreen deployments
const deletedObjects = await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList
manifestObjects.deploymentEntityList,
timeout
)
// delete trafficsplit and extra services
const cleanupResult = await cleanupSMI(
kubectl,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
return {routeResult, deleteResult: [].concat(deletedObjects, cleanupResult)}
+141 -1
View File
@@ -16,7 +16,9 @@ import {
import {
routeBlueGreenIngress,
routeBlueGreenService,
routeBlueGreenForDeploy
routeBlueGreenForDeploy,
routeBlueGreenSMI,
routeBlueGreenIngressUnchanged
} from './route'
jest.mock('../../types/kubectl')
@@ -117,3 +119,141 @@ describe('route function tests', () => {
).toHaveLength(2)
})
})
// Timeout tests
describe('route timeout tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('routeBlueGreenService with timeout', async () => {
const timeout = '240s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
const value = await routeBlueGreenService(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(value.objects).toHaveLength(1)
deployObjectsSpy.mockRestore()
})
test('routeBlueGreenSMI with timeout', async () => {
const timeout = '300s'
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
// Mock deployObjects and createTrafficSplitObject to capture timeout parameter
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
const createTrafficSplitSpy = jest
.spyOn(require('./smiBlueGreenHelper'), 'createTrafficSplitObject')
.mockResolvedValue({
metadata: {name: 'nginx-service-trafficsplit'},
spec: {backends: []}
})
const value = await routeBlueGreenSMI(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList,
timeout
)
expect(createTrafficSplitSpy).toHaveBeenCalledWith(
kc,
'nginx-service',
GREEN_LABEL_VALUE,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
deployObjectsSpy.mockRestore()
createTrafficSplitSpy.mockRestore()
})
test('routeBlueGreenIngressUnchanged with timeout', async () => {
const timeout = '180s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
const value = await routeBlueGreenIngressUnchanged(
kc,
testObjects.serviceNameMap,
testObjects.ingressEntityList,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
deployObjectsSpy.mockRestore()
})
test('route functions without timeout should pass undefined', async () => {
const deployObjectsSpy = jest
.spyOn(require('./blueGreenHelper'), 'deployObjects')
.mockResolvedValue({
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
})
// Test routeBlueGreenService without timeout
await routeBlueGreenService(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
deployObjectsSpy.mockRestore()
})
})
+22 -13
View File
@@ -25,7 +25,8 @@ import {getBufferTime} from '../../inputUtils'
export async function routeBlueGreenForDeploy(
kubectl: Kubectl,
inputManifestFiles: string[],
routeStrategy: RouteStrategy
routeStrategy: RouteStrategy,
timeout?: string
): Promise<BlueGreenDeployment> {
// sleep for buffer time
const bufferTime: number = getBufferTime()
@@ -47,19 +48,22 @@ export async function routeBlueGreenForDeploy(
return await routeBlueGreenIngress(
kubectl,
manifestObjects.serviceNameMap,
manifestObjects.ingressEntityList
manifestObjects.ingressEntityList,
timeout
)
} else if (routeStrategy == RouteStrategy.SMI) {
return await routeBlueGreenSMI(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
} else {
return await routeBlueGreenService(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
}
}
@@ -67,7 +71,8 @@ export async function routeBlueGreenForDeploy(
export async function routeBlueGreenIngress(
kubectl: Kubectl,
serviceNameMap: Map<string, string>,
ingressEntityList: any[]
ingressEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
// const newObjectsList = []
const newObjectsList: K8sObject[] = ingressEntityList.map((obj) => {
@@ -84,7 +89,7 @@ export async function routeBlueGreenIngress(
}
})
const deployResult = await deployObjects(kubectl, newObjectsList)
const deployResult = await deployObjects(kubectl, newObjectsList, timeout)
return {deployResult, objects: newObjectsList}
}
@@ -92,26 +97,28 @@ export async function routeBlueGreenIngress(
export async function routeBlueGreenIngressUnchanged(
kubectl: Kubectl,
serviceNameMap: Map<string, string>,
ingressEntityList: any[]
ingressEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const objects = ingressEntityList.filter((ingress) =>
isIngressRouted(ingress, serviceNameMap)
)
const deployResult = await deployObjects(kubectl, objects)
const deployResult = await deployObjects(kubectl, objects, timeout)
return {deployResult, objects}
}
export async function routeBlueGreenService(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const objects = serviceEntityList.map((serviceObject) =>
getUpdatedBlueGreenService(serviceObject, nextLabel)
)
const deployResult = await deployObjects(kubectl, objects)
const deployResult = await deployObjects(kubectl, objects, timeout)
return {deployResult, objects}
}
@@ -119,7 +126,8 @@ export async function routeBlueGreenService(
export async function routeBlueGreenSMI(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
// let tsObjects: TrafficSplitObject[] = []
@@ -128,14 +136,15 @@ export async function routeBlueGreenSMI(
const tsObject: TrafficSplitObject = await createTrafficSplitObject(
kubectl,
serviceObject.metadata.name,
nextLabel
nextLabel,
timeout
)
return tsObject
})
)
const deployResult = await deployObjects(kubectl, tsObjects)
const deployResult = await deployObjects(kubectl, tsObjects, timeout)
return {deployResult, objects: tsObjects}
}
@@ -197,4 +197,191 @@ describe('SMI Helper tests', () => {
expect(deleteObjects[0].name).toBe('nginx-service-green')
expect(deleteObjects[0].kind).toBe('Service')
})
// Timeout-specific tests
test('setupSMI with timeout test', async () => {
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
result: 'success'
} as any)
const timeout = '300s'
const smiResults = await setupSMI(
kc,
testObjects.serviceEntityList,
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(smiResults.objects).toBeDefined()
expect(smiResults.deployResult).toBeDefined()
deployObjectsSpy.mockRestore()
})
test('createTrafficSplitObject with timeout test', async () => {
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '180s'
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
NONE_LABEL_VALUE,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.arrayContaining([
expect.objectContaining({
name: 'nginx-service-trafficsplit',
kind: TRAFFIC_SPLIT_OBJECT
})
]),
timeout
)
expect(tsObject.metadata.name).toBe('nginx-service-trafficsplit')
expect(tsObject.spec.backends).toHaveLength(2)
deleteObjectsSpy.mockRestore()
})
test('createTrafficSplitObject with GREEN_LABEL_VALUE and timeout test', async () => {
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '240s'
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
GREEN_LABEL_VALUE,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
// Verify weights are correct for green deployment
for (const be of tsObject.spec.backends) {
if (be.service === 'nginx-service-stable') {
expect(be.weight).toBe(MIN_VAL)
}
if (be.service === 'nginx-service-green') {
expect(be.weight).toBe(MAX_VAL)
}
}
deleteObjectsSpy.mockRestore()
})
test('cleanupSMI with timeout test', async () => {
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '120s'
const deleteObjects = await cleanupSMI(
kc,
testObjects.serviceEntityList,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.arrayContaining([
expect.objectContaining({
name: 'nginx-service-green',
kind: 'Service'
})
]),
timeout
)
expect(deleteObjects).toHaveLength(1)
expect(deleteObjects[0].name).toBe('nginx-service-green')
expect(deleteObjects[0].kind).toBe('Service')
deleteObjectsSpy.mockRestore()
})
test('setupSMI without timeout test', async () => {
const deployObjectsSpy = jest
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
result: 'success'
} as any)
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
// Verify deployObjects was called without timeout (undefined)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(smiResults.objects).toBeDefined()
expect(smiResults.deployResult).toBeDefined()
deployObjectsSpy.mockRestore()
})
test('createTrafficSplitObject without timeout test', async () => {
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
NONE_LABEL_VALUE
)
// Verify deleteObjects was called without timeout (undefined)
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(tsObject.metadata.name).toBe('nginx-service-trafficsplit')
deleteObjectsSpy.mockRestore()
})
test('cleanupSMI without timeout test', async () => {
const deleteObjectsSpy = jest
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const deleteObjects = await cleanupSMI(kc, testObjects.serviceEntityList)
// Verify deleteObjects was called without timeout (undefined)
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(deleteObjects).toHaveLength(1)
deleteObjectsSpy.mockRestore()
})
})
@@ -28,7 +28,8 @@ export const MAX_VAL = 100
export async function setupSMI(
kubectl: Kubectl,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const newObjectsList = []
const trafficObjectList = []
@@ -49,7 +50,8 @@ export async function setupSMI(
const tsObject = await createTrafficSplitObject(
kubectl,
svc.metadata.name,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
tsObjects.push(tsObject as TrafficSplitObject)
}
@@ -59,7 +61,8 @@ export async function setupSMI(
// create services
const smiDeploymentResult: DeployResult = await deployObjects(
kubectl,
objectsToDeploy
objectsToDeploy,
timeout
)
return {
@@ -73,7 +76,8 @@ let trafficSplitAPIVersion = ''
export async function createTrafficSplitObject(
kubectl: Kubectl,
name: string,
nextLabel: string
nextLabel: string,
timeout?: string
): Promise<TrafficSplitObject> {
// cache traffic split api version
if (!trafficSplitAPIVersion)
@@ -112,6 +116,13 @@ export async function createTrafficSplitObject(
}
}
const deleteList: K8sDeleteObject[] = [
{
name: trafficSplitObject.metadata.name,
kind: trafficSplitObject.kind
}
]
await deleteObjects(kubectl, deleteList, timeout)
return trafficSplitObject
}
@@ -173,7 +184,8 @@ export async function validateTrafficSplitsState(
export async function cleanupSMI(
kubectl: Kubectl,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<K8sDeleteObject[]> {
const deleteList: K8sDeleteObject[] = []
@@ -189,7 +201,7 @@ export async function cleanupSMI(
})
// delete all objects
await deleteObjects(kubectl, deleteList)
await deleteObjects(kubectl, deleteList, timeout)
return deleteList
}