Compare commits

..

10 Commits

Author SHA1 Message Date
Vidya Reddy 6b03d40a74 revert the docker changes 2022-07-28 19:48:37 +05:30
Vidya Reddy e456a366fc Fixes for docker with no image 2022-07-28 19:36:14 +05:30
Vidya Reddy c97519147a updated the annotation prefix 2022-07-27 22:35:03 +05:30
Vidya Reddy 1516e1430d updated the annotation key label 2022-07-27 22:23:32 +05:30
Vidya Reddy 0a3b59b275 add annotation key prefix 2022-07-27 14:38:28 +05:30
Vidya Reddy d96b9b2965 add annotation key prefix 2022-07-27 14:33:12 +05:30
Vidya Reddy 71e93a71d4 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>
2022-07-25 13:43:13 -04:00
Oliver King 19d66d6bdb add clean function (#211) 2022-07-06 16:15:31 -04:00
Hariharan Subramanian 72a09f4051 Logging Changes for Promote, Reject actions (#207) 2022-07-06 10:41:48 -04:00
Vidya Reddy a17f35ba63 Add ncc build to build script (#208)
Co-authored-by: Vidya Reddy <vidyareddy@microsoft.com>
2022-07-05 10:16:41 -07:00
13 changed files with 131 additions and 80 deletions
+3
View File
@@ -74,6 +74,9 @@ Following are the key capabilities of this action:
<td>traffic-split-method </br></br>(Optional)</td>
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
</tr>
<tr>
<td>traffic-split-annotations </br></br>(Optional)</td>
<td>Annotations in the form of key/value pair to be added to TrafficSplit.</td>
<tr>
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
<td>Used to compute the number of replicas of &#39;-baseline&#39; and &#39;-canary&#39; variants of the workloads found in manifest files. For the specified percentage input, if (percentage * numberOfDesirerdReplicas)/100 is not a round number, the floor of this number is used while creating &#39;-baseline&#39; and &#39;-canary&#39;.<br/><br/>For example, if Deployment hello-world was found in the input manifest file with &#39;replicas: 4&#39; and if &#39;strategy: canary&#39; and &#39;percentage: 25&#39; are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The &#39;-baseline&#39; variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the &#39;-canary&#39; variant is created with the image and tag corresponding to the new changes being deployed</td>
+3
View File
@@ -35,6 +35,9 @@ inputs:
description: 'Traffic split method to be used. Allowed values are pod and smi'
required: false
default: 'pod'
traffic-split-annotations:
description: 'Annotations in the form of key/value pair to be added to TrafficSplit. Relevant only if deployement strategy is blue-green or canary'
required: false
baseline-and-canary-replicas:
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
required: false
+4 -2
View File
@@ -19,7 +19,8 @@ import {parseRouteStrategy} from '../types/routeStrategy'
export async function deploy(
kubectl: Kubectl,
manifestFilePaths: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) {
// update manifests
const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths)
@@ -34,7 +35,8 @@ export async function deploy(
inputManifestFiles,
deploymentStrategy,
kubectl,
trafficSplitMethod
trafficSplitMethod,
annotations
)
core.endGroup()
core.debug('Deployed manifest files: ' + deployedManifestFiles)
+26 -12
View File
@@ -40,14 +40,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function promote(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await promoteCanary(kubectl, manifests)
break
case DeploymentStrategy.BLUE_GREEN:
await promoteBlueGreen(kubectl, manifests)
await promoteBlueGreen(kubectl, manifests, annotations)
break
default:
throw Error('Invalid promote deployment strategy')
@@ -65,26 +66,30 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
// In case of SMI traffic split strategy when deployment is promoted, first we will redirect traffic to
// canary deployment, then update stable deployment and then redirect traffic to stable deployment
core.info('Redirecting traffic to canary deployment')
core.startGroup('Redirecting traffic to canary deployment')
await SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(
kubectl,
manifests
)
core.endGroup()
core.info('Deploying input manifests with SMI canary strategy')
core.startGroup('Deploying input manifests with SMI canary strategy')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.endGroup()
core.info('Redirecting traffic to stable deployment')
core.startGroup('Redirecting traffic to stable deployment')
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
)
core.endGroup()
} else {
core.info('Deploying input manifests')
core.startGroup('Deploying input manifests')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.endGroup()
}
core.info('Deleting canary and baseline workloads')
core.startGroup('Deleting canary and baseline workloads')
try {
await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl,
@@ -97,9 +102,14 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
ex
)
}
core.endGroup()
}
async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
async function promoteBlueGreen(
kubectl: Kubectl,
manifests: string[],
annotations: {[key: string]: string} = {}
) {
// update container images and pull secrets
const inputManifestFiles: string[] = updateManifestFiles(manifests)
const manifestObjects: BlueGreenManifests =
@@ -109,7 +119,7 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
core.getInput('route-method', {required: true})
)
core.info('Deleting old deployment and making new one')
core.startGroup('Deleting old deployment and making new one')
let result
if (routeStrategy == RouteStrategy.INGRESS) {
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
@@ -118,9 +128,10 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
} else {
result = await promoteBlueGreenService(kubectl, manifestObjects)
}
core.endGroup()
// checking stability of newly created deployments
core.info('Checking manifest stability')
core.startGroup('Checking manifest stability')
const deployedManifestFiles = result.newFilePaths
const resources: Resource[] = getResources(
deployedManifestFiles,
@@ -129,8 +140,9 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
])
)
await KubernetesManifestUtility.checkManifestStability(kubectl, resources)
core.endGroup()
core.info(
core.startGroup(
'Routing to new deployments and deleting old workloads and services'
)
if (routeStrategy == RouteStrategy.INGRESS) {
@@ -150,7 +162,8 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
annotations
)
await deleteWorkloadsWithLabel(
kubectl,
@@ -170,4 +183,5 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
manifestObjects.deploymentEntityList
)
}
core.endGroup()
}
+15 -7
View File
@@ -15,14 +15,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function reject(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await rejectCanary(kubectl, manifests)
break
case DeploymentStrategy.BLUE_GREEN:
await rejectBlueGreen(kubectl, manifests)
await rejectBlueGreen(kubectl, manifests, annotations)
break
default:
throw 'Invalid delete deployment strategy'
@@ -36,24 +37,30 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
core.getInput('traffic-split-method', {required: true})
)
if (trafficSplitMethod == TrafficSplitMethod.SMI) {
core.info('Rejecting deployment with SMI canary strategy')
core.startGroup('Rejecting deployment with SMI canary strategy')
includeServices = true
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
)
core.endGroup()
}
core.info('Deleting baseline and canary workloads')
core.startGroup('Deleting baseline and canary workloads')
await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl,
manifests,
includeServices
)
core.endGroup()
}
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
core.info('Rejecting deployment with blue green strategy')
async function rejectBlueGreen(
kubectl: Kubectl,
manifests: string[],
annotations: {[key: string]: string} = {}
) {
core.startGroup('Rejecting deployment with blue green strategy')
const routeStrategy = parseRouteStrategy(
core.getInput('route-method', {required: true})
@@ -61,8 +68,9 @@ async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
if (routeStrategy == RouteStrategy.INGRESS) {
await rejectBlueGreenIngress(kubectl, manifests)
} else if (routeStrategy == RouteStrategy.SMI) {
await rejectBlueGreenSMI(kubectl, manifests)
await rejectBlueGreenSMI(kubectl, manifests, annotations)
} else {
await rejectBlueGreenService(kubectl, manifests)
}
core.endGroup()
}
+7 -3
View File
@@ -6,6 +6,7 @@ import {reject} from './actions/reject'
import {Action, parseAction} from './types/action'
import {parseDeploymentStrategy} from './types/deploymentStrategy'
import {getFilesFromDirectories} from './utilities/fileUtils'
import {parseAnnotations} from './types/annotations'
export async function run() {
// verify kubeconfig is set
@@ -18,6 +19,9 @@ export async function run() {
const action: Action | undefined = parseAction(
core.getInput('action', {required: true})
)
const annotations = parseAnnotations(
core.getInput('annotations', {required: false})
)
const strategy = parseDeploymentStrategy(core.getInput('strategy'))
const manifestsInput = core.getInput('manifests', {required: true})
const manifestFilePaths = manifestsInput
@@ -34,15 +38,15 @@ export async function run() {
// run action
switch (action) {
case Action.DEPLOY: {
await deploy(kubectl, fullManifestFilePaths, strategy)
await deploy(kubectl, fullManifestFilePaths, strategy, annotations)
break
}
case Action.PROMOTE: {
await promote(kubectl, fullManifestFilePaths, strategy)
await promote(kubectl, fullManifestFilePaths, strategy, annotations)
break
}
case Action.REJECT: {
await reject(kubectl, fullManifestFilePaths, strategy)
await reject(kubectl, fullManifestFilePaths, strategy, annotations)
break
}
default: {
@@ -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(
@@ -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,7 +38,7 @@ 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(
@@ -68,16 +69,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 +94,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 +124,8 @@ export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
createTrafficSplitObject(
kubectl,
inputObject.metadata.name,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
annotations
)
})
}
@@ -127,7 +135,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 +154,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 +204,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: [
+9 -7
View File
@@ -23,7 +23,8 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
import {ExecOutput} from '@actions/exec'
import {
getWorkflowAnnotationKeyLabel,
getWorkflowAnnotations
getWorkflowAnnotations,
cleanLabel
} from '../utilities/workflowAnnotationUtils'
import {
annotateChildPods,
@@ -40,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: {
@@ -62,7 +64,7 @@ export async function deployManifests(
(routeStrategy == RouteStrategy.INGRESS &&
deployBlueGreenIngress(kubectl, files)) ||
(routeStrategy == RouteStrategy.SMI &&
deployBlueGreenSMI(kubectl, files)) ||
deployBlueGreenSMI(kubectl, files, annotations)) ||
deployBlueGreenService(kubectl, files)
)
@@ -141,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,
@@ -214,10 +216,10 @@ async function labelResources(
label: string
) {
const labels = [
`workflowFriendlyName=${normalizeWorkflowStrLabel(
process.env.GITHUB_WORKFLOW
`workflowFriendlyName=${cleanLabel(
normalizeWorkflowStrLabel(process.env.GITHUB_WORKFLOW)
)}`,
`workflow=${label}`
`workflow=${cleanLabel(label)}`
]
checkForErrors([await kubectl.labelFiles(files, labels)], true)
+8
View File
@@ -0,0 +1,8 @@
export function parseAnnotations(str: string) {
if (str == '') {
return {}
} else {
const annotaion = JSON.parse(str)
return new Map(annotaion)
}
}
+10 -13
View File
@@ -1,18 +1,15 @@
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils'
import {cleanLabel} from '../utilities/workflowAnnotationUtils'
describe('WorkflowAnnotationUtils', () => {
describe('prefixObjectKeys', () => {
it('should prefix an object with a given prefix', () => {
const obj = {
foo: 'bar',
baz: 'qux'
}
const prefix = 'prefix.'
const expected = {
'prefix.foo': 'bar',
'prefix.baz': 'qux'
}
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
describe('cleanLabel', () => {
it('should clean label', () => {
const alreadyClean = 'alreadyClean'
expect(cleanLabel(alreadyClean)).toEqual(alreadyClean)
expect(cleanLabel('.startInvalid')).toEqual('startInvalid')
expect(cleanLabel('with%S0ME&invalid#chars')).toEqual(
'withS0MEinvalidchars'
)
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
})
})
})
+15 -21
View File
@@ -1,13 +1,6 @@
import {DeploymentConfig} from '../types/deploymentConfig'
const ANNOTATION_PREFIX = 'actions.github.com/'
export function prefixObjectKeys(obj: any, prefix: string): any {
return Object.keys(obj).reduce((newObj, key) => {
newObj[prefix + key] = obj[key]
return newObj
}, {})
}
const ANNOTATION_PREFIX = 'actions.github.com'
export function getWorkflowAnnotations(
lastSuccessRunSha: string,
@@ -31,19 +24,20 @@ export function getWorkflowAnnotations(
helmChartPaths: deploymentConfig.helmChartFilePaths,
provider: 'GitHub'
}
const prefixedAnnotationObject = prefixObjectKeys(
annotationObject,
ANNOTATION_PREFIX
)
return JSON.stringify(prefixedAnnotationObject)
return JSON.stringify(annotationObject)
}
export function getWorkflowAnnotationKeyLabel(
workflowFilePath: string
): string {
const hashKey = require('crypto')
.createHash('MD5')
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
.digest('hex')
return `githubWorkflow_${hashKey}`
export function getWorkflowAnnotationKeyLabel(): string {
return `${ANNOTATION_PREFIX}/k8s-deploy`
}
/**
* Cleans label to match valid kubernetes label specification by removing invalid characters
* @param label
* @returns cleaned label
*/
export function cleanLabel(label: string): string {
const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '')
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
return regex.exec(removedInvalidChars)[0] || ''
}