mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-21 18:59:27 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b03d40a74 | |||
| e456a366fc | |||
| c97519147a | |||
| 1516e1430d | |||
| 0a3b59b275 | |||
| d96b9b2965 | |||
| 71e93a71d4 | |||
| 19d66d6bdb |
@@ -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 '-baseline' and '-canary' 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 '-baseline' and '-canary'.<br/><br/>For example, if Deployment hello-world was found in the input manifest file with 'replicas: 4' and if 'strategy: canary' and 'percentage: 25' are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The '-baseline' variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the '-canary' variant is created with the image and tag corresponding to the new changes being deployed</td>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+10
-4
@@ -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')
|
||||
@@ -104,7 +105,11 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
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 =
|
||||
@@ -157,7 +162,8 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
await routeBlueGreenSMI(
|
||||
kubectl,
|
||||
NONE_LABEL_VALUE,
|
||||
manifestObjects.serviceEntityList
|
||||
manifestObjects.serviceEntityList,
|
||||
annotations
|
||||
)
|
||||
await deleteWorkloadsWithLabel(
|
||||
kubectl,
|
||||
|
||||
@@ -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'
|
||||
@@ -54,7 +55,11 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
async function rejectBlueGreen(
|
||||
kubectl: Kubectl,
|
||||
manifests: string[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
core.startGroup('Rejecting deployment with blue green strategy')
|
||||
|
||||
const routeStrategy = parseRouteStrategy(
|
||||
@@ -63,7 +68,7 @@ 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)
|
||||
}
|
||||
|
||||
+7
-3
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function parseAnnotations(str: string) {
|
||||
if (str == '') {
|
||||
return {}
|
||||
} else {
|
||||
const annotaion = JSON.parse(str)
|
||||
return new Map(annotaion)
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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] || ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user