v4 new release (#224)

* Add missing API switch for GHES (#200)

* Vidya reddy/prettier code (#203)

* switch none deployment strategy to basic (#204)

* switch none deployment strategy to basic

* update readme

* update deployment strategy fallthrough logic

* comment fixed

* add disclaimer for basic strategy only supporting deploy action

* Hari/beautify logs (#206)

* Logging changes for deploy

* Logging Changes with group

* format check changes

* Add ncc build to build script (#208)

Co-authored-by: Vidya Reddy <vidyareddy@microsoft.com>

* Logging Changes for Promote, Reject actions (#207)

* add clean function (#211)

* Added Traffic split annotations (#215)

* Added Traffic split annotations

* traffic split - blueGreen deployment

* traffic split - canary deployment

* Traffic split annotations - canary deployment

* updated Readme and action.yml

* Traffic split - canary deployment

* clean code

* Clean code

* Clean code

* Create annotation object

* Updated Readme and action.yml

* Spelling correction

Co-authored-by: Vidya Reddy <vidyareddy@microsoft.com>

* Swap annotation key to actions.github.com prefix (#216)

* Private Cluster functionality (#214)

* Fixed Blue/Green Strategy Ingress Route-Method Glitch  (#217)

* Added some tests, not sure what else to try but gonna think of more examples

* forgot some files

* reverted package-lock.json

* Added empty dir test

* Cleaned up some extra spaces

* Add node modules and compiled JavaScript from main

* forgot to actually include functionality

* removed unnecessary files

* Update .gitignore

* Update .gitignore

* Update .gitignore

* thx david

* renamed searchFilesRec

* integrations test fix

* added examples to README

* added note about depth

* added additional note

* removed ticks

* changed version string

* removed conflict on readme

* Added tests for bluegreen helper and resolved issue with ingress not being read correctly, still have to figure out why new services aren't showing up

* resolved services name issue

* looks functional, beginning refactor now

* refactored deploy methods for type error

* Removed refactor comments

* prettier

* implemented Oliver's feedback

* prettier

* added optional chaining operator

* removed refactor comment

Co-authored-by: Jaiveer Katariya <jaiveerkatariya@Jaiveers-MacBook-Pro.local>
Co-authored-by: Oliver King <oking3@uncc.edu>
Co-authored-by: Jaiveer Katariya <jaiveerkatariya@Jaiveers-MBP.lan>

* Add node modules and compiled JavaScript from main

Co-authored-by: nv35 <76777923+nv35@users.noreply.github.com>
Co-authored-by: Vidya <59590642+Vidya2606@users.noreply.github.com>
Co-authored-by: David Gamero <david340804@gmail.com>
Co-authored-by: Hariharan Subramanian <105889062+hsubramanianaks@users.noreply.github.com>
Co-authored-by: Vidya Reddy <vidyareddy@microsoft.com>
Co-authored-by: Oliver King <oking3@uncc.edu>
Co-authored-by: Marcus-Hines <marcus.chris.hines@gmail.com>
Co-authored-by: Jaiveer Katariya <35347859+jaiveerk@users.noreply.github.com>
Co-authored-by: Jaiveer Katariya <jaiveerkatariya@Jaiveers-MacBook-Pro.local>
Co-authored-by: Jaiveer Katariya <jaiveerkatariya@Jaiveers-MBP.lan>
This commit is contained in:
github-actions[bot]
2022-08-01 15:01:16 -04:00
committed by GitHub
parent 497ce6351c
commit 4e60e959ea
2284 changed files with 85427 additions and 132489 deletions
@@ -0,0 +1,128 @@
import {
createWorkloadsWithLabel,
deleteWorkloadsAndServicesWithLabel,
getManifestObjects,
getNewBlueGreenObject,
GREEN_LABEL_VALUE,
isServiceRouted,
NONE_LABEL_VALUE
} from './blueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
jest.mock('../../types/kubectl')
describe('bluegreenhelper functions', () => {
let testObjects
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(['test/unit/manifests/test-ingress.yml'])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('it should parse objects correctly from one file', () => {
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
expect(
testObjects.deploymentEntityList[0].spec.selector.matchLabels.app
).toBe('nginx')
})
test('correctly makes new blue green object', () => {
const modifiedDeployment = getNewBlueGreenObject(
testObjects.deploymentEntityList[0],
GREEN_LABEL_VALUE
)
//@ts-ignore
expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green')
//@ts-ignore
expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe(
'green'
)
const modifiedSvc = getNewBlueGreenObject(
testObjects.serviceEntityList[0],
GREEN_LABEL_VALUE
)
//@ts-ignore
expect(modifiedSvc.metadata.name).toBe('nginx-service-green')
//@ts-ignore
expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green')
})
test('correctly makes labeled workloads', () => {
const kubectl = new Kubectl('')
expect(Kubectl).toBeCalledTimes(1)
const cwlResult = createWorkloadsWithLabel(
kubectl,
testObjects.deploymentEntityList,
GREEN_LABEL_VALUE
)
cwlResult.then((value) => {
//@ts-ignore
expect(value.newFilePaths[0]).toBe('')
})
})
test('correctly classifies routed services', () => {
expect(
isServiceRouted(
testObjects.serviceEntityList[0],
testObjects.deploymentEntityList
)
).toBe(true)
testObjects.serviceEntityList[0].spec.selector.app = 'fakeapp'
expect(
isServiceRouted(
testObjects.serviceEntityList[0],
testObjects.deploymentEntityList
)
).toBe(false)
})
test('correctly deletes services and workloads according to label', () => {
const kubectl = new Kubectl('')
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
let objectsToDelete = deleteWorkloadsAndServicesWithLabel(
kubectl,
NONE_LABEL_VALUE,
testObjects.deploymentEntityList,
testObjects.serviceEntityList
)
objectsToDelete.then((value) => {
expect(value).toHaveLength(2)
expect(value).toContainEqual
;({name: 'nginx-service', kind: 'Service'})
expect(value).toContainEqual({
name: 'nginx-deployment',
kind: 'Deployment'
})
})
objectsToDelete = deleteWorkloadsAndServicesWithLabel(
kubectl,
GREEN_LABEL_VALUE,
testObjects.deploymentEntityList,
testObjects.serviceEntityList
)
objectsToDelete.then((value) => {
expect(value).toHaveLength(2)
expect(value).toContainEqual({
name: 'nginx-service-green',
kind: 'Service'
})
expect(value).toContainEqual({
name: 'nginx-deployment-green',
kind: 'Deployment'
})
})
})
})
@@ -40,7 +40,8 @@ export interface BlueGreenManifests {
export async function routeBlueGreen(
kubectl: Kubectl,
inputManifestFiles: string[],
routeStrategy: RouteStrategy
routeStrategy: RouteStrategy,
annotations: {[key: string]: string} = {}
) {
// sleep for buffer time
const bufferTime: number = parseInt(
@@ -74,7 +75,8 @@ export async function routeBlueGreen(
await routeBlueGreenSMI(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
annotations
)
} else {
await routeBlueGreenService(
@@ -110,6 +112,7 @@ export async function deleteWorkloadsWithLabel(
})
await deleteObjects(kubectl, resourcesToDelete)
return resourcesToDelete
}
export async function deleteWorkloadsAndServicesWithLabel(
@@ -141,6 +144,7 @@ export async function deleteWorkloadsAndServicesWithLabel(
})
await deleteObjects(kubectl, resourcesToDelete)
return resourcesToDelete
}
export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) {
@@ -235,9 +239,6 @@ export async function createWorkloadsWithLabel(
deploymentObjectList.forEach((inputObject) => {
// creating deployment with label
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel)
core.debug(
'New blue-green object is: ' + JSON.stringify(newBlueGreenObject)
)
newObjectsList.push(newBlueGreenObject)
})
@@ -278,7 +279,7 @@ export function addBlueGreenLabelsAndAnnotations(
updateObjectLabels(inputObject, newLabels, false)
updateSelectorLabels(inputObject, newLabels, false)
// updating spec labels if it is a service
// updating spec labels if it is not a service
if (!isServiceEntity(inputObject.kind)) {
updateSpecLabels(inputObject, newLabels, false)
}
@@ -0,0 +1,88 @@
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
import {
deployBlueGreenIngress,
getUpdatedBlueGreenIngress,
isIngressRouted,
routeBlueGreenIngress
} from './ingressBlueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
jest.mock('../../types/kubectl')
describe('ingress blue green helpers', () => {
let testObjects
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
})
test('it should correctly classify ingresses', () => {
expect(
isIngressRouted(
testObjects.ingressEntityList[0],
testObjects.serviceNameMap
)
).toBe(true)
testObjects.ingressEntityList[0].spec.rules[0].http.paths = {}
expect(
isIngressRouted(
testObjects.ingressEntityList[0],
testObjects.serviceNameMap
)
).toBe(false)
expect(
isIngressRouted(
getManifestObjects(betaFilepath).ingressEntityList[0],
testObjects.serviceNameMap
)
).toBe(true)
})
test('it should correctly update ingresses', () => {
const updatedIng = getUpdatedBlueGreenIngress(
testObjects.ingressEntityList[0],
testObjects.serviceNameMap,
GREEN_LABEL_VALUE
)
//@ts-ignore
expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green')
//@ts-ignore
expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe(
'nginx-service-green'
)
})
test('correctly prepares blue/green ingresses for deployment', () => {
const kc = new Kubectl('')
const generatedObjects = routeBlueGreenIngress(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceNameMap,
testObjects.ingressEntityList
)
generatedObjects.then((value) => {
expect(value).toHaveLength(1)
//@ts-ignore
expect(value[0].metadata.name).toBe('nginx-ingress')
})
})
test('correctly deploys services', () => {
const kc = new Kubectl('')
const result = deployBlueGreenIngress(kc, ingressFilepath)
result.then((value) => {
const nol = value.newObjectsList
//@ts-ignore
expect(nol[0].metadata.name).toBe('nginx-service-green')
})
})
})
@@ -14,7 +14,7 @@ import {
} from './blueGreenHelper'
import * as core from '@actions/core'
const BACKEND = 'BACKEND'
const BACKEND = 'backend'
export async function deployBlueGreenIngress(
kubectl: Kubectl,
@@ -24,13 +24,12 @@ export async function deployBlueGreenIngress(
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// create deployments with green label value
const result = createWorkloadsWithLabel(
const workloadDeployment = await createWorkloadsWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
)
// create new services and other objects
let newObjectsList = []
manifestObjects.serviceEntityList.forEach((inputObject) => {
const newBlueGreenObject = getNewBlueGreenObject(
@@ -46,7 +45,12 @@ export async function deployBlueGreenIngress(
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
await kubectl.apply(manifestFiles)
return result
core.debug(
'new objects after processing services and other objects: \n' +
JSON.stringify(newObjectsList)
)
return {workloadDeployment, newObjectsList}
}
export async function promoteBlueGreenIngress(
@@ -54,14 +58,13 @@ export async function promoteBlueGreenIngress(
manifestObjects
) {
//checking if anything to promote
if (
!validateIngressesState(
kubectl,
manifestObjects.ingressEntityList,
manifestObjects.serviceNameMap
)
) {
throw 'Ingress not in promote state'
const {areValid, invalidIngresses} = validateIngresses(
kubectl,
manifestObjects.ingressEntityList,
manifestObjects.serviceNameMap
)
if (!areValid) {
throw 'Ingresses are not in promote state' + invalidIngresses.toString()
}
// create stable deployments with new configuration
@@ -138,17 +141,18 @@ export async function routeBlueGreenIngress(
})
}
core.debug('New objects: ' + JSON.stringify(newObjectsList))
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
await kubectl.apply(manifestFiles)
return newObjectsList
}
export function validateIngressesState(
export function validateIngresses(
kubectl: Kubectl,
ingressEntityList: any[],
serviceNameMap: Map<string, string>
): boolean {
let areIngressesTargetingNewServices: boolean = true
): {areValid: boolean; invalidIngresses: string[]} {
let areValid: boolean = true
const invalidIngresses = []
ingressEntityList.forEach(async (inputObject) => {
if (isIngressRouted(inputObject, serviceNameMap)) {
//querying existing ingress
@@ -158,33 +162,32 @@ export function validateIngressesState(
inputObject.metadata.name
)
if (!!existingIngress) {
const currentLabel: string =
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL]
// if not green label, then wrong configuration
if (currentLabel != GREEN_LABEL_VALUE)
areIngressesTargetingNewServices = false
} else {
// no ingress at all, so nothing to promote
areIngressesTargetingNewServices = false
let isValid =
!!existingIngress &&
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] ===
GREEN_LABEL_VALUE
if (!isValid) {
invalidIngresses.push(inputObject.metadata.name)
}
// to be valid, ingress should exist and should be green
areValid = areValid && isValid
}
})
return areIngressesTargetingNewServices
return {areValid, invalidIngresses}
}
function isIngressRouted(
export function isIngressRouted(
ingressObject: any,
serviceNameMap: Map<string, string>
): boolean {
let isIngressRouted: boolean = false
// check if ingress targets a service in the given manifests
JSON.parse(JSON.stringify(ingressObject), (key, value) => {
if (key === 'serviceName' && serviceNameMap.has(value)) {
isIngressRouted = true
}
isIngressRouted =
isIngressRouted || (key === 'service' && value.hasOwnProperty('name'))
isIngressRouted =
isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value))
return value
})
@@ -206,15 +209,18 @@ export function getUpdatedBlueGreenIngress(
addBlueGreenLabelsAndAnnotations(newObject, type)
// update ingress labels
if (inputObject.apiVersion === 'networking.k8s.io/v1beta1') {
return updateIngressBackendBetaV1(newObject, serviceNameMap)
}
return updateIngressBackend(newObject, serviceNameMap)
}
export function updateIngressBackend(
export function updateIngressBackendBetaV1(
inputObject: any,
serviceNameMap: Map<string, string>
): any {
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
if (key.toUpperCase() === BACKEND) {
if (key.toLowerCase() === BACKEND) {
const {serviceName} = value
if (serviceNameMap.has(serviceName)) {
// update service name with corresponding bluegreen name only if service is provied in given manifests
@@ -227,3 +233,20 @@ export function updateIngressBackend(
return inputObject
}
export function updateIngressBackend(
inputObject: any,
serviceNameMap: Map<string, string>
): any {
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
if (
key.toLowerCase() === BACKEND &&
serviceNameMap.has(value?.service?.name)
) {
value.service.name = serviceNameMap.get(value.service.name)
}
return value
})
return inputObject
}
@@ -19,21 +19,21 @@ export async function deployBlueGreenService(
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// create deployments with green label value
const result = await createWorkloadsWithLabel(
const workloadDeployment = await createWorkloadsWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
)
// create other non deployment and non service entities
const newObjectsList = manifestObjects.otherObjects
.concat(manifestObjects.ingressEntityList)
.concat(manifestObjects.unroutedServiceEntityList)
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
if (manifestFiles.length > 0) await kubectl.apply(manifestFiles)
// returning deployment details to check for rollout stability
return result
return {workloadDeployment, newObjectsList}
}
export async function promoteBlueGreenService(
@@ -76,7 +76,6 @@ export async function rejectBlueGreenService(
manifestObjects.deploymentEntityList
)
}
export async function routeBlueGreenService(
kubectl: Kubectl,
nextLabel: string,
@@ -23,7 +23,8 @@ const MAX_VAL = 100
export async function deployBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
annotations: {[key: string]: string} = {}
) {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -37,14 +38,16 @@ export async function deployBlueGreenSMI(
await kubectl.apply(manifestFiles)
// make extraservices and trafficsplit
await setupSMI(kubectl, manifestObjects.serviceEntityList)
await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations)
// create new deloyments
return await createWorkloadsWithLabel(
const workloadDeployment = await createWorkloadsWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
)
return {workloadDeployment, newObjectsList}
}
export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
@@ -68,16 +71,18 @@ export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
export async function rejectBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
annotations: {[key: string]: string} = {}
) {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// route trafficsplit to stable deploymetns
// route trafficsplit to stable deployments
await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
annotations
)
// delete rejected new bluegreen deployments
@@ -91,7 +96,11 @@ export async function rejectBlueGreenSMI(
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
}
export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
export async function setupSMI(
kubectl: Kubectl,
serviceEntityList: any[],
annotations: {[key: string]: string} = {}
) {
const newObjectsList = []
const trafficObjectList = []
@@ -117,7 +126,8 @@ export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
createTrafficSplitObject(
kubectl,
inputObject.metadata.name,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
annotations
)
})
}
@@ -127,7 +137,8 @@ let trafficSplitAPIVersion = ''
async function createTrafficSplitObject(
kubectl: Kubectl,
name: string,
nextLabel: string
nextLabel: string,
annotations: {[key: string]: string} = {}
): Promise<any> {
// cache traffic split api version
if (!trafficSplitAPIVersion)
@@ -145,7 +156,8 @@ async function createTrafficSplitObject(
apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit',
metadata: {
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
annotations: annotations
},
spec: {
service: name,
@@ -194,14 +206,16 @@ export function getSMIServiceResource(
export async function routeBlueGreenSMI(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[]
serviceEntityList: any[],
annotations: {[key: string]: string} = {}
) {
for (const serviceObject of serviceEntityList) {
// route trafficsplit to given label
await createTrafficSplitObject(
kubectl,
serviceObject.metadata.name,
nextLabel
nextLabel,
annotations
)
}
}
@@ -288,7 +288,8 @@ async function getTrafficSplitObject(
name: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
canaryWeight: number,
annotations: {[key: string]: string} = {}
): Promise<string> {
// cached version
if (!trafficSplitAPIVersion) {
@@ -301,7 +302,8 @@ async function getTrafficSplitObject(
apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit',
metadata: {
name: getTrafficSplitResourceName(name)
name: getTrafficSplitResourceName(name),
annotations: annotations
},
spec: {
backends: [
+7 -6
View File
@@ -41,7 +41,8 @@ export async function deployManifests(
files: string[],
deploymentStrategy: DeploymentStrategy,
kubectl: Kubectl,
trafficSplitMethod: TrafficSplitMethod
trafficSplitMethod: TrafficSplitMethod,
annotations: {[key: string]: string} = {}
): Promise<string[]> {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: {
@@ -59,16 +60,16 @@ export async function deployManifests(
core.getInput('route-method', {required: true})
)
const {result, newFilePaths} = await Promise.resolve(
const {workloadDeployment, newObjectsList} = await Promise.resolve(
(routeStrategy == RouteStrategy.INGRESS &&
deployBlueGreenIngress(kubectl, files)) ||
(routeStrategy == RouteStrategy.SMI &&
deployBlueGreenSMI(kubectl, files)) ||
deployBlueGreenSMI(kubectl, files, annotations)) ||
deployBlueGreenService(kubectl, files)
)
checkForErrors([result])
return newFilePaths
checkForErrors([workloadDeployment.result])
return workloadDeployment.newFilePaths
}
case DeploymentStrategy.BASIC: {
@@ -142,7 +143,7 @@ export async function annotateAndLabelResources(
const workflowFilePath = await getWorkflowFilePath(githubToken)
const deploymentConfig = await getDeploymentConfig()
const annotationKeyLabel = getWorkflowAnnotationKeyLabel(workflowFilePath)
const annotationKeyLabel = getWorkflowAnnotationKeyLabel()
await annotateResources(
files,