Compare commits

...

13 Commits

Author SHA1 Message Date
Vidya Reddy ef850829ef updated the description 2022-08-06 10:27:15 +05:30
Vidya Reddy c78473ff1f fixes 2022-08-04 12:43:56 +05:30
Vidya Reddy bd41735c15 added runner info to bug report 2022-08-03 12:20:23 +05:30
Vidya Reddy fe047348a8 Added bug report and feature req form 2022-08-02 17:30:47 +05:30
Jaiveer Katariya 531cfdcc3d 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>
2022-07-29 10:58:58 -04:00
Marcus-Hines 0b5795551a Private Cluster functionality (#214) 2022-07-28 17:14:02 -04:00
Vidya Reddy bb0278db72 Swap annotation key to actions.github.com prefix (#216) 2022-07-27 13:53:57 -04:00
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
Hariharan Subramanian 7b11ddb1d5 Hari/beautify logs (#206)
* Logging changes for deploy

* Logging Changes with group

* format check changes
2022-06-29 11:26:44 -04:00
David Gamero ecec5912ba 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
2022-06-28 16:33:13 -04:00
29 changed files with 3147 additions and 2127 deletions
+28
View File
@@ -0,0 +1,28 @@
name: Bug Report
description: File a bug report, we will respond to this thread with any questions.
title: 'Bug: '
labels: ['bug', 'triage']
assignees: '@Azure/aks-atlanta'
body:
- type: input
id: What-happened
attributes:
label: What happened?
description: Tell us what happened and how is it different form the expected?
placeholder: Tell us what you see!
validations:
required: true
- type: input
id: Runner
attributes:
label: Runner
description: What runner are you using?
placeholder: Mention the runner info (self-hosted, operating system)
validations:
required: true
- type: input
id: Logs
attributes:
label: Relevant log output
description: Run in debug mode for the most verbose logs. Please feel free to attach a screenshot of the logs
render: shell
+6
View File
@@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Action "aks-set-context" Support
url: https://github.com/Azure/aks-set-context
security: https://github.com/Azure/aks-set-context/blob/main/SECURITY.md
about: Please ask and answer questions here.
@@ -0,0 +1,13 @@
name: Feature Request
description: File a Feature Request form, we will respond to this thread with any questions.
title: 'Feature Request: '
labels: ['Feature']
assignees: '@Azure/aks-atlanta'
body:
- type: input
id: Feature request
attributes:
label: Feature request
description: Provide example functionality and links to relevant docs
validations:
required: true
+36 -6
View File
@@ -43,7 +43,15 @@ Following are the key capabilities of this action:
<tr> <tr>
<td>manifests </br></br>(Required)</td> <td>manifests </br></br>(Required)</td>
<td>Path to the manifest files to be used for deployment. These can also be directories containing manifest files, in which case, all manifest files in the referenced directory at every depth will be deployed. Files not ending in .yml or .yaml will be ignored.</td> <td>Path to the manifest files to be used for deployment. These can also be directories containing manifest files, in which case, all manifest files in the referenced directory at every depth will be deployed. Files not ending in .yml or .yaml will be ignored.</td>
</tr> </tr>
<tr>
<td>strategy </br></br>(Required)</td>
<td>Acceptable values: basic/canary/blue-green. <br>
Default value: basic
<br>Deployment strategy to be used while applying manifest files on the cluster.
<br>basic - Template is force applied to all pods when deploying to cluster. NOTE: Can only be used with action == deploy
<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
</tr>
<tr> <tr>
<td>namespace </br></br>(Optional) <td>namespace </br></br>(Optional)
<td>Namespace within the cluster to deploy to.</td> <td>Namespace within the cluster to deploy to.</td>
@@ -62,15 +70,13 @@ Following are the key capabilities of this action:
<td>pull-images</br></br>(Optional)</td> <td>pull-images</br></br>(Optional)</td>
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations</td> <td>Acceptable values: true/false</br>Default value: true</br>Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations</td>
</tr> </tr>
<tr>
<td>strategy </br></br>(Optional)</td>
<td>Acceptable values: none/canary/blue-green. <br>
Deployment strategy to be used while applying manifest files on the cluster.<br>none - No deployment strategy is used when deploying.<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
</tr>
<tr> <tr>
<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>
@@ -90,6 +96,10 @@ Following are the key capabilities of this action:
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td> <td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
<td>Acceptable values: 1-300.</br>Default value: 0.</br>Waits for the given input in minutes before routing traffic to '-green' workloads.</td> <td>Acceptable values: 1-300.</br>Default value: 0.</br>Waits for the given input in minutes before routing traffic to '-green' workloads.</td>
</tr> </tr>
<tr>
<td>private-cluster </br></br>(Optional and relevant only using K8's deploy for a cluster with private cluster enabled)</td>
<td>Acceptable values: true, false</br>Default value: false.</td>
</tr>
<tr> <tr>
<td>force </br></br>(Optional)</td> <td>force </br></br>(Optional)</td>
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td> <td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
@@ -116,6 +126,26 @@ Following are the key capabilities of this action:
image-pull-secret2 image-pull-secret2
``` ```
### Private cluster deployment
```yaml
- uses: Azure/k8s-deploy@v4
with:
resource-group: yourResourceGroup
name: yourClusterName
action: deploy
strategy: basic
private-cluster: true
manifests: |
manifests/azure-vote-backend-deployment.yaml
manifests/azure-vote-backend-service.yaml
manifests/azure-vote-frontend-deployment.yaml
manifests/azure-vote-frontend-service.yaml
images: |
registry.azurecr.io/containername
```
### Canary deployment without service mesh ### Canary deployment without service mesh
```yaml ```yaml
+16 -3
View File
@@ -20,9 +20,9 @@ inputs:
required: false required: false
default: true default: true
strategy: strategy:
description: 'Deployment strategy to be used. Allowed values are none, canary and blue-green' description: 'Deployment strategy to be used. Allowed values are basic, canary and blue-green'
required: false required: true
default: 'none' default: 'basic'
route-method: route-method:
description: 'Route based on service, ingress or SMI for blue-green strategy' description: 'Route based on service, ingress or SMI for blue-green strategy'
required: false required: false
@@ -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
@@ -59,6 +62,16 @@ inputs:
description: 'Annotate the target namespace' description: 'Annotate the target namespace'
required: false required: false
default: true default: true
private-cluster:
description: 'True if cluster is AKS private cluster'
required: false
default: false
resource-group:
description: 'Name of resource group - Only required if using private cluster'
required: false
name:
description: 'Resource group name - Only required if using private cluster'
required: false
branding: branding:
color: 'green' color: 'green'
+2404 -1972
View File
File diff suppressed because it is too large Load Diff
+2 -2
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 ."
@@ -24,7 +24,7 @@
"@types/js-yaml": "^3.12.7", "@types/js-yaml": "^3.12.7",
"@types/node": "^12.20.41", "@types/node": "^12.20.41",
"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",
"typescript": "3.9.5" "typescript": "3.9.5"
} }
+14 -7
View File
@@ -19,14 +19,15 @@ 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)
core.debug('Input manifest files: ' + inputManifestFiles) core.debug('Input manifest files: ' + inputManifestFiles)
// deploy manifests // deploy manifests
core.info('Deploying manifests') core.startGroup('Deploying manifests')
const trafficSplitMethod = parseTrafficSplitMethod( const trafficSplitMethod = parseTrafficSplitMethod(
core.getInput('traffic-split-method', {required: true}) core.getInput('traffic-split-method', {required: true})
) )
@@ -34,12 +35,14 @@ export async function deploy(
inputManifestFiles, inputManifestFiles,
deploymentStrategy, deploymentStrategy,
kubectl, kubectl,
trafficSplitMethod trafficSplitMethod,
annotations
) )
core.endGroup()
core.debug('Deployed manifest files: ' + deployedManifestFiles) core.debug('Deployed manifest files: ' + deployedManifestFiles)
// check manifest stability // check manifest stability
core.info('Checking manifest stability') core.startGroup('Checking manifest stability')
const resourceTypes: Resource[] = getResources( const resourceTypes: Resource[] = getResources(
deployedManifestFiles, deployedManifestFiles,
models.DEPLOYMENT_TYPES.concat([ models.DEPLOYMENT_TYPES.concat([
@@ -47,17 +50,19 @@ export async function deploy(
]) ])
) )
await checkManifestStability(kubectl, resourceTypes) await checkManifestStability(kubectl, resourceTypes)
core.endGroup()
if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) { if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) {
core.info('Routing blue green') core.startGroup('Routing blue green')
const routeStrategy = parseRouteStrategy( const routeStrategy = parseRouteStrategy(
core.getInput('route-method', {required: true}) core.getInput('route-method', {required: true})
) )
await routeBlueGreen(kubectl, inputManifestFiles, routeStrategy) await routeBlueGreen(kubectl, inputManifestFiles, routeStrategy)
core.endGroup()
} }
// print ingresses // print ingresses
core.info('Printing ingresses') core.startGroup('Printing ingresses')
const ingressResources: Resource[] = getResources(deployedManifestFiles, [ const ingressResources: Resource[] = getResources(deployedManifestFiles, [
KubernetesConstants.DiscoveryAndLoadBalancerResource.INGRESS KubernetesConstants.DiscoveryAndLoadBalancerResource.INGRESS
]) ])
@@ -67,9 +72,10 @@ export async function deploy(
ingressResource.name ingressResource.name
) )
} }
core.endGroup()
// annotate resources // annotate resources
core.info('Annotating resources') core.startGroup('Annotating resources')
let allPods let allPods
try { try {
allPods = JSON.parse((await kubectl.getAllPods()).stdout) allPods = JSON.parse((await kubectl.getAllPods()).stdout)
@@ -82,4 +88,5 @@ export async function deploy(
resourceTypes, resourceTypes,
allPods allPods
) )
core.endGroup()
} }
+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()
} }
+22 -5
View File
@@ -6,6 +6,8 @@ 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 {PrivateKubectl} from './types/privatekubectl'
import {parseAnnotations} from './types/annotations'
export async function run() { export async function run() {
// verify kubeconfig is set // verify kubeconfig is set
@@ -18,6 +20,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
@@ -26,23 +31,35 @@ export async function run() {
.filter((manifest) => manifest.length > 0) // remove any blanks .filter((manifest) => manifest.length > 0) // remove any blanks
const fullManifestFilePaths = getFilesFromDirectories(manifestFilePaths) const fullManifestFilePaths = getFilesFromDirectories(manifestFilePaths)
// create kubectl
const kubectlPath = await getKubectlPath() const kubectlPath = await getKubectlPath()
const namespace = core.getInput('namespace') || 'default' const namespace = core.getInput('namespace') || 'default'
const kubectl = new Kubectl(kubectlPath, namespace, true) const isPrivateCluster =
core.getInput('private-cluster').toLowerCase() === 'true'
const resourceGroup = core.getInput('resource-group') || ''
const resourceName = core.getInput('name') || ''
const kubectl = isPrivateCluster
? new PrivateKubectl(
kubectlPath,
namespace,
true,
resourceGroup,
resourceName
)
: new Kubectl(kubectlPath, namespace, true)
// 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: {
@@ -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( 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(
@@ -110,6 +112,7 @@ export async function deleteWorkloadsWithLabel(
}) })
await deleteObjects(kubectl, resourcesToDelete) await deleteObjects(kubectl, resourcesToDelete)
return resourcesToDelete
} }
export async function deleteWorkloadsAndServicesWithLabel( export async function deleteWorkloadsAndServicesWithLabel(
@@ -141,6 +144,7 @@ export async function deleteWorkloadsAndServicesWithLabel(
}) })
await deleteObjects(kubectl, resourcesToDelete) await deleteObjects(kubectl, resourcesToDelete)
return resourcesToDelete
} }
export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) { export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) {
@@ -235,9 +239,6 @@ export async function createWorkloadsWithLabel(
deploymentObjectList.forEach((inputObject) => { deploymentObjectList.forEach((inputObject) => {
// creating deployment with label // creating deployment with label
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel) const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel)
core.debug(
'New blue-green object is: ' + JSON.stringify(newBlueGreenObject)
)
newObjectsList.push(newBlueGreenObject) newObjectsList.push(newBlueGreenObject)
}) })
@@ -278,7 +279,7 @@ export function addBlueGreenLabelsAndAnnotations(
updateObjectLabels(inputObject, newLabels, false) updateObjectLabels(inputObject, newLabels, false)
updateSelectorLabels(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)) { if (!isServiceEntity(inputObject.kind)) {
updateSpecLabels(inputObject, newLabels, false) 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' } from './blueGreenHelper'
import * as core from '@actions/core' import * as core from '@actions/core'
const BACKEND = 'BACKEND' const BACKEND = 'backend'
export async function deployBlueGreenIngress( export async function deployBlueGreenIngress(
kubectl: Kubectl, kubectl: Kubectl,
@@ -24,13 +24,12 @@ export async function deployBlueGreenIngress(
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths) const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// create deployments with green label value // create deployments with green label value
const result = createWorkloadsWithLabel( const workloadDeployment = await createWorkloadsWithLabel(
kubectl, kubectl,
manifestObjects.deploymentEntityList, manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE GREEN_LABEL_VALUE
) )
// create new services and other objects
let newObjectsList = [] let newObjectsList = []
manifestObjects.serviceEntityList.forEach((inputObject) => { manifestObjects.serviceEntityList.forEach((inputObject) => {
const newBlueGreenObject = getNewBlueGreenObject( const newBlueGreenObject = getNewBlueGreenObject(
@@ -46,7 +45,12 @@ export async function deployBlueGreenIngress(
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
await kubectl.apply(manifestFiles) 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( export async function promoteBlueGreenIngress(
@@ -54,14 +58,13 @@ export async function promoteBlueGreenIngress(
manifestObjects manifestObjects
) { ) {
//checking if anything to promote //checking if anything to promote
if ( const {areValid, invalidIngresses} = validateIngresses(
!validateIngressesState( kubectl,
kubectl, manifestObjects.ingressEntityList,
manifestObjects.ingressEntityList, manifestObjects.serviceNameMap
manifestObjects.serviceNameMap )
) if (!areValid) {
) { throw 'Ingresses are not in promote state' + invalidIngresses.toString()
throw 'Ingress not in promote state'
} }
// create stable deployments with new configuration // 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) const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
await kubectl.apply(manifestFiles) await kubectl.apply(manifestFiles)
return newObjectsList
} }
export function validateIngressesState( export function validateIngresses(
kubectl: Kubectl, kubectl: Kubectl,
ingressEntityList: any[], ingressEntityList: any[],
serviceNameMap: Map<string, string> serviceNameMap: Map<string, string>
): boolean { ): {areValid: boolean; invalidIngresses: string[]} {
let areIngressesTargetingNewServices: boolean = true let areValid: boolean = true
const invalidIngresses = []
ingressEntityList.forEach(async (inputObject) => { ingressEntityList.forEach(async (inputObject) => {
if (isIngressRouted(inputObject, serviceNameMap)) { if (isIngressRouted(inputObject, serviceNameMap)) {
//querying existing ingress //querying existing ingress
@@ -158,33 +162,32 @@ export function validateIngressesState(
inputObject.metadata.name inputObject.metadata.name
) )
if (!!existingIngress) { let isValid =
const currentLabel: string = !!existingIngress &&
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] ===
GREEN_LABEL_VALUE
// if not green label, then wrong configuration if (!isValid) {
if (currentLabel != GREEN_LABEL_VALUE) invalidIngresses.push(inputObject.metadata.name)
areIngressesTargetingNewServices = false
} else {
// no ingress at all, so nothing to promote
areIngressesTargetingNewServices = false
} }
// 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, ingressObject: any,
serviceNameMap: Map<string, string> serviceNameMap: Map<string, string>
): boolean { ): boolean {
let isIngressRouted: boolean = false let isIngressRouted: boolean = false
// check if ingress targets a service in the given manifests // check if ingress targets a service in the given manifests
JSON.parse(JSON.stringify(ingressObject), (key, value) => { JSON.parse(JSON.stringify(ingressObject), (key, value) => {
if (key === 'serviceName' && serviceNameMap.has(value)) { isIngressRouted =
isIngressRouted = true isIngressRouted || (key === 'service' && value.hasOwnProperty('name'))
} isIngressRouted =
isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value))
return value return value
}) })
@@ -206,15 +209,18 @@ export function getUpdatedBlueGreenIngress(
addBlueGreenLabelsAndAnnotations(newObject, type) addBlueGreenLabelsAndAnnotations(newObject, type)
// update ingress labels // update ingress labels
if (inputObject.apiVersion === 'networking.k8s.io/v1beta1') {
return updateIngressBackendBetaV1(newObject, serviceNameMap)
}
return updateIngressBackend(newObject, serviceNameMap) return updateIngressBackend(newObject, serviceNameMap)
} }
export function updateIngressBackend( export function updateIngressBackendBetaV1(
inputObject: any, inputObject: any,
serviceNameMap: Map<string, string> serviceNameMap: Map<string, string>
): any { ): any {
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => { inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
if (key.toUpperCase() === BACKEND) { if (key.toLowerCase() === BACKEND) {
const {serviceName} = value const {serviceName} = value
if (serviceNameMap.has(serviceName)) { if (serviceNameMap.has(serviceName)) {
// update service name with corresponding bluegreen name only if service is provied in given manifests // update service name with corresponding bluegreen name only if service is provied in given manifests
@@ -227,3 +233,20 @@ export function updateIngressBackend(
return inputObject 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) const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// create deployments with green label value // create deployments with green label value
const result = await createWorkloadsWithLabel( const workloadDeployment = await createWorkloadsWithLabel(
kubectl, kubectl,
manifestObjects.deploymentEntityList, manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE GREEN_LABEL_VALUE
) )
// create other non deployment and non service entities
const newObjectsList = manifestObjects.otherObjects const newObjectsList = manifestObjects.otherObjects
.concat(manifestObjects.ingressEntityList) .concat(manifestObjects.ingressEntityList)
.concat(manifestObjects.unroutedServiceEntityList) .concat(manifestObjects.unroutedServiceEntityList)
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList) const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
if (manifestFiles.length > 0) await kubectl.apply(manifestFiles) if (manifestFiles.length > 0) await kubectl.apply(manifestFiles)
// returning deployment details to check for rollout stability // returning deployment details to check for rollout stability
return result return {workloadDeployment, newObjectsList}
} }
export async function promoteBlueGreenService( export async function promoteBlueGreenService(
@@ -76,7 +76,6 @@ export async function rejectBlueGreenService(
manifestObjects.deploymentEntityList manifestObjects.deploymentEntityList
) )
} }
export async function routeBlueGreenService( export async function routeBlueGreenService(
kubectl: Kubectl, kubectl: Kubectl,
nextLabel: string, nextLabel: string,
@@ -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,14 +38,16 @@ 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( const workloadDeployment = await createWorkloadsWithLabel(
kubectl, kubectl,
manifestObjects.deploymentEntityList, manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE GREEN_LABEL_VALUE
) )
return {workloadDeployment, newObjectsList}
} }
export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) { export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
@@ -68,16 +71,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 +96,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 +126,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 +137,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 +156,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 +206,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: [
+17 -14
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: {
@@ -58,22 +60,19 @@ export async function deployManifests(
core.getInput('route-method', {required: true}) core.getInput('route-method', {required: true})
) )
const {result, newFilePaths} = await Promise.resolve( const {workloadDeployment, newObjectsList} = await Promise.resolve(
(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)
) )
checkForErrors([result]) checkForErrors([workloadDeployment.result])
return newFilePaths return workloadDeployment.newFilePaths
} }
case undefined: { case DeploymentStrategy.BASIC: {
core.warning('Deployment strategy is not recognized.')
}
default: {
const trafficSplitMethod = parseTrafficSplitMethod( const trafficSplitMethod = parseTrafficSplitMethod(
core.getInput('traffic-split-method', {required: true}) core.getInput('traffic-split-method', {required: true})
) )
@@ -94,6 +93,10 @@ export async function deployManifests(
return files return files
} }
default: {
throw new Error('Deployment strategy is not recognized.')
}
} }
} }
@@ -140,7 +143,7 @@ export async function annotateAndLabelResources(
const workflowFilePath = await getWorkflowFilePath(githubToken) const workflowFilePath = await getWorkflowFilePath(githubToken)
const deploymentConfig = await getDeploymentConfig() const deploymentConfig = await getDeploymentConfig()
const annotationKeyLabel = getWorkflowAnnotationKeyLabel(workflowFilePath) const annotationKeyLabel = getWorkflowAnnotationKeyLabel()
await annotateResources( await annotateResources(
files, files,
@@ -213,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)
}
}
+1
View File
@@ -5,6 +5,7 @@ describe('Deployment strategy type', () => {
const vals = <any>Object.values(DeploymentStrategy) const vals = <any>Object.values(DeploymentStrategy)
expect(vals.includes('canary')).toBe(true) expect(vals.includes('canary')).toBe(true)
expect(vals.includes('blue-green')).toBe(true) expect(vals.includes('blue-green')).toBe(true)
expect(vals.includes('basic')).toBe(true)
}) })
test('it can parse valid values from a string', () => { test('it can parse valid values from a string', () => {
+1
View File
@@ -1,4 +1,5 @@
export enum DeploymentStrategy { export enum DeploymentStrategy {
BASIC = 'basic',
CANARY = 'canary', CANARY = 'canary',
BLUE_GREEN = 'blue-green' BLUE_GREEN = 'blue-green'
} }
+12 -5
View File
@@ -10,18 +10,25 @@ export interface Resource {
} }
export class Kubectl { export class Kubectl {
private readonly kubectlPath: string protected readonly kubectlPath: string
private readonly namespace: string protected readonly namespace: string
private readonly ignoreSSLErrors: boolean protected readonly ignoreSSLErrors: boolean
protected readonly resourceGroup: string
protected readonly name: string
protected isPrivateCluster: boolean
constructor( constructor(
kubectlPath: string, kubectlPath: string,
namespace: string = 'default', namespace: string = 'default',
ignoreSSLErrors: boolean = false ignoreSSLErrors: boolean = false,
resourceGroup: string = '',
name: string = ''
) { ) {
this.kubectlPath = kubectlPath this.kubectlPath = kubectlPath
this.ignoreSSLErrors = !!ignoreSSLErrors this.ignoreSSLErrors = !!ignoreSSLErrors
this.namespace = namespace this.namespace = namespace
this.resourceGroup = resourceGroup
this.name = name
} }
public async apply( public async apply(
@@ -155,7 +162,7 @@ export class Kubectl {
return this.execute(['delete', ...args]) return this.execute(['delete', ...args])
} }
private async execute(args: string[], silent: boolean = false) { protected async execute(args: string[], silent: boolean = false) {
if (this.ignoreSSLErrors) { if (this.ignoreSSLErrors) {
args.push('--insecure-skip-tls-verify') args.push('--insecure-skip-tls-verify')
} }
+135
View File
@@ -0,0 +1,135 @@
import {Kubectl} from './kubectl'
import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec'
import * as core from '@actions/core'
import * as os from 'os'
import * as fs from 'fs'
import * as path from 'path'
export class PrivateKubectl extends Kubectl {
protected async execute(args: string[], silent: boolean = false) {
args.unshift('kubectl')
let kubectlCmd = args.join(' ')
let addFileFlag = false
let eo = <ExecOptions>{silent}
if (this.containsFilenames(kubectlCmd)) {
// For private clusters, files will referenced solely by their basename
kubectlCmd = this.replaceFilnamesWithBasenames(kubectlCmd)
addFileFlag = true
}
const privateClusterArgs = [
'aks',
'command',
'invoke',
'--resource-group',
this.resourceGroup,
'--name',
this.name,
'--command',
kubectlCmd
]
if (addFileFlag) {
const filenames = this.extractFilesnames(kubectlCmd).split(' ')
const tempDirectory =
process.env['runner.tempDirectory'] || os.tmpdir() + '/manifests'
eo.cwd = tempDirectory
privateClusterArgs.push(...['--file', '.'])
let filenamesArr = filenames[0].split(',')
for (let index = 0; index < filenamesArr.length; index++) {
const file = filenamesArr[index]
if (!file) {
continue
}
this.moveFileToTempManifestDir(file)
}
}
core.debug(
`private cluster Kubectl run with invoke command: ${kubectlCmd}`
)
return await getExecOutput('az', privateClusterArgs, eo)
}
private replaceFilnamesWithBasenames(kubectlCmd: string) {
let exFilenames = this.extractFilesnames(kubectlCmd)
let filenames = exFilenames.split(' ')
let filenamesArr = filenames[0].split(',')
for (let index = 0; index < filenamesArr.length; index++) {
filenamesArr[index] = path.basename(filenamesArr[index])
}
let baseFilenames = filenamesArr.join()
let result = kubectlCmd.replace(exFilenames, baseFilenames)
return result
}
public extractFilesnames(strToParse: string) {
let start = strToParse.indexOf('-filename')
let offset = 7
if (start == -1) {
start = strToParse.indexOf('-f')
if (start == -1) {
return ''
}
offset = 0
}
let temp = strToParse.substring(start + offset)
let end = temp.indexOf(' -')
//End could be case where the -f flag was last, or -f is followed by some additonal flag and it's arguments
return temp.substring(3, end == -1 ? temp.length : end).trim()
}
private containsFilenames(str: string) {
return str.includes('-f ') || str.includes('filename ')
}
private createTempManifestsDirectory() {
const manifestsDir = '/tmp/manifests'
if (!fs.existsSync('/tmp/manifests')) {
fs.mkdirSync('/tmp/manifests', {recursive: true})
}
}
private moveFileToTempManifestDir(file: string) {
this.createTempManifestsDirectory()
if (!fs.existsSync('/tmp/' + file)) {
core.debug(
'/tmp/' +
file +
' does not exist, and therefore cannot be moved to the manifest directory'
)
}
fs.copyFile('/tmp/' + file, '/tmp/manifests/' + file, function (err) {
if (err) {
core.debug(
'Could not rename ' +
'/tmp/' +
file +
' to ' +
'/tmp/manifests/' +
file +
' ERROR: ' +
err
)
return
}
core.debug(
"Successfully moved file '" +
file +
"' from /tmp to /tmp/manifest directory"
)
})
}
}
+3 -2
View File
@@ -12,11 +12,12 @@ describe('File utils', () => {
'test/unit/manifests/manifest_test_dir/another_layer/deep-service.yaml', 'test/unit/manifests/manifest_test_dir/another_layer/deep-service.yaml',
'test/unit/manifests/manifest_test_dir/nested-test-service.yaml', 'test/unit/manifests/manifest_test_dir/nested-test-service.yaml',
'test/unit/manifests/test-ingress.yml', 'test/unit/manifests/test-ingress.yml',
'test/unit/manifests/test-ingress-new.yml',
'test/unit/manifests/test-service.yml' 'test/unit/manifests/test-service.yml'
] ]
// is there a more efficient way to test equality w random order? // is there a more efficient way to test equality w random order?
expect(testSearch).toHaveLength(5) expect(testSearch).toHaveLength(6)
expectedManifests.forEach((fileName) => { expectedManifests.forEach((fileName) => {
expect(testSearch).toContain(fileName) expect(testSearch).toContain(fileName)
}) })
@@ -53,7 +54,7 @@ describe('File utils', () => {
expect( expect(
getFilesFromDirectories([outerPath, fileAtOuter, innerPath]) getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
).toHaveLength(5) ).toHaveLength(6)
}) })
}) })
+1 -1
View File
@@ -57,7 +57,7 @@ export function writeManifestToFile(
} }
function getManifestFileName(kind: string, name: string) { function getManifestFileName(kind: string, name: string) {
const filePath = `${kind}_${name}_ ${getCurrentTime().toString()}` const filePath = `${kind}_${name}_${getCurrentTime().toString()}`
const tempDirectory = getTempDirectory() const tempDirectory = getTempDirectory()
return path.join(tempDirectory, path.basename(filePath)) return path.join(tempDirectory, path.basename(filePath))
} }
+10 -13
View File
@@ -1,18 +1,15 @@
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils' import {cleanLabel} from '../utilities/workflowAnnotationUtils'
describe('WorkflowAnnotationUtils', () => { describe('WorkflowAnnotationUtils', () => {
describe('prefixObjectKeys', () => { describe('cleanLabel', () => {
it('should prefix an object with a given prefix', () => { it('should clean label', () => {
const obj = { const alreadyClean = 'alreadyClean'
foo: 'bar', expect(cleanLabel(alreadyClean)).toEqual(alreadyClean)
baz: 'qux' expect(cleanLabel('.startInvalid')).toEqual('startInvalid')
} expect(cleanLabel('with%S0ME&invalid#chars')).toEqual(
const prefix = 'prefix.' 'withS0MEinvalidchars'
const expected = { )
'prefix.foo': 'bar', expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
'prefix.baz': 'qux'
}
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
}) })
}) })
}) })
+15 -21
View File
@@ -1,13 +1,6 @@
import {DeploymentConfig} from '../types/deploymentConfig' import {DeploymentConfig} from '../types/deploymentConfig'
const ANNOTATION_PREFIX = 'actions.github.com/' 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
}, {})
}
export function getWorkflowAnnotations( export function getWorkflowAnnotations(
lastSuccessRunSha: string, lastSuccessRunSha: string,
@@ -31,19 +24,20 @@ export function getWorkflowAnnotations(
helmChartPaths: deploymentConfig.helmChartFilePaths, helmChartPaths: deploymentConfig.helmChartFilePaths,
provider: 'GitHub' provider: 'GitHub'
} }
const prefixedAnnotationObject = prefixObjectKeys( return JSON.stringify(annotationObject)
annotationObject,
ANNOTATION_PREFIX
)
return JSON.stringify(prefixedAnnotationObject)
} }
export function getWorkflowAnnotationKeyLabel( export function getWorkflowAnnotationKeyLabel(): string {
workflowFilePath: string return `${ANNOTATION_PREFIX}/k8s-deploy`
): string { }
const hashKey = require('crypto')
.createHash('MD5') /**
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`) * Cleans label to match valid kubernetes label specification by removing invalid characters
.digest('hex') * @param label
return `githubWorkflow_${hashKey}` * @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] || ''
} }
+50
View File
@@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
backend:
service:
name: nginx-service
port:
number: 80