Compare commits

...

8 Commits

Author SHA1 Message Date
Vidya Reddy ce6bc40bc4 added regex to check whitespace 2022-08-06 10:25:18 +05:30
Vidya Reddy 36041268eb fixes 2022-08-04 15:45:13 +05:30
Vidya Reddy 36b55e389b fixes 2022-07-29 17:58:29 +05:30
Vidya Reddy 6e560d48e7 Fixes to the docker with no images 2022-07-28 19:54:59 +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
16 changed files with 156 additions and 48 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>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> <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>
<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> <tr>
<td>percentage </br></br>(Optional but required if strategy is canary)</td> <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> <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' description: 'Traffic split method to be used. Allowed values are pod and smi'
required: false required: false
default: 'pod' 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: baseline-and-canary-replicas:
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)' description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
required: false required: false
+16
View File
@@ -22,6 +22,7 @@
"@types/jest": "^26.0.0", "@types/jest": "^26.0.0",
"@types/js-yaml": "^3.12.7", "@types/js-yaml": "^3.12.7",
"@types/node": "^12.20.41", "@types/node": "^12.20.41",
"@vercel/ncc": "^0.34.0",
"jest": "^26.0.0", "jest": "^26.0.0",
"prettier": "2.7.1", "prettier": "2.7.1",
"ts-jest": "^26.0.0", "ts-jest": "^26.0.0",
@@ -1155,6 +1156,15 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
"dev": true "dev": true
}, },
"node_modules/@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
"dev": true,
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/abab": { "node_modules/abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -7086,6 +7096,12 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
"dev": true "dev": true
}, },
"@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
"dev": true
},
"abab": { "abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
+2 -1
View File
@@ -4,7 +4,7 @@
"author": "Deepak Sattiraju", "author": "Deepak Sattiraju",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "tsc --outDir ./lib --rootDir ./src", "build": "ncc build src/run.ts -o lib",
"test": "jest", "test": "jest",
"format": "prettier --write .", "format": "prettier --write .",
"format-check": "prettier --check ." "format-check": "prettier --check ."
@@ -23,6 +23,7 @@
"@types/jest": "^26.0.0", "@types/jest": "^26.0.0",
"@types/js-yaml": "^3.12.7", "@types/js-yaml": "^3.12.7",
"@types/node": "^12.20.41", "@types/node": "^12.20.41",
"@vercel/ncc": "^0.34.0",
"jest": "^26.0.0", "jest": "^26.0.0",
"prettier": "2.7.1", "prettier": "2.7.1",
"ts-jest": "^26.0.0", "ts-jest": "^26.0.0",
+4 -2
View File
@@ -19,7 +19,8 @@ import {parseRouteStrategy} from '../types/routeStrategy'
export async function deploy( export async function deploy(
kubectl: Kubectl, kubectl: Kubectl,
manifestFilePaths: string[], manifestFilePaths: string[],
deploymentStrategy: DeploymentStrategy deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) { ) {
// update manifests // update manifests
const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths) const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths)
@@ -34,7 +35,8 @@ export async function deploy(
inputManifestFiles, inputManifestFiles,
deploymentStrategy, deploymentStrategy,
kubectl, kubectl,
trafficSplitMethod trafficSplitMethod,
annotations
) )
core.endGroup() core.endGroup()
core.debug('Deployed manifest files: ' + deployedManifestFiles) core.debug('Deployed manifest files: ' + deployedManifestFiles)
+26 -12
View File
@@ -40,14 +40,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function promote( export async function promote(
kubectl: Kubectl, kubectl: Kubectl,
manifests: string[], manifests: string[],
deploymentStrategy: DeploymentStrategy deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) { ) {
switch (deploymentStrategy) { switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: case DeploymentStrategy.CANARY:
await promoteCanary(kubectl, manifests) await promoteCanary(kubectl, manifests)
break break
case DeploymentStrategy.BLUE_GREEN: case DeploymentStrategy.BLUE_GREEN:
await promoteBlueGreen(kubectl, manifests) await promoteBlueGreen(kubectl, manifests, annotations)
break break
default: default:
throw Error('Invalid promote deployment strategy') 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 // 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 // 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( await SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(
kubectl, kubectl,
manifests 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) 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( await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl, kubectl,
manifests manifests
) )
core.endGroup()
} else { } else {
core.info('Deploying input manifests') core.startGroup('Deploying input manifests')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY) await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.endGroup()
} }
core.info('Deleting canary and baseline workloads') core.startGroup('Deleting canary and baseline workloads')
try { try {
await canaryDeploymentHelper.deleteCanaryDeployment( await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl, kubectl,
@@ -97,9 +102,14 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
ex 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 // update container images and pull secrets
const inputManifestFiles: string[] = updateManifestFiles(manifests) const inputManifestFiles: string[] = updateManifestFiles(manifests)
const manifestObjects: BlueGreenManifests = const manifestObjects: BlueGreenManifests =
@@ -109,7 +119,7 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
core.getInput('route-method', {required: true}) 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 let result
if (routeStrategy == RouteStrategy.INGRESS) { if (routeStrategy == RouteStrategy.INGRESS) {
result = await promoteBlueGreenIngress(kubectl, manifestObjects) result = await promoteBlueGreenIngress(kubectl, manifestObjects)
@@ -118,9 +128,10 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
} else { } else {
result = await promoteBlueGreenService(kubectl, manifestObjects) result = await promoteBlueGreenService(kubectl, manifestObjects)
} }
core.endGroup()
// checking stability of newly created deployments // checking stability of newly created deployments
core.info('Checking manifest stability') core.startGroup('Checking manifest stability')
const deployedManifestFiles = result.newFilePaths const deployedManifestFiles = result.newFilePaths
const resources: Resource[] = getResources( const resources: Resource[] = getResources(
deployedManifestFiles, deployedManifestFiles,
@@ -129,8 +140,9 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
]) ])
) )
await KubernetesManifestUtility.checkManifestStability(kubectl, resources) await KubernetesManifestUtility.checkManifestStability(kubectl, resources)
core.endGroup()
core.info( core.startGroup(
'Routing to new deployments and deleting old workloads and services' 'Routing to new deployments and deleting old workloads and services'
) )
if (routeStrategy == RouteStrategy.INGRESS) { if (routeStrategy == RouteStrategy.INGRESS) {
@@ -150,7 +162,8 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
await routeBlueGreenSMI( await routeBlueGreenSMI(
kubectl, kubectl,
NONE_LABEL_VALUE, NONE_LABEL_VALUE,
manifestObjects.serviceEntityList manifestObjects.serviceEntityList,
annotations
) )
await deleteWorkloadsWithLabel( await deleteWorkloadsWithLabel(
kubectl, kubectl,
@@ -170,4 +183,5 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
manifestObjects.deploymentEntityList manifestObjects.deploymentEntityList
) )
} }
core.endGroup()
} }
+15 -7
View File
@@ -15,14 +15,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function reject( export async function reject(
kubectl: Kubectl, kubectl: Kubectl,
manifests: string[], manifests: string[],
deploymentStrategy: DeploymentStrategy deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
) { ) {
switch (deploymentStrategy) { switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: case DeploymentStrategy.CANARY:
await rejectCanary(kubectl, manifests) await rejectCanary(kubectl, manifests)
break break
case DeploymentStrategy.BLUE_GREEN: case DeploymentStrategy.BLUE_GREEN:
await rejectBlueGreen(kubectl, manifests) await rejectBlueGreen(kubectl, manifests, annotations)
break break
default: default:
throw 'Invalid delete deployment strategy' throw 'Invalid delete deployment strategy'
@@ -36,24 +37,30 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
core.getInput('traffic-split-method', {required: true}) core.getInput('traffic-split-method', {required: true})
) )
if (trafficSplitMethod == TrafficSplitMethod.SMI) { if (trafficSplitMethod == TrafficSplitMethod.SMI) {
core.info('Rejecting deployment with SMI canary strategy') core.startGroup('Rejecting deployment with SMI canary strategy')
includeServices = true includeServices = true
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment( await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl, kubectl,
manifests manifests
) )
core.endGroup()
} }
core.info('Deleting baseline and canary workloads') core.startGroup('Deleting baseline and canary workloads')
await canaryDeploymentHelper.deleteCanaryDeployment( await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl, kubectl,
manifests, manifests,
includeServices includeServices
) )
core.endGroup()
} }
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) { async function rejectBlueGreen(
core.info('Rejecting deployment with blue green strategy') kubectl: Kubectl,
manifests: string[],
annotations: {[key: string]: string} = {}
) {
core.startGroup('Rejecting deployment with blue green strategy')
const routeStrategy = parseRouteStrategy( const routeStrategy = parseRouteStrategy(
core.getInput('route-method', {required: true}) core.getInput('route-method', {required: true})
@@ -61,8 +68,9 @@ async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
if (routeStrategy == RouteStrategy.INGRESS) { if (routeStrategy == RouteStrategy.INGRESS) {
await rejectBlueGreenIngress(kubectl, manifests) await rejectBlueGreenIngress(kubectl, manifests)
} else if (routeStrategy == RouteStrategy.SMI) { } else if (routeStrategy == RouteStrategy.SMI) {
await rejectBlueGreenSMI(kubectl, manifests) await rejectBlueGreenSMI(kubectl, manifests, annotations)
} else { } else {
await rejectBlueGreenService(kubectl, manifests) 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 {Action, parseAction} from './types/action'
import {parseDeploymentStrategy} from './types/deploymentStrategy' import {parseDeploymentStrategy} from './types/deploymentStrategy'
import {getFilesFromDirectories} from './utilities/fileUtils' import {getFilesFromDirectories} from './utilities/fileUtils'
import {parseAnnotations} from './types/annotations'
export async function run() { export async function run() {
// verify kubeconfig is set // verify kubeconfig is set
@@ -18,6 +19,9 @@ export async function run() {
const action: Action | undefined = parseAction( const action: Action | undefined = parseAction(
core.getInput('action', {required: true}) core.getInput('action', {required: true})
) )
const annotations = parseAnnotations(
core.getInput('annotations', {required: false})
)
const strategy = parseDeploymentStrategy(core.getInput('strategy')) const strategy = parseDeploymentStrategy(core.getInput('strategy'))
const manifestsInput = core.getInput('manifests', {required: true}) const manifestsInput = core.getInput('manifests', {required: true})
const manifestFilePaths = manifestsInput const manifestFilePaths = manifestsInput
@@ -34,15 +38,15 @@ export async function run() {
// run action // run action
switch (action) { switch (action) {
case Action.DEPLOY: { case Action.DEPLOY: {
await deploy(kubectl, fullManifestFilePaths, strategy) await deploy(kubectl, fullManifestFilePaths, strategy, annotations)
break break
} }
case Action.PROMOTE: { case Action.PROMOTE: {
await promote(kubectl, fullManifestFilePaths, strategy) await promote(kubectl, fullManifestFilePaths, strategy, annotations)
break break
} }
case Action.REJECT: { case Action.REJECT: {
await reject(kubectl, fullManifestFilePaths, strategy) await reject(kubectl, fullManifestFilePaths, strategy, annotations)
break break
} }
default: { default: {
@@ -40,7 +40,8 @@ export interface BlueGreenManifests {
export async function routeBlueGreen( export async function routeBlueGreen(
kubectl: Kubectl, kubectl: Kubectl,
inputManifestFiles: string[], inputManifestFiles: string[],
routeStrategy: RouteStrategy routeStrategy: RouteStrategy,
annotations: {[key: string]: string} = {}
) { ) {
// sleep for buffer time // sleep for buffer time
const bufferTime: number = parseInt( const bufferTime: number = parseInt(
@@ -74,7 +75,8 @@ export async function routeBlueGreen(
await routeBlueGreenSMI( await routeBlueGreenSMI(
kubectl, kubectl,
GREEN_LABEL_VALUE, GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList manifestObjects.serviceEntityList,
annotations
) )
} else { } else {
await routeBlueGreenService( await routeBlueGreenService(
@@ -23,7 +23,8 @@ const MAX_VAL = 100
export async function deployBlueGreenSMI( export async function deployBlueGreenSMI(
kubectl: Kubectl, kubectl: Kubectl,
filePaths: string[] filePaths: string[],
annotations: {[key: string]: string} = {}
) { ) {
// get all kubernetes objects defined in manifest files // get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -37,7 +38,7 @@ export async function deployBlueGreenSMI(
await kubectl.apply(manifestFiles) await kubectl.apply(manifestFiles)
// make extraservices and trafficsplit // make extraservices and trafficsplit
await setupSMI(kubectl, manifestObjects.serviceEntityList) await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations)
// create new deloyments // create new deloyments
return await createWorkloadsWithLabel( return await createWorkloadsWithLabel(
@@ -68,16 +69,18 @@ export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
export async function rejectBlueGreenSMI( export async function rejectBlueGreenSMI(
kubectl: Kubectl, kubectl: Kubectl,
filePaths: string[] filePaths: string[],
annotations: {[key: string]: string} = {}
) { ) {
// get all kubernetes objects defined in manifest files // get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// route trafficsplit to stable deploymetns // route trafficsplit to stable deployments
await routeBlueGreenSMI( await routeBlueGreenSMI(
kubectl, kubectl,
NONE_LABEL_VALUE, NONE_LABEL_VALUE,
manifestObjects.serviceEntityList manifestObjects.serviceEntityList,
annotations
) )
// delete rejected new bluegreen deployments // delete rejected new bluegreen deployments
@@ -91,7 +94,11 @@ export async function rejectBlueGreenSMI(
await cleanupSMI(kubectl, manifestObjects.serviceEntityList) 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 newObjectsList = []
const trafficObjectList = [] const trafficObjectList = []
@@ -117,7 +124,8 @@ export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
createTrafficSplitObject( createTrafficSplitObject(
kubectl, kubectl,
inputObject.metadata.name, inputObject.metadata.name,
NONE_LABEL_VALUE NONE_LABEL_VALUE,
annotations
) )
}) })
} }
@@ -127,7 +135,8 @@ let trafficSplitAPIVersion = ''
async function createTrafficSplitObject( async function createTrafficSplitObject(
kubectl: Kubectl, kubectl: Kubectl,
name: string, name: string,
nextLabel: string nextLabel: string,
annotations: {[key: string]: string} = {}
): Promise<any> { ): Promise<any> {
// cache traffic split api version // cache traffic split api version
if (!trafficSplitAPIVersion) if (!trafficSplitAPIVersion)
@@ -145,7 +154,8 @@ async function createTrafficSplitObject(
apiVersion: trafficSplitAPIVersion, apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit', kind: 'TrafficSplit',
metadata: { metadata: {
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX) name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
annotations: annotations
}, },
spec: { spec: {
service: name, service: name,
@@ -194,14 +204,16 @@ export function getSMIServiceResource(
export async function routeBlueGreenSMI( export async function routeBlueGreenSMI(
kubectl: Kubectl, kubectl: Kubectl,
nextLabel: string, nextLabel: string,
serviceEntityList: any[] serviceEntityList: any[],
annotations: {[key: string]: string} = {}
) { ) {
for (const serviceObject of serviceEntityList) { for (const serviceObject of serviceEntityList) {
// route trafficsplit to given label // route trafficsplit to given label
await createTrafficSplitObject( await createTrafficSplitObject(
kubectl, kubectl,
serviceObject.metadata.name, serviceObject.metadata.name,
nextLabel nextLabel,
annotations
) )
} }
} }
@@ -288,7 +288,8 @@ async function getTrafficSplitObject(
name: string, name: string,
stableWeight: number, stableWeight: number,
baselineWeight: number, baselineWeight: number,
canaryWeight: number canaryWeight: number,
annotations: {[key: string]: string} = {}
): Promise<string> { ): Promise<string> {
// cached version // cached version
if (!trafficSplitAPIVersion) { if (!trafficSplitAPIVersion) {
@@ -301,7 +302,8 @@ async function getTrafficSplitObject(
apiVersion: trafficSplitAPIVersion, apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit', kind: 'TrafficSplit',
metadata: { metadata: {
name: getTrafficSplitResourceName(name) name: getTrafficSplitResourceName(name),
annotations: annotations
}, },
spec: { spec: {
backends: [ backends: [
+8 -6
View File
@@ -23,7 +23,8 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
import {ExecOutput} from '@actions/exec' import {ExecOutput} from '@actions/exec'
import { import {
getWorkflowAnnotationKeyLabel, getWorkflowAnnotationKeyLabel,
getWorkflowAnnotations getWorkflowAnnotations,
cleanLabel
} from '../utilities/workflowAnnotationUtils' } from '../utilities/workflowAnnotationUtils'
import { import {
annotateChildPods, annotateChildPods,
@@ -40,7 +41,8 @@ export async function deployManifests(
files: string[], files: string[],
deploymentStrategy: DeploymentStrategy, deploymentStrategy: DeploymentStrategy,
kubectl: Kubectl, kubectl: Kubectl,
trafficSplitMethod: TrafficSplitMethod trafficSplitMethod: TrafficSplitMethod,
annotations: {[key: string]: string} = {}
): Promise<string[]> { ): Promise<string[]> {
switch (deploymentStrategy) { switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: { case DeploymentStrategy.CANARY: {
@@ -62,7 +64,7 @@ export async function deployManifests(
(routeStrategy == RouteStrategy.INGRESS && (routeStrategy == RouteStrategy.INGRESS &&
deployBlueGreenIngress(kubectl, files)) || deployBlueGreenIngress(kubectl, files)) ||
(routeStrategy == RouteStrategy.SMI && (routeStrategy == RouteStrategy.SMI &&
deployBlueGreenSMI(kubectl, files)) || deployBlueGreenSMI(kubectl, files, annotations)) ||
deployBlueGreenService(kubectl, files) deployBlueGreenService(kubectl, files)
) )
@@ -214,10 +216,10 @@ async function labelResources(
label: string label: string
) { ) {
const labels = [ const labels = [
`workflowFriendlyName=${normalizeWorkflowStrLabel( `workflowFriendlyName=${cleanLabel(
process.env.GITHUB_WORKFLOW normalizeWorkflowStrLabel(process.env.GITHUB_WORKFLOW)
)}`, )}`,
`workflow=${label}` `workflow=${cleanLabel(label)}`
] ]
checkForErrors([await kubectl.labelFiles(files, labels)], true) 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)
}
}
+6 -1
View File
@@ -23,7 +23,12 @@ export async function getDeploymentConfig(): Promise<DeploymentConfig> {
) )
} }
const imageNames = core.getInput('images').split('\n') || [] let imageNames = core.getInput('images').split('\n') || []
imageNames.forEach((element) => {
if (element === null || '/^s+$/') {
imageNames.pop()
}
})
const imageDockerfilePathMap: {[id: string]: string} = {} const imageDockerfilePathMap: {[id: string]: string} = {}
const pullImages = !(core.getInput('pull-images').toLowerCase() === 'false') const pullImages = !(core.getInput('pull-images').toLowerCase() === 'false')
+16 -1
View File
@@ -1,4 +1,7 @@
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils' import {
cleanLabel,
prefixObjectKeys
} from '../utilities/workflowAnnotationUtils'
describe('WorkflowAnnotationUtils', () => { describe('WorkflowAnnotationUtils', () => {
describe('prefixObjectKeys', () => { describe('prefixObjectKeys', () => {
@@ -15,4 +18,16 @@ describe('WorkflowAnnotationUtils', () => {
expect(prefixObjectKeys(obj, prefix)).toEqual(expected) 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')
})
})
}) })
+11
View File
@@ -47,3 +47,14 @@ export function getWorkflowAnnotationKeyLabel(
.digest('hex') .digest('hex')
return `githubWorkflow_${hashKey}` return `githubWorkflow_${hashKey}`
} }
/**
* 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] || ''
}