mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-23 04:59:26 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fd4e1ad9d | |||
| c117b29f9e | |||
| 01a65512ea | |||
| 531cfdcc3d | |||
| 0b5795551a | |||
| bb0278db72 | |||
| 71e93a71d4 | |||
| 19d66d6bdb |
+3
-1
@@ -2,4 +2,6 @@ node_modules
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
lib/
|
lib/
|
||||||
|
|
||||||
|
coverage/
|
||||||
@@ -74,6 +74,9 @@ Following are the key capabilities of this action:
|
|||||||
<td>traffic-split-method </br></br>(Optional)</td>
|
<td>traffic-split-method </br></br>(Optional)</td>
|
||||||
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
|
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>traffic-split-annotations </br></br>(Optional)</td>
|
||||||
|
<td>Annotations in the form of key/value pair to be added to TrafficSplit.</td>
|
||||||
<tr>
|
<tr>
|
||||||
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
|
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
|
||||||
<td>Used to compute the number of replicas of '-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>
|
||||||
@@ -93,6 +96,10 @@ Following are the key capabilities of this action:
|
|||||||
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||||
<td>Acceptable values: 1-300.</br>Default value: 0.</br>Waits for the given input in minutes before routing traffic to '-green' workloads.</td>
|
<td>Acceptable values: 1-300.</br>Default value: 0.</br>Waits for the given input in minutes before routing traffic to '-green' workloads.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>private-cluster </br></br>(Optional and relevant only using K8's deploy for a cluster with private cluster enabled)</td>
|
||||||
|
<td>Acceptable values: true, false</br>Default value: false.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>force </br></br>(Optional)</td>
|
<td>force </br></br>(Optional)</td>
|
||||||
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
|
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
|
||||||
@@ -119,6 +126,26 @@ Following are the key capabilities of this action:
|
|||||||
image-pull-secret2
|
image-pull-secret2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Private cluster deployment
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: Azure/k8s-deploy@v4
|
||||||
|
with:
|
||||||
|
resource-group: yourResourceGroup
|
||||||
|
name: yourClusterName
|
||||||
|
action: deploy
|
||||||
|
strategy: basic
|
||||||
|
|
||||||
|
private-cluster: true
|
||||||
|
manifests: |
|
||||||
|
manifests/azure-vote-backend-deployment.yaml
|
||||||
|
manifests/azure-vote-backend-service.yaml
|
||||||
|
manifests/azure-vote-frontend-deployment.yaml
|
||||||
|
manifests/azure-vote-frontend-service.yaml
|
||||||
|
images: |
|
||||||
|
registry.azurecr.io/containername
|
||||||
|
```
|
||||||
|
|
||||||
### Canary deployment without service mesh
|
### Canary deployment without service mesh
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -193,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 reject if you want to reject
|
action: reject # substitute promote if you want to promote
|
||||||
```
|
```
|
||||||
|
|
||||||
### Blue-Green deployment with different route methods
|
### Blue-Green deployment with different route methods
|
||||||
|
|||||||
+13
@@ -35,6 +35,9 @@ inputs:
|
|||||||
description: 'Traffic split method to be used. Allowed values are pod and smi'
|
description: 'Traffic split method to be used. Allowed values are pod and smi'
|
||||||
required: false
|
required: false
|
||||||
default: 'pod'
|
default: 'pod'
|
||||||
|
traffic-split-annotations:
|
||||||
|
description: 'Annotations in the form of key/value pair to be added to TrafficSplit. Relevant only if deployement strategy is blue-green or canary'
|
||||||
|
required: false
|
||||||
baseline-and-canary-replicas:
|
baseline-and-canary-replicas:
|
||||||
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
|
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
|
||||||
required: false
|
required: false
|
||||||
@@ -59,6 +62,16 @@ inputs:
|
|||||||
description: 'Annotate the target namespace'
|
description: 'Annotate the target namespace'
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
|
private-cluster:
|
||||||
|
description: 'True if cluster is AKS private cluster'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
resource-group:
|
||||||
|
description: 'Name of resource group - Only required if using private cluster'
|
||||||
|
required: false
|
||||||
|
name:
|
||||||
|
description: 'Resource group name - Only required if using private cluster'
|
||||||
|
required: false
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
color: 'green'
|
color: 'green'
|
||||||
|
|||||||
Generated
+2398
-1982
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -6,6 +6,7 @@
|
|||||||
"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 ."
|
||||||
},
|
},
|
||||||
@@ -23,9 +24,8 @@
|
|||||||
"@types/jest": "^26.0.0",
|
"@types/jest": "^26.0.0",
|
||||||
"@types/js-yaml": "^3.12.7",
|
"@types/js-yaml": "^3.12.7",
|
||||||
"@types/node": "^12.20.41",
|
"@types/node": "^12.20.41",
|
||||||
"@vercel/ncc": "^0.34.0",
|
|
||||||
"jest": "^26.0.0",
|
"jest": "^26.0.0",
|
||||||
"prettier": "2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"ts-jest": "^26.0.0",
|
"ts-jest": "^26.0.0",
|
||||||
"typescript": "3.9.5"
|
"typescript": "3.9.5"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-14
@@ -6,7 +6,6 @@ import {
|
|||||||
getResources,
|
getResources,
|
||||||
updateManifestFiles
|
updateManifestFiles
|
||||||
} from '../utilities/manifestUpdateUtils'
|
} from '../utilities/manifestUpdateUtils'
|
||||||
import {routeBlueGreen} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
|
||||||
import {
|
import {
|
||||||
annotateAndLabelResources,
|
annotateAndLabelResources,
|
||||||
checkManifestStability,
|
checkManifestStability,
|
||||||
@@ -14,7 +13,6 @@ 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,
|
||||||
@@ -23,7 +21,7 @@ export async function deploy(
|
|||||||
) {
|
) {
|
||||||
// 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')
|
||||||
@@ -36,8 +34,8 @@ export async function deploy(
|
|||||||
kubectl,
|
kubectl,
|
||||||
trafficSplitMethod
|
trafficSplitMethod
|
||||||
)
|
)
|
||||||
|
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')
|
||||||
@@ -50,15 +48,6 @@ 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, [
|
||||||
@@ -78,7 +67,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,
|
||||||
|
|||||||
+37
-41
@@ -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 {
|
||||||
BlueGreenManifests,
|
deleteGreenObjects,
|
||||||
deleteWorkloadsAndServicesWithLabel,
|
|
||||||
deleteWorkloadsWithLabel,
|
|
||||||
getManifestObjects,
|
getManifestObjects,
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
NONE_LABEL_VALUE
|
NONE_LABEL_VALUE
|
||||||
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
||||||
import {
|
|
||||||
promoteBlueGreenService,
|
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
||||||
routeBlueGreenService
|
|
||||||
} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
|
||||||
import {
|
import {
|
||||||
promoteBlueGreenIngress,
|
promoteBlueGreenIngress,
|
||||||
routeBlueGreenIngress
|
promoteBlueGreenService,
|
||||||
} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
promoteBlueGreenSMI
|
||||||
|
} from '../strategyHelpers/blueGreen/promote'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cleanupSMI,
|
routeBlueGreenService,
|
||||||
promoteBlueGreenSMI,
|
routeBlueGreenIngressUnchanged,
|
||||||
routeBlueGreenSMI
|
routeBlueGreenSMI
|
||||||
} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
} from '../strategyHelpers/blueGreen/route'
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -97,8 +97,7 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
)
|
)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.warning(
|
core.warning(
|
||||||
'Exception occurred while deleting canary and baseline workloads: ' +
|
`Exception occurred while deleting canary and baseline workloads: ${ex}`
|
||||||
ex
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
@@ -114,20 +113,24 @@ 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 one')
|
core.startGroup('Deleting old deployment and making new stable deployment')
|
||||||
let result
|
|
||||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
const {deployResult} = await (async () => {
|
||||||
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
|
switch (routeStrategy) {
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
case RouteStrategy.INGRESS:
|
||||||
result = await promoteBlueGreenSMI(kubectl, manifestObjects)
|
return await promoteBlueGreenIngress(kubectl, manifestObjects)
|
||||||
} else {
|
case RouteStrategy.SMI:
|
||||||
result = await promoteBlueGreenService(kubectl, manifestObjects)
|
return await promoteBlueGreenSMI(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 = result.newFilePaths
|
const deployedManifestFiles = deployResult.manifestFiles
|
||||||
const resources: Resource[] = getResources(
|
const resources: Resource[] = getResources(
|
||||||
deployedManifestFiles,
|
deployedManifestFiles,
|
||||||
models.DEPLOYMENT_TYPES.concat([
|
models.DEPLOYMENT_TYPES.concat([
|
||||||
@@ -141,17 +144,18 @@ 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 routeBlueGreenIngress(
|
await routeBlueGreenIngressUnchanged(
|
||||||
kubectl,
|
kubectl,
|
||||||
null,
|
|
||||||
manifestObjects.serviceNameMap,
|
manifestObjects.serviceNameMap,
|
||||||
manifestObjects.ingressEntityList
|
manifestObjects.ingressEntityList
|
||||||
)
|
)
|
||||||
await deleteWorkloadsAndServicesWithLabel(
|
|
||||||
|
await deleteGreenObjects(
|
||||||
kubectl,
|
kubectl,
|
||||||
GREEN_LABEL_VALUE,
|
[].concat(
|
||||||
manifestObjects.deploymentEntityList,
|
manifestObjects.deploymentEntityList,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await routeBlueGreenSMI(
|
await routeBlueGreenSMI(
|
||||||
@@ -159,11 +163,7 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
await deleteWorkloadsWithLabel(
|
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||||
} else {
|
} else {
|
||||||
await routeBlueGreenService(
|
await routeBlueGreenService(
|
||||||
@@ -171,11 +171,7 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
await deleteWorkloadsWithLabel(
|
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-8
@@ -2,9 +2,13 @@ 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 {rejectBlueGreenService} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
||||||
import {rejectBlueGreenIngress} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
import {
|
||||||
import {rejectBlueGreenSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
rejectBlueGreenIngress,
|
||||||
|
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,
|
||||||
@@ -55,17 +59,19 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
async function rejectBlueGreen(kubectl: Kubectl, manifests: 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, manifests)
|
await rejectBlueGreenIngress(kubectl, manifestObjects)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await rejectBlueGreenSMI(kubectl, manifests)
|
await rejectBlueGreenSMI(kubectl, manifestObjects)
|
||||||
} else {
|
} else {
|
||||||
await rejectBlueGreenService(kubectl, manifests)
|
await rejectBlueGreenService(kubectl, manifestObjects)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+15
-2
@@ -6,6 +6,7 @@ import {reject} from './actions/reject'
|
|||||||
import {Action, parseAction} from './types/action'
|
import {Action, parseAction} from './types/action'
|
||||||
import {parseDeploymentStrategy} from './types/deploymentStrategy'
|
import {parseDeploymentStrategy} from './types/deploymentStrategy'
|
||||||
import {getFilesFromDirectories} from './utilities/fileUtils'
|
import {getFilesFromDirectories} from './utilities/fileUtils'
|
||||||
|
import {PrivateKubectl} from './types/privatekubectl'
|
||||||
|
|
||||||
export async function run() {
|
export async function run() {
|
||||||
// verify kubeconfig is set
|
// verify kubeconfig is set
|
||||||
@@ -26,10 +27,22 @@ export async function run() {
|
|||||||
.filter((manifest) => manifest.length > 0) // remove any blanks
|
.filter((manifest) => manifest.length > 0) // remove any blanks
|
||||||
|
|
||||||
const fullManifestFilePaths = getFilesFromDirectories(manifestFilePaths)
|
const fullManifestFilePaths = getFilesFromDirectories(manifestFilePaths)
|
||||||
// create kubectl
|
|
||||||
const kubectlPath = await getKubectlPath()
|
const kubectlPath = await getKubectlPath()
|
||||||
const namespace = core.getInput('namespace') || 'default'
|
const namespace = core.getInput('namespace') || 'default'
|
||||||
const kubectl = new Kubectl(kubectlPath, namespace, true)
|
const isPrivateCluster =
|
||||||
|
core.getInput('private-cluster').toLowerCase() === 'true'
|
||||||
|
const resourceGroup = core.getInput('resource-group') || ''
|
||||||
|
const resourceName = core.getInput('name') || ''
|
||||||
|
|
||||||
|
const kubectl = isPrivateCluster
|
||||||
|
? new PrivateKubectl(
|
||||||
|
kubectlPath,
|
||||||
|
namespace,
|
||||||
|
true,
|
||||||
|
resourceGroup,
|
||||||
|
resourceName
|
||||||
|
)
|
||||||
|
: new Kubectl(kubectlPath, namespace, true)
|
||||||
|
|
||||||
// run action
|
// run action
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import {
|
||||||
|
deployWithLabel,
|
||||||
|
deleteGreenObjects,
|
||||||
|
fetchResource,
|
||||||
|
getDeploymentMatchLabels,
|
||||||
|
getManifestObjects,
|
||||||
|
getNewBlueGreenObject,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
isServiceRouted
|
||||||
|
} from './blueGreenHelper'
|
||||||
|
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
||||||
|
import * as bgHelper from './blueGreenHelper'
|
||||||
|
import {Kubectl} from '../../types/kubectl'
|
||||||
|
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')
|
||||||
|
|
||||||
|
const kubectl = new Kubectl('')
|
||||||
|
|
||||||
|
describe('bluegreenhelper functions', () => {
|
||||||
|
let testObjects
|
||||||
|
beforeEach(() => {
|
||||||
|
//@ts-ignore
|
||||||
|
Kubectl.mockClear()
|
||||||
|
testObjects = getManifestObjects(['test/unit/manifests/test-ingress.yml'])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(fileHelper, 'writeObjectsToFile')
|
||||||
|
.mockImplementationOnce(() => [''])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('correctly deletes services and workloads according to label', async () => {
|
||||||
|
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.serviceEntityList[0].kind).toBe('Service')
|
||||||
|
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
testObjects.deploymentEntityList[0].spec.selector.matchLabels.app
|
||||||
|
).toBe('nginx')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses other kinds of objects (getManifestObjects)', () => {
|
||||||
|
const otherObjectsCollection = getManifestObjects([
|
||||||
|
'test/unit/manifests/anomaly-objects-test.yml'
|
||||||
|
])
|
||||||
|
expect(
|
||||||
|
otherObjectsCollection.unroutedServiceEntityList[0].metadata.name
|
||||||
|
).toBe('unrouted-service')
|
||||||
|
expect(otherObjectsCollection.otherObjects[0].metadata.name).toBe(
|
||||||
|
'foobar-rollout'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('correctly classifies routed services', () => {
|
||||||
|
expect(
|
||||||
|
isServiceRouted(
|
||||||
|
testObjects.serviceEntityList[0],
|
||||||
|
testObjects.deploymentEntityList
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
testObjects.serviceEntityList[0].spec.selector.app = 'fakeapp'
|
||||||
|
expect(
|
||||||
|
isServiceRouted(
|
||||||
|
testObjects.serviceEntityList[0],
|
||||||
|
testObjects.deploymentEntityList
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('correctly makes labeled workloads', async () => {
|
||||||
|
const cwlResult: BlueGreenDeployment = await deployWithLabel(
|
||||||
|
kubectl,
|
||||||
|
testObjects.deploymentEntityList,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
expect(cwlResult.deployResult.manifestFiles[0]).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('correctly makes new blue green object (getNewBlueGreenObject and addBlueGreenLabelsAndAnnotations)', () => {
|
||||||
|
const modifiedDeployment = getNewBlueGreenObject(
|
||||||
|
testObjects.deploymentEntityList[0],
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
const mockLabels = new Map<string, string>()
|
||||||
|
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
|
||||||
|
const mockPodObject: K8sObject = {
|
||||||
|
kind: 'Pod',
|
||||||
|
metadata: {name: 'testPod', labels: mockLabels},
|
||||||
|
spec: {}
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
getDeploymentMatchLabels(mockPodObject)[
|
||||||
|
bgHelper.BLUE_GREEN_VERSION_LABEL
|
||||||
|
]
|
||||||
|
).toBe(GREEN_LABEL_VALUE)
|
||||||
|
expect(
|
||||||
|
getDeploymentMatchLabels(testObjects.deploymentEntityList[0])['app']
|
||||||
|
).toBe('nginx')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
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,
|
||||||
@@ -8,19 +11,18 @@ 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 {routeBlueGreenService} from './serviceBlueGreenHelper'
|
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
|
||||||
import {routeBlueGreenIngress} from './ingressBlueGreenHelper'
|
import {checkForErrors} from '../../utilities/kubectlUtils'
|
||||||
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'
|
||||||
@@ -28,140 +30,46 @@ 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 interface BlueGreenManifests {
|
export async function deleteGreenObjects(
|
||||||
serviceEntityList: any[]
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
unroutedServiceEntityList: any[]
|
|
||||||
deploymentEntityList: any[]
|
|
||||||
ingressEntityList: any[]
|
|
||||||
otherObjects: any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreen(
|
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
inputManifestFiles: string[],
|
toDelete: K8sObject[]
|
||||||
routeStrategy: RouteStrategy
|
): Promise<K8sDeleteObject[]> {
|
||||||
) {
|
// const resourcesToDelete: K8sDeleteObject[] = []
|
||||||
// sleep for buffer time
|
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
|
||||||
const bufferTime: number = parseInt(
|
return {
|
||||||
core.getInput('version-switch-buffer') || '0'
|
name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX),
|
||||||
)
|
kind: obj.kind
|
||||||
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
|
|
||||||
)
|
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWorkloadsAndServicesWithLabel(
|
export async function deleteObjects(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deleteLabel: string,
|
deleteList: K8sDeleteObject[]
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
// Ignore failures of delete if it doesn't exist
|
core.debug(`failed to delete object ${delObject.name}: ${ex}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// other common functions
|
// other common functions
|
||||||
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
||||||
const deploymentEntityList = []
|
const deploymentEntityList: K8sObject[] = []
|
||||||
const routedServiceEntityList = []
|
const routedServiceEntityList: K8sObject[] = []
|
||||||
const unroutedServiceEntityList = []
|
const unroutedServiceEntityList: K8sObject[] = []
|
||||||
const ingressEntityList = []
|
const ingressEntityList: K8sObject[] = []
|
||||||
const otherEntitiesList = []
|
const otherEntitiesList: K8sObject[] = []
|
||||||
const serviceNameMap = new Map<string, string>()
|
const serviceNameMap = new Map<string, string>()
|
||||||
|
|
||||||
filePaths.forEach((filePath: string) => {
|
filePaths.forEach((filePath: string) => {
|
||||||
@@ -206,51 +114,41 @@ 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 shouldBeRouted
|
return (
|
||||||
|
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 createWorkloadsWithLabel(
|
export async function deployWithLabel(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deploymentObjectList: any[],
|
deploymentObjectList: any[],
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
) {
|
): Promise<BlueGreenDeployment> {
|
||||||
const newObjectsList = []
|
const newObjectsList = deploymentObjectList.map((inputObject) =>
|
||||||
deploymentObjectList.forEach((inputObject) => {
|
getNewBlueGreenObject(inputObject, nextLabel)
|
||||||
// creating deployment with label
|
)
|
||||||
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel)
|
|
||||||
core.debug(
|
|
||||||
'New blue-green object is: ' + JSON.stringify(newBlueGreenObject)
|
|
||||||
)
|
|
||||||
newObjectsList.push(newBlueGreenObject)
|
|
||||||
})
|
|
||||||
|
|
||||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
core.debug(
|
||||||
const result = await kubectl.apply(manifestFiles)
|
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
|
||||||
|
)
|
||||||
return {result: result, newFilePaths: manifestFiles}
|
const deployResult = await deployObjects(kubectl, newObjectsList)
|
||||||
|
return {deployResult, objects: newObjectsList}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNewBlueGreenObject(
|
export function getNewBlueGreenObject(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): object {
|
): K8sObject {
|
||||||
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
|
||||||
@@ -278,7 +176,7 @@ export function addBlueGreenLabelsAndAnnotations(
|
|||||||
updateObjectLabels(inputObject, newLabels, false)
|
updateObjectLabels(inputObject, newLabels, false)
|
||||||
updateSelectorLabels(inputObject, newLabels, false)
|
updateSelectorLabels(inputObject, newLabels, false)
|
||||||
|
|
||||||
// updating spec labels if it is a service
|
// updating spec labels if it is not a service
|
||||||
if (!isServiceEntity(inputObject.kind)) {
|
if (!isServiceEntity(inputObject.kind)) {
|
||||||
updateSpecLabels(inputObject, newLabels, false)
|
updateSpecLabels(inputObject, newLabels, false)
|
||||||
}
|
}
|
||||||
@@ -337,14 +235,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)
|
const resource = JSON.parse(result.stdout) as K8sObject
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UnsetClusterSpecificDetails(resource)
|
UnsetClusterSpecificDetails(resource)
|
||||||
@@ -356,3 +254,13 @@ 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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
|
||||||
|
import * as bgHelper from './blueGreenHelper'
|
||||||
|
import {
|
||||||
|
getUpdatedBlueGreenIngress,
|
||||||
|
isIngressRouted,
|
||||||
|
validateIngresses
|
||||||
|
} from './ingressBlueGreenHelper'
|
||||||
|
import {Kubectl} from '../../types/kubectl'
|
||||||
|
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')
|
||||||
|
|
||||||
|
describe('ingress blue green helpers', () => {
|
||||||
|
let testObjects
|
||||||
|
beforeEach(() => {
|
||||||
|
//@ts-ignore
|
||||||
|
Kubectl.mockClear()
|
||||||
|
testObjects = getManifestObjects(ingressFilepath)
|
||||||
|
jest
|
||||||
|
.spyOn(fileHelper, 'writeObjectsToFile')
|
||||||
|
.mockImplementationOnce(() => [''])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it should correctly classify ingresses', () => {
|
||||||
|
expect(
|
||||||
|
isIngressRouted(
|
||||||
|
testObjects.ingressEntityList[0],
|
||||||
|
testObjects.serviceNameMap
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
testObjects.ingressEntityList[0].spec.rules[0].http.paths = {}
|
||||||
|
expect(
|
||||||
|
isIngressRouted(
|
||||||
|
testObjects.ingressEntityList[0],
|
||||||
|
testObjects.serviceNameMap
|
||||||
|
)
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
isIngressRouted(
|
||||||
|
getManifestObjects(betaFilepath).ingressEntityList[0],
|
||||||
|
testObjects.serviceNameMap
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it should correctly update ingresses', () => {
|
||||||
|
const updatedIng = getUpdatedBlueGreenIngress(
|
||||||
|
testObjects.ingressEntityList[0],
|
||||||
|
testObjects.serviceNameMap,
|
||||||
|
GREEN_LABEL_VALUE
|
||||||
|
)
|
||||||
|
expect(updatedIng.metadata.name).toBe('nginx-ingress')
|
||||||
|
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.spec.rules[0].http.paths[0].backend.service.name).toBe(
|
||||||
|
'nginx-service-green'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it should validate ingresses', async () => {
|
||||||
|
// what if nothing gets returned from fetchResource?
|
||||||
|
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
|
||||||
|
let validResponse = await validateIngresses(
|
||||||
|
kubectl,
|
||||||
|
testObjects.ingressEntityList,
|
||||||
|
testObjects.serviceNameMap
|
||||||
|
)
|
||||||
|
expect(validResponse.areValid).toBe(false)
|
||||||
|
|
||||||
|
// test valid ingress
|
||||||
|
let mockIngress = JSON.parse(
|
||||||
|
JSON.stringify(testObjects.ingressEntityList[0])
|
||||||
|
)
|
||||||
|
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,220 +1,37 @@
|
|||||||
import {Kubectl} from '../../types/kubectl'
|
import * as core from '@actions/core'
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import {K8sIngress} from '../../types/k8sObject'
|
||||||
import {
|
import {
|
||||||
addBlueGreenLabelsAndAnnotations,
|
addBlueGreenLabelsAndAnnotations,
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
BLUE_GREEN_VERSION_LABEL,
|
||||||
BlueGreenManifests,
|
|
||||||
createWorkloadsWithLabel,
|
|
||||||
deleteWorkloadsAndServicesWithLabel,
|
|
||||||
fetchResource,
|
|
||||||
getManifestObjects,
|
|
||||||
getNewBlueGreenObject,
|
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
NONE_LABEL_VALUE
|
fetchResource
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import * as core from '@actions/core'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
|
|
||||||
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 result = createWorkloadsWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create new services and other objects
|
|
||||||
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)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promoteBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
) {
|
|
||||||
//checking if anything to promote
|
|
||||||
if (
|
|
||||||
!validateIngressesState(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw 'Ingress not in promote state'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
core.debug('New objects: ' + JSON.stringify(newObjectsList))
|
|
||||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
|
||||||
await kubectl.apply(manifestFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateIngressesState(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
ingressEntityList: any[],
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
): boolean {
|
|
||||||
let areIngressesTargetingNewServices: boolean = true
|
|
||||||
ingressEntityList.forEach(async (inputObject) => {
|
|
||||||
if (isIngressRouted(inputObject, serviceNameMap)) {
|
|
||||||
//querying existing ingress
|
|
||||||
const existingIngress = await fetchResource(
|
|
||||||
kubectl,
|
|
||||||
inputObject.kind,
|
|
||||||
inputObject.metadata.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!!existingIngress) {
|
|
||||||
const currentLabel: string =
|
|
||||||
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL]
|
|
||||||
|
|
||||||
// if not green label, then wrong configuration
|
|
||||||
if (currentLabel != GREEN_LABEL_VALUE)
|
|
||||||
areIngressesTargetingNewServices = false
|
|
||||||
} else {
|
|
||||||
// no ingress at all, so nothing to promote
|
|
||||||
areIngressesTargetingNewServices = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return areIngressesTargetingNewServices
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
if (key === 'serviceName' && serviceNameMap.has(value)) {
|
|
||||||
isIngressRouted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
): object {
|
): K8sIngress {
|
||||||
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)
|
||||||
|
|
||||||
// update ingress labels
|
// update ingress labels
|
||||||
|
if (inputObject.apiVersion === 'networking.k8s.io/v1beta1') {
|
||||||
|
return updateIngressBackendBetaV1(newObject, serviceNameMap)
|
||||||
|
}
|
||||||
return updateIngressBackend(newObject, serviceNameMap)
|
return updateIngressBackend(newObject, serviceNameMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateIngressBackend(
|
export function updateIngressBackendBetaV1(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
serviceNameMap: Map<string, string>
|
serviceNameMap: Map<string, string>
|
||||||
): any {
|
): any {
|
||||||
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||||
if (key.toUpperCase() === BACKEND) {
|
if (key.toLowerCase() === BACKEND) {
|
||||||
const {serviceName} = value
|
const {serviceName} = value
|
||||||
if (serviceNameMap.has(serviceName)) {
|
if (serviceNameMap.has(serviceName)) {
|
||||||
// update service name with corresponding bluegreen name only if service is provied in given manifests
|
// update service name with corresponding bluegreen name only if service is provied in given manifests
|
||||||
@@ -227,3 +44,77 @@ export function updateIngressBackend(
|
|||||||
|
|
||||||
return inputObject
|
return inputObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateIngressBackend(
|
||||||
|
inputObject: any,
|
||||||
|
serviceNameMap: Map<string, string>
|
||||||
|
): any {
|
||||||
|
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||||
|
if (
|
||||||
|
key.toLowerCase() === BACKEND &&
|
||||||
|
serviceNameMap.has(value.service.name)
|
||||||
|
) {
|
||||||
|
value.service.name = serviceNameMap.get(value.service.name)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
|
||||||
|
return inputObject
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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)}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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,106 +1,18 @@
|
|||||||
|
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,
|
||||||
getManifestObjects,
|
GREEN_LABEL_VALUE
|
||||||
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 result = await createWorkloadsWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create other non deployment and non service entities
|
|
||||||
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 result
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
function getUpdatedBlueGreenService(
|
export function getUpdatedBlueGreenService(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): object {
|
): K8sServiceObject {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
|
||||||
// Adding labels and annotations.
|
// Adding labels and annotations.
|
||||||
@@ -122,25 +34,16 @@ export async function validateServicesState(
|
|||||||
serviceObject.metadata.name
|
serviceObject.metadata.name
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!!existingService) {
|
let isServiceGreen =
|
||||||
const currentLabel: string = getServiceSpecLabel(existingService)
|
!!existingService &&
|
||||||
if (currentLabel != GREEN_LABEL_VALUE) {
|
getServiceSpecLabel(existingService as K8sServiceObject) ==
|
||||||
// service should be targeting deployments with green label
|
GREEN_LABEL_VALUE
|
||||||
areServicesGreen = false
|
areServicesGreen = areServicesGreen && isServiceGreen
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// service targeting deployment doesn't exist
|
|
||||||
areServicesGreen = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return areServicesGreen
|
return areServicesGreen
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceSpecLabel(inputObject: any): string {
|
export function getServiceSpecLabel(inputObject: K8sServiceObject): string {
|
||||||
if (inputObject?.spec?.selector[BLUE_GREEN_VERSION_LABEL]) {
|
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
||||||
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
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,97 +1,35 @@
|
|||||||
|
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,
|
||||||
deleteWorkloadsWithLabel,
|
deployObjects,
|
||||||
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'
|
||||||
|
|
||||||
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
||||||
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
||||||
const MIN_VAL = 0
|
export const MIN_VAL = 0
|
||||||
const MAX_VAL = 100
|
export const MAX_VAL = 100
|
||||||
|
|
||||||
export async function deployBlueGreenSMI(
|
export async function setupSMI(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
filePaths: string[]
|
serviceEntityList: any[]
|
||||||
) {
|
): Promise<BlueGreenDeployment> {
|
||||||
// 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)
|
|
||||||
|
|
||||||
// create new deloyments
|
|
||||||
return await createWorkloadsWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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[]
|
|
||||||
) {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// route trafficsplit to stable deploymetns
|
|
||||||
await routeBlueGreenSMI(
|
|
||||||
kubectl,
|
|
||||||
NONE_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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(kubectl: Kubectl, serviceEntityList: any[]) {
|
|
||||||
const newObjectsList = []
|
const newObjectsList = []
|
||||||
const trafficObjectList = []
|
const trafficObjectList = []
|
||||||
|
|
||||||
@@ -99,53 +37,66 @@ export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
|||||||
// 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 = getSMIServiceResource(
|
const newStableService = getStableSMIServiceResource(serviceObject)
|
||||||
serviceObject,
|
const newGreenService = getGreenSMIServiceResource(serviceObject)
|
||||||
STABLE_SUFFIX
|
|
||||||
)
|
|
||||||
const newGreenService = getSMIServiceResource(serviceObject, GREEN_SUFFIX)
|
|
||||||
newObjectsList.push(newStableService)
|
newObjectsList.push(newStableService)
|
||||||
newObjectsList.push(newGreenService)
|
newObjectsList.push(newGreenService)
|
||||||
})
|
})
|
||||||
|
|
||||||
// create services
|
const tsObjects: TrafficSplitObject[] = []
|
||||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
|
||||||
await kubectl.apply(manifestFiles)
|
|
||||||
|
|
||||||
// route to stable service
|
// route to stable service
|
||||||
trafficObjectList.forEach((inputObject) => {
|
for (const svc of trafficObjectList) {
|
||||||
createTrafficSplitObject(
|
const tsObject = await createTrafficSplitObject(
|
||||||
kubectl,
|
kubectl,
|
||||||
inputObject.metadata.name,
|
svc.metadata.name,
|
||||||
NONE_LABEL_VALUE
|
NONE_LABEL_VALUE
|
||||||
)
|
)
|
||||||
})
|
tsObjects.push(tsObject as TrafficSplitObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectsToDeploy = [].concat(newObjectsList, tsObjects)
|
||||||
|
|
||||||
|
// create services
|
||||||
|
const smiDeploymentResult: DeployResult = await deployObjects(
|
||||||
|
kubectl,
|
||||||
|
objectsToDeploy
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
objects: objectsToDeploy,
|
||||||
|
deployResult: smiDeploymentResult
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let trafficSplitAPIVersion = ''
|
let trafficSplitAPIVersion = ''
|
||||||
|
|
||||||
async function createTrafficSplitObject(
|
export async function createTrafficSplitObject(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
name: string,
|
name: string,
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
): Promise<any> {
|
): Promise<TrafficSplitObject> {
|
||||||
// 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 = JSON.stringify({
|
const trafficSplitObject: TrafficSplitObject = {
|
||||||
apiVersion: trafficSplitAPIVersion,
|
apiVersion: trafficSplitAPIVersion,
|
||||||
kind: 'TrafficSplit',
|
kind: TRAFFIC_SPLIT_OBJECT,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
||||||
|
annotations: annotations,
|
||||||
|
labels: new Map<string, string>()
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
service: name,
|
service: name,
|
||||||
@@ -160,50 +111,24 @@ async function createTrafficSplitObject(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// create traffic split object
|
return trafficSplitObject
|
||||||
const trafficSplitManifestFile = fileHelper.writeManifestToFile(
|
|
||||||
trafficSplitObject,
|
|
||||||
TRAFFIC_SPLIT_OBJECT,
|
|
||||||
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
|
||||||
)
|
|
||||||
|
|
||||||
await kubectl.apply(trafficSplitManifestFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSMIServiceResource(
|
export function getStableSMIServiceResource(inputObject: K8sObject): K8sObject {
|
||||||
inputObject: any,
|
|
||||||
suffix: string
|
|
||||||
): object {
|
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
// adding stable suffix to service name
|
||||||
if (suffix === STABLE_SUFFIX) {
|
newObject.metadata.name = getBlueGreenResourceName(
|
||||||
// adding stable suffix to service name
|
inputObject.metadata.name,
|
||||||
newObject.metadata.name = getBlueGreenResourceName(
|
STABLE_SUFFIX
|
||||||
inputObject.metadata.name,
|
)
|
||||||
STABLE_SUFFIX
|
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
||||||
)
|
|
||||||
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
|
||||||
} else {
|
|
||||||
// green label will be added for these
|
|
||||||
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function routeBlueGreenSMI(
|
export function getGreenSMIServiceResource(inputObject: K8sObject): K8sObject {
|
||||||
kubectl: Kubectl,
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
nextLabel: string,
|
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
||||||
serviceEntityList: any[]
|
|
||||||
) {
|
|
||||||
for (const serviceObject of serviceEntityList) {
|
|
||||||
// route trafficsplit to given label
|
|
||||||
await createTrafficSplitObject(
|
|
||||||
kubectl,
|
|
||||||
serviceObject.metadata.name,
|
|
||||||
nextLabel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateTrafficSplitsState(
|
export async function validateTrafficSplitsState(
|
||||||
@@ -219,32 +144,38 @@ 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) {
|
||||||
// no traffic split exits
|
core.debug(`no traffic split exits for ${name}`)
|
||||||
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)) {
|
||||||
if (element.weight != MAX_VAL) trafficSplitsInRightState = false
|
trafficSplitsInRightState =
|
||||||
|
trafficSplitsInRightState && element.weight == MAX_VAL
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
||||||
) {
|
) {
|
||||||
if (element.weight != MIN_VAL) trafficSplitsInRightState = false
|
trafficSplitsInRightState =
|
||||||
|
trafficSplitsInRightState && element.weight == MIN_VAL
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return trafficSplitsInRightState
|
return trafficSplitsInRightState
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
export async function cleanupSMI(
|
||||||
const deleteList = []
|
kubectl: Kubectl,
|
||||||
|
serviceEntityList: any[]
|
||||||
|
): Promise<K8sDeleteObject[]> {
|
||||||
|
const deleteList: K8sDeleteObject[] = []
|
||||||
|
|
||||||
serviceEntityList.forEach((serviceObject) => {
|
serviceEntityList.forEach((serviceObject) => {
|
||||||
deleteList.push({
|
deleteList.push({
|
||||||
@@ -274,4 +205,6 @@ export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
|||||||
|
|
||||||
// delete all objects
|
// delete all objects
|
||||||
await deleteObjects(kubectl, deleteList)
|
await deleteObjects(kubectl, deleteList)
|
||||||
|
|
||||||
|
return deleteList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
|||||||
import * as canaryDeploymentHelper from './canaryHelper'
|
import * as canaryDeploymentHelper from './canaryHelper'
|
||||||
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'
|
||||||
@@ -301,7 +302,8 @@ async function getTrafficSplitObject(
|
|||||||
apiVersion: trafficSplitAPIVersion,
|
apiVersion: trafficSplitAPIVersion,
|
||||||
kind: 'TrafficSplit',
|
kind: 'TrafficSplit',
|
||||||
metadata: {
|
metadata: {
|
||||||
name: getTrafficSplitResourceName(name)
|
name: getTrafficSplitResourceName(name),
|
||||||
|
annotations: inputAnnotations
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
backends: [
|
backends: [
|
||||||
|
|||||||
@@ -10,20 +10,24 @@ 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 {deployBlueGreenService} from './blueGreen/serviceBlueGreenHelper'
|
import {
|
||||||
import {deployBlueGreenIngress} from './blueGreen/ingressBlueGreenHelper'
|
deployBlueGreen,
|
||||||
import {deployBlueGreenSMI} from './blueGreen/smiBlueGreenHelper'
|
deployBlueGreenIngress,
|
||||||
|
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, RouteStrategy} from '../types/routeStrategy'
|
import {parseRouteStrategy} from '../types/routeStrategy'
|
||||||
import {ExecOutput} from '@actions/exec'
|
import {ExecOutput} from '@actions/exec'
|
||||||
import {
|
import {
|
||||||
getWorkflowAnnotationKeyLabel,
|
getWorkflowAnnotationKeyLabel,
|
||||||
getWorkflowAnnotations
|
getWorkflowAnnotations,
|
||||||
|
cleanLabel
|
||||||
} from '../utilities/workflowAnnotationUtils'
|
} from '../utilities/workflowAnnotationUtils'
|
||||||
import {
|
import {
|
||||||
annotateChildPods,
|
annotateChildPods,
|
||||||
@@ -57,17 +61,19 @@ 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(
|
||||||
const {result, newFilePaths} = await Promise.resolve(
|
kubectl,
|
||||||
(routeStrategy == RouteStrategy.INGRESS &&
|
files,
|
||||||
deployBlueGreenIngress(kubectl, files)) ||
|
routeStrategy
|
||||||
(routeStrategy == RouteStrategy.SMI &&
|
)
|
||||||
deployBlueGreenSMI(kubectl, files)) ||
|
core.debug(
|
||||||
deployBlueGreenService(kubectl, files)
|
`objects deployed for ${routeStrategy}: ${JSON.stringify(
|
||||||
|
blueGreenDeployment.objects
|
||||||
|
)} `
|
||||||
)
|
)
|
||||||
|
|
||||||
checkForErrors([result])
|
checkForErrors([blueGreenDeployment.deployResult.execResult])
|
||||||
return newFilePaths
|
return blueGreenDeployment.deployResult.manifestFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
case DeploymentStrategy.BASIC: {
|
case DeploymentStrategy.BASIC: {
|
||||||
@@ -141,7 +147,7 @@ export async function annotateAndLabelResources(
|
|||||||
const workflowFilePath = await getWorkflowFilePath(githubToken)
|
const workflowFilePath = await getWorkflowFilePath(githubToken)
|
||||||
|
|
||||||
const deploymentConfig = await getDeploymentConfig()
|
const deploymentConfig = await getDeploymentConfig()
|
||||||
const annotationKeyLabel = getWorkflowAnnotationKeyLabel(workflowFilePath)
|
const annotationKeyLabel = getWorkflowAnnotationKeyLabel()
|
||||||
|
|
||||||
await annotateResources(
|
await annotateResources(
|
||||||
files,
|
files,
|
||||||
@@ -214,10 +220,10 @@ async function labelResources(
|
|||||||
label: string
|
label: string
|
||||||
) {
|
) {
|
||||||
const labels = [
|
const labels = [
|
||||||
`workflowFriendlyName=${normalizeWorkflowStrLabel(
|
`workflowFriendlyName=${cleanLabel(
|
||||||
process.env.GITHUB_WORKFLOW
|
normalizeWorkflowStrLabel(process.env.GITHUB_WORKFLOW)
|
||||||
)}`,
|
)}`,
|
||||||
`workflow=${label}`
|
`workflow=${cleanLabel(label)}`
|
||||||
]
|
]
|
||||||
|
|
||||||
checkForErrors([await kubectl.labelFiles(files, labels)], true)
|
checkForErrors([await kubectl.labelFiles(files, labels)], true)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function parseAnnotations(str: string) {
|
||||||
|
if (str == '') {
|
||||||
|
return new Map<string, string>()
|
||||||
|
} else {
|
||||||
|
const annotation = JSON.parse(str)
|
||||||
|
return new Map<string, string>(annotation)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {ExecOutput} from '@actions/exec'
|
||||||
|
|
||||||
|
export interface DeployResult {
|
||||||
|
execResult: ExecOutput
|
||||||
|
manifestFiles: string[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
+12
-5
@@ -10,18 +10,25 @@ export interface Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Kubectl {
|
export class Kubectl {
|
||||||
private readonly kubectlPath: string
|
protected readonly kubectlPath: string
|
||||||
private readonly namespace: string
|
protected readonly namespace: string
|
||||||
private readonly ignoreSSLErrors: boolean
|
protected readonly ignoreSSLErrors: boolean
|
||||||
|
protected readonly resourceGroup: string
|
||||||
|
protected readonly name: string
|
||||||
|
protected isPrivateCluster: boolean
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
kubectlPath: string,
|
kubectlPath: string,
|
||||||
namespace: string = 'default',
|
namespace: string = 'default',
|
||||||
ignoreSSLErrors: boolean = false
|
ignoreSSLErrors: boolean = false,
|
||||||
|
resourceGroup: string = '',
|
||||||
|
name: string = ''
|
||||||
) {
|
) {
|
||||||
this.kubectlPath = kubectlPath
|
this.kubectlPath = kubectlPath
|
||||||
this.ignoreSSLErrors = !!ignoreSSLErrors
|
this.ignoreSSLErrors = !!ignoreSSLErrors
|
||||||
this.namespace = namespace
|
this.namespace = namespace
|
||||||
|
this.resourceGroup = resourceGroup
|
||||||
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
public async apply(
|
public async apply(
|
||||||
@@ -155,7 +162,7 @@ export class Kubectl {
|
|||||||
return this.execute(['delete', ...args])
|
return this.execute(['delete', ...args])
|
||||||
}
|
}
|
||||||
|
|
||||||
private async execute(args: string[], silent: boolean = false) {
|
protected async execute(args: string[], silent: boolean = false) {
|
||||||
if (this.ignoreSSLErrors) {
|
if (this.ignoreSSLErrors) {
|
||||||
args.push('--insecure-skip-tls-verify')
|
args.push('--insecure-skip-tls-verify')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import {Kubectl} from './kubectl'
|
||||||
|
import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
export class PrivateKubectl extends Kubectl {
|
||||||
|
protected async execute(args: string[], silent: boolean = false) {
|
||||||
|
args.unshift('kubectl')
|
||||||
|
let kubectlCmd = args.join(' ')
|
||||||
|
let addFileFlag = false
|
||||||
|
let eo = <ExecOptions>{silent}
|
||||||
|
|
||||||
|
if (this.containsFilenames(kubectlCmd)) {
|
||||||
|
// For private clusters, files will referenced solely by their basename
|
||||||
|
kubectlCmd = this.replaceFilnamesWithBasenames(kubectlCmd)
|
||||||
|
addFileFlag = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateClusterArgs = [
|
||||||
|
'aks',
|
||||||
|
'command',
|
||||||
|
'invoke',
|
||||||
|
'--resource-group',
|
||||||
|
this.resourceGroup,
|
||||||
|
'--name',
|
||||||
|
this.name,
|
||||||
|
'--command',
|
||||||
|
kubectlCmd
|
||||||
|
]
|
||||||
|
|
||||||
|
if (addFileFlag) {
|
||||||
|
const filenames = this.extractFilesnames(kubectlCmd).split(' ')
|
||||||
|
|
||||||
|
const tempDirectory =
|
||||||
|
process.env['runner.tempDirectory'] || os.tmpdir() + '/manifests'
|
||||||
|
eo.cwd = tempDirectory
|
||||||
|
privateClusterArgs.push(...['--file', '.'])
|
||||||
|
|
||||||
|
let filenamesArr = filenames[0].split(',')
|
||||||
|
for (let index = 0; index < filenamesArr.length; index++) {
|
||||||
|
const file = filenamesArr[index]
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.moveFileToTempManifestDir(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`private cluster Kubectl run with invoke command: ${kubectlCmd}`
|
||||||
|
)
|
||||||
|
return await getExecOutput('az', privateClusterArgs, eo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private replaceFilnamesWithBasenames(kubectlCmd: string) {
|
||||||
|
let exFilenames = this.extractFilesnames(kubectlCmd)
|
||||||
|
let filenames = exFilenames.split(' ')
|
||||||
|
let filenamesArr = filenames[0].split(',')
|
||||||
|
|
||||||
|
for (let index = 0; index < filenamesArr.length; index++) {
|
||||||
|
filenamesArr[index] = path.basename(filenamesArr[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseFilenames = filenamesArr.join()
|
||||||
|
|
||||||
|
let result = kubectlCmd.replace(exFilenames, baseFilenames)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public extractFilesnames(strToParse: string) {
|
||||||
|
let start = strToParse.indexOf('-filename')
|
||||||
|
let offset = 7
|
||||||
|
|
||||||
|
if (start == -1) {
|
||||||
|
start = strToParse.indexOf('-f')
|
||||||
|
|
||||||
|
if (start == -1) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp = strToParse.substring(start + offset)
|
||||||
|
let end = temp.indexOf(' -')
|
||||||
|
|
||||||
|
//End could be case where the -f flag was last, or -f is followed by some additonal flag and it's arguments
|
||||||
|
return temp.substring(3, end == -1 ? temp.length : end).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
private containsFilenames(str: string) {
|
||||||
|
return str.includes('-f ') || str.includes('filename ')
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTempManifestsDirectory() {
|
||||||
|
const manifestsDir = '/tmp/manifests'
|
||||||
|
if (!fs.existsSync('/tmp/manifests')) {
|
||||||
|
fs.mkdirSync('/tmp/manifests', {recursive: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveFileToTempManifestDir(file: string) {
|
||||||
|
this.createTempManifestsDirectory()
|
||||||
|
if (!fs.existsSync('/tmp/' + file)) {
|
||||||
|
core.debug(
|
||||||
|
'/tmp/' +
|
||||||
|
file +
|
||||||
|
' does not exist, and therefore cannot be moved to the manifest directory'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFile('/tmp/' + file, '/tmp/manifests/' + file, function (err) {
|
||||||
|
if (err) {
|
||||||
|
core.debug(
|
||||||
|
'Could not rename ' +
|
||||||
|
'/tmp/' +
|
||||||
|
file +
|
||||||
|
' to ' +
|
||||||
|
'/tmp/manifests/' +
|
||||||
|
file +
|
||||||
|
' ERROR: ' +
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
core.debug(
|
||||||
|
"Successfully moved file '" +
|
||||||
|
file +
|
||||||
|
"' from /tmp to /tmp/manifest directory"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,12 @@ describe('File utils', () => {
|
|||||||
'test/unit/manifests/manifest_test_dir/another_layer/deep-service.yaml',
|
'test/unit/manifests/manifest_test_dir/another_layer/deep-service.yaml',
|
||||||
'test/unit/manifests/manifest_test_dir/nested-test-service.yaml',
|
'test/unit/manifests/manifest_test_dir/nested-test-service.yaml',
|
||||||
'test/unit/manifests/test-ingress.yml',
|
'test/unit/manifests/test-ingress.yml',
|
||||||
|
'test/unit/manifests/test-ingress-new.yml',
|
||||||
'test/unit/manifests/test-service.yml'
|
'test/unit/manifests/test-service.yml'
|
||||||
]
|
]
|
||||||
|
|
||||||
// is there a more efficient way to test equality w random order?
|
// is there a more efficient way to test equality w random order?
|
||||||
expect(testSearch).toHaveLength(5)
|
expect(testSearch).toHaveLength(7)
|
||||||
expectedManifests.forEach((fileName) => {
|
expectedManifests.forEach((fileName) => {
|
||||||
expect(testSearch).toContain(fileName)
|
expect(testSearch).toContain(fileName)
|
||||||
})
|
})
|
||||||
@@ -53,7 +54,7 @@ describe('File utils', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
||||||
).toHaveLength(5)
|
).toHaveLength(7)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function writeManifestToFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getManifestFileName(kind: string, name: string) {
|
function getManifestFileName(kind: string, name: string) {
|
||||||
const filePath = `${kind}_${name}_ ${getCurrentTime().toString()}`
|
const filePath = `${kind}_${name}_${getCurrentTime().toString()}`
|
||||||
const tempDirectory = getTempDirectory()
|
const tempDirectory = getTempDirectory()
|
||||||
return path.join(tempDirectory, path.basename(filePath))
|
return path.join(tempDirectory, path.basename(filePath))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils'
|
import {cleanLabel} from '../utilities/workflowAnnotationUtils'
|
||||||
|
|
||||||
describe('WorkflowAnnotationUtils', () => {
|
describe('WorkflowAnnotationUtils', () => {
|
||||||
describe('prefixObjectKeys', () => {
|
describe('cleanLabel', () => {
|
||||||
it('should prefix an object with a given prefix', () => {
|
it('should clean label', () => {
|
||||||
const obj = {
|
const alreadyClean = 'alreadyClean'
|
||||||
foo: 'bar',
|
expect(cleanLabel(alreadyClean)).toEqual(alreadyClean)
|
||||||
baz: 'qux'
|
expect(cleanLabel('.startInvalid')).toEqual('startInvalid')
|
||||||
}
|
expect(cleanLabel('with%S0ME&invalid#chars')).toEqual(
|
||||||
const prefix = 'prefix.'
|
'withS0MEinvalidchars'
|
||||||
const expected = {
|
)
|
||||||
'prefix.foo': 'bar',
|
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
||||||
'prefix.baz': 'qux'
|
})
|
||||||
}
|
it('should remove slashes from label', () => {
|
||||||
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
|
expect(
|
||||||
|
cleanLabel('Workflow Name / With Slashes / And Spaces')
|
||||||
|
).toEqual('Workflow_Name_-_With_Slashes_-_And_Spaces')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {DeploymentConfig} from '../types/deploymentConfig'
|
import {DeploymentConfig} from '../types/deploymentConfig'
|
||||||
|
|
||||||
const ANNOTATION_PREFIX = 'actions.github.com/'
|
const ANNOTATION_PREFIX = 'actions.github.com'
|
||||||
|
|
||||||
export function prefixObjectKeys(obj: any, prefix: string): any {
|
|
||||||
return Object.keys(obj).reduce((newObj, key) => {
|
|
||||||
newObj[prefix + key] = obj[key]
|
|
||||||
return newObj
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWorkflowAnnotations(
|
export function getWorkflowAnnotations(
|
||||||
lastSuccessRunSha: string,
|
lastSuccessRunSha: string,
|
||||||
@@ -31,19 +24,24 @@ export function getWorkflowAnnotations(
|
|||||||
helmChartPaths: deploymentConfig.helmChartFilePaths,
|
helmChartPaths: deploymentConfig.helmChartFilePaths,
|
||||||
provider: 'GitHub'
|
provider: 'GitHub'
|
||||||
}
|
}
|
||||||
const prefixedAnnotationObject = prefixObjectKeys(
|
return JSON.stringify(annotationObject)
|
||||||
annotationObject,
|
|
||||||
ANNOTATION_PREFIX
|
|
||||||
)
|
|
||||||
return JSON.stringify(prefixedAnnotationObject)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkflowAnnotationKeyLabel(
|
export function getWorkflowAnnotationKeyLabel(): string {
|
||||||
workflowFilePath: string
|
return `${ANNOTATION_PREFIX}/k8s-deploy`
|
||||||
): string {
|
}
|
||||||
const hashKey = require('crypto')
|
|
||||||
.createHash('MD5')
|
/**
|
||||||
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
|
* Cleans label to match valid kubernetes label specification by removing invalid characters
|
||||||
.digest('hex')
|
* @param label
|
||||||
return `githubWorkflow_${hashKey}`
|
* @returns cleaned label
|
||||||
|
*/
|
||||||
|
export function cleanLabel(label: string): string {
|
||||||
|
let removedInvalidChars = label
|
||||||
|
.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]/
|
||||||
|
return regex.exec(removedInvalidChars)[0] || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-deployment
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.14.2
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nginx-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: nginx
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /testpath
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: nginx-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
Reference in New Issue
Block a user