mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-21 18:59:27 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef850829ef | |||
| c78473ff1f | |||
| bd41735c15 | |||
| fe047348a8 |
@@ -1,24 +1,17 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: File a bug report specifying all inputs you provided for the action, we will respond to this thread with any questions.
|
description: File a bug report, we will respond to this thread with any questions.
|
||||||
title: 'Bug: '
|
title: 'Bug: '
|
||||||
labels: ['bug', 'triage']
|
labels: ['bug', 'triage']
|
||||||
assignees: '@Azure/aks-atlanta'
|
assignees: '@Azure/aks-atlanta'
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: input
|
||||||
id: What-happened
|
id: What-happened
|
||||||
attributes:
|
attributes:
|
||||||
label: What happened?
|
label: What happened?
|
||||||
description: Tell us what happened and how is it different from the expected?
|
description: Tell us what happened and how is it different form the expected?
|
||||||
placeholder: Tell us what you see!
|
placeholder: Tell us what you see!
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
|
||||||
id: Version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
options:
|
|
||||||
- label: I am using the latest version
|
|
||||||
required: true
|
|
||||||
- type: input
|
- type: input
|
||||||
id: Runner
|
id: Runner
|
||||||
attributes:
|
attributes:
|
||||||
@@ -27,10 +20,9 @@ body:
|
|||||||
placeholder: Mention the runner info (self-hosted, operating system)
|
placeholder: Mention the runner info (self-hosted, operating system)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: input
|
||||||
id: Logs
|
id: Logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant log output
|
label: Relevant log output
|
||||||
description: Run in debug mode for the most verbose logs. Please feel free to attach a screenshot of the logs
|
description: Run in debug mode for the most verbose logs. Please feel free to attach a screenshot of the logs
|
||||||
validations:
|
render: shell
|
||||||
required: true
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: GitHub Action "k8s-deploy" Support
|
- name: GitHub Action "aks-set-context" Support
|
||||||
url: https://github.com/Azure/k8s-deploy
|
url: https://github.com/Azure/aks-set-context
|
||||||
security: https://github.com/Azure/k8s-deploy/blob/main/SECURITY.md
|
security: https://github.com/Azure/aks-set-context/blob/main/SECURITY.md
|
||||||
about: Please ask and answer questions here.
|
about: Please ask and answer questions here.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ title: 'Feature Request: '
|
|||||||
labels: ['Feature']
|
labels: ['Feature']
|
||||||
assignees: '@Azure/aks-atlanta'
|
assignees: '@Azure/aks-atlanta'
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: input
|
||||||
id: Feature_request
|
id: Feature request
|
||||||
attributes:
|
attributes:
|
||||||
label: Feature request
|
label: Feature request
|
||||||
description: Provide example functionality and links to relevant docs
|
description: Provide example functionality and links to relevant docs
|
||||||
|
|||||||
+1
-3
@@ -2,6 +2,4 @@ node_modules
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
lib/
|
lib/
|
||||||
|
|
||||||
coverage/
|
|
||||||
@@ -4,15 +4,6 @@ This action is used to deploy manifests to Kubernetes clusters. It requires that
|
|||||||
|
|
||||||
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
||||||
|
|
||||||
This action requires the following permissions from your workflow:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
```
|
|
||||||
|
|
||||||
## Action capabilities
|
## Action capabilities
|
||||||
|
|
||||||
Following are the key capabilities of this action:
|
Following are the key capabilities of this action:
|
||||||
@@ -91,8 +82,8 @@ Following are the key capabilities of this action:
|
|||||||
<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>
|
<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>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>baseline-and-canary-replicas </br></br> (Optional and relevant only if strategy is canary and traffic-split-method is smi)</td>
|
<td>baseline-and-canary-replicas </br></br> (Optional and relevant only if traffic-split-method is canary)</td>
|
||||||
<td>The number of baseline and canary replicas. Percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action </br></br><code> strategy: canary<br> trafficSplitMethod: smi<br> percentage: 20<br> baselineAndCanaryReplicas: 1</code></br></br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas each, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
|
<td>The number of baseline and canary replicas. Percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action </br></br><code> strategy: canary<br> trafficSplitMethod: smi<br> percentage: 20<br> baselineAndCanaryReplicas: 1</code></br></br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
<td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||||
@@ -229,7 +220,7 @@ To promote/reject the canary created by the above snippet, the following YAML sn
|
|||||||
dir/manifestsDirectory
|
dir/manifestsDirectory
|
||||||
strategy: canary
|
strategy: canary
|
||||||
traffic-split-method: smi
|
traffic-split-method: smi
|
||||||
action: reject # substitute promote if you want to promote
|
action: reject # substitute reject if you want to reject
|
||||||
```
|
```
|
||||||
|
|
||||||
### Blue-Green deployment with different route methods
|
### Blue-Green deployment with different route methods
|
||||||
@@ -471,7 +462,3 @@ provided by the bot. You will only need to do this once across all repos using o
|
|||||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
k8s-deploy is an open source project that is [**not** covered by the Microsoft Azure support policy](https://support.microsoft.com/en-us/help/2941892/support-for-linux-and-open-source-technology-in-azure). [Please search open issues here](https://github.com/Azure/k8s-deploy/issues), and if your issue isn't already represented please [open a new one](https://github.com/Azure/k8s-deploy/issues/new/choose). The project maintainers will respond to the best of their abilities.
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ inputs:
|
|||||||
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
|
||||||
default: ''
|
default: 0
|
||||||
percentage:
|
percentage:
|
||||||
description: 'Percentage of traffic redirect to canary deployment'
|
description: 'Percentage of traffic redirect to canary deployment'
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
Generated
+15
-26
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.0",
|
"@actions/exec": "^1.0.0",
|
||||||
"@actions/io": "^1.0.0",
|
"@actions/io": "^1.0.0",
|
||||||
"@actions/tool-cache": "1.1.2",
|
"@actions/tool-cache": "1.1.2",
|
||||||
@@ -29,20 +29,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@actions/core/node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/exec": {
|
"node_modules/@actions/exec": {
|
||||||
@@ -6416,19 +6407,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/exec": {
|
"@actions/exec": {
|
||||||
@@ -7380,6 +7363,12 @@
|
|||||||
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
|
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
|
||||||
"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.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||||
|
|||||||
+1
-2
@@ -6,12 +6,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build src/run.ts -o lib",
|
"build": "ncc build src/run.ts -o lib",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"coverage": "jest --coverage=true",
|
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.0",
|
"@actions/exec": "^1.0.0",
|
||||||
"@actions/io": "^1.0.0",
|
"@actions/io": "^1.0.0",
|
||||||
"@actions/tool-cache": "1.1.2",
|
"@actions/tool-cache": "1.1.2",
|
||||||
|
|||||||
+18
-5
@@ -6,6 +6,7 @@ import {
|
|||||||
getResources,
|
getResources,
|
||||||
updateManifestFiles
|
updateManifestFiles
|
||||||
} from '../utilities/manifestUpdateUtils'
|
} from '../utilities/manifestUpdateUtils'
|
||||||
|
import {routeBlueGreen} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
||||||
import {
|
import {
|
||||||
annotateAndLabelResources,
|
annotateAndLabelResources,
|
||||||
checkManifestStability,
|
checkManifestStability,
|
||||||
@@ -13,15 +14,17 @@ import {
|
|||||||
} from '../strategyHelpers/deploymentHelper'
|
} from '../strategyHelpers/deploymentHelper'
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod'
|
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod'
|
||||||
|
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.startGroup('Deploying manifests')
|
core.startGroup('Deploying manifests')
|
||||||
@@ -32,10 +35,11 @@ export async function deploy(
|
|||||||
inputManifestFiles,
|
inputManifestFiles,
|
||||||
deploymentStrategy,
|
deploymentStrategy,
|
||||||
kubectl,
|
kubectl,
|
||||||
trafficSplitMethod
|
trafficSplitMethod,
|
||||||
|
annotations
|
||||||
)
|
)
|
||||||
core.debug(`Deployed manifest files: ${deployedManifestFiles}`)
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
core.debug('Deployed manifest files: ' + deployedManifestFiles)
|
||||||
|
|
||||||
// check manifest stability
|
// check manifest stability
|
||||||
core.startGroup('Checking manifest stability')
|
core.startGroup('Checking manifest stability')
|
||||||
@@ -48,6 +52,15 @@ export async function deploy(
|
|||||||
await checkManifestStability(kubectl, resourceTypes)
|
await checkManifestStability(kubectl, resourceTypes)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
|
if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) {
|
||||||
|
core.startGroup('Routing blue green')
|
||||||
|
const routeStrategy = parseRouteStrategy(
|
||||||
|
core.getInput('route-method', {required: true})
|
||||||
|
)
|
||||||
|
await routeBlueGreen(kubectl, inputManifestFiles, routeStrategy)
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
|
|
||||||
// print ingresses
|
// print ingresses
|
||||||
core.startGroup('Printing ingresses')
|
core.startGroup('Printing ingresses')
|
||||||
const ingressResources: Resource[] = getResources(deployedManifestFiles, [
|
const ingressResources: Resource[] = getResources(deployedManifestFiles, [
|
||||||
@@ -67,7 +80,7 @@ export async function deploy(
|
|||||||
try {
|
try {
|
||||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
core.debug(`Unable to parse pods: ${e}`)
|
core.debug('Unable to parse pods: ' + e)
|
||||||
}
|
}
|
||||||
await annotateAndLabelResources(
|
await annotateAndLabelResources(
|
||||||
deployedManifestFiles,
|
deployedManifestFiles,
|
||||||
|
|||||||
+56
-58
@@ -1,7 +1,7 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
import * as deploy from './deploy'
|
||||||
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
||||||
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
||||||
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper'
|
|
||||||
import {
|
import {
|
||||||
getResources,
|
getResources,
|
||||||
updateManifestFiles
|
updateManifestFiles
|
||||||
@@ -9,26 +9,26 @@ import {
|
|||||||
import * as models from '../types/kubernetesTypes'
|
import * as models from '../types/kubernetesTypes'
|
||||||
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
|
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
|
||||||
import {
|
import {
|
||||||
deleteGreenObjects,
|
BlueGreenManifests,
|
||||||
|
deleteWorkloadsAndServicesWithLabel,
|
||||||
|
deleteWorkloadsWithLabel,
|
||||||
getManifestObjects,
|
getManifestObjects,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
NONE_LABEL_VALUE
|
NONE_LABEL_VALUE
|
||||||
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
||||||
|
import {
|
||||||
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
promoteBlueGreenService,
|
||||||
|
routeBlueGreenService
|
||||||
|
} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
||||||
import {
|
import {
|
||||||
promoteBlueGreenIngress,
|
promoteBlueGreenIngress,
|
||||||
promoteBlueGreenService,
|
routeBlueGreenIngress
|
||||||
promoteBlueGreenSMI
|
} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
||||||
} from '../strategyHelpers/blueGreen/promote'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
routeBlueGreenService,
|
cleanupSMI,
|
||||||
routeBlueGreenIngressUnchanged,
|
promoteBlueGreenSMI,
|
||||||
routeBlueGreenSMI
|
routeBlueGreenSMI
|
||||||
} from '../strategyHelpers/blueGreen/route'
|
} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
||||||
|
|
||||||
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
|
||||||
import {Kubectl, Resource} from '../types/kubectl'
|
import {Kubectl, Resource} from '../types/kubectl'
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import {
|
import {
|
||||||
@@ -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')
|
||||||
@@ -57,8 +58,6 @@ export async function promote(
|
|||||||
async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
||||||
let includeServices = false
|
let includeServices = false
|
||||||
|
|
||||||
const manifestFilesForDeployment: string[] = updateManifestFiles(manifests)
|
|
||||||
|
|
||||||
const trafficSplitMethod = parseTrafficSplitMethod(
|
const trafficSplitMethod = parseTrafficSplitMethod(
|
||||||
core.getInput('traffic-split-method', {required: true})
|
core.getInput('traffic-split-method', {required: true})
|
||||||
)
|
)
|
||||||
@@ -74,14 +73,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
)
|
)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
core.startGroup(
|
core.startGroup('Deploying input manifests with SMI canary strategy')
|
||||||
'Deploying input manifests with SMI canary strategy from promote'
|
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
|
||||||
)
|
|
||||||
await SMICanaryDeploymentHelper.deploySMICanary(
|
|
||||||
manifestFilesForDeployment,
|
|
||||||
kubectl,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
core.startGroup('Redirecting traffic to stable deployment')
|
core.startGroup('Redirecting traffic to stable deployment')
|
||||||
@@ -91,12 +84,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
)
|
)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
} else {
|
} else {
|
||||||
core.startGroup('Deploying input manifests from promote')
|
core.startGroup('Deploying input manifests')
|
||||||
await PodCanaryHelper.deployPodCanary(
|
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
|
||||||
manifestFilesForDeployment,
|
|
||||||
kubectl,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +98,18 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
)
|
)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.warning(
|
core.warning(
|
||||||
`Exception occurred while deleting canary and baseline workloads: ${ex}`
|
'Exception occurred while deleting canary and baseline workloads: ' +
|
||||||
|
ex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
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 =
|
||||||
@@ -125,24 +119,20 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
core.getInput('route-method', {required: true})
|
core.getInput('route-method', {required: true})
|
||||||
)
|
)
|
||||||
|
|
||||||
core.startGroup('Deleting old deployment and making new stable deployment')
|
core.startGroup('Deleting old deployment and making new one')
|
||||||
|
let result
|
||||||
const {deployResult} = await (async () => {
|
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||||
switch (routeStrategy) {
|
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
|
||||||
case RouteStrategy.INGRESS:
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
return await promoteBlueGreenIngress(kubectl, manifestObjects)
|
result = await promoteBlueGreenSMI(kubectl, manifestObjects)
|
||||||
case RouteStrategy.SMI:
|
} else {
|
||||||
return await promoteBlueGreenSMI(kubectl, manifestObjects)
|
result = await promoteBlueGreenService(kubectl, manifestObjects)
|
||||||
default:
|
}
|
||||||
return await promoteBlueGreenService(kubectl, manifestObjects)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
// checking stability of newly created deployments
|
// checking stability of newly created deployments
|
||||||
core.startGroup('Checking manifest stability')
|
core.startGroup('Checking manifest stability')
|
||||||
const deployedManifestFiles = deployResult.manifestFiles
|
const deployedManifestFiles = result.newFilePaths
|
||||||
const resources: Resource[] = getResources(
|
const resources: Resource[] = getResources(
|
||||||
deployedManifestFiles,
|
deployedManifestFiles,
|
||||||
models.DEPLOYMENT_TYPES.concat([
|
models.DEPLOYMENT_TYPES.concat([
|
||||||
@@ -156,26 +146,30 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
'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) {
|
||||||
await routeBlueGreenIngressUnchanged(
|
await routeBlueGreenIngress(
|
||||||
kubectl,
|
kubectl,
|
||||||
|
null,
|
||||||
manifestObjects.serviceNameMap,
|
manifestObjects.serviceNameMap,
|
||||||
manifestObjects.ingressEntityList
|
manifestObjects.ingressEntityList
|
||||||
)
|
)
|
||||||
|
await deleteWorkloadsAndServicesWithLabel(
|
||||||
await deleteGreenObjects(
|
|
||||||
kubectl,
|
kubectl,
|
||||||
[].concat(
|
GREEN_LABEL_VALUE,
|
||||||
manifestObjects.deploymentEntityList,
|
manifestObjects.deploymentEntityList,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await routeBlueGreenSMI(
|
await routeBlueGreenSMI(
|
||||||
kubectl,
|
kubectl,
|
||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList,
|
||||||
|
annotations
|
||||||
|
)
|
||||||
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
)
|
)
|
||||||
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
|
||||||
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||||
} else {
|
} else {
|
||||||
await routeBlueGreenService(
|
await routeBlueGreenService(
|
||||||
@@ -183,7 +177,11 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
|
)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-17
@@ -2,13 +2,9 @@ import * as core from '@actions/core'
|
|||||||
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
||||||
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
||||||
import {Kubectl} from '../types/kubectl'
|
import {Kubectl} from '../types/kubectl'
|
||||||
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
import {rejectBlueGreenService} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
||||||
import {
|
import {rejectBlueGreenIngress} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
||||||
rejectBlueGreenIngress,
|
import {rejectBlueGreenSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
||||||
rejectBlueGreenService,
|
|
||||||
rejectBlueGreenSMI
|
|
||||||
} from '../strategyHelpers/blueGreen/reject'
|
|
||||||
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import {
|
import {
|
||||||
parseTrafficSplitMethod,
|
parseTrafficSplitMethod,
|
||||||
@@ -19,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'
|
||||||
@@ -58,20 +55,22 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
core.endGroup()
|
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(
|
const routeStrategy = parseRouteStrategy(
|
||||||
core.getInput('route-method', {required: true})
|
core.getInput('route-method', {required: true})
|
||||||
)
|
)
|
||||||
core.startGroup('Rejecting deployment with blue green strategy')
|
|
||||||
core.info(`using routeMethod ${routeStrategy}`)
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(manifests)
|
|
||||||
|
|
||||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||||
await rejectBlueGreenIngress(kubectl, manifestObjects)
|
await rejectBlueGreenIngress(kubectl, manifests)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await rejectBlueGreenSMI(kubectl, manifestObjects)
|
await rejectBlueGreenSMI(kubectl, manifests, annotations)
|
||||||
} else {
|
} else {
|
||||||
await rejectBlueGreenService(kubectl, manifestObjects)
|
await rejectBlueGreenService(kubectl, manifests)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {parseAnnotations} from './types/annotations'
|
|
||||||
|
|
||||||
export const inputAnnotations = parseAnnotations(
|
|
||||||
core.getInput('annotations', {required: false})
|
|
||||||
)
|
|
||||||
|
|
||||||
export function getBufferTime(): number {
|
|
||||||
const inputBufferTime = parseInt(
|
|
||||||
core.getInput('version-switch-buffer') || '0'
|
|
||||||
)
|
|
||||||
if (inputBufferTime < 0 || inputBufferTime > 300)
|
|
||||||
throw Error('Version switch buffer must be between 0 and 300 (inclusive)')
|
|
||||||
|
|
||||||
return inputBufferTime
|
|
||||||
}
|
|
||||||
+7
-3
@@ -7,6 +7,7 @@ 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 {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
|
||||||
@@ -19,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
|
||||||
@@ -47,15 +51,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: {
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
deployWithLabel,
|
createWorkloadsWithLabel,
|
||||||
deleteGreenObjects,
|
deleteWorkloadsAndServicesWithLabel,
|
||||||
fetchResource,
|
|
||||||
getDeploymentMatchLabels,
|
|
||||||
getManifestObjects,
|
getManifestObjects,
|
||||||
getNewBlueGreenObject,
|
getNewBlueGreenObject,
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
isServiceRouted
|
isServiceRouted,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
import * as bgHelper from './blueGreenHelper'
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {K8sObject} from '../../types/k8sObject'
|
|
||||||
import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils'
|
|
||||||
import {ExecOutput} from '@actions/exec'
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
jest.mock('../../types/kubectl')
|
||||||
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
describe('bluegreenhelper functions', () => {
|
describe('bluegreenhelper functions', () => {
|
||||||
let testObjects
|
let testObjects
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -32,29 +25,7 @@ describe('bluegreenhelper functions', () => {
|
|||||||
.mockImplementationOnce(() => [''])
|
.mockImplementationOnce(() => [''])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('correctly deletes services and workloads according to label', async () => {
|
test('it should parse objects correctly from one file', () => {
|
||||||
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
|
|
||||||
|
|
||||||
const value = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
[].concat(
|
|
||||||
testObjects.deploymentEntityList,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(value).toHaveLength(2)
|
|
||||||
expect(value).toContainEqual({
|
|
||||||
name: 'nginx-service-green',
|
|
||||||
kind: 'Service'
|
|
||||||
})
|
|
||||||
expect(value).toContainEqual({
|
|
||||||
name: 'nginx-deployment-green',
|
|
||||||
kind: 'Deployment'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parses objects correctly from one file (getManifestObjects)', () => {
|
|
||||||
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
|
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
|
||||||
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
|
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
|
||||||
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
|
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
|
||||||
@@ -64,16 +35,40 @@ describe('bluegreenhelper functions', () => {
|
|||||||
).toBe('nginx')
|
).toBe('nginx')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('parses other kinds of objects (getManifestObjects)', () => {
|
test('correctly makes new blue green object', () => {
|
||||||
const otherObjectsCollection = getManifestObjects([
|
const modifiedDeployment = getNewBlueGreenObject(
|
||||||
'test/unit/manifests/anomaly-objects-test.yml'
|
testObjects.deploymentEntityList[0],
|
||||||
])
|
GREEN_LABEL_VALUE
|
||||||
expect(
|
|
||||||
otherObjectsCollection.unroutedServiceEntityList[0].metadata.name
|
|
||||||
).toBe('unrouted-service')
|
|
||||||
expect(otherObjectsCollection.otherObjects[0].metadata.name).toBe(
|
|
||||||
'foobar-rollout'
|
|
||||||
)
|
)
|
||||||
|
//@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', () => {
|
test('correctly classifies routed services', () => {
|
||||||
@@ -92,105 +87,42 @@ describe('bluegreenhelper functions', () => {
|
|||||||
).toBe(false)
|
).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('correctly makes labeled workloads', async () => {
|
test('correctly deletes services and workloads according to label', () => {
|
||||||
const cwlResult: BlueGreenDeployment = await deployWithLabel(
|
const kubectl = new Kubectl('')
|
||||||
|
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
|
||||||
|
|
||||||
|
let objectsToDelete = deleteWorkloadsAndServicesWithLabel(
|
||||||
kubectl,
|
kubectl,
|
||||||
|
NONE_LABEL_VALUE,
|
||||||
testObjects.deploymentEntityList,
|
testObjects.deploymentEntityList,
|
||||||
GREEN_LABEL_VALUE
|
testObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
expect(cwlResult.deployResult.manifestFiles[0]).toBe('')
|
objectsToDelete.then((value) => {
|
||||||
})
|
expect(value).toHaveLength(2)
|
||||||
|
expect(value).toContainEqual
|
||||||
test('correctly makes new blue green object (getNewBlueGreenObject and addBlueGreenLabelsAndAnnotations)', () => {
|
;({name: 'nginx-service', kind: 'Service'})
|
||||||
const modifiedDeployment = getNewBlueGreenObject(
|
expect(value).toContainEqual({
|
||||||
testObjects.deploymentEntityList[0],
|
name: 'nginx-deployment',
|
||||||
GREEN_LABEL_VALUE
|
kind: 'Deployment'
|
||||||
)
|
|
||||||
|
|
||||||
expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green')
|
|
||||||
expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe(
|
|
||||||
'green'
|
|
||||||
)
|
|
||||||
|
|
||||||
const modifiedSvc = getNewBlueGreenObject(
|
|
||||||
testObjects.serviceEntityList[0],
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(modifiedSvc.metadata.name).toBe('nginx-service-green')
|
|
||||||
expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly fetches k8s objects', async () => {
|
|
||||||
const mockExecOutput = {
|
|
||||||
stderr: '',
|
|
||||||
stdout: JSON.stringify(testObjects.deploymentEntityList[0]),
|
|
||||||
exitCode: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(kubectl, 'getResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockExecOutput))
|
|
||||||
const fetched = await fetchResource(
|
|
||||||
kubectl,
|
|
||||||
'nginx-deployment',
|
|
||||||
'Deployment'
|
|
||||||
)
|
|
||||||
expect(fetched.metadata.name).toBe('nginx-deployment')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('exits when fails to fetch k8s objects', async () => {
|
|
||||||
const mockExecOutput = {
|
|
||||||
stdout: 'this should not matter',
|
|
||||||
exitCode: 0,
|
|
||||||
stderr: 'this is a fake error'
|
|
||||||
} as ExecOutput
|
|
||||||
jest
|
|
||||||
.spyOn(kubectl, 'getResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockExecOutput))
|
|
||||||
let fetched = await fetchResource(
|
|
||||||
kubectl,
|
|
||||||
'nginx-deployment',
|
|
||||||
'Deployment'
|
|
||||||
)
|
|
||||||
expect(fetched).toBe(null)
|
|
||||||
|
|
||||||
jest.spyOn(kubectl, 'getResource').mockImplementation()
|
|
||||||
fetched = await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
|
|
||||||
expect(fetched).toBe(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null when fetch fails to unset k8s objects', async () => {
|
|
||||||
const mockExecOutput = {
|
|
||||||
stdout: 'this should not matter',
|
|
||||||
exitCode: 0,
|
|
||||||
stderr: 'this is a fake error'
|
|
||||||
} as ExecOutput
|
|
||||||
jest
|
|
||||||
.spyOn(manifestUpdateUtils, 'UnsetClusterSpecificDetails')
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Error('test error')
|
|
||||||
})
|
})
|
||||||
expect(
|
})
|
||||||
await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
|
|
||||||
).toBe(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('gets deployment labels', () => {
|
objectsToDelete = deleteWorkloadsAndServicesWithLabel(
|
||||||
const mockLabels = new Map<string, string>()
|
kubectl,
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
|
GREEN_LABEL_VALUE,
|
||||||
const mockPodObject: K8sObject = {
|
testObjects.deploymentEntityList,
|
||||||
kind: 'Pod',
|
testObjects.serviceEntityList
|
||||||
metadata: {name: 'testPod', labels: mockLabels},
|
)
|
||||||
spec: {}
|
objectsToDelete.then((value) => {
|
||||||
}
|
expect(value).toHaveLength(2)
|
||||||
expect(
|
expect(value).toContainEqual({
|
||||||
getDeploymentMatchLabels(mockPodObject)[
|
name: 'nginx-service-green',
|
||||||
bgHelper.BLUE_GREEN_VERSION_LABEL
|
kind: 'Service'
|
||||||
]
|
})
|
||||||
).toBe(GREEN_LABEL_VALUE)
|
expect(value).toContainEqual({
|
||||||
expect(
|
name: 'nginx-deployment-green',
|
||||||
getDeploymentMatchLabels(testObjects.deploymentEntityList[0])['app']
|
kind: 'Deployment'
|
||||||
).toBe('nginx')
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
|
|
||||||
import {DeployResult} from '../../types/deployResult'
|
|
||||||
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import {
|
import {
|
||||||
isDeploymentEntity,
|
isDeploymentEntity,
|
||||||
@@ -11,18 +8,19 @@ import {
|
|||||||
isServiceEntity,
|
isServiceEntity,
|
||||||
KubernetesWorkload
|
KubernetesWorkload
|
||||||
} from '../../types/kubernetesTypes'
|
} from '../../types/kubernetesTypes'
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
|
import {routeBlueGreenService} from './serviceBlueGreenHelper'
|
||||||
import {checkForErrors} from '../../utilities/kubectlUtils'
|
import {routeBlueGreenIngress} from './ingressBlueGreenHelper'
|
||||||
|
import {routeBlueGreenSMI} from './smiBlueGreenHelper'
|
||||||
import {
|
import {
|
||||||
UnsetClusterSpecificDetails,
|
UnsetClusterSpecificDetails,
|
||||||
updateObjectLabels,
|
updateObjectLabels,
|
||||||
updateSelectorLabels
|
updateSelectorLabels
|
||||||
} from '../../utilities/manifestUpdateUtils'
|
} from '../../utilities/manifestUpdateUtils'
|
||||||
|
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
|
||||||
|
import {checkForErrors} from '../../utilities/kubectlUtils'
|
||||||
|
import {sleep} from '../../utilities/timeUtils'
|
||||||
|
import {RouteStrategy} from '../../types/routeStrategy'
|
||||||
|
|
||||||
export const GREEN_LABEL_VALUE = 'green'
|
export const GREEN_LABEL_VALUE = 'green'
|
||||||
export const NONE_LABEL_VALUE = 'None'
|
export const NONE_LABEL_VALUE = 'None'
|
||||||
@@ -30,46 +28,144 @@ export const BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color'
|
|||||||
export const GREEN_SUFFIX = '-green'
|
export const GREEN_SUFFIX = '-green'
|
||||||
export const STABLE_SUFFIX = '-stable'
|
export const STABLE_SUFFIX = '-stable'
|
||||||
|
|
||||||
export async function deleteGreenObjects(
|
export interface BlueGreenManifests {
|
||||||
|
serviceEntityList: any[]
|
||||||
|
serviceNameMap: Map<string, string>
|
||||||
|
unroutedServiceEntityList: any[]
|
||||||
|
deploymentEntityList: any[]
|
||||||
|
ingressEntityList: any[]
|
||||||
|
otherObjects: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function routeBlueGreen(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
toDelete: K8sObject[]
|
inputManifestFiles: string[],
|
||||||
): Promise<K8sDeleteObject[]> {
|
routeStrategy: RouteStrategy,
|
||||||
// const resourcesToDelete: K8sDeleteObject[] = []
|
annotations: {[key: string]: string} = {}
|
||||||
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
|
) {
|
||||||
return {
|
// sleep for buffer time
|
||||||
name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX),
|
const bufferTime: number = parseInt(
|
||||||
kind: obj.kind
|
core.getInput('version-switch-buffer') || '0'
|
||||||
|
)
|
||||||
|
if (bufferTime < 0 || bufferTime > 300)
|
||||||
|
throw Error('Version switch buffer must be between 0 and 300 (inclusive)')
|
||||||
|
const startSleepDate = new Date()
|
||||||
|
core.info(
|
||||||
|
`Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}`
|
||||||
|
)
|
||||||
|
await sleep(bufferTime * 1000 * 60)
|
||||||
|
const endSleepDate = new Date()
|
||||||
|
core.info(
|
||||||
|
`Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const manifestObjects: BlueGreenManifests =
|
||||||
|
getManifestObjects(inputManifestFiles)
|
||||||
|
core.debug('Manifest objects: ' + JSON.stringify(manifestObjects))
|
||||||
|
|
||||||
|
// route to new deployments
|
||||||
|
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||||
|
await routeBlueGreenIngress(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.serviceNameMap,
|
||||||
|
manifestObjects.ingressEntityList
|
||||||
|
)
|
||||||
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
|
await routeBlueGreenSMI(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.serviceEntityList,
|
||||||
|
annotations
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await routeBlueGreenService(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.serviceEntityList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWorkloadsWithLabel(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
deleteLabel: string,
|
||||||
|
deploymentEntityList: any[]
|
||||||
|
) {
|
||||||
|
const resourcesToDelete = []
|
||||||
|
deploymentEntityList.forEach((inputObject) => {
|
||||||
|
const name = inputObject.metadata.name
|
||||||
|
const kind = inputObject.kind
|
||||||
|
|
||||||
|
if (deleteLabel === NONE_LABEL_VALUE) {
|
||||||
|
// delete stable deployments
|
||||||
|
const resourceToDelete = {name, kind}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
|
} else {
|
||||||
|
// delete new green deployments
|
||||||
|
const resourceToDelete = {
|
||||||
|
name: getBlueGreenResourceName(name, GREEN_SUFFIX),
|
||||||
|
kind: kind
|
||||||
|
}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`)
|
|
||||||
|
|
||||||
await deleteObjects(kubectl, resourcesToDelete)
|
await deleteObjects(kubectl, resourcesToDelete)
|
||||||
return resourcesToDelete
|
return resourcesToDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteObjects(
|
export async function deleteWorkloadsAndServicesWithLabel(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deleteList: K8sDeleteObject[]
|
deleteLabel: string,
|
||||||
|
deploymentEntityList: any[],
|
||||||
|
serviceEntityList: any[]
|
||||||
) {
|
) {
|
||||||
|
// need to delete services and deployments
|
||||||
|
const deletionEntitiesList = deploymentEntityList.concat(serviceEntityList)
|
||||||
|
const resourcesToDelete = []
|
||||||
|
|
||||||
|
deletionEntitiesList.forEach((inputObject) => {
|
||||||
|
const name = inputObject.metadata.name
|
||||||
|
const kind = inputObject.kind
|
||||||
|
|
||||||
|
if (deleteLabel === NONE_LABEL_VALUE) {
|
||||||
|
// delete stable objects
|
||||||
|
const resourceToDelete = {name, kind}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
|
} else {
|
||||||
|
// delete green labels
|
||||||
|
const resourceToDelete = {
|
||||||
|
name: getBlueGreenResourceName(name, GREEN_SUFFIX),
|
||||||
|
kind: kind
|
||||||
|
}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await deleteObjects(kubectl, resourcesToDelete)
|
||||||
|
return resourcesToDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) {
|
||||||
// delete services and deployments
|
// delete services and deployments
|
||||||
for (const delObject of deleteList) {
|
for (const delObject of deleteList) {
|
||||||
try {
|
try {
|
||||||
const result = await kubectl.delete([delObject.kind, delObject.name])
|
const result = await kubectl.delete([delObject.kind, delObject.name])
|
||||||
checkForErrors([result])
|
checkForErrors([result])
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.debug(`failed to delete object ${delObject.name}: ${ex}`)
|
// Ignore failures of delete if it doesn't exist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// other common functions
|
// other common functions
|
||||||
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
||||||
const deploymentEntityList: K8sObject[] = []
|
const deploymentEntityList = []
|
||||||
const routedServiceEntityList: K8sObject[] = []
|
const routedServiceEntityList = []
|
||||||
const unroutedServiceEntityList: K8sObject[] = []
|
const unroutedServiceEntityList = []
|
||||||
const ingressEntityList: K8sObject[] = []
|
const ingressEntityList = []
|
||||||
const otherEntitiesList: K8sObject[] = []
|
const otherEntitiesList = []
|
||||||
const serviceNameMap = new Map<string, string>()
|
const serviceNameMap = new Map<string, string>()
|
||||||
|
|
||||||
filePaths.forEach((filePath: string) => {
|
filePaths.forEach((filePath: string) => {
|
||||||
@@ -114,41 +210,48 @@ export function isServiceRouted(
|
|||||||
serviceObject: any[],
|
serviceObject: any[],
|
||||||
deploymentEntityList: any[]
|
deploymentEntityList: any[]
|
||||||
): boolean {
|
): boolean {
|
||||||
|
let shouldBeRouted: boolean = false
|
||||||
const serviceSelector: any = getServiceSelector(serviceObject)
|
const serviceSelector: any = getServiceSelector(serviceObject)
|
||||||
|
if (serviceSelector) {
|
||||||
|
if (
|
||||||
|
deploymentEntityList.some((depObject) => {
|
||||||
|
// finding if there is a deployment in the given manifests the service targets
|
||||||
|
const matchLabels: any = getDeploymentMatchLabels(depObject)
|
||||||
|
return (
|
||||||
|
matchLabels &&
|
||||||
|
isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
shouldBeRouted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return shouldBeRouted
|
||||||
serviceSelector &&
|
|
||||||
deploymentEntityList.some((depObject) => {
|
|
||||||
// finding if there is a deployment in the given manifests the service targets
|
|
||||||
const matchLabels: any = getDeploymentMatchLabels(depObject)
|
|
||||||
return (
|
|
||||||
matchLabels &&
|
|
||||||
isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deployWithLabel(
|
export async function createWorkloadsWithLabel(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deploymentObjectList: any[],
|
deploymentObjectList: any[],
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
): Promise<BlueGreenDeployment> {
|
) {
|
||||||
const newObjectsList = deploymentObjectList.map((inputObject) =>
|
const newObjectsList = []
|
||||||
getNewBlueGreenObject(inputObject, nextLabel)
|
deploymentObjectList.forEach((inputObject) => {
|
||||||
)
|
// creating deployment with label
|
||||||
|
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel)
|
||||||
|
newObjectsList.push(newBlueGreenObject)
|
||||||
|
})
|
||||||
|
|
||||||
core.debug(
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
|
const result = await kubectl.apply(manifestFiles)
|
||||||
)
|
|
||||||
const deployResult = await deployObjects(kubectl, newObjectsList)
|
return {result: result, newFilePaths: manifestFiles}
|
||||||
return {deployResult, objects: newObjectsList}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNewBlueGreenObject(
|
export function getNewBlueGreenObject(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): K8sObject {
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
|
||||||
// Updating name only if label is green label is given
|
// Updating name only if label is green label is given
|
||||||
@@ -235,14 +338,14 @@ export async function fetchResource(
|
|||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
kind: string,
|
kind: string,
|
||||||
name: string
|
name: string
|
||||||
): Promise<K8sObject> {
|
) {
|
||||||
const result = await kubectl.getResource(kind, name)
|
const result = await kubectl.getResource(kind, name)
|
||||||
if (result == null || !!result.stderr) {
|
if (result == null || !!result.stderr) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!result.stdout) {
|
if (!!result.stdout) {
|
||||||
const resource = JSON.parse(result.stdout) as K8sObject
|
const resource = JSON.parse(result.stdout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UnsetClusterSpecificDetails(resource)
|
UnsetClusterSpecificDetails(resource)
|
||||||
@@ -254,13 +357,3 @@ export async function fetchResource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deployObjects(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
objectsList: any[]
|
|
||||||
): Promise<DeployResult> {
|
|
||||||
const manifestFiles = fileHelper.writeObjectsToFile(objectsList)
|
|
||||||
const execResult = await kubectl.apply(manifestFiles)
|
|
||||||
|
|
||||||
return {execResult, manifestFiles}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {deployBlueGreen, deployBlueGreenIngress} from './deploy'
|
|
||||||
import * as routeTester from './route'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
describe('deploy tests', () => {
|
|
||||||
let testObjects
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly determines deploy type and acts accordingly', async () => {
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
const mockBgDeployment: BlueGreenDeployment = {
|
|
||||||
deployResult: {
|
|
||||||
execResult: {exitCode: 0, stderr: '', stdout: ''},
|
|
||||||
manifestFiles: []
|
|
||||||
},
|
|
||||||
objects: []
|
|
||||||
}
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(routeTester, 'routeBlueGreenForDeploy')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockBgDeployment))
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
|
|
||||||
const ingressResult = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.INGRESS
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(ingressResult.objects.length).toBe(2)
|
|
||||||
|
|
||||||
const result = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SERVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.objects.length).toBe(2)
|
|
||||||
|
|
||||||
const smiResult = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SMI
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(smiResult.objects.length).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly deploys blue/green ingress', async () => {
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
const value = await deployBlueGreenIngress(kc, ingressFilepath)
|
|
||||||
const nol = value.objects.map((obj) => {
|
|
||||||
if (obj.kind === 'Service') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-service-green')
|
|
||||||
}
|
|
||||||
if (obj.kind === 'Deployment') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-deployment-green')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
|
|
||||||
import {
|
|
||||||
deployWithLabel,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
deployObjects
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import {setupSMI} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
import {routeBlueGreenForDeploy} from './route'
|
|
||||||
|
|
||||||
export async function deployBlueGreen(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
files: string[],
|
|
||||||
routeStrategy: RouteStrategy
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const blueGreenDeployment = await (async () => {
|
|
||||||
switch (routeStrategy) {
|
|
||||||
case RouteStrategy.INGRESS:
|
|
||||||
return await deployBlueGreenIngress(kubectl, files)
|
|
||||||
case RouteStrategy.SMI:
|
|
||||||
return await deployBlueGreenSMI(kubectl, files)
|
|
||||||
default:
|
|
||||||
return await deployBlueGreenService(kubectl, files)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
core.startGroup('Routing blue green')
|
|
||||||
await routeBlueGreenForDeploy(kubectl, files, routeStrategy)
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
return blueGreenDeployment
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create services and other objects
|
|
||||||
const newObjectsList = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.serviceEntityList,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
await deployObjects(kubectl, newObjectsList)
|
|
||||||
|
|
||||||
// make extraservices and trafficsplit
|
|
||||||
await setupSMI(kubectl, manifestObjects.serviceEntityList)
|
|
||||||
|
|
||||||
// create new deloyments
|
|
||||||
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
deployResult: blueGreenDeployment.deployResult,
|
|
||||||
objects: [].concat(blueGreenDeployment.objects, newObjectsList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create deployments with green label value
|
|
||||||
const servicesAndDeployments = [].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
const workloadDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
servicesAndDeployments,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
const otherObjects = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
await deployObjects(kubectl, otherObjects)
|
|
||||||
core.debug(
|
|
||||||
`new objects after processing services and other objects: \n
|
|
||||||
${JSON.stringify(servicesAndDeployments)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
deployResult: workloadDeployment.deployResult,
|
|
||||||
objects: [].concat(workloadDeployment.objects, otherObjects)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create deployments with green label value
|
|
||||||
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create other non deployment and non service entities
|
|
||||||
const newObjectsList = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
await deployObjects(kubectl, newObjectsList)
|
|
||||||
// returning deployment details to check for rollout stability
|
|
||||||
return {
|
|
||||||
deployResult: blueGreenDeployment.deployResult,
|
|
||||||
objects: [].concat(blueGreenDeployment.objects, newObjectsList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
|
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
import {
|
import {
|
||||||
|
deployBlueGreenIngress,
|
||||||
getUpdatedBlueGreenIngress,
|
getUpdatedBlueGreenIngress,
|
||||||
isIngressRouted,
|
isIngressRouted,
|
||||||
validateIngresses
|
routeBlueGreenIngress
|
||||||
} from './ingressBlueGreenHelper'
|
} from './ingressBlueGreenHelper'
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
|
|
||||||
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
jest.mock('../../types/kubectl')
|
jest.mock('../../types/kubectl')
|
||||||
|
|
||||||
describe('ingress blue green helpers', () => {
|
describe('ingress blue green helpers', () => {
|
||||||
let testObjects
|
let testObjects
|
||||||
|
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
|
||||||
|
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
Kubectl.mockClear()
|
Kubectl.mockClear()
|
||||||
@@ -38,6 +38,7 @@ describe('ingress blue green helpers', () => {
|
|||||||
testObjects.serviceNameMap
|
testObjects.serviceNameMap
|
||||||
)
|
)
|
||||||
).toBe(false)
|
).toBe(false)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
isIngressRouted(
|
isIngressRouted(
|
||||||
getManifestObjects(betaFilepath).ingressEntityList[0],
|
getManifestObjects(betaFilepath).ingressEntityList[0],
|
||||||
@@ -52,72 +53,36 @@ describe('ingress blue green helpers', () => {
|
|||||||
testObjects.serviceNameMap,
|
testObjects.serviceNameMap,
|
||||||
GREEN_LABEL_VALUE
|
GREEN_LABEL_VALUE
|
||||||
)
|
)
|
||||||
expect(updatedIng.metadata.name).toBe('nginx-ingress')
|
//@ts-ignore
|
||||||
expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green')
|
|
||||||
expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe(
|
|
||||||
'nginx-service-green'
|
|
||||||
)
|
|
||||||
|
|
||||||
const oldIngObjects = getManifestObjects(betaFilepath)
|
|
||||||
const oldIng = getUpdatedBlueGreenIngress(
|
|
||||||
oldIngObjects.ingressEntityList[0],
|
|
||||||
oldIngObjects.serviceNameMap,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green')
|
expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green')
|
||||||
|
//@ts-ignore
|
||||||
expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe(
|
expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe(
|
||||||
'nginx-service-green'
|
'nginx-service-green'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it should validate ingresses', async () => {
|
test('correctly prepares blue/green ingresses for deployment', () => {
|
||||||
// what if nothing gets returned from fetchResource?
|
const kc = new Kubectl('')
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
|
const generatedObjects = routeBlueGreenIngress(
|
||||||
let validResponse = await validateIngresses(
|
kc,
|
||||||
kubectl,
|
GREEN_LABEL_VALUE,
|
||||||
testObjects.ingressEntityList,
|
testObjects.serviceNameMap,
|
||||||
testObjects.serviceNameMap
|
testObjects.ingressEntityList
|
||||||
)
|
)
|
||||||
expect(validResponse.areValid).toBe(false)
|
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)
|
||||||
|
|
||||||
// test valid ingress
|
result.then((value) => {
|
||||||
let mockIngress = JSON.parse(
|
const nol = value.newObjectsList
|
||||||
JSON.stringify(testObjects.ingressEntityList[0])
|
//@ts-ignore
|
||||||
)
|
expect(nol[0].metadata.name).toBe('nginx-service-green')
|
||||||
mockIngress.spec.rules[0].http.paths[0].backend.service.name =
|
})
|
||||||
'nginx-service-green'
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
|
|
||||||
mockIngress.metadata.labels = mockLabels
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockIngress))
|
|
||||||
validResponse = await validateIngresses(
|
|
||||||
kubectl,
|
|
||||||
testObjects.ingressEntityList,
|
|
||||||
testObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
expect(validResponse.areValid).toBe(true)
|
|
||||||
|
|
||||||
// test invalid labels
|
|
||||||
mockIngress.metadata.labels[bgHelper.BLUE_GREEN_VERSION_LABEL] =
|
|
||||||
bgHelper.NONE_LABEL_VALUE
|
|
||||||
mockIngress.spec.rules[0].http.paths[0].backend.service.name =
|
|
||||||
'nginx-service'
|
|
||||||
validResponse = await validateIngresses(
|
|
||||||
kubectl,
|
|
||||||
testObjects.ingressEntityList,
|
|
||||||
testObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
expect(validResponse.areValid).toBe(false)
|
|
||||||
|
|
||||||
// test missing fields
|
|
||||||
mockIngress = {}
|
|
||||||
validResponse = await validateIngresses(
|
|
||||||
kubectl,
|
|
||||||
testObjects.ingressEntityList,
|
|
||||||
testObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
expect(validResponse.areValid).toBe(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,209 @@
|
|||||||
import * as core from '@actions/core'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import {K8sIngress} from '../../types/k8sObject'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
addBlueGreenLabelsAndAnnotations,
|
addBlueGreenLabelsAndAnnotations,
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
BLUE_GREEN_VERSION_LABEL,
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
|
deleteWorkloadsAndServicesWithLabel,
|
||||||
|
fetchResource,
|
||||||
|
getManifestObjects,
|
||||||
|
getNewBlueGreenObject,
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
fetchResource
|
NONE_LABEL_VALUE
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
const BACKEND = 'backend'
|
const BACKEND = 'backend'
|
||||||
|
|
||||||
|
export async function deployBlueGreenIngress(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// create deployments with green label value
|
||||||
|
const workloadDeployment = await createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
|
let newObjectsList = []
|
||||||
|
manifestObjects.serviceEntityList.forEach((inputObject) => {
|
||||||
|
const newBlueGreenObject = getNewBlueGreenObject(
|
||||||
|
inputObject,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
newObjectsList.push(newBlueGreenObject)
|
||||||
|
})
|
||||||
|
newObjectsList = newObjectsList
|
||||||
|
.concat(manifestObjects.otherObjects)
|
||||||
|
.concat(manifestObjects.unroutedServiceEntityList)
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
'new objects after processing services and other objects: \n' +
|
||||||
|
JSON.stringify(newObjectsList)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {workloadDeployment, newObjectsList}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promoteBlueGreenIngress(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
manifestObjects
|
||||||
|
) {
|
||||||
|
//checking if anything to promote
|
||||||
|
const {areValid, invalidIngresses} = validateIngresses(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.ingressEntityList,
|
||||||
|
manifestObjects.serviceNameMap
|
||||||
|
)
|
||||||
|
if (!areValid) {
|
||||||
|
throw 'Ingresses are not in promote state' + invalidIngresses.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// create stable deployments with new configuration
|
||||||
|
const result = createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
|
// create stable services with new configuration
|
||||||
|
const newObjectsList = []
|
||||||
|
manifestObjects.serviceEntityList.forEach((inputObject) => {
|
||||||
|
const newBlueGreenObject = getNewBlueGreenObject(
|
||||||
|
inputObject,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
|
)
|
||||||
|
newObjectsList.push(newBlueGreenObject)
|
||||||
|
})
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rejectBlueGreenIngress(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// route ingress to stables services
|
||||||
|
await routeBlueGreenIngress(
|
||||||
|
kubectl,
|
||||||
|
null,
|
||||||
|
manifestObjects.serviceNameMap,
|
||||||
|
manifestObjects.ingressEntityList
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete green services and deployments
|
||||||
|
await deleteWorkloadsAndServicesWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
manifestObjects.serviceEntityList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function routeBlueGreenIngress(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
nextLabel: string,
|
||||||
|
serviceNameMap: Map<string, string>,
|
||||||
|
ingressEntityList: any[]
|
||||||
|
) {
|
||||||
|
let newObjectsList = []
|
||||||
|
|
||||||
|
if (!nextLabel) {
|
||||||
|
newObjectsList = ingressEntityList.filter((ingress) =>
|
||||||
|
isIngressRouted(ingress, serviceNameMap)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ingressEntityList.forEach((inputObject) => {
|
||||||
|
if (isIngressRouted(inputObject, serviceNameMap)) {
|
||||||
|
const newBlueGreenIngressObject = getUpdatedBlueGreenIngress(
|
||||||
|
inputObject,
|
||||||
|
serviceNameMap,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
newObjectsList.push(newBlueGreenIngressObject)
|
||||||
|
} else {
|
||||||
|
newObjectsList.push(inputObject)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
return newObjectsList
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateIngresses(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
ingressEntityList: any[],
|
||||||
|
serviceNameMap: Map<string, string>
|
||||||
|
): {areValid: boolean; invalidIngresses: string[]} {
|
||||||
|
let areValid: boolean = true
|
||||||
|
const invalidIngresses = []
|
||||||
|
ingressEntityList.forEach(async (inputObject) => {
|
||||||
|
if (isIngressRouted(inputObject, serviceNameMap)) {
|
||||||
|
//querying existing ingress
|
||||||
|
const existingIngress = await fetchResource(
|
||||||
|
kubectl,
|
||||||
|
inputObject.kind,
|
||||||
|
inputObject.metadata.name
|
||||||
|
)
|
||||||
|
|
||||||
|
let isValid =
|
||||||
|
!!existingIngress &&
|
||||||
|
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] ===
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
if (!isValid) {
|
||||||
|
invalidIngresses.push(inputObject.metadata.name)
|
||||||
|
}
|
||||||
|
// to be valid, ingress should exist and should be green
|
||||||
|
areValid = areValid && isValid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {areValid, invalidIngresses}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIngressRouted(
|
||||||
|
ingressObject: any,
|
||||||
|
serviceNameMap: Map<string, string>
|
||||||
|
): boolean {
|
||||||
|
let isIngressRouted: boolean = false
|
||||||
|
// check if ingress targets a service in the given manifests
|
||||||
|
JSON.parse(JSON.stringify(ingressObject), (key, value) => {
|
||||||
|
isIngressRouted =
|
||||||
|
isIngressRouted || (key === 'service' && value.hasOwnProperty('name'))
|
||||||
|
isIngressRouted =
|
||||||
|
isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
|
||||||
|
return isIngressRouted
|
||||||
|
}
|
||||||
|
|
||||||
export function getUpdatedBlueGreenIngress(
|
export function getUpdatedBlueGreenIngress(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
serviceNameMap: Map<string, string>,
|
serviceNameMap: Map<string, string>,
|
||||||
type: string
|
type: string
|
||||||
): K8sIngress {
|
): object {
|
||||||
|
if (!type) {
|
||||||
|
return inputObject
|
||||||
|
}
|
||||||
|
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
// add green labels and values
|
// add green labels and values
|
||||||
addBlueGreenLabelsAndAnnotations(newObject, type)
|
addBlueGreenLabelsAndAnnotations(newObject, type)
|
||||||
@@ -52,7 +241,7 @@ export function updateIngressBackend(
|
|||||||
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||||
if (
|
if (
|
||||||
key.toLowerCase() === BACKEND &&
|
key.toLowerCase() === BACKEND &&
|
||||||
serviceNameMap.has(value.service.name)
|
serviceNameMap.has(value?.service?.name)
|
||||||
) {
|
) {
|
||||||
value.service.name = serviceNameMap.get(value.service.name)
|
value.service.name = serviceNameMap.get(value.service.name)
|
||||||
}
|
}
|
||||||
@@ -61,60 +250,3 @@ export function updateIngressBackend(
|
|||||||
|
|
||||||
return inputObject
|
return inputObject
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIngressRouted(
|
|
||||||
ingressObject: any,
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
): boolean {
|
|
||||||
let isIngressRouted: boolean = false
|
|
||||||
// check if ingress targets a service in the given manifests
|
|
||||||
JSON.parse(JSON.stringify(ingressObject), (key, value) => {
|
|
||||||
isIngressRouted =
|
|
||||||
isIngressRouted ||
|
|
||||||
(key === 'service' &&
|
|
||||||
value.hasOwnProperty('name') &&
|
|
||||||
serviceNameMap.has(value.name))
|
|
||||||
isIngressRouted =
|
|
||||||
isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value))
|
|
||||||
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
|
|
||||||
return isIngressRouted
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateIngresses(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
ingressEntityList: any[],
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
): Promise<{areValid: boolean; invalidIngresses: string[]}> {
|
|
||||||
let areValid: boolean = true
|
|
||||||
const invalidIngresses = []
|
|
||||||
|
|
||||||
for (const inputObject of ingressEntityList) {
|
|
||||||
if (isIngressRouted(inputObject, serviceNameMap)) {
|
|
||||||
//querying existing ingress
|
|
||||||
const existingIngress = await fetchResource(
|
|
||||||
kubectl,
|
|
||||||
inputObject.kind,
|
|
||||||
inputObject.metadata.name
|
|
||||||
)
|
|
||||||
|
|
||||||
const isValid =
|
|
||||||
!!existingIngress &&
|
|
||||||
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] ===
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
if (!isValid) {
|
|
||||||
core.debug(
|
|
||||||
`Invalid ingress detected (must be in green state): ${JSON.stringify(
|
|
||||||
inputObject
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
invalidIngresses.push(inputObject.metadata.name)
|
|
||||||
}
|
|
||||||
// to be valid, ingress should exist and should be green
|
|
||||||
areValid = areValid && isValid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {areValid, invalidIngresses}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {
|
|
||||||
promoteBlueGreenIngress,
|
|
||||||
promoteBlueGreenService,
|
|
||||||
promoteBlueGreenSMI
|
|
||||||
} from './promote'
|
|
||||||
import {TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import * as servicesTester from './serviceBlueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper'
|
|
||||||
import * as smiTester from './smiBlueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
|
|
||||||
let testObjects
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
describe('promote tests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green ingress', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Ingress',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const value = await promoteBlueGreenIngress(kubectl, testObjects)
|
|
||||||
|
|
||||||
const objects = value.objects
|
|
||||||
expect(objects).toHaveLength(2)
|
|
||||||
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj.kind === 'Service') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-service')
|
|
||||||
} else if (obj.kind == 'Deployment') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-deployment')
|
|
||||||
}
|
|
||||||
expect(obj.metadata.labels['k8s.deploy.color']).toBe('None')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fail to promote invalid blue/green ingress', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Ingress',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
promoteBlueGreenIngress(kubectl, testObjects)
|
|
||||||
).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green service', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {selector: mockLabels},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-service-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
let value = await promoteBlueGreenService(kubectl, testObjects)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(1)
|
|
||||||
expect(
|
|
||||||
value.objects[0].metadata.labels[bgHelper.BLUE_GREEN_VERSION_LABEL]
|
|
||||||
).toBe(bgHelper.NONE_LABEL_VALUE)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-deployment')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fail to promote invalid blue/green service', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
jest
|
|
||||||
.spyOn(servicesTester, 'validateServicesState')
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(false))
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
promoteBlueGreenService(kubectl, testObjects)
|
|
||||||
).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green SMI', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
|
|
||||||
const mockTsObject: TrafficSplitObject = {
|
|
||||||
apiVersion: 'v1alpha3',
|
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
|
||||||
metadata: {
|
|
||||||
name: 'nginx-service-trafficsplit',
|
|
||||||
labels: new Map<string, string>(),
|
|
||||||
annotations: new Map<string, string>()
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
service: 'nginx-service',
|
|
||||||
backends: [
|
|
||||||
{
|
|
||||||
service: 'nginx-service-stable',
|
|
||||||
weight: MIN_VAL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'nginx-service-green',
|
|
||||||
weight: MAX_VAL
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsObject))
|
|
||||||
|
|
||||||
const deployResult = await promoteBlueGreenSMI(kubectl, testObjects)
|
|
||||||
|
|
||||||
expect(deployResult.objects).toHaveLength(1)
|
|
||||||
expect(deployResult.objects[0].metadata.name).toBe('nginx-deployment')
|
|
||||||
expect(
|
|
||||||
deployResult.objects[0].metadata.labels[
|
|
||||||
bgHelper.BLUE_GREEN_VERSION_LABEL
|
|
||||||
]
|
|
||||||
).toBe(bgHelper.NONE_LABEL_VALUE)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green SMI with bad trafficsplit', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest
|
|
||||||
.spyOn(smiTester, 'validateTrafficSplitsState')
|
|
||||||
.mockImplementation(() => Promise.resolve(false))
|
|
||||||
|
|
||||||
expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
|
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {validateIngresses} from './ingressBlueGreenHelper'
|
|
||||||
import {validateServicesState} from './serviceBlueGreenHelper'
|
|
||||||
import {validateTrafficSplitsState} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
export async function promoteBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
//checking if anything to promote
|
|
||||||
const {areValid, invalidIngresses} = await validateIngresses(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
if (!areValid) {
|
|
||||||
throw new Error(
|
|
||||||
`Ingresses are not in promote state: ${invalidIngresses.toString()}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create stable deployments with new configuration
|
|
||||||
const result: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
[].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
),
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create stable services with new configuration
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promoteBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// checking if services are in the right state ie. targeting green deployments
|
|
||||||
if (
|
|
||||||
!(await validateServicesState(kubectl, manifestObjects.serviceEntityList))
|
|
||||||
) {
|
|
||||||
throw new Error('Found services not in promote state')
|
|
||||||
}
|
|
||||||
|
|
||||||
// creating stable deployments with new configurations
|
|
||||||
return await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promoteBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// checking if there is something to promote
|
|
||||||
if (
|
|
||||||
!(await validateTrafficSplitsState(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
throw Error('Not in promote state SMI')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create stable deployments with new configuration
|
|
||||||
return await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {BlueGreenRejectResult} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
import {
|
|
||||||
rejectBlueGreenIngress,
|
|
||||||
rejectBlueGreenService,
|
|
||||||
rejectBlueGreenSMI
|
|
||||||
} from './reject'
|
|
||||||
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
describe('reject tests', () => {
|
|
||||||
let testObjects
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green ingress', async () => {
|
|
||||||
const value = await rejectBlueGreenIngress(kubectl, testObjects)
|
|
||||||
|
|
||||||
const bgDeployment = value.routeResult
|
|
||||||
const deleteResult = value.deleteResult
|
|
||||||
|
|
||||||
expect(deleteResult).toHaveLength(2)
|
|
||||||
for (const obj of deleteResult) {
|
|
||||||
if (obj.kind == 'Service') {
|
|
||||||
expect(obj.name).toBe('nginx-service-green')
|
|
||||||
}
|
|
||||||
if (obj.kind == 'Deployment') {
|
|
||||||
expect(obj.name).toBe('nginx-deployment-green')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(bgDeployment.objects).toHaveLength(1)
|
|
||||||
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green service', async () => {
|
|
||||||
const value = await rejectBlueGreenService(kubectl, testObjects)
|
|
||||||
|
|
||||||
const bgDeployment = value.routeResult
|
|
||||||
const deleteResult = value.deleteResult
|
|
||||||
|
|
||||||
expect(deleteResult).toHaveLength(1)
|
|
||||||
expect(deleteResult[0].name).toBe('nginx-deployment-green')
|
|
||||||
|
|
||||||
expect(bgDeployment.objects).toHaveLength(1)
|
|
||||||
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green SMI', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
const rejectResult = await rejectBlueGreenSMI(kubectl, testObjects)
|
|
||||||
expect(rejectResult.deleteResult).toHaveLength(4)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import {K8sDeleteObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests,
|
|
||||||
BlueGreenRejectResult
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper'
|
|
||||||
import {routeBlueGreenSMI} from './route'
|
|
||||||
import {cleanupSMI} from './smiBlueGreenHelper'
|
|
||||||
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route'
|
|
||||||
|
|
||||||
export async function rejectBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
// route ingress to stables services
|
|
||||||
const routeResult = await routeBlueGreenIngressUnchanged(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceNameMap,
|
|
||||||
manifestObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete green services and deployments
|
|
||||||
const deleteResult = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
[].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// route to stable objects
|
|
||||||
const routeResult = await routeBlueGreenService(
|
|
||||||
kubectl,
|
|
||||||
NONE_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete new deployments with green suffix
|
|
||||||
const deleteResult = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// route trafficsplit to stable deployments
|
|
||||||
const routeResult = await routeBlueGreenSMI(
|
|
||||||
kubectl,
|
|
||||||
NONE_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete rejected new bluegreen deployments
|
|
||||||
const deletedObjects = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete trafficsplit and extra services
|
|
||||||
const cleanupResult = await cleanupSMI(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult: [].concat(deletedObjects, cleanupResult)}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import {getBufferTime} from '../../inputUtils'
|
|
||||||
import * as inputUtils from '../../inputUtils'
|
|
||||||
import {BlueGreenManifests} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import {
|
|
||||||
routeBlueGreenIngress,
|
|
||||||
routeBlueGreenService,
|
|
||||||
routeBlueGreenForDeploy
|
|
||||||
} from './route'
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
|
|
||||||
describe('route function tests', () => {
|
|
||||||
let testObjects: BlueGreenManifests
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
jest
|
|
||||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
|
||||||
.mockImplementationOnce(() => [''])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly prepares blue/green ingresses for deployment', async () => {
|
|
||||||
const unroutedIngCopy: K8sIngress = JSON.parse(
|
|
||||||
JSON.stringify(testObjects.ingressEntityList[0])
|
|
||||||
)
|
|
||||||
unroutedIngCopy.metadata.name = 'nginx-ingress-unrouted'
|
|
||||||
unroutedIngCopy.spec.rules[0].http.paths[0].backend.service.name =
|
|
||||||
'fake-service'
|
|
||||||
testObjects.ingressEntityList.push(unroutedIngCopy)
|
|
||||||
const value = await routeBlueGreenIngress(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceNameMap,
|
|
||||||
testObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(2)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
expect(
|
|
||||||
(value.objects[0] as K8sIngress).spec.rules[0].http.paths[0].backend
|
|
||||||
.service.name
|
|
||||||
).toBe('nginx-service-green')
|
|
||||||
|
|
||||||
expect(value.objects[1].metadata.name).toBe('nginx-ingress-unrouted')
|
|
||||||
// unrouted services shouldn't get their service name changed
|
|
||||||
expect(
|
|
||||||
(value.objects[1] as K8sIngress).spec.rules[0].http.paths[0].backend
|
|
||||||
.service.name
|
|
||||||
).toBe('fake-service')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly prepares blue/green services for deployment', async () => {
|
|
||||||
const value = await routeBlueGreenService(
|
|
||||||
kc,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(1)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
|
|
||||||
expect(value.objects[0].metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly identifies route pattern and acts accordingly', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
|
|
||||||
const ingressResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.INGRESS
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(ingressResult.objects.length).toBe(1)
|
|
||||||
expect(ingressResult.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
|
|
||||||
const serviceResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SERVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(serviceResult.objects.length).toBe(1)
|
|
||||||
expect(serviceResult.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
|
|
||||||
const smiResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SMI
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(smiResult.objects).toHaveLength(1)
|
|
||||||
expect(smiResult.objects[0].metadata.name).toBe(
|
|
||||||
'nginx-service-trafficsplit'
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
(smiResult.objects as TrafficSplitObject[])[0].spec.backends
|
|
||||||
).toHaveLength(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import {sleep} from '../../utilities/timeUtils'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
deployObjects
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {
|
|
||||||
getUpdatedBlueGreenIngress,
|
|
||||||
isIngressRouted
|
|
||||||
} from './ingressBlueGreenHelper'
|
|
||||||
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper'
|
|
||||||
import {createTrafficSplitObject} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {getBufferTime} from '../../inputUtils'
|
|
||||||
|
|
||||||
export async function routeBlueGreenForDeploy(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
inputManifestFiles: string[],
|
|
||||||
routeStrategy: RouteStrategy
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// sleep for buffer time
|
|
||||||
const bufferTime: number = getBufferTime()
|
|
||||||
const startSleepDate = new Date()
|
|
||||||
core.info(
|
|
||||||
`Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}`
|
|
||||||
)
|
|
||||||
await sleep(bufferTime * 1000 * 60)
|
|
||||||
const endSleepDate = new Date()
|
|
||||||
core.info(
|
|
||||||
`Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const manifestObjects: BlueGreenManifests =
|
|
||||||
getManifestObjects(inputManifestFiles)
|
|
||||||
|
|
||||||
// route to new deployments
|
|
||||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
|
||||||
return await routeBlueGreenIngress(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceNameMap,
|
|
||||||
manifestObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
|
||||||
return await routeBlueGreenSMI(
|
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return await routeBlueGreenService(
|
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
serviceNameMap: Map<string, string>,
|
|
||||||
ingressEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// const newObjectsList = []
|
|
||||||
const newObjectsList: K8sObject[] = ingressEntityList.map((obj) => {
|
|
||||||
if (isIngressRouted(obj, serviceNameMap)) {
|
|
||||||
const newBlueGreenIngressObject = getUpdatedBlueGreenIngress(
|
|
||||||
obj,
|
|
||||||
serviceNameMap,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
return newBlueGreenIngressObject
|
|
||||||
} else {
|
|
||||||
core.debug(`unrouted ingress detected ${obj.metadata.name}`)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, newObjectsList)
|
|
||||||
|
|
||||||
return {deployResult, objects: newObjectsList}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenIngressUnchanged(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
serviceNameMap: Map<string, string>,
|
|
||||||
ingressEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const objects = ingressEntityList.filter((ingress) =>
|
|
||||||
isIngressRouted(ingress, serviceNameMap)
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, objects)
|
|
||||||
return {deployResult, objects}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
nextLabel: string,
|
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const objects = serviceEntityList.map((serviceObject) =>
|
|
||||||
getUpdatedBlueGreenService(serviceObject, nextLabel)
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, objects)
|
|
||||||
|
|
||||||
return {deployResult, objects}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
nextLabel: string,
|
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// let tsObjects: TrafficSplitObject[] = []
|
|
||||||
|
|
||||||
const tsObjects: TrafficSplitObject[] = await Promise.all(
|
|
||||||
serviceEntityList.map(async (serviceObject) => {
|
|
||||||
const tsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kubectl,
|
|
||||||
serviceObject.metadata.name,
|
|
||||||
nextLabel
|
|
||||||
)
|
|
||||||
|
|
||||||
return tsObject
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, tsObjects)
|
|
||||||
|
|
||||||
return {deployResult, objects: tsObjects}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
getServiceSpecLabel,
|
|
||||||
getUpdatedBlueGreenService,
|
|
||||||
validateServicesState
|
|
||||||
} from './serviceBlueGreenHelper'
|
|
||||||
|
|
||||||
let testObjects
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
describe('blue/green service helper tests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getUpdatedBlueGreenService', () => {
|
|
||||||
const newService = getUpdatedBlueGreenService(
|
|
||||||
testObjects.serviceEntityList[0],
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(newService.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(newService.spec.selector[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('validateServicesState', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
const mockSelectors = new Map<string, string>()
|
|
||||||
mockSelectors[BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {selector: mockSelectors},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-service-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
await validateServicesState(kubectl, testObjects.serviceEntityList)
|
|
||||||
).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getServiceSpecLabel', () => {
|
|
||||||
testObjects.serviceEntityList[0].spec.selector[BLUE_GREEN_VERSION_LABEL] =
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
|
|
||||||
expect(getServiceSpecLabel(testObjects.serviceEntityList[0])).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,105 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sServiceObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
addBlueGreenLabelsAndAnnotations,
|
addBlueGreenLabelsAndAnnotations,
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
BLUE_GREEN_VERSION_LABEL,
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
|
deleteWorkloadsWithLabel,
|
||||||
fetchResource,
|
fetchResource,
|
||||||
GREEN_LABEL_VALUE
|
getManifestObjects,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
|
|
||||||
|
export async function deployBlueGreenService(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// create deployments with green label value
|
||||||
|
const workloadDeployment = await createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
|
const newObjectsList = manifestObjects.otherObjects
|
||||||
|
.concat(manifestObjects.ingressEntityList)
|
||||||
|
.concat(manifestObjects.unroutedServiceEntityList)
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
|
||||||
|
if (manifestFiles.length > 0) await kubectl.apply(manifestFiles)
|
||||||
|
|
||||||
|
// returning deployment details to check for rollout stability
|
||||||
|
return {workloadDeployment, newObjectsList}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promoteBlueGreenService(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
manifestObjects
|
||||||
|
) {
|
||||||
|
// checking if services are in the right state ie. targeting green deployments
|
||||||
|
if (
|
||||||
|
!(await validateServicesState(kubectl, manifestObjects.serviceEntityList))
|
||||||
|
) {
|
||||||
|
throw 'Not inP promote state'
|
||||||
|
}
|
||||||
|
|
||||||
|
// creating stable deployments with new configurations
|
||||||
|
return await createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rejectBlueGreenService(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// route to stable objects
|
||||||
|
await routeBlueGreenService(
|
||||||
|
kubectl,
|
||||||
|
NONE_LABEL_VALUE,
|
||||||
|
manifestObjects.serviceEntityList
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete new deployments with green suffix
|
||||||
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export async function routeBlueGreenService(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
nextLabel: string,
|
||||||
|
serviceEntityList: any[]
|
||||||
|
) {
|
||||||
|
const newObjectsList = []
|
||||||
|
serviceEntityList.forEach((serviceObject) => {
|
||||||
|
const newBlueGreenServiceObject = getUpdatedBlueGreenService(
|
||||||
|
serviceObject,
|
||||||
|
nextLabel
|
||||||
|
)
|
||||||
|
newObjectsList.push(newBlueGreenServiceObject)
|
||||||
|
})
|
||||||
|
|
||||||
|
// configures the services
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
}
|
||||||
|
|
||||||
// add green labels to configure existing service
|
// add green labels to configure existing service
|
||||||
export function getUpdatedBlueGreenService(
|
function getUpdatedBlueGreenService(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): K8sServiceObject {
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
|
||||||
// Adding labels and annotations.
|
// Adding labels and annotations.
|
||||||
@@ -34,16 +121,25 @@ export async function validateServicesState(
|
|||||||
serviceObject.metadata.name
|
serviceObject.metadata.name
|
||||||
)
|
)
|
||||||
|
|
||||||
let isServiceGreen =
|
if (!!existingService) {
|
||||||
!!existingService &&
|
const currentLabel: string = getServiceSpecLabel(existingService)
|
||||||
getServiceSpecLabel(existingService as K8sServiceObject) ==
|
if (currentLabel != GREEN_LABEL_VALUE) {
|
||||||
GREEN_LABEL_VALUE
|
// service should be targeting deployments with green label
|
||||||
areServicesGreen = areServicesGreen && isServiceGreen
|
areServicesGreen = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// service targeting deployment doesn't exist
|
||||||
|
areServicesGreen = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return areServicesGreen
|
return areServicesGreen
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceSpecLabel(inputObject: K8sServiceObject): string {
|
export function getServiceSpecLabel(inputObject: any): string {
|
||||||
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
if (inputObject?.spec?.selector[BLUE_GREEN_VERSION_LABEL]) {
|
||||||
|
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
|
|
||||||
import {BlueGreenManifests} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {
|
|
||||||
cleanupSMI,
|
|
||||||
createTrafficSplitObject,
|
|
||||||
getGreenSMIServiceResource,
|
|
||||||
getStableSMIServiceResource,
|
|
||||||
MAX_VAL,
|
|
||||||
MIN_VAL,
|
|
||||||
setupSMI,
|
|
||||||
TRAFFIC_SPLIT_OBJECT,
|
|
||||||
TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX,
|
|
||||||
validateTrafficSplitsState
|
|
||||||
} from './smiBlueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const mockTsObject: TrafficSplitObject = {
|
|
||||||
apiVersion: 'v1alpha3',
|
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
|
||||||
metadata: {
|
|
||||||
name: 'nginx-service-trafficsplit',
|
|
||||||
labels: new Map<string, string>(),
|
|
||||||
annotations: new Map<string, string>()
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
service: 'nginx-service',
|
|
||||||
backends: [
|
|
||||||
{
|
|
||||||
service: 'nginx-service-stable',
|
|
||||||
weight: MIN_VAL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'nginx-service-green',
|
|
||||||
weight: MAX_VAL
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SMI Helper tests', () => {
|
|
||||||
let testObjects: BlueGreenManifests
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve(''))
|
|
||||||
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
jest
|
|
||||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
|
||||||
.mockImplementationOnce(() => [''])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('setupSMI tests', async () => {
|
|
||||||
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
|
|
||||||
|
|
||||||
let found = 0
|
|
||||||
for (const obj of smiResults.objects) {
|
|
||||||
if (obj.metadata.name === 'nginx-service-stable') {
|
|
||||||
expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(obj.spec.selector.app).toBe('nginx')
|
|
||||||
found++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.metadata.name === 'nginx-service-green') {
|
|
||||||
expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
found++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.metadata.name === 'nginx-service-trafficsplit') {
|
|
||||||
found++
|
|
||||||
// expect stable weight to be max val
|
|
||||||
const casted = obj as TrafficSplitObject
|
|
||||||
expect(casted.spec.backends).toHaveLength(2)
|
|
||||||
for (const be of casted.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(found).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('createTrafficSplitObject tests', async () => {
|
|
||||||
const noneTsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList[0].metadata.name,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(noneTsObject.metadata.name).toBe('nginx-service-trafficsplit')
|
|
||||||
for (let be of noneTsObject.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const greenTsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList[0].metadata.name,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(greenTsObject.metadata.name).toBe('nginx-service-trafficsplit')
|
|
||||||
for (const be of greenTsObject.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getSMIServiceResource test', () => {
|
|
||||||
const stableResult = getStableSMIServiceResource(
|
|
||||||
testObjects.serviceEntityList[0]
|
|
||||||
)
|
|
||||||
const greenResult = getGreenSMIServiceResource(
|
|
||||||
testObjects.serviceEntityList[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(stableResult.metadata.name).toBe('nginx-service-stable')
|
|
||||||
expect(stableResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(greenResult.metadata.name).toBe('nginx-service-green')
|
|
||||||
expect(greenResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('validateTrafficSplitsState', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsObject))
|
|
||||||
|
|
||||||
let valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(valResult).toBe(true)
|
|
||||||
|
|
||||||
const mockTsCopy = JSON.parse(JSON.stringify(mockTsObject))
|
|
||||||
mockTsCopy.spec.backends[0].weight = MAX_VAL
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsCopy))
|
|
||||||
|
|
||||||
valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
expect(valResult).toBe(false)
|
|
||||||
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
|
|
||||||
valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
expect(valResult).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('cleanupSMI test', async () => {
|
|
||||||
const deleteObjects = await cleanupSMI(kc, testObjects.serviceEntityList)
|
|
||||||
expect(deleteObjects).toHaveLength(3)
|
|
||||||
expect(deleteObjects[0].name).toBe('nginx-service-trafficsplit')
|
|
||||||
expect(deleteObjects[1].name).toBe('nginx-service-green')
|
|
||||||
expect(deleteObjects[1].kind).toBe('Service')
|
|
||||||
expect(deleteObjects[2].name).toBe('nginx-service-stable')
|
|
||||||
expect(deleteObjects[2].kind).toBe('Service')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,35 +1,106 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
||||||
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
deleteObjects,
|
deleteObjects,
|
||||||
deployObjects,
|
deleteWorkloadsWithLabel,
|
||||||
fetchResource,
|
fetchResource,
|
||||||
getBlueGreenResourceName,
|
getBlueGreenResourceName,
|
||||||
|
getManifestObjects,
|
||||||
getNewBlueGreenObject,
|
getNewBlueGreenObject,
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
GREEN_SUFFIX,
|
GREEN_SUFFIX,
|
||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
STABLE_SUFFIX
|
STABLE_SUFFIX
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
K8sDeleteObject,
|
|
||||||
K8sObject,
|
|
||||||
TrafficSplitObject
|
|
||||||
} from '../../types/k8sObject'
|
|
||||||
import {DeployResult} from '../../types/deployResult'
|
|
||||||
import {inputAnnotations} from '../../inputUtils'
|
|
||||||
|
|
||||||
export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
||||||
export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
||||||
export const MIN_VAL = 0
|
const MIN_VAL = 0
|
||||||
export const MAX_VAL = 100
|
const MAX_VAL = 100
|
||||||
|
|
||||||
|
export async function deployBlueGreenSMI(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[],
|
||||||
|
annotations: {[key: string]: string} = {}
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// create services and other objects
|
||||||
|
const newObjectsList = manifestObjects.otherObjects
|
||||||
|
.concat(manifestObjects.serviceEntityList)
|
||||||
|
.concat(manifestObjects.ingressEntityList)
|
||||||
|
.concat(manifestObjects.unroutedServiceEntityList)
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
|
||||||
|
// make extraservices and trafficsplit
|
||||||
|
await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations)
|
||||||
|
|
||||||
|
// create new deloyments
|
||||||
|
const workloadDeployment = await createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
|
return {workloadDeployment, newObjectsList}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
|
||||||
|
// checking if there is something to promote
|
||||||
|
if (
|
||||||
|
!(await validateTrafficSplitsState(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.serviceEntityList
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
throw Error('Not in promote state SMI')
|
||||||
|
}
|
||||||
|
|
||||||
|
// create stable deployments with new configuration
|
||||||
|
return await createWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
manifestObjects.deploymentEntityList,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rejectBlueGreenSMI(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[],
|
||||||
|
annotations: {[key: string]: string} = {}
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// route trafficsplit to stable deployments
|
||||||
|
await routeBlueGreenSMI(
|
||||||
|
kubectl,
|
||||||
|
NONE_LABEL_VALUE,
|
||||||
|
manifestObjects.serviceEntityList,
|
||||||
|
annotations
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete rejected new bluegreen deployments
|
||||||
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete trafficsplit and extra services
|
||||||
|
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupSMI(
|
export async function setupSMI(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
serviceEntityList: any[]
|
serviceEntityList: any[],
|
||||||
): Promise<BlueGreenDeployment> {
|
annotations: {[key: string]: string} = {}
|
||||||
|
) {
|
||||||
const newObjectsList = []
|
const newObjectsList = []
|
||||||
const trafficObjectList = []
|
const trafficObjectList = []
|
||||||
|
|
||||||
@@ -37,66 +108,56 @@ export async function setupSMI(
|
|||||||
// create a trafficsplit for service
|
// create a trafficsplit for service
|
||||||
trafficObjectList.push(serviceObject)
|
trafficObjectList.push(serviceObject)
|
||||||
// set up the services for trafficsplit
|
// set up the services for trafficsplit
|
||||||
const newStableService = getStableSMIServiceResource(serviceObject)
|
const newStableService = getSMIServiceResource(
|
||||||
const newGreenService = getGreenSMIServiceResource(serviceObject)
|
serviceObject,
|
||||||
|
STABLE_SUFFIX
|
||||||
|
)
|
||||||
|
const newGreenService = getSMIServiceResource(serviceObject, GREEN_SUFFIX)
|
||||||
newObjectsList.push(newStableService)
|
newObjectsList.push(newStableService)
|
||||||
newObjectsList.push(newGreenService)
|
newObjectsList.push(newGreenService)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tsObjects: TrafficSplitObject[] = []
|
|
||||||
// route to stable service
|
|
||||||
for (const svc of trafficObjectList) {
|
|
||||||
const tsObject = await createTrafficSplitObject(
|
|
||||||
kubectl,
|
|
||||||
svc.metadata.name,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
tsObjects.push(tsObject as TrafficSplitObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectsToDeploy = [].concat(newObjectsList, tsObjects)
|
|
||||||
|
|
||||||
// create services
|
// create services
|
||||||
const smiDeploymentResult: DeployResult = await deployObjects(
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
kubectl,
|
await kubectl.apply(manifestFiles)
|
||||||
objectsToDeploy
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
// route to stable service
|
||||||
objects: objectsToDeploy,
|
trafficObjectList.forEach((inputObject) => {
|
||||||
deployResult: smiDeploymentResult
|
createTrafficSplitObject(
|
||||||
}
|
kubectl,
|
||||||
|
inputObject.metadata.name,
|
||||||
|
NONE_LABEL_VALUE,
|
||||||
|
annotations
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let trafficSplitAPIVersion = ''
|
let trafficSplitAPIVersion = ''
|
||||||
|
|
||||||
export async function createTrafficSplitObject(
|
async function createTrafficSplitObject(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
name: string,
|
name: string,
|
||||||
nextLabel: string
|
nextLabel: string,
|
||||||
): Promise<TrafficSplitObject> {
|
annotations: {[key: string]: string} = {}
|
||||||
|
): Promise<any> {
|
||||||
// cache traffic split api version
|
// cache traffic split api version
|
||||||
if (!trafficSplitAPIVersion)
|
if (!trafficSplitAPIVersion)
|
||||||
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
|
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
|
||||||
kubectl
|
kubectl
|
||||||
)
|
)
|
||||||
|
|
||||||
// retrieve annotations for TS object
|
|
||||||
const annotations = inputAnnotations
|
|
||||||
|
|
||||||
// decide weights based on nextlabel
|
// decide weights based on nextlabel
|
||||||
const stableWeight: number =
|
const stableWeight: number =
|
||||||
nextLabel === GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL
|
nextLabel === GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL
|
||||||
const greenWeight: number =
|
const greenWeight: number =
|
||||||
nextLabel === GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL
|
nextLabel === GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL
|
||||||
|
|
||||||
const trafficSplitObject: TrafficSplitObject = {
|
const trafficSplitObject = JSON.stringify({
|
||||||
apiVersion: trafficSplitAPIVersion,
|
apiVersion: trafficSplitAPIVersion,
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
kind: 'TrafficSplit',
|
||||||
metadata: {
|
metadata: {
|
||||||
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
||||||
annotations: annotations,
|
annotations: annotations
|
||||||
labels: new Map<string, string>()
|
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
service: name,
|
service: name,
|
||||||
@@ -111,24 +172,52 @@ export async function createTrafficSplitObject(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return trafficSplitObject
|
// create traffic split object
|
||||||
}
|
const trafficSplitManifestFile = fileHelper.writeManifestToFile(
|
||||||
|
trafficSplitObject,
|
||||||
export function getStableSMIServiceResource(inputObject: K8sObject): K8sObject {
|
TRAFFIC_SPLIT_OBJECT,
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||||
// adding stable suffix to service name
|
|
||||||
newObject.metadata.name = getBlueGreenResourceName(
|
|
||||||
inputObject.metadata.name,
|
|
||||||
STABLE_SUFFIX
|
|
||||||
)
|
)
|
||||||
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
|
||||||
|
await kubectl.apply(trafficSplitManifestFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGreenSMIServiceResource(inputObject: K8sObject): K8sObject {
|
export function getSMIServiceResource(
|
||||||
|
inputObject: any,
|
||||||
|
suffix: string
|
||||||
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
|
||||||
|
if (suffix === STABLE_SUFFIX) {
|
||||||
|
// adding stable suffix to service name
|
||||||
|
newObject.metadata.name = getBlueGreenResourceName(
|
||||||
|
inputObject.metadata.name,
|
||||||
|
STABLE_SUFFIX
|
||||||
|
)
|
||||||
|
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
||||||
|
} else {
|
||||||
|
// green label will be added for these
|
||||||
|
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function routeBlueGreenSMI(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
nextLabel: string,
|
||||||
|
serviceEntityList: any[],
|
||||||
|
annotations: {[key: string]: string} = {}
|
||||||
|
) {
|
||||||
|
for (const serviceObject of serviceEntityList) {
|
||||||
|
// route trafficsplit to given label
|
||||||
|
await createTrafficSplitObject(
|
||||||
|
kubectl,
|
||||||
|
serviceObject.metadata.name,
|
||||||
|
nextLabel,
|
||||||
|
annotations
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateTrafficSplitsState(
|
export async function validateTrafficSplitsState(
|
||||||
@@ -144,38 +233,32 @@ export async function validateTrafficSplitsState(
|
|||||||
TRAFFIC_SPLIT_OBJECT,
|
TRAFFIC_SPLIT_OBJECT,
|
||||||
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||||
)
|
)
|
||||||
core.debug(
|
|
||||||
`ts object extracted was ${JSON.stringify(trafficSplitObject)}`
|
|
||||||
)
|
|
||||||
if (!trafficSplitObject) {
|
if (!trafficSplitObject) {
|
||||||
core.debug(`no traffic split exits for ${name}`)
|
// no traffic split exits
|
||||||
trafficSplitsInRightState = false
|
trafficSplitsInRightState = false
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trafficSplitObject = JSON.parse(JSON.stringify(trafficSplitObject))
|
||||||
trafficSplitObject.spec.backends.forEach((element) => {
|
trafficSplitObject.spec.backends.forEach((element) => {
|
||||||
// checking if trafficsplit in right state to deploy
|
// checking if trafficsplit in right state to deploy
|
||||||
if (element.service === getBlueGreenResourceName(name, GREEN_SUFFIX)) {
|
if (element.service === getBlueGreenResourceName(name, GREEN_SUFFIX)) {
|
||||||
trafficSplitsInRightState =
|
if (element.weight != MAX_VAL) trafficSplitsInRightState = false
|
||||||
trafficSplitsInRightState && element.weight == MAX_VAL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
||||||
) {
|
) {
|
||||||
trafficSplitsInRightState =
|
if (element.weight != MIN_VAL) trafficSplitsInRightState = false
|
||||||
trafficSplitsInRightState && element.weight == MIN_VAL
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return trafficSplitsInRightState
|
return trafficSplitsInRightState
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupSMI(
|
export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
||||||
kubectl: Kubectl,
|
const deleteList = []
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<K8sDeleteObject[]> {
|
|
||||||
const deleteList: K8sDeleteObject[] = []
|
|
||||||
|
|
||||||
serviceEntityList.forEach((serviceObject) => {
|
serviceEntityList.forEach((serviceObject) => {
|
||||||
deleteList.push({
|
deleteList.push({
|
||||||
@@ -205,6 +288,4 @@ export async function cleanupSMI(
|
|||||||
|
|
||||||
// delete all objects
|
// delete all objects
|
||||||
await deleteObjects(kubectl, deleteList)
|
await deleteObjects(kubectl, deleteList)
|
||||||
|
|
||||||
return deleteList
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {Kubectl} from '../../types/kubectl'
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {ExecOutput} from '@actions/exec'
|
|
||||||
import {
|
import {
|
||||||
isDeploymentEntity,
|
isDeploymentEntity,
|
||||||
isServiceEntity,
|
isServiceEntity,
|
||||||
@@ -31,7 +30,7 @@ export async function deleteCanaryDeployment(
|
|||||||
includeServices: boolean
|
includeServices: boolean
|
||||||
) {
|
) {
|
||||||
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
|
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
|
||||||
throw new Error('Manifest files for deleting canary deployment not found')
|
throw new Error('Manifest file not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
await cleanUpCanary(kubectl, manifestFilePaths, includeServices)
|
await cleanUpCanary(kubectl, manifestFilePaths, includeServices)
|
||||||
@@ -55,7 +54,7 @@ export function isResourceMarkedAsStable(inputObject: any): boolean {
|
|||||||
|
|
||||||
export function getStableResource(inputObject: any): object {
|
export function getStableResource(inputObject: any): object {
|
||||||
const replicaCount = specContainsReplicas(inputObject.kind)
|
const replicaCount = specContainsReplicas(inputObject.kind)
|
||||||
? inputObject.spec.replicas
|
? inputObject.metadata.replicas
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE)
|
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE)
|
||||||
@@ -80,12 +79,7 @@ export async function fetchResource(
|
|||||||
kind: string,
|
kind: string,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
let result: ExecOutput
|
const result = await kubectl.getResource(kind, name)
|
||||||
try {
|
|
||||||
result = await kubectl.getResource(kind, name)
|
|
||||||
} catch (e) {
|
|
||||||
core.debug(`detected error while fetching resources: ${e}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result || result?.stderr) {
|
if (!result || result?.stderr) {
|
||||||
return null
|
return null
|
||||||
@@ -99,7 +93,7 @@ export async function fetchResource(
|
|||||||
return resource
|
return resource
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.debug(
|
core.debug(
|
||||||
`Exception occurred while parsing ${resource} in JSON object: ${ex}`
|
`Exception occurred while Parsing ${resource} in JSON object: ${ex}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,26 +111,6 @@ export function getStableResourceName(name: string) {
|
|||||||
return name + STABLE_SUFFIX
|
return name + STABLE_SUFFIX
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBaselineDeploymentFromStableDeployment(
|
|
||||||
inputObject: any,
|
|
||||||
replicaCount: number
|
|
||||||
): object {
|
|
||||||
// TODO: REFACTOR TO MAKE EVERYTHING TYPE SAFE
|
|
||||||
const oldName = inputObject.metadata.name
|
|
||||||
const newName =
|
|
||||||
oldName.substring(0, oldName.length - STABLE_SUFFIX.length) +
|
|
||||||
BASELINE_SUFFIX
|
|
||||||
|
|
||||||
const newObject = getNewCanaryObject(
|
|
||||||
inputObject,
|
|
||||||
replicaCount,
|
|
||||||
BASELINE_LABEL_VALUE
|
|
||||||
) as any
|
|
||||||
newObject.metadata.name = newName
|
|
||||||
|
|
||||||
return newObject
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNewCanaryObject(
|
function getNewCanaryObject(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
replicas: number,
|
replicas: number,
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ import * as canaryDeploymentHelper from './canaryHelper'
|
|||||||
import {isDeploymentEntity} from '../../types/kubernetesTypes'
|
import {isDeploymentEntity} from '../../types/kubernetesTypes'
|
||||||
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
|
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
|
||||||
|
|
||||||
export async function deployPodCanary(
|
export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
|
||||||
filePaths: string[],
|
|
||||||
kubectl: Kubectl,
|
|
||||||
onlyDeployStable: boolean = false
|
|
||||||
) {
|
|
||||||
const newObjectsList = []
|
const newObjectsList = []
|
||||||
const percentage = parseInt(core.getInput('percentage', {required: true}))
|
const percentage = parseInt(core.getInput('percentage'))
|
||||||
|
|
||||||
if (percentage < 0 || percentage > 100)
|
if (percentage < 0 || percentage > 100)
|
||||||
throw Error('Percentage must be between 0 and 100')
|
throw Error('Percentage must be between 0 and 100')
|
||||||
@@ -26,7 +22,7 @@ export async function deployPodCanary(
|
|||||||
const name = inputObject.metadata.name
|
const name = inputObject.metadata.name
|
||||||
const kind = inputObject.kind
|
const kind = inputObject.kind
|
||||||
|
|
||||||
if (!onlyDeployStable && isDeploymentEntity(kind)) {
|
if (isDeploymentEntity(kind)) {
|
||||||
core.debug('Calculating replica count for canary')
|
core.debug('Calculating replica count for canary')
|
||||||
const canaryReplicaCount = calculateReplicaCountForCanary(
|
const canaryReplicaCount = calculateReplicaCountForCanary(
|
||||||
inputObject,
|
inputObject,
|
||||||
@@ -34,22 +30,37 @@ export async function deployPodCanary(
|
|||||||
)
|
)
|
||||||
core.debug('Replica count is ' + canaryReplicaCount)
|
core.debug('Replica count is ' + canaryReplicaCount)
|
||||||
|
|
||||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(
|
// Get stable object
|
||||||
inputObject,
|
core.debug('Querying stable object')
|
||||||
canaryReplicaCount
|
|
||||||
)
|
|
||||||
newObjectsList.push(newCanaryObject)
|
|
||||||
|
|
||||||
// if there's already a stable object, deploy baseline as well
|
|
||||||
const stableObject = await canaryDeploymentHelper.fetchResource(
|
const stableObject = await canaryDeploymentHelper.fetchResource(
|
||||||
kubectl,
|
kubectl,
|
||||||
kind,
|
kind,
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
if (stableObject) {
|
|
||||||
|
if (!stableObject) {
|
||||||
|
core.debug('Stable object not found. Creating canary object')
|
||||||
|
const newCanaryObject =
|
||||||
|
canaryDeploymentHelper.getNewCanaryResource(
|
||||||
|
inputObject,
|
||||||
|
canaryReplicaCount
|
||||||
|
)
|
||||||
|
newObjectsList.push(newCanaryObject)
|
||||||
|
} else {
|
||||||
core.debug(
|
core.debug(
|
||||||
`Stable object found for ${kind} ${name}. Creating baseline objects`
|
'Creating canary and baseline objects. Stable object found: ' +
|
||||||
|
JSON.stringify(stableObject)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const newCanaryObject =
|
||||||
|
canaryDeploymentHelper.getNewCanaryResource(
|
||||||
|
inputObject,
|
||||||
|
canaryReplicaCount
|
||||||
|
)
|
||||||
|
core.debug(
|
||||||
|
'New canary object: ' + JSON.stringify(newCanaryObject)
|
||||||
|
)
|
||||||
|
|
||||||
const newBaselineObject =
|
const newBaselineObject =
|
||||||
canaryDeploymentHelper.getNewBaselineResource(
|
canaryDeploymentHelper.getNewBaselineResource(
|
||||||
stableObject,
|
stableObject,
|
||||||
@@ -58,10 +69,12 @@ export async function deployPodCanary(
|
|||||||
core.debug(
|
core.debug(
|
||||||
'New baseline object: ' + JSON.stringify(newBaselineObject)
|
'New baseline object: ' + JSON.stringify(newBaselineObject)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
newObjectsList.push(newCanaryObject)
|
||||||
newObjectsList.push(newBaselineObject)
|
newObjectsList.push(newBaselineObject)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// deploy non deployment entity or regular deployments for promote as they are
|
// update non deployment entity as it is
|
||||||
newObjectsList.push(inputObject)
|
newObjectsList.push(inputObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,10 +88,7 @@ export async function deployPodCanary(
|
|||||||
return {result, newFilePaths: manifestFiles}
|
return {result, newFilePaths: manifestFiles}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateReplicaCountForCanary(
|
function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
|
||||||
inputObject: any,
|
|
||||||
percentage: number
|
|
||||||
) {
|
|
||||||
const inputReplicaCount = getReplicaCount(inputObject)
|
const inputReplicaCount = getReplicaCount(inputObject)
|
||||||
return Math.max(1, Math.round((inputReplicaCount * percentage) / 100))
|
return Math.round((inputReplicaCount * percentage) / 100)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,94 +6,73 @@ import * as yaml from 'js-yaml'
|
|||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
||||||
import * as canaryDeploymentHelper from './canaryHelper'
|
import * as canaryDeploymentHelper from './canaryHelper'
|
||||||
import * as podCanaryHelper from './podCanaryHelper'
|
|
||||||
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
|
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
|
||||||
import {checkForErrors} from '../../utilities/kubectlUtils'
|
import {checkForErrors} from '../../utilities/kubectlUtils'
|
||||||
import {inputAnnotations} from '../../inputUtils'
|
|
||||||
|
|
||||||
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
|
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
|
||||||
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
||||||
|
|
||||||
export async function deploySMICanary(
|
export async function deploySMICanary(filePaths: string[], kubectl: Kubectl) {
|
||||||
filePaths: string[],
|
const canaryReplicaCount = parseInt(
|
||||||
kubectl: Kubectl,
|
core.getInput('baseline-and-canary-replicas')
|
||||||
onlyDeployStable: boolean = false
|
)
|
||||||
) {
|
if (canaryReplicaCount < 0 || canaryReplicaCount > 100)
|
||||||
const canaryReplicasInput = core.getInput('baseline-and-canary-replicas')
|
|
||||||
let canaryReplicaCount
|
|
||||||
let calculateReplicas = true
|
|
||||||
if (canaryReplicasInput !== '') {
|
|
||||||
canaryReplicaCount = parseInt(canaryReplicasInput)
|
|
||||||
calculateReplicas = false
|
|
||||||
core.debug(
|
|
||||||
`read replica count ${canaryReplicaCount} from input: ${canaryReplicasInput}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canaryReplicaCount < 0 && canaryReplicaCount > 100)
|
|
||||||
throw Error('Baseline-and-canary-replicas must be between 0 and 100')
|
throw Error('Baseline-and-canary-replicas must be between 0 and 100')
|
||||||
|
|
||||||
const newObjectsList = []
|
const newObjectsList = []
|
||||||
for await (const filePath of filePaths) {
|
filePaths.forEach((filePath: string) => {
|
||||||
const fileContents = fs.readFileSync(filePath).toString()
|
const fileContents = fs.readFileSync(filePath).toString()
|
||||||
const inputObjects = yaml.safeLoadAll(fileContents)
|
yaml.safeLoadAll(fileContents, (inputObject) => {
|
||||||
for (const inputObject of inputObjects) {
|
|
||||||
const name = inputObject.metadata.name
|
const name = inputObject.metadata.name
|
||||||
const kind = inputObject.kind
|
const kind = inputObject.kind
|
||||||
|
|
||||||
if (!onlyDeployStable && isDeploymentEntity(kind)) {
|
if (isDeploymentEntity(kind)) {
|
||||||
if (calculateReplicas) {
|
const stableObject = canaryDeploymentHelper.fetchResource(
|
||||||
// calculate for each object
|
|
||||||
const percentage = parseInt(
|
|
||||||
core.getInput('percentage', {required: true})
|
|
||||||
)
|
|
||||||
canaryReplicaCount =
|
|
||||||
podCanaryHelper.calculateReplicaCountForCanary(
|
|
||||||
inputObject,
|
|
||||||
percentage
|
|
||||||
)
|
|
||||||
core.debug(`calculated replica count ${canaryReplicaCount}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug('Creating canary object')
|
|
||||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(
|
|
||||||
inputObject,
|
|
||||||
canaryReplicaCount
|
|
||||||
)
|
|
||||||
newObjectsList.push(newCanaryObject)
|
|
||||||
|
|
||||||
const stableObject = await canaryDeploymentHelper.fetchResource(
|
|
||||||
kubectl,
|
kubectl,
|
||||||
kind,
|
kind,
|
||||||
canaryDeploymentHelper.getStableResourceName(name)
|
name
|
||||||
)
|
)
|
||||||
if (stableObject) {
|
|
||||||
|
if (!stableObject) {
|
||||||
core.debug(
|
core.debug(
|
||||||
`Stable object found for ${kind} ${name}. Creating baseline objects`
|
'Stable object not found. Creating only canary object'
|
||||||
)
|
)
|
||||||
|
const newCanaryObject =
|
||||||
|
canaryDeploymentHelper.getNewCanaryResource(
|
||||||
|
inputObject,
|
||||||
|
canaryReplicaCount
|
||||||
|
)
|
||||||
|
newObjectsList.push(newCanaryObject)
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)
|
||||||
|
) {
|
||||||
|
throw Error(`StableSpecSelectorNotExist : ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
'Stable object found. Creating canary and baseline objects'
|
||||||
|
)
|
||||||
|
const newCanaryObject =
|
||||||
|
canaryDeploymentHelper.getNewCanaryResource(
|
||||||
|
inputObject,
|
||||||
|
canaryReplicaCount
|
||||||
|
)
|
||||||
const newBaselineObject =
|
const newBaselineObject =
|
||||||
canaryDeploymentHelper.getBaselineDeploymentFromStableDeployment(
|
canaryDeploymentHelper.getNewBaselineResource(
|
||||||
stableObject,
|
stableObject,
|
||||||
canaryReplicaCount
|
canaryReplicaCount
|
||||||
)
|
)
|
||||||
|
newObjectsList.push(newCanaryObject)
|
||||||
newObjectsList.push(newBaselineObject)
|
newObjectsList.push(newBaselineObject)
|
||||||
}
|
}
|
||||||
} else if (isDeploymentEntity(kind)) {
|
|
||||||
core.debug(
|
|
||||||
`creating stable deployment with ${inputObject.spec.replicas} replicas`
|
|
||||||
)
|
|
||||||
const stableDeployment =
|
|
||||||
canaryDeploymentHelper.getStableResource(inputObject)
|
|
||||||
newObjectsList.push(stableDeployment)
|
|
||||||
} else {
|
} else {
|
||||||
// Update non deployment entity or stable deployment as it is
|
// Update non deployment entity as it is
|
||||||
newObjectsList.push(inputObject)
|
newObjectsList.push(inputObject)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
core.debug(
|
|
||||||
`deploying canary objects with SMI: \n ${JSON.stringify(newObjectsList)}`
|
|
||||||
)
|
|
||||||
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList)
|
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
|
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
|
||||||
const result = await kubectl.apply(newFilePaths, forceDeployment)
|
const result = await kubectl.apply(newFilePaths, forceDeployment)
|
||||||
@@ -103,7 +82,7 @@ export async function deploySMICanary(
|
|||||||
|
|
||||||
async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
|
async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
|
||||||
const newObjectsList = []
|
const newObjectsList = []
|
||||||
const trafficObjectsList: string[] = []
|
const trafficObjectsList = []
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
const fileContents = fs.readFileSync(filePath).toString()
|
const fileContents = fs.readFileSync(filePath).toString()
|
||||||
@@ -113,7 +92,6 @@ async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
|
|||||||
const kind = inputObject.kind
|
const kind = inputObject.kind
|
||||||
|
|
||||||
if (isServiceEntity(kind)) {
|
if (isServiceEntity(kind)) {
|
||||||
core.debug(`Creating services for ${kind} ${name}`)
|
|
||||||
const newCanaryServiceObject =
|
const newCanaryServiceObject =
|
||||||
canaryDeploymentHelper.getNewCanaryResource(inputObject)
|
canaryDeploymentHelper.getNewCanaryResource(inputObject)
|
||||||
newObjectsList.push(newCanaryServiceObject)
|
newObjectsList.push(newCanaryServiceObject)
|
||||||
@@ -176,7 +154,7 @@ async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
|
|||||||
name
|
name
|
||||||
)
|
)
|
||||||
trafficObjectsList.push(
|
trafficObjectsList.push(
|
||||||
await updateTrafficSplitObject(kubectl, name)
|
updateTrafficSplitObject(kubectl, name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +229,7 @@ async function updateTrafficSplitObject(
|
|||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
serviceName: string
|
serviceName: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const percentage = parseInt(core.getInput('percentage', {required: true}))
|
const percentage = parseInt(core.getInput('percentage'))
|
||||||
if (percentage < 0 || percentage > 100)
|
if (percentage < 0 || percentage > 100)
|
||||||
throw Error('Percentage must be between 0 and 100')
|
throw Error('Percentage must be between 0 and 100')
|
||||||
|
|
||||||
@@ -262,9 +240,9 @@ async function updateTrafficSplitObject(
|
|||||||
core.debug(
|
core.debug(
|
||||||
'Creating the traffic object with canary weight: ' +
|
'Creating the traffic object with canary weight: ' +
|
||||||
baselineAndCanaryWeight +
|
baselineAndCanaryWeight +
|
||||||
', baseline weight: ' +
|
',baseling weight: ' +
|
||||||
baselineAndCanaryWeight +
|
baselineAndCanaryWeight +
|
||||||
', stable weight: ' +
|
',stable: ' +
|
||||||
stableDeploymentWeight
|
stableDeploymentWeight
|
||||||
)
|
)
|
||||||
return await createTrafficSplitManifestFile(
|
return await createTrafficSplitManifestFile(
|
||||||
@@ -310,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) {
|
||||||
@@ -324,7 +303,7 @@ async function getTrafficSplitObject(
|
|||||||
kind: 'TrafficSplit',
|
kind: 'TrafficSplit',
|
||||||
metadata: {
|
metadata: {
|
||||||
name: getTrafficSplitResourceName(name),
|
name: getTrafficSplitResourceName(name),
|
||||||
annotations: inputAnnotations
|
annotations: annotations
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
backends: [
|
backends: [
|
||||||
|
|||||||
@@ -10,19 +10,16 @@ import {Kubectl, Resource} from '../types/kubectl'
|
|||||||
import {deployPodCanary} from './canary/podCanaryHelper'
|
import {deployPodCanary} from './canary/podCanaryHelper'
|
||||||
import {deploySMICanary} from './canary/smiCanaryHelper'
|
import {deploySMICanary} from './canary/smiCanaryHelper'
|
||||||
import {DeploymentConfig} from '../types/deploymentConfig'
|
import {DeploymentConfig} from '../types/deploymentConfig'
|
||||||
import {
|
import {deployBlueGreenService} from './blueGreen/serviceBlueGreenHelper'
|
||||||
deployBlueGreen,
|
import {deployBlueGreenIngress} from './blueGreen/ingressBlueGreenHelper'
|
||||||
deployBlueGreenIngress,
|
import {deployBlueGreenSMI} from './blueGreen/smiBlueGreenHelper'
|
||||||
deployBlueGreenService
|
|
||||||
} from './blueGreen/deploy'
|
|
||||||
import {deployBlueGreenSMI} from './blueGreen/deploy'
|
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {
|
import {
|
||||||
parseTrafficSplitMethod,
|
parseTrafficSplitMethod,
|
||||||
TrafficSplitMethod
|
TrafficSplitMethod
|
||||||
} from '../types/trafficSplitMethod'
|
} from '../types/trafficSplitMethod'
|
||||||
import {parseRouteStrategy} from '../types/routeStrategy'
|
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
|
||||||
import {ExecOutput} from '@actions/exec'
|
import {ExecOutput} from '@actions/exec'
|
||||||
import {
|
import {
|
||||||
getWorkflowAnnotationKeyLabel,
|
getWorkflowAnnotationKeyLabel,
|
||||||
@@ -44,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: {
|
||||||
@@ -61,19 +59,17 @@ export async function deployManifests(
|
|||||||
const routeStrategy = parseRouteStrategy(
|
const routeStrategy = parseRouteStrategy(
|
||||||
core.getInput('route-method', {required: true})
|
core.getInput('route-method', {required: true})
|
||||||
)
|
)
|
||||||
const blueGreenDeployment = await deployBlueGreen(
|
|
||||||
kubectl,
|
const {workloadDeployment, newObjectsList} = await Promise.resolve(
|
||||||
files,
|
(routeStrategy == RouteStrategy.INGRESS &&
|
||||||
routeStrategy
|
deployBlueGreenIngress(kubectl, files)) ||
|
||||||
)
|
(routeStrategy == RouteStrategy.SMI &&
|
||||||
core.debug(
|
deployBlueGreenSMI(kubectl, files, annotations)) ||
|
||||||
`objects deployed for ${routeStrategy}: ${JSON.stringify(
|
deployBlueGreenService(kubectl, files)
|
||||||
blueGreenDeployment.objects
|
|
||||||
)} `
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checkForErrors([blueGreenDeployment.deployResult.execResult])
|
checkForErrors([workloadDeployment.result])
|
||||||
return blueGreenDeployment.deployResult.manifestFiles
|
return workloadDeployment.newFilePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
case DeploymentStrategy.BASIC: {
|
case DeploymentStrategy.BASIC: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export function parseAnnotations(str: string) {
|
export function parseAnnotations(str: string) {
|
||||||
if (str == '') {
|
if (str == '') {
|
||||||
return new Map<string, string>()
|
return {}
|
||||||
} else {
|
} else {
|
||||||
const annotation = JSON.parse(str)
|
const annotaion = JSON.parse(str)
|
||||||
return new Map<string, string>(annotation)
|
return new Map(annotaion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import {DeployResult} from './deployResult'
|
|
||||||
import {K8sObject, K8sDeleteObject} from './k8sObject'
|
|
||||||
|
|
||||||
export interface BlueGreenDeployment {
|
|
||||||
deployResult: DeployResult
|
|
||||||
objects: K8sObject[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlueGreenManifests {
|
|
||||||
serviceEntityList: K8sObject[]
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
unroutedServiceEntityList: K8sObject[]
|
|
||||||
deploymentEntityList: K8sObject[]
|
|
||||||
ingressEntityList: K8sObject[]
|
|
||||||
otherObjects: K8sObject[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlueGreenRejectResult {
|
|
||||||
deleteResult: K8sDeleteObject[]
|
|
||||||
routeResult: BlueGreenDeployment
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import {ExecOutput} from '@actions/exec'
|
|
||||||
|
|
||||||
export interface DeployResult {
|
|
||||||
execResult: ExecOutput
|
|
||||||
manifestFiles: string[]
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
export interface K8sObject {
|
|
||||||
metadata: {
|
|
||||||
name: string
|
|
||||||
labels: Map<string, string>
|
|
||||||
}
|
|
||||||
kind: string
|
|
||||||
spec: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sServiceObject extends K8sObject {
|
|
||||||
spec: {
|
|
||||||
selector: Map<string, string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sDeleteObject {
|
|
||||||
name: string
|
|
||||||
kind: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sIngress extends K8sObject {
|
|
||||||
spec: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
http: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
backend: {
|
|
||||||
service: {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrafficSplitObject extends K8sObject {
|
|
||||||
apiVersion: string
|
|
||||||
metadata: {
|
|
||||||
name: string
|
|
||||||
labels: Map<string, string>
|
|
||||||
annotations: Map<string, string>
|
|
||||||
}
|
|
||||||
spec: {
|
|
||||||
service: string
|
|
||||||
backends: TrafficSplitBackend[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrafficSplitBackend {
|
|
||||||
service: string
|
|
||||||
weight: number
|
|
||||||
}
|
|
||||||
+8
-14
@@ -3,7 +3,6 @@ import {createInlineArray} from '../utilities/arrayUtils'
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import {exec} from 'child_process'
|
|
||||||
|
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
name: string
|
name: string
|
||||||
@@ -143,16 +142,14 @@ export class Kubectl {
|
|||||||
|
|
||||||
public async getResource(
|
public async getResource(
|
||||||
resourceType: string,
|
resourceType: string,
|
||||||
name: string,
|
name: string
|
||||||
silentFailure: boolean = false
|
|
||||||
): Promise<ExecOutput> {
|
): Promise<ExecOutput> {
|
||||||
core.debug(
|
return await this.execute([
|
||||||
'fetching resource of type ' + resourceType + ' and name ' + name
|
'get',
|
||||||
)
|
`${resourceType}/${name}`,
|
||||||
return await this.execute(
|
'-o',
|
||||||
['get', `${resourceType}/${name}`, '-o', 'json'],
|
'json'
|
||||||
silentFailure
|
])
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public executeCommand(command: string, args?: string) {
|
public executeCommand(command: string, args?: string) {
|
||||||
@@ -173,10 +170,7 @@ export class Kubectl {
|
|||||||
args = args.concat(['--namespace', this.namespace])
|
args = args.concat(['--namespace', this.namespace])
|
||||||
}
|
}
|
||||||
core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
|
core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
|
||||||
|
return await getExecOutput(this.kubectlPath, args, {silent})
|
||||||
return await getExecOutput(this.kubectlPath, args, {
|
|
||||||
silent
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('File utils', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
// 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(7)
|
expect(testSearch).toHaveLength(6)
|
||||||
expectedManifests.forEach((fileName) => {
|
expectedManifests.forEach((fileName) => {
|
||||||
expect(testSearch).toContain(fileName)
|
expect(testSearch).toContain(fileName)
|
||||||
})
|
})
|
||||||
@@ -54,7 +54,7 @@ describe('File utils', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
||||||
).toHaveLength(7)
|
).toHaveLength(6)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,5 @@ describe('WorkflowAnnotationUtils', () => {
|
|||||||
)
|
)
|
||||||
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
||||||
})
|
})
|
||||||
it('should remove slashes from label', () => {
|
|
||||||
expect(
|
|
||||||
cleanLabel('Workflow Name / With Slashes / And Spaces')
|
|
||||||
).toEqual('Workflow_Name_-_With_Slashes_-_And_Spaces')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,11 +37,7 @@ export function getWorkflowAnnotationKeyLabel(): string {
|
|||||||
* @returns cleaned label
|
* @returns cleaned label
|
||||||
*/
|
*/
|
||||||
export function cleanLabel(label: string): string {
|
export function cleanLabel(label: string): string {
|
||||||
let removedInvalidChars = label
|
const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '')
|
||||||
.replace(/\s/gi, '_')
|
|
||||||
.replace(/[\/\\\|]/gi, '-')
|
|
||||||
.replace(/[^-A-Za-z0-9_.]/gi, '')
|
|
||||||
|
|
||||||
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
||||||
return regex.exec(removedInvalidChars)[0] || ''
|
return regex.exec(removedInvalidChars)[0] || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: unrouted-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: fake-application
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 80
|
|
||||||
targetPort: 80
|
|
||||||
---
|
|
||||||
kind: TrafficSplit
|
|
||||||
metadata:
|
|
||||||
name: foobar-rollout
|
|
||||||
spec:
|
|
||||||
service: foobar
|
|
||||||
backends:
|
|
||||||
- service: foobar-v1
|
|
||||||
weight: 1000
|
|
||||||
- service: foobar-v2
|
|
||||||
weight: 500
|
|
||||||
Reference in New Issue
Block a user