mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-23 13:09:27 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebc294c887 |
+1
-3
@@ -2,6 +2,4 @@ node_modules
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
lib/
|
lib/
|
||||||
|
|
||||||
coverage/
|
|
||||||
@@ -4,15 +4,6 @@ This action is used to deploy manifests to Kubernetes clusters. It requires that
|
|||||||
|
|
||||||
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
||||||
|
|
||||||
This action requires the following permissions from your workflow:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
```
|
|
||||||
|
|
||||||
## Action capabilities
|
## Action capabilities
|
||||||
|
|
||||||
Following are the key capabilities of this action:
|
Following are the key capabilities of this action:
|
||||||
@@ -83,9 +74,6 @@ 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>
|
||||||
@@ -105,10 +93,6 @@ 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>
|
||||||
@@ -135,26 +119,6 @@ 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
|
||||||
@@ -229,7 +193,7 @@ To promote/reject the canary created by the above snippet, the following YAML sn
|
|||||||
dir/manifestsDirectory
|
dir/manifestsDirectory
|
||||||
strategy: canary
|
strategy: canary
|
||||||
traffic-split-method: smi
|
traffic-split-method: smi
|
||||||
action: reject # substitute promote if you want to promote
|
action: reject # substitute reject if you want to reject
|
||||||
```
|
```
|
||||||
|
|
||||||
### Blue-Green deployment with different route methods
|
### Blue-Green deployment with different route methods
|
||||||
|
|||||||
-13
@@ -35,9 +35,6 @@ 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
|
||||||
@@ -62,16 +59,6 @@ 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
+1984
-2400
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -6,7 +6,6 @@
|
|||||||
"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 ."
|
||||||
},
|
},
|
||||||
@@ -24,8 +23,9 @@
|
|||||||
"@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"
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -6,6 +6,7 @@ import {
|
|||||||
getResources,
|
getResources,
|
||||||
updateManifestFiles
|
updateManifestFiles
|
||||||
} from '../utilities/manifestUpdateUtils'
|
} from '../utilities/manifestUpdateUtils'
|
||||||
|
import {routeBlueGreen} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
||||||
import {
|
import {
|
||||||
annotateAndLabelResources,
|
annotateAndLabelResources,
|
||||||
checkManifestStability,
|
checkManifestStability,
|
||||||
@@ -13,6 +14,7 @@ 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,
|
||||||
@@ -21,7 +23,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')
|
||||||
@@ -34,8 +36,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')
|
||||||
@@ -48,6 +50,15 @@ export async function deploy(
|
|||||||
await checkManifestStability(kubectl, resourceTypes)
|
await checkManifestStability(kubectl, resourceTypes)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
|
if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) {
|
||||||
|
core.startGroup('Routing blue green')
|
||||||
|
const routeStrategy = parseRouteStrategy(
|
||||||
|
core.getInput('route-method', {required: true})
|
||||||
|
)
|
||||||
|
await routeBlueGreen(kubectl, inputManifestFiles, routeStrategy)
|
||||||
|
core.endGroup()
|
||||||
|
}
|
||||||
|
|
||||||
// print ingresses
|
// print ingresses
|
||||||
core.startGroup('Printing ingresses')
|
core.startGroup('Printing ingresses')
|
||||||
const ingressResources: Resource[] = getResources(deployedManifestFiles, [
|
const ingressResources: Resource[] = getResources(deployedManifestFiles, [
|
||||||
@@ -67,7 +78,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,
|
||||||
|
|||||||
+41
-37
@@ -9,26 +9,26 @@ import {
|
|||||||
import * as models from '../types/kubernetesTypes'
|
import * as models from '../types/kubernetesTypes'
|
||||||
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
|
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
|
||||||
import {
|
import {
|
||||||
deleteGreenObjects,
|
BlueGreenManifests,
|
||||||
|
deleteWorkloadsAndServicesWithLabel,
|
||||||
|
deleteWorkloadsWithLabel,
|
||||||
getManifestObjects,
|
getManifestObjects,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
NONE_LABEL_VALUE
|
NONE_LABEL_VALUE
|
||||||
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
||||||
|
import {
|
||||||
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
promoteBlueGreenService,
|
||||||
|
routeBlueGreenService
|
||||||
|
} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
||||||
import {
|
import {
|
||||||
promoteBlueGreenIngress,
|
promoteBlueGreenIngress,
|
||||||
promoteBlueGreenService,
|
routeBlueGreenIngress
|
||||||
promoteBlueGreenSMI
|
} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
||||||
} from '../strategyHelpers/blueGreen/promote'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
routeBlueGreenService,
|
cleanupSMI,
|
||||||
routeBlueGreenIngressUnchanged,
|
promoteBlueGreenSMI,
|
||||||
routeBlueGreenSMI
|
routeBlueGreenSMI
|
||||||
} from '../strategyHelpers/blueGreen/route'
|
} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
||||||
|
|
||||||
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
|
||||||
import {Kubectl, Resource} from '../types/kubectl'
|
import {Kubectl, Resource} from '../types/kubectl'
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import {
|
import {
|
||||||
@@ -97,7 +97,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
|||||||
)
|
)
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.warning(
|
core.warning(
|
||||||
`Exception occurred while deleting canary and baseline workloads: ${ex}`
|
'Exception occurred while deleting canary and baseline workloads: ' +
|
||||||
|
ex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
@@ -113,24 +114,20 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
core.getInput('route-method', {required: true})
|
core.getInput('route-method', {required: true})
|
||||||
)
|
)
|
||||||
|
|
||||||
core.startGroup('Deleting old deployment and making new stable deployment')
|
core.startGroup('Deleting old deployment and making new one')
|
||||||
|
let result
|
||||||
const {deployResult} = await (async () => {
|
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||||
switch (routeStrategy) {
|
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
|
||||||
case RouteStrategy.INGRESS:
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
return await promoteBlueGreenIngress(kubectl, manifestObjects)
|
result = await promoteBlueGreenSMI(kubectl, manifestObjects)
|
||||||
case RouteStrategy.SMI:
|
} else {
|
||||||
return await promoteBlueGreenSMI(kubectl, manifestObjects)
|
result = await promoteBlueGreenService(kubectl, manifestObjects)
|
||||||
default:
|
}
|
||||||
return await promoteBlueGreenService(kubectl, manifestObjects)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
// checking stability of newly created deployments
|
// checking stability of newly created deployments
|
||||||
core.startGroup('Checking manifest stability')
|
core.startGroup('Checking manifest stability')
|
||||||
const deployedManifestFiles = deployResult.manifestFiles
|
const deployedManifestFiles = result.newFilePaths
|
||||||
const resources: Resource[] = getResources(
|
const resources: Resource[] = getResources(
|
||||||
deployedManifestFiles,
|
deployedManifestFiles,
|
||||||
models.DEPLOYMENT_TYPES.concat([
|
models.DEPLOYMENT_TYPES.concat([
|
||||||
@@ -144,18 +141,17 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
'Routing to new deployments and deleting old workloads and services'
|
'Routing to new deployments and deleting old workloads and services'
|
||||||
)
|
)
|
||||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||||
await routeBlueGreenIngressUnchanged(
|
await routeBlueGreenIngress(
|
||||||
kubectl,
|
kubectl,
|
||||||
|
null,
|
||||||
manifestObjects.serviceNameMap,
|
manifestObjects.serviceNameMap,
|
||||||
manifestObjects.ingressEntityList
|
manifestObjects.ingressEntityList
|
||||||
)
|
)
|
||||||
|
await deleteWorkloadsAndServicesWithLabel(
|
||||||
await deleteGreenObjects(
|
|
||||||
kubectl,
|
kubectl,
|
||||||
[].concat(
|
GREEN_LABEL_VALUE,
|
||||||
manifestObjects.deploymentEntityList,
|
manifestObjects.deploymentEntityList,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await routeBlueGreenSMI(
|
await routeBlueGreenSMI(
|
||||||
@@ -163,7 +159,11 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
|
)
|
||||||
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||||
} else {
|
} else {
|
||||||
await routeBlueGreenService(
|
await routeBlueGreenService(
|
||||||
@@ -171,7 +171,11 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
|||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
manifestObjects.serviceEntityList
|
manifestObjects.serviceEntityList
|
||||||
)
|
)
|
||||||
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
|
await deleteWorkloadsWithLabel(
|
||||||
|
kubectl,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
manifestObjects.deploymentEntityList
|
||||||
|
)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-14
@@ -2,13 +2,9 @@ import * as core from '@actions/core'
|
|||||||
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
|
||||||
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
|
||||||
import {Kubectl} from '../types/kubectl'
|
import {Kubectl} from '../types/kubectl'
|
||||||
import {BlueGreenManifests} from '../types/blueGreenTypes'
|
import {rejectBlueGreenService} from '../strategyHelpers/blueGreen/serviceBlueGreenHelper'
|
||||||
import {
|
import {rejectBlueGreenIngress} from '../strategyHelpers/blueGreen/ingressBlueGreenHelper'
|
||||||
rejectBlueGreenIngress,
|
import {rejectBlueGreenSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
|
||||||
rejectBlueGreenService,
|
|
||||||
rejectBlueGreenSMI
|
|
||||||
} from '../strategyHelpers/blueGreen/reject'
|
|
||||||
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper'
|
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import {
|
import {
|
||||||
parseTrafficSplitMethod,
|
parseTrafficSplitMethod,
|
||||||
@@ -59,19 +55,17 @@ 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, manifestObjects)
|
await rejectBlueGreenIngress(kubectl, manifests)
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||||
await rejectBlueGreenSMI(kubectl, manifestObjects)
|
await rejectBlueGreenSMI(kubectl, manifests)
|
||||||
} else {
|
} else {
|
||||||
await rejectBlueGreenService(kubectl, manifestObjects)
|
await rejectBlueGreenService(kubectl, manifests)
|
||||||
}
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {parseAnnotations} from './types/annotations'
|
|
||||||
|
|
||||||
export const inputAnnotations = parseAnnotations(
|
|
||||||
core.getInput('annotations', {required: false})
|
|
||||||
)
|
|
||||||
|
|
||||||
export function getBufferTime(): number {
|
|
||||||
const inputBufferTime = parseInt(
|
|
||||||
core.getInput('version-switch-buffer') || '0'
|
|
||||||
)
|
|
||||||
if (inputBufferTime < 0 || inputBufferTime > 300)
|
|
||||||
throw Error('Version switch buffer must be between 0 and 300 (inclusive)')
|
|
||||||
|
|
||||||
return inputBufferTime
|
|
||||||
}
|
|
||||||
+2
-15
@@ -6,7 +6,6 @@ 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
|
||||||
@@ -27,22 +26,10 @@ 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 isPrivateCluster =
|
const kubectl = new Kubectl(kubectlPath, namespace, true)
|
||||||
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) {
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
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,9 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as yaml from 'js-yaml'
|
import * as yaml from 'js-yaml'
|
||||||
|
|
||||||
import {DeployResult} from '../../types/deployResult'
|
|
||||||
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import {
|
import {
|
||||||
isDeploymentEntity,
|
isDeploymentEntity,
|
||||||
@@ -11,18 +8,19 @@ import {
|
|||||||
isServiceEntity,
|
isServiceEntity,
|
||||||
KubernetesWorkload
|
KubernetesWorkload
|
||||||
} from '../../types/kubernetesTypes'
|
} from '../../types/kubernetesTypes'
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
|
import {routeBlueGreenService} from './serviceBlueGreenHelper'
|
||||||
import {checkForErrors} from '../../utilities/kubectlUtils'
|
import {routeBlueGreenIngress} from './ingressBlueGreenHelper'
|
||||||
|
import {routeBlueGreenSMI} from './smiBlueGreenHelper'
|
||||||
import {
|
import {
|
||||||
UnsetClusterSpecificDetails,
|
UnsetClusterSpecificDetails,
|
||||||
updateObjectLabels,
|
updateObjectLabels,
|
||||||
updateSelectorLabels
|
updateSelectorLabels
|
||||||
} from '../../utilities/manifestUpdateUtils'
|
} from '../../utilities/manifestUpdateUtils'
|
||||||
|
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
|
||||||
|
import {checkForErrors} from '../../utilities/kubectlUtils'
|
||||||
|
import {sleep} from '../../utilities/timeUtils'
|
||||||
|
import {RouteStrategy} from '../../types/routeStrategy'
|
||||||
|
|
||||||
export const GREEN_LABEL_VALUE = 'green'
|
export const GREEN_LABEL_VALUE = 'green'
|
||||||
export const NONE_LABEL_VALUE = 'None'
|
export const NONE_LABEL_VALUE = 'None'
|
||||||
@@ -30,46 +28,140 @@ export const BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color'
|
|||||||
export const GREEN_SUFFIX = '-green'
|
export const GREEN_SUFFIX = '-green'
|
||||||
export const STABLE_SUFFIX = '-stable'
|
export const STABLE_SUFFIX = '-stable'
|
||||||
|
|
||||||
export async function deleteGreenObjects(
|
export interface BlueGreenManifests {
|
||||||
|
serviceEntityList: any[]
|
||||||
|
serviceNameMap: Map<string, string>
|
||||||
|
unroutedServiceEntityList: any[]
|
||||||
|
deploymentEntityList: any[]
|
||||||
|
ingressEntityList: any[]
|
||||||
|
otherObjects: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function routeBlueGreen(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
toDelete: K8sObject[]
|
inputManifestFiles: string[],
|
||||||
): Promise<K8sDeleteObject[]> {
|
routeStrategy: RouteStrategy
|
||||||
// const resourcesToDelete: K8sDeleteObject[] = []
|
) {
|
||||||
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
|
// sleep for buffer time
|
||||||
return {
|
const bufferTime: number = parseInt(
|
||||||
name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX),
|
core.getInput('version-switch-buffer') || '0'
|
||||||
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 deleteObjects(
|
export async function deleteWorkloadsAndServicesWithLabel(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deleteList: K8sDeleteObject[]
|
deleteLabel: string,
|
||||||
|
deploymentEntityList: any[],
|
||||||
|
serviceEntityList: any[]
|
||||||
) {
|
) {
|
||||||
|
// need to delete services and deployments
|
||||||
|
const deletionEntitiesList = deploymentEntityList.concat(serviceEntityList)
|
||||||
|
const resourcesToDelete = []
|
||||||
|
|
||||||
|
deletionEntitiesList.forEach((inputObject) => {
|
||||||
|
const name = inputObject.metadata.name
|
||||||
|
const kind = inputObject.kind
|
||||||
|
|
||||||
|
if (deleteLabel === NONE_LABEL_VALUE) {
|
||||||
|
// delete stable objects
|
||||||
|
const resourceToDelete = {name, kind}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
|
} else {
|
||||||
|
// delete green labels
|
||||||
|
const resourceToDelete = {
|
||||||
|
name: getBlueGreenResourceName(name, GREEN_SUFFIX),
|
||||||
|
kind: kind
|
||||||
|
}
|
||||||
|
resourcesToDelete.push(resourceToDelete)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await deleteObjects(kubectl, resourcesToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) {
|
||||||
// delete services and deployments
|
// delete services and deployments
|
||||||
for (const delObject of deleteList) {
|
for (const delObject of deleteList) {
|
||||||
try {
|
try {
|
||||||
const result = await kubectl.delete([delObject.kind, delObject.name])
|
const result = await kubectl.delete([delObject.kind, delObject.name])
|
||||||
checkForErrors([result])
|
checkForErrors([result])
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
core.debug(`failed to delete object ${delObject.name}: ${ex}`)
|
// Ignore failures of delete if it doesn't exist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// other common functions
|
// other common functions
|
||||||
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
||||||
const deploymentEntityList: K8sObject[] = []
|
const deploymentEntityList = []
|
||||||
const routedServiceEntityList: K8sObject[] = []
|
const routedServiceEntityList = []
|
||||||
const unroutedServiceEntityList: K8sObject[] = []
|
const unroutedServiceEntityList = []
|
||||||
const ingressEntityList: K8sObject[] = []
|
const ingressEntityList = []
|
||||||
const otherEntitiesList: K8sObject[] = []
|
const otherEntitiesList = []
|
||||||
const serviceNameMap = new Map<string, string>()
|
const serviceNameMap = new Map<string, string>()
|
||||||
|
|
||||||
filePaths.forEach((filePath: string) => {
|
filePaths.forEach((filePath: string) => {
|
||||||
@@ -114,41 +206,51 @@ export function isServiceRouted(
|
|||||||
serviceObject: any[],
|
serviceObject: any[],
|
||||||
deploymentEntityList: any[]
|
deploymentEntityList: any[]
|
||||||
): boolean {
|
): boolean {
|
||||||
|
let shouldBeRouted: boolean = false
|
||||||
const serviceSelector: any = getServiceSelector(serviceObject)
|
const serviceSelector: any = getServiceSelector(serviceObject)
|
||||||
|
if (serviceSelector) {
|
||||||
|
if (
|
||||||
|
deploymentEntityList.some((depObject) => {
|
||||||
|
// finding if there is a deployment in the given manifests the service targets
|
||||||
|
const matchLabels: any = getDeploymentMatchLabels(depObject)
|
||||||
|
return (
|
||||||
|
matchLabels &&
|
||||||
|
isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
shouldBeRouted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return shouldBeRouted
|
||||||
serviceSelector &&
|
|
||||||
deploymentEntityList.some((depObject) => {
|
|
||||||
// finding if there is a deployment in the given manifests the service targets
|
|
||||||
const matchLabels: any = getDeploymentMatchLabels(depObject)
|
|
||||||
return (
|
|
||||||
matchLabels &&
|
|
||||||
isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deployWithLabel(
|
export async function createWorkloadsWithLabel(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
deploymentObjectList: any[],
|
deploymentObjectList: any[],
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
): Promise<BlueGreenDeployment> {
|
) {
|
||||||
const newObjectsList = deploymentObjectList.map((inputObject) =>
|
const newObjectsList = []
|
||||||
getNewBlueGreenObject(inputObject, nextLabel)
|
deploymentObjectList.forEach((inputObject) => {
|
||||||
)
|
// creating deployment with label
|
||||||
|
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel)
|
||||||
|
core.debug(
|
||||||
|
'New blue-green object is: ' + JSON.stringify(newBlueGreenObject)
|
||||||
|
)
|
||||||
|
newObjectsList.push(newBlueGreenObject)
|
||||||
|
})
|
||||||
|
|
||||||
core.debug(
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
|
const result = await kubectl.apply(manifestFiles)
|
||||||
)
|
|
||||||
const deployResult = await deployObjects(kubectl, newObjectsList)
|
return {result: result, newFilePaths: manifestFiles}
|
||||||
return {deployResult, objects: newObjectsList}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNewBlueGreenObject(
|
export function getNewBlueGreenObject(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): K8sObject {
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
|
||||||
// Updating name only if label is green label is given
|
// Updating name only if label is green label is given
|
||||||
@@ -176,7 +278,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 not a service
|
// updating spec labels if it is a service
|
||||||
if (!isServiceEntity(inputObject.kind)) {
|
if (!isServiceEntity(inputObject.kind)) {
|
||||||
updateSpecLabels(inputObject, newLabels, false)
|
updateSpecLabels(inputObject, newLabels, false)
|
||||||
}
|
}
|
||||||
@@ -235,14 +337,14 @@ export async function fetchResource(
|
|||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
kind: string,
|
kind: string,
|
||||||
name: string
|
name: string
|
||||||
): Promise<K8sObject> {
|
) {
|
||||||
const result = await kubectl.getResource(kind, name)
|
const result = await kubectl.getResource(kind, name)
|
||||||
if (result == null || !!result.stderr) {
|
if (result == null || !!result.stderr) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!result.stdout) {
|
if (!!result.stdout) {
|
||||||
const resource = JSON.parse(result.stdout) as K8sObject
|
const resource = JSON.parse(result.stdout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UnsetClusterSpecificDetails(resource)
|
UnsetClusterSpecificDetails(resource)
|
||||||
@@ -254,13 +356,3 @@ export async function fetchResource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deployObjects(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
objectsList: any[]
|
|
||||||
): Promise<DeployResult> {
|
|
||||||
const manifestFiles = fileHelper.writeObjectsToFile(objectsList)
|
|
||||||
const execResult = await kubectl.apply(manifestFiles)
|
|
||||||
|
|
||||||
return {execResult, manifestFiles}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {deployBlueGreen, deployBlueGreenIngress} from './deploy'
|
|
||||||
import * as routeTester from './route'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
describe('deploy tests', () => {
|
|
||||||
let testObjects
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly determines deploy type and acts accordingly', async () => {
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
const mockBgDeployment: BlueGreenDeployment = {
|
|
||||||
deployResult: {
|
|
||||||
execResult: {exitCode: 0, stderr: '', stdout: ''},
|
|
||||||
manifestFiles: []
|
|
||||||
},
|
|
||||||
objects: []
|
|
||||||
}
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(routeTester, 'routeBlueGreenForDeploy')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockBgDeployment))
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
|
|
||||||
const ingressResult = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.INGRESS
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(ingressResult.objects.length).toBe(2)
|
|
||||||
|
|
||||||
const result = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SERVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.objects.length).toBe(2)
|
|
||||||
|
|
||||||
const smiResult = await deployBlueGreen(
|
|
||||||
kubectl,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SMI
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(smiResult.objects.length).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly deploys blue/green ingress', async () => {
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
const value = await deployBlueGreenIngress(kc, ingressFilepath)
|
|
||||||
const nol = value.objects.map((obj) => {
|
|
||||||
if (obj.kind === 'Service') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-service-green')
|
|
||||||
}
|
|
||||||
if (obj.kind === 'Deployment') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-deployment-green')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
|
|
||||||
import {
|
|
||||||
deployWithLabel,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
deployObjects
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import {setupSMI} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
import {routeBlueGreenForDeploy} from './route'
|
|
||||||
|
|
||||||
export async function deployBlueGreen(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
files: string[],
|
|
||||||
routeStrategy: RouteStrategy
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const blueGreenDeployment = await (async () => {
|
|
||||||
switch (routeStrategy) {
|
|
||||||
case RouteStrategy.INGRESS:
|
|
||||||
return await deployBlueGreenIngress(kubectl, files)
|
|
||||||
case RouteStrategy.SMI:
|
|
||||||
return await deployBlueGreenSMI(kubectl, files)
|
|
||||||
default:
|
|
||||||
return await deployBlueGreenService(kubectl, files)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
core.startGroup('Routing blue green')
|
|
||||||
await routeBlueGreenForDeploy(kubectl, files, routeStrategy)
|
|
||||||
core.endGroup()
|
|
||||||
|
|
||||||
return blueGreenDeployment
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create services and other objects
|
|
||||||
const newObjectsList = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.serviceEntityList,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
await deployObjects(kubectl, newObjectsList)
|
|
||||||
|
|
||||||
// make extraservices and trafficsplit
|
|
||||||
await setupSMI(kubectl, manifestObjects.serviceEntityList)
|
|
||||||
|
|
||||||
// create new deloyments
|
|
||||||
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
deployResult: blueGreenDeployment.deployResult,
|
|
||||||
objects: [].concat(blueGreenDeployment.objects, newObjectsList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create deployments with green label value
|
|
||||||
const servicesAndDeployments = [].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
const workloadDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
servicesAndDeployments,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
const otherObjects = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
await deployObjects(kubectl, otherObjects)
|
|
||||||
core.debug(
|
|
||||||
`new objects after processing services and other objects: \n
|
|
||||||
${JSON.stringify(servicesAndDeployments)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
deployResult: workloadDeployment.deployResult,
|
|
||||||
objects: [].concat(workloadDeployment.objects, otherObjects)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deployBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
filePaths: string[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
|
||||||
|
|
||||||
// create deployments with green label value
|
|
||||||
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create other non deployment and non service entities
|
|
||||||
const newObjectsList = [].concat(
|
|
||||||
manifestObjects.otherObjects,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.unroutedServiceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
await deployObjects(kubectl, newObjectsList)
|
|
||||||
// returning deployment details to check for rollout stability
|
|
||||||
return {
|
|
||||||
deployResult: blueGreenDeployment.deployResult,
|
|
||||||
objects: [].concat(blueGreenDeployment.objects, newObjectsList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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,37 +1,220 @@
|
|||||||
import * as core from '@actions/core'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import {K8sIngress} from '../../types/k8sObject'
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
addBlueGreenLabelsAndAnnotations,
|
addBlueGreenLabelsAndAnnotations,
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
BLUE_GREEN_VERSION_LABEL,
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
|
deleteWorkloadsAndServicesWithLabel,
|
||||||
|
fetchResource,
|
||||||
|
getManifestObjects,
|
||||||
|
getNewBlueGreenObject,
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
fetchResource
|
NONE_LABEL_VALUE
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
const BACKEND = 'backend'
|
const BACKEND = 'BACKEND'
|
||||||
|
|
||||||
|
export async function deployBlueGreenIngress(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
// get all kubernetes objects defined in manifest files
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// create deployments with green label value
|
||||||
|
const 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
|
||||||
): K8sIngress {
|
): object {
|
||||||
|
if (!type) {
|
||||||
|
return inputObject
|
||||||
|
}
|
||||||
|
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
// add green labels and values
|
// add green labels and values
|
||||||
addBlueGreenLabelsAndAnnotations(newObject, type)
|
addBlueGreenLabelsAndAnnotations(newObject, type)
|
||||||
|
|
||||||
// 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 updateIngressBackendBetaV1(
|
export function updateIngressBackend(
|
||||||
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.toLowerCase() === BACKEND) {
|
if (key.toUpperCase() === 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
|
||||||
@@ -44,77 +227,3 @@ export function updateIngressBackendBetaV1(
|
|||||||
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {
|
|
||||||
promoteBlueGreenIngress,
|
|
||||||
promoteBlueGreenService,
|
|
||||||
promoteBlueGreenSMI
|
|
||||||
} from './promote'
|
|
||||||
import {TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import * as servicesTester from './serviceBlueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper'
|
|
||||||
import * as smiTester from './smiBlueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
|
|
||||||
let testObjects
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
describe('promote tests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green ingress', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Ingress',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const value = await promoteBlueGreenIngress(kubectl, testObjects)
|
|
||||||
|
|
||||||
const objects = value.objects
|
|
||||||
expect(objects).toHaveLength(2)
|
|
||||||
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj.kind === 'Service') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-service')
|
|
||||||
} else if (obj.kind == 'Deployment') {
|
|
||||||
expect(obj.metadata.name).toBe('nginx-deployment')
|
|
||||||
}
|
|
||||||
expect(obj.metadata.labels['k8s.deploy.color']).toBe('None')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fail to promote invalid blue/green ingress', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Ingress',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
promoteBlueGreenIngress(kubectl, testObjects)
|
|
||||||
).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green service', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {selector: mockLabels},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-service-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
let value = await promoteBlueGreenService(kubectl, testObjects)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(1)
|
|
||||||
expect(
|
|
||||||
value.objects[0].metadata.labels[bgHelper.BLUE_GREEN_VERSION_LABEL]
|
|
||||||
).toBe(bgHelper.NONE_LABEL_VALUE)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-deployment')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fail to promote invalid blue/green service', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
jest
|
|
||||||
.spyOn(servicesTester, 'validateServicesState')
|
|
||||||
.mockImplementationOnce(() => Promise.resolve(false))
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
promoteBlueGreenService(kubectl, testObjects)
|
|
||||||
).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green SMI', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
|
|
||||||
const mockTsObject: TrafficSplitObject = {
|
|
||||||
apiVersion: 'v1alpha3',
|
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
|
||||||
metadata: {
|
|
||||||
name: 'nginx-service-trafficsplit',
|
|
||||||
labels: new Map<string, string>(),
|
|
||||||
annotations: new Map<string, string>()
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
service: 'nginx-service',
|
|
||||||
backends: [
|
|
||||||
{
|
|
||||||
service: 'nginx-service-stable',
|
|
||||||
weight: MIN_VAL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'nginx-service-green',
|
|
||||||
weight: MAX_VAL
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsObject))
|
|
||||||
|
|
||||||
const deployResult = await promoteBlueGreenSMI(kubectl, testObjects)
|
|
||||||
|
|
||||||
expect(deployResult.objects).toHaveLength(1)
|
|
||||||
expect(deployResult.objects[0].metadata.name).toBe('nginx-deployment')
|
|
||||||
expect(
|
|
||||||
deployResult.objects[0].metadata.labels[
|
|
||||||
bgHelper.BLUE_GREEN_VERSION_LABEL
|
|
||||||
]
|
|
||||||
).toBe(bgHelper.NONE_LABEL_VALUE)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('promote blue/green SMI with bad trafficsplit', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
|
|
||||||
jest
|
|
||||||
.spyOn(smiTester, 'validateTrafficSplitsState')
|
|
||||||
.mockImplementation(() => Promise.resolve(false))
|
|
||||||
|
|
||||||
expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrowError()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
|
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {validateIngresses} from './ingressBlueGreenHelper'
|
|
||||||
import {validateServicesState} from './serviceBlueGreenHelper'
|
|
||||||
import {validateTrafficSplitsState} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
export async function promoteBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
//checking if anything to promote
|
|
||||||
const {areValid, invalidIngresses} = await validateIngresses(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.ingressEntityList,
|
|
||||||
manifestObjects.serviceNameMap
|
|
||||||
)
|
|
||||||
if (!areValid) {
|
|
||||||
throw new Error(
|
|
||||||
`Ingresses are not in promote state: ${invalidIngresses.toString()}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create stable deployments with new configuration
|
|
||||||
const result: BlueGreenDeployment = await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
[].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
),
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
// create stable services with new configuration
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promoteBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// checking if services are in the right state ie. targeting green deployments
|
|
||||||
if (
|
|
||||||
!(await validateServicesState(kubectl, manifestObjects.serviceEntityList))
|
|
||||||
) {
|
|
||||||
throw new Error('Found services not in promote state')
|
|
||||||
}
|
|
||||||
|
|
||||||
// creating stable deployments with new configurations
|
|
||||||
return await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function promoteBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// checking if there is something to promote
|
|
||||||
if (
|
|
||||||
!(await validateTrafficSplitsState(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
throw Error('Not in promote state SMI')
|
|
||||||
}
|
|
||||||
|
|
||||||
// create stable deployments with new configuration
|
|
||||||
return await deployWithLabel(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import {getManifestObjects} from './blueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {BlueGreenRejectResult} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
import {
|
|
||||||
rejectBlueGreenIngress,
|
|
||||||
rejectBlueGreenService,
|
|
||||||
rejectBlueGreenSMI
|
|
||||||
} from './reject'
|
|
||||||
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
describe('reject tests', () => {
|
|
||||||
let testObjects
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green ingress', async () => {
|
|
||||||
const value = await rejectBlueGreenIngress(kubectl, testObjects)
|
|
||||||
|
|
||||||
const bgDeployment = value.routeResult
|
|
||||||
const deleteResult = value.deleteResult
|
|
||||||
|
|
||||||
expect(deleteResult).toHaveLength(2)
|
|
||||||
for (const obj of deleteResult) {
|
|
||||||
if (obj.kind == 'Service') {
|
|
||||||
expect(obj.name).toBe('nginx-service-green')
|
|
||||||
}
|
|
||||||
if (obj.kind == 'Deployment') {
|
|
||||||
expect(obj.name).toBe('nginx-deployment-green')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(bgDeployment.objects).toHaveLength(1)
|
|
||||||
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green service', async () => {
|
|
||||||
const value = await rejectBlueGreenService(kubectl, testObjects)
|
|
||||||
|
|
||||||
const bgDeployment = value.routeResult
|
|
||||||
const deleteResult = value.deleteResult
|
|
||||||
|
|
||||||
expect(deleteResult).toHaveLength(1)
|
|
||||||
expect(deleteResult[0].name).toBe('nginx-deployment-green')
|
|
||||||
|
|
||||||
expect(bgDeployment.objects).toHaveLength(1)
|
|
||||||
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reject blue/green SMI', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
const rejectResult = await rejectBlueGreenSMI(kubectl, testObjects)
|
|
||||||
expect(rejectResult.deleteResult).toHaveLength(4)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import {K8sDeleteObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests,
|
|
||||||
BlueGreenRejectResult
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper'
|
|
||||||
import {routeBlueGreenSMI} from './route'
|
|
||||||
import {cleanupSMI} from './smiBlueGreenHelper'
|
|
||||||
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route'
|
|
||||||
|
|
||||||
export async function rejectBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// get all kubernetes objects defined in manifest files
|
|
||||||
// route ingress to stables services
|
|
||||||
const routeResult = await routeBlueGreenIngressUnchanged(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceNameMap,
|
|
||||||
manifestObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete green services and deployments
|
|
||||||
const deleteResult = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
[].concat(
|
|
||||||
manifestObjects.deploymentEntityList,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// route to stable objects
|
|
||||||
const routeResult = await routeBlueGreenService(
|
|
||||||
kubectl,
|
|
||||||
NONE_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete new deployments with green suffix
|
|
||||||
const deleteResult = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rejectBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
manifestObjects: BlueGreenManifests
|
|
||||||
): Promise<BlueGreenRejectResult> {
|
|
||||||
// route trafficsplit to stable deployments
|
|
||||||
const routeResult = await routeBlueGreenSMI(
|
|
||||||
kubectl,
|
|
||||||
NONE_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete rejected new bluegreen deployments
|
|
||||||
const deletedObjects = await deleteGreenObjects(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.deploymentEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
// delete trafficsplit and extra services
|
|
||||||
const cleanupResult = await cleanupSMI(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
return {routeResult, deleteResult: [].concat(deletedObjects, cleanupResult)}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import {getBufferTime} from '../../inputUtils'
|
|
||||||
import * as inputUtils from '../../inputUtils'
|
|
||||||
import {BlueGreenManifests} from '../../types/blueGreenTypes'
|
|
||||||
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import {
|
|
||||||
routeBlueGreenIngress,
|
|
||||||
routeBlueGreenService,
|
|
||||||
routeBlueGreenForDeploy
|
|
||||||
} from './route'
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
|
|
||||||
describe('route function tests', () => {
|
|
||||||
let testObjects: BlueGreenManifests
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
jest
|
|
||||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
|
||||||
.mockImplementationOnce(() => [''])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly prepares blue/green ingresses for deployment', async () => {
|
|
||||||
const unroutedIngCopy: K8sIngress = JSON.parse(
|
|
||||||
JSON.stringify(testObjects.ingressEntityList[0])
|
|
||||||
)
|
|
||||||
unroutedIngCopy.metadata.name = 'nginx-ingress-unrouted'
|
|
||||||
unroutedIngCopy.spec.rules[0].http.paths[0].backend.service.name =
|
|
||||||
'fake-service'
|
|
||||||
testObjects.ingressEntityList.push(unroutedIngCopy)
|
|
||||||
const value = await routeBlueGreenIngress(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceNameMap,
|
|
||||||
testObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(2)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
expect(
|
|
||||||
(value.objects[0] as K8sIngress).spec.rules[0].http.paths[0].backend
|
|
||||||
.service.name
|
|
||||||
).toBe('nginx-service-green')
|
|
||||||
|
|
||||||
expect(value.objects[1].metadata.name).toBe('nginx-ingress-unrouted')
|
|
||||||
// unrouted services shouldn't get their service name changed
|
|
||||||
expect(
|
|
||||||
(value.objects[1] as K8sIngress).spec.rules[0].http.paths[0].backend
|
|
||||||
.service.name
|
|
||||||
).toBe('fake-service')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly prepares blue/green services for deployment', async () => {
|
|
||||||
const value = await routeBlueGreenService(
|
|
||||||
kc,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(value.objects).toHaveLength(1)
|
|
||||||
expect(value.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
|
|
||||||
expect(value.objects[0].metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('correctly identifies route pattern and acts accordingly', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve('v1alpha3'))
|
|
||||||
|
|
||||||
const ingressResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.INGRESS
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(ingressResult.objects.length).toBe(1)
|
|
||||||
expect(ingressResult.objects[0].metadata.name).toBe('nginx-ingress')
|
|
||||||
|
|
||||||
const serviceResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SERVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(serviceResult.objects.length).toBe(1)
|
|
||||||
expect(serviceResult.objects[0].metadata.name).toBe('nginx-service')
|
|
||||||
|
|
||||||
const smiResult = await routeBlueGreenForDeploy(
|
|
||||||
kc,
|
|
||||||
ingressFilepath,
|
|
||||||
RouteStrategy.SMI
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(smiResult.objects).toHaveLength(1)
|
|
||||||
expect(smiResult.objects[0].metadata.name).toBe(
|
|
||||||
'nginx-service-trafficsplit'
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
(smiResult.objects as TrafficSplitObject[])[0].spec.backends
|
|
||||||
).toHaveLength(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import {sleep} from '../../utilities/timeUtils'
|
|
||||||
import {RouteStrategy} from '../../types/routeStrategy'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
BlueGreenDeployment,
|
|
||||||
BlueGreenManifests
|
|
||||||
} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
deployObjects
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {
|
|
||||||
getUpdatedBlueGreenIngress,
|
|
||||||
isIngressRouted
|
|
||||||
} from './ingressBlueGreenHelper'
|
|
||||||
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper'
|
|
||||||
import {createTrafficSplitObject} from './smiBlueGreenHelper'
|
|
||||||
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {getBufferTime} from '../../inputUtils'
|
|
||||||
|
|
||||||
export async function routeBlueGreenForDeploy(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
inputManifestFiles: string[],
|
|
||||||
routeStrategy: RouteStrategy
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// sleep for buffer time
|
|
||||||
const bufferTime: number = getBufferTime()
|
|
||||||
const startSleepDate = new Date()
|
|
||||||
core.info(
|
|
||||||
`Starting buffer time of ${bufferTime} minute(s) at ${startSleepDate.toISOString()}`
|
|
||||||
)
|
|
||||||
await sleep(bufferTime * 1000 * 60)
|
|
||||||
const endSleepDate = new Date()
|
|
||||||
core.info(
|
|
||||||
`Stopping buffer time of ${bufferTime} minute(s) at ${endSleepDate.toISOString()}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const manifestObjects: BlueGreenManifests =
|
|
||||||
getManifestObjects(inputManifestFiles)
|
|
||||||
|
|
||||||
// route to new deployments
|
|
||||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
|
||||||
return await routeBlueGreenIngress(
|
|
||||||
kubectl,
|
|
||||||
manifestObjects.serviceNameMap,
|
|
||||||
manifestObjects.ingressEntityList
|
|
||||||
)
|
|
||||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
|
||||||
return await routeBlueGreenSMI(
|
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return await routeBlueGreenService(
|
|
||||||
kubectl,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
manifestObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenIngress(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
serviceNameMap: Map<string, string>,
|
|
||||||
ingressEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// const newObjectsList = []
|
|
||||||
const newObjectsList: K8sObject[] = ingressEntityList.map((obj) => {
|
|
||||||
if (isIngressRouted(obj, serviceNameMap)) {
|
|
||||||
const newBlueGreenIngressObject = getUpdatedBlueGreenIngress(
|
|
||||||
obj,
|
|
||||||
serviceNameMap,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
return newBlueGreenIngressObject
|
|
||||||
} else {
|
|
||||||
core.debug(`unrouted ingress detected ${obj.metadata.name}`)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, newObjectsList)
|
|
||||||
|
|
||||||
return {deployResult, objects: newObjectsList}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenIngressUnchanged(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
serviceNameMap: Map<string, string>,
|
|
||||||
ingressEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const objects = ingressEntityList.filter((ingress) =>
|
|
||||||
isIngressRouted(ingress, serviceNameMap)
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, objects)
|
|
||||||
return {deployResult, objects}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenService(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
nextLabel: string,
|
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
const objects = serviceEntityList.map((serviceObject) =>
|
|
||||||
getUpdatedBlueGreenService(serviceObject, nextLabel)
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, objects)
|
|
||||||
|
|
||||||
return {deployResult, objects}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function routeBlueGreenSMI(
|
|
||||||
kubectl: Kubectl,
|
|
||||||
nextLabel: string,
|
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<BlueGreenDeployment> {
|
|
||||||
// let tsObjects: TrafficSplitObject[] = []
|
|
||||||
|
|
||||||
const tsObjects: TrafficSplitObject[] = await Promise.all(
|
|
||||||
serviceEntityList.map(async (serviceObject) => {
|
|
||||||
const tsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kubectl,
|
|
||||||
serviceObject.metadata.name,
|
|
||||||
nextLabel
|
|
||||||
)
|
|
||||||
|
|
||||||
return tsObject
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployResult = await deployObjects(kubectl, tsObjects)
|
|
||||||
|
|
||||||
return {deployResult, objects: tsObjects}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import {
|
|
||||||
getServiceSpecLabel,
|
|
||||||
getUpdatedBlueGreenService,
|
|
||||||
validateServicesState
|
|
||||||
} from './serviceBlueGreenHelper'
|
|
||||||
|
|
||||||
let testObjects
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
const kubectl = new Kubectl('')
|
|
||||||
|
|
||||||
describe('blue/green service helper tests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getUpdatedBlueGreenService', () => {
|
|
||||||
const newService = getUpdatedBlueGreenService(
|
|
||||||
testObjects.serviceEntityList[0],
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(newService.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(newService.spec.selector[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('validateServicesState', async () => {
|
|
||||||
const mockLabels = new Map<string, string>()
|
|
||||||
mockLabels[BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
|
|
||||||
const mockSelectors = new Map<string, string>()
|
|
||||||
mockSelectors[BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
kind: 'Service',
|
|
||||||
spec: {selector: mockSelectors},
|
|
||||||
metadata: {labels: mockLabels, name: 'nginx-service-green'}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
await validateServicesState(kubectl, testObjects.serviceEntityList)
|
|
||||||
).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getServiceSpecLabel', () => {
|
|
||||||
testObjects.serviceEntityList[0].spec.selector[BLUE_GREEN_VERSION_LABEL] =
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
|
|
||||||
expect(getServiceSpecLabel(testObjects.serviceEntityList[0])).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,106 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {K8sServiceObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
addBlueGreenLabelsAndAnnotations,
|
addBlueGreenLabelsAndAnnotations,
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
BLUE_GREEN_VERSION_LABEL,
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
|
deleteWorkloadsWithLabel,
|
||||||
fetchResource,
|
fetchResource,
|
||||||
GREEN_LABEL_VALUE
|
getManifestObjects,
|
||||||
|
GREEN_LABEL_VALUE,
|
||||||
|
NONE_LABEL_VALUE
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
|
|
||||||
|
export async function deployBlueGreenService(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
filePaths: string[]
|
||||||
|
) {
|
||||||
|
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||||
|
|
||||||
|
// create deployments with green label value
|
||||||
|
const 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
|
||||||
export function getUpdatedBlueGreenService(
|
function getUpdatedBlueGreenService(
|
||||||
inputObject: any,
|
inputObject: any,
|
||||||
labelValue: string
|
labelValue: string
|
||||||
): K8sServiceObject {
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
|
|
||||||
// Adding labels and annotations.
|
// Adding labels and annotations.
|
||||||
@@ -34,16 +122,25 @@ export async function validateServicesState(
|
|||||||
serviceObject.metadata.name
|
serviceObject.metadata.name
|
||||||
)
|
)
|
||||||
|
|
||||||
let isServiceGreen =
|
if (!!existingService) {
|
||||||
!!existingService &&
|
const currentLabel: string = getServiceSpecLabel(existingService)
|
||||||
getServiceSpecLabel(existingService as K8sServiceObject) ==
|
if (currentLabel != GREEN_LABEL_VALUE) {
|
||||||
GREEN_LABEL_VALUE
|
// service should be targeting deployments with green label
|
||||||
areServicesGreen = areServicesGreen && isServiceGreen
|
areServicesGreen = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// service targeting deployment doesn't exist
|
||||||
|
areServicesGreen = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return areServicesGreen
|
return areServicesGreen
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceSpecLabel(inputObject: K8sServiceObject): string {
|
export function getServiceSpecLabel(inputObject: any): string {
|
||||||
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
if (inputObject?.spec?.selector[BLUE_GREEN_VERSION_LABEL]) {
|
||||||
|
return inputObject.spec.selector[BLUE_GREEN_VERSION_LABEL]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {TrafficSplitObject} from '../../types/k8sObject'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
|
||||||
import * as fileHelper from '../../utilities/fileUtils'
|
|
||||||
import * as TSutils from '../../utilities/trafficSplitUtils'
|
|
||||||
|
|
||||||
import {BlueGreenManifests} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
BLUE_GREEN_VERSION_LABEL,
|
|
||||||
getManifestObjects,
|
|
||||||
GREEN_LABEL_VALUE,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
} from './blueGreenHelper'
|
|
||||||
|
|
||||||
import {
|
|
||||||
cleanupSMI,
|
|
||||||
createTrafficSplitObject,
|
|
||||||
getGreenSMIServiceResource,
|
|
||||||
getStableSMIServiceResource,
|
|
||||||
MAX_VAL,
|
|
||||||
MIN_VAL,
|
|
||||||
setupSMI,
|
|
||||||
TRAFFIC_SPLIT_OBJECT,
|
|
||||||
TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX,
|
|
||||||
validateTrafficSplitsState
|
|
||||||
} from './smiBlueGreenHelper'
|
|
||||||
import * as bgHelper from './blueGreenHelper'
|
|
||||||
|
|
||||||
jest.mock('../../types/kubectl')
|
|
||||||
|
|
||||||
const kc = new Kubectl('')
|
|
||||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
|
||||||
const mockTsObject: TrafficSplitObject = {
|
|
||||||
apiVersion: 'v1alpha3',
|
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
|
||||||
metadata: {
|
|
||||||
name: 'nginx-service-trafficsplit',
|
|
||||||
labels: new Map<string, string>(),
|
|
||||||
annotations: new Map<string, string>()
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
service: 'nginx-service',
|
|
||||||
backends: [
|
|
||||||
{
|
|
||||||
service: 'nginx-service-stable',
|
|
||||||
weight: MIN_VAL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
service: 'nginx-service-green',
|
|
||||||
weight: MAX_VAL
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SMI Helper tests', () => {
|
|
||||||
let testObjects: BlueGreenManifests
|
|
||||||
beforeEach(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
Kubectl.mockClear()
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
|
|
||||||
.mockImplementation(() => Promise.resolve(''))
|
|
||||||
|
|
||||||
testObjects = getManifestObjects(ingressFilepath)
|
|
||||||
jest
|
|
||||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
|
||||||
.mockImplementationOnce(() => [''])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('setupSMI tests', async () => {
|
|
||||||
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
|
|
||||||
|
|
||||||
let found = 0
|
|
||||||
for (const obj of smiResults.objects) {
|
|
||||||
if (obj.metadata.name === 'nginx-service-stable') {
|
|
||||||
expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(obj.spec.selector.app).toBe('nginx')
|
|
||||||
found++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.metadata.name === 'nginx-service-green') {
|
|
||||||
expect(obj.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
found++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.metadata.name === 'nginx-service-trafficsplit') {
|
|
||||||
found++
|
|
||||||
// expect stable weight to be max val
|
|
||||||
const casted = obj as TrafficSplitObject
|
|
||||||
expect(casted.spec.backends).toHaveLength(2)
|
|
||||||
for (const be of casted.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(found).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('createTrafficSplitObject tests', async () => {
|
|
||||||
const noneTsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList[0].metadata.name,
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(noneTsObject.metadata.name).toBe('nginx-service-trafficsplit')
|
|
||||||
for (let be of noneTsObject.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const greenTsObject: TrafficSplitObject = await createTrafficSplitObject(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList[0].metadata.name,
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
expect(greenTsObject.metadata.name).toBe('nginx-service-trafficsplit')
|
|
||||||
for (const be of greenTsObject.spec.backends) {
|
|
||||||
if (be.service === 'nginx-service-stable') {
|
|
||||||
expect(be.weight).toBe(MIN_VAL)
|
|
||||||
}
|
|
||||||
if (be.service === 'nginx-service-green') {
|
|
||||||
expect(be.weight).toBe(MAX_VAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('getSMIServiceResource test', () => {
|
|
||||||
const stableResult = getStableSMIServiceResource(
|
|
||||||
testObjects.serviceEntityList[0]
|
|
||||||
)
|
|
||||||
const greenResult = getGreenSMIServiceResource(
|
|
||||||
testObjects.serviceEntityList[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(stableResult.metadata.name).toBe('nginx-service-stable')
|
|
||||||
expect(stableResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
NONE_LABEL_VALUE
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(greenResult.metadata.name).toBe('nginx-service-green')
|
|
||||||
expect(greenResult.metadata.labels[BLUE_GREEN_VERSION_LABEL]).toBe(
|
|
||||||
GREEN_LABEL_VALUE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('validateTrafficSplitsState', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsObject))
|
|
||||||
|
|
||||||
let valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(valResult).toBe(true)
|
|
||||||
|
|
||||||
const mockTsCopy = JSON.parse(JSON.stringify(mockTsObject))
|
|
||||||
mockTsCopy.spec.backends[0].weight = MAX_VAL
|
|
||||||
jest
|
|
||||||
.spyOn(bgHelper, 'fetchResource')
|
|
||||||
.mockImplementation(() => Promise.resolve(mockTsCopy))
|
|
||||||
|
|
||||||
valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
expect(valResult).toBe(false)
|
|
||||||
|
|
||||||
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
|
|
||||||
valResult = await validateTrafficSplitsState(
|
|
||||||
kc,
|
|
||||||
testObjects.serviceEntityList
|
|
||||||
)
|
|
||||||
expect(valResult).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('cleanupSMI test', async () => {
|
|
||||||
const deleteObjects = await cleanupSMI(kc, testObjects.serviceEntityList)
|
|
||||||
expect(deleteObjects).toHaveLength(3)
|
|
||||||
expect(deleteObjects[0].name).toBe('nginx-service-trafficsplit')
|
|
||||||
expect(deleteObjects[1].name).toBe('nginx-service-green')
|
|
||||||
expect(deleteObjects[1].kind).toBe('Service')
|
|
||||||
expect(deleteObjects[2].name).toBe('nginx-service-stable')
|
|
||||||
expect(deleteObjects[2].kind).toBe('Service')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,35 +1,97 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {Kubectl} from '../../types/kubectl'
|
import {Kubectl} from '../../types/kubectl'
|
||||||
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
|
||||||
|
import * as fileHelper from '../../utilities/fileUtils'
|
||||||
import {
|
import {
|
||||||
|
BlueGreenManifests,
|
||||||
|
createWorkloadsWithLabel,
|
||||||
deleteObjects,
|
deleteObjects,
|
||||||
deployObjects,
|
deleteWorkloadsWithLabel,
|
||||||
fetchResource,
|
fetchResource,
|
||||||
getBlueGreenResourceName,
|
getBlueGreenResourceName,
|
||||||
|
getManifestObjects,
|
||||||
getNewBlueGreenObject,
|
getNewBlueGreenObject,
|
||||||
GREEN_LABEL_VALUE,
|
GREEN_LABEL_VALUE,
|
||||||
GREEN_SUFFIX,
|
GREEN_SUFFIX,
|
||||||
NONE_LABEL_VALUE,
|
NONE_LABEL_VALUE,
|
||||||
STABLE_SUFFIX
|
STABLE_SUFFIX
|
||||||
} from './blueGreenHelper'
|
} from './blueGreenHelper'
|
||||||
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
|
|
||||||
import {
|
|
||||||
K8sDeleteObject,
|
|
||||||
K8sObject,
|
|
||||||
TrafficSplitObject
|
|
||||||
} from '../../types/k8sObject'
|
|
||||||
import {DeployResult} from '../../types/deployResult'
|
|
||||||
import {inputAnnotations} from '../../inputUtils'
|
|
||||||
|
|
||||||
export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
|
||||||
export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
|
||||||
export const MIN_VAL = 0
|
const MIN_VAL = 0
|
||||||
export const MAX_VAL = 100
|
const MAX_VAL = 100
|
||||||
|
|
||||||
export async function setupSMI(
|
export async function deployBlueGreenSMI(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
serviceEntityList: any[]
|
filePaths: string[]
|
||||||
): 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 = []
|
||||||
|
|
||||||
@@ -37,66 +99,53 @@ export async function setupSMI(
|
|||||||
// create a trafficsplit for service
|
// create a trafficsplit for service
|
||||||
trafficObjectList.push(serviceObject)
|
trafficObjectList.push(serviceObject)
|
||||||
// set up the services for trafficsplit
|
// set up the services for trafficsplit
|
||||||
const newStableService = getStableSMIServiceResource(serviceObject)
|
const newStableService = getSMIServiceResource(
|
||||||
const newGreenService = getGreenSMIServiceResource(serviceObject)
|
serviceObject,
|
||||||
|
STABLE_SUFFIX
|
||||||
|
)
|
||||||
|
const newGreenService = getSMIServiceResource(serviceObject, GREEN_SUFFIX)
|
||||||
newObjectsList.push(newStableService)
|
newObjectsList.push(newStableService)
|
||||||
newObjectsList.push(newGreenService)
|
newObjectsList.push(newGreenService)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tsObjects: TrafficSplitObject[] = []
|
// create services
|
||||||
|
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||||
|
await kubectl.apply(manifestFiles)
|
||||||
|
|
||||||
// route to stable service
|
// route to stable service
|
||||||
for (const svc of trafficObjectList) {
|
trafficObjectList.forEach((inputObject) => {
|
||||||
const tsObject = await createTrafficSplitObject(
|
createTrafficSplitObject(
|
||||||
kubectl,
|
kubectl,
|
||||||
svc.metadata.name,
|
inputObject.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 = ''
|
||||||
|
|
||||||
export async function createTrafficSplitObject(
|
async function createTrafficSplitObject(
|
||||||
kubectl: Kubectl,
|
kubectl: Kubectl,
|
||||||
name: string,
|
name: string,
|
||||||
nextLabel: string
|
nextLabel: string
|
||||||
): Promise<TrafficSplitObject> {
|
): Promise<any> {
|
||||||
// cache traffic split api version
|
// cache traffic split api version
|
||||||
if (!trafficSplitAPIVersion)
|
if (!trafficSplitAPIVersion)
|
||||||
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
|
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
|
||||||
kubectl
|
kubectl
|
||||||
)
|
)
|
||||||
|
|
||||||
// retrieve annotations for TS object
|
|
||||||
const annotations = inputAnnotations
|
|
||||||
|
|
||||||
// decide weights based on nextlabel
|
// decide weights based on nextlabel
|
||||||
const stableWeight: number =
|
const stableWeight: number =
|
||||||
nextLabel === GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL
|
nextLabel === GREEN_LABEL_VALUE ? MIN_VAL : MAX_VAL
|
||||||
const greenWeight: number =
|
const greenWeight: number =
|
||||||
nextLabel === GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL
|
nextLabel === GREEN_LABEL_VALUE ? MAX_VAL : MIN_VAL
|
||||||
|
|
||||||
const trafficSplitObject: TrafficSplitObject = {
|
const trafficSplitObject = JSON.stringify({
|
||||||
apiVersion: trafficSplitAPIVersion,
|
apiVersion: trafficSplitAPIVersion,
|
||||||
kind: TRAFFIC_SPLIT_OBJECT,
|
kind: 'TrafficSplit',
|
||||||
metadata: {
|
metadata: {
|
||||||
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||||
annotations: annotations,
|
|
||||||
labels: new Map<string, string>()
|
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
service: name,
|
service: name,
|
||||||
@@ -111,24 +160,50 @@ export async function createTrafficSplitObject(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return trafficSplitObject
|
// create traffic split object
|
||||||
}
|
const trafficSplitManifestFile = fileHelper.writeManifestToFile(
|
||||||
|
trafficSplitObject,
|
||||||
export function getStableSMIServiceResource(inputObject: K8sObject): K8sObject {
|
TRAFFIC_SPLIT_OBJECT,
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||||
// adding stable suffix to service name
|
|
||||||
newObject.metadata.name = getBlueGreenResourceName(
|
|
||||||
inputObject.metadata.name,
|
|
||||||
STABLE_SUFFIX
|
|
||||||
)
|
)
|
||||||
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
|
||||||
|
await kubectl.apply(trafficSplitManifestFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGreenSMIServiceResource(inputObject: K8sObject): K8sObject {
|
export function getSMIServiceResource(
|
||||||
|
inputObject: any,
|
||||||
|
suffix: string
|
||||||
|
): object {
|
||||||
const newObject = JSON.parse(JSON.stringify(inputObject))
|
const newObject = JSON.parse(JSON.stringify(inputObject))
|
||||||
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
|
||||||
|
if (suffix === STABLE_SUFFIX) {
|
||||||
|
// adding stable suffix to service name
|
||||||
|
newObject.metadata.name = getBlueGreenResourceName(
|
||||||
|
inputObject.metadata.name,
|
||||||
|
STABLE_SUFFIX
|
||||||
|
)
|
||||||
|
return getNewBlueGreenObject(newObject, NONE_LABEL_VALUE)
|
||||||
|
} else {
|
||||||
|
// green label will be added for these
|
||||||
|
return getNewBlueGreenObject(newObject, GREEN_LABEL_VALUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function routeBlueGreenSMI(
|
||||||
|
kubectl: Kubectl,
|
||||||
|
nextLabel: string,
|
||||||
|
serviceEntityList: any[]
|
||||||
|
) {
|
||||||
|
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(
|
||||||
@@ -144,38 +219,32 @@ export async function validateTrafficSplitsState(
|
|||||||
TRAFFIC_SPLIT_OBJECT,
|
TRAFFIC_SPLIT_OBJECT,
|
||||||
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||||
)
|
)
|
||||||
core.debug(
|
|
||||||
`ts object extracted was ${JSON.stringify(trafficSplitObject)}`
|
|
||||||
)
|
|
||||||
if (!trafficSplitObject) {
|
if (!trafficSplitObject) {
|
||||||
core.debug(`no traffic split exits for ${name}`)
|
// no traffic split exits
|
||||||
trafficSplitsInRightState = false
|
trafficSplitsInRightState = false
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trafficSplitObject = JSON.parse(JSON.stringify(trafficSplitObject))
|
||||||
trafficSplitObject.spec.backends.forEach((element) => {
|
trafficSplitObject.spec.backends.forEach((element) => {
|
||||||
// checking if trafficsplit in right state to deploy
|
// checking if trafficsplit in right state to deploy
|
||||||
if (element.service === getBlueGreenResourceName(name, GREEN_SUFFIX)) {
|
if (element.service === getBlueGreenResourceName(name, GREEN_SUFFIX)) {
|
||||||
trafficSplitsInRightState =
|
if (element.weight != MAX_VAL) trafficSplitsInRightState = false
|
||||||
trafficSplitsInRightState && element.weight == MAX_VAL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
element.service === getBlueGreenResourceName(name, STABLE_SUFFIX)
|
||||||
) {
|
) {
|
||||||
trafficSplitsInRightState =
|
if (element.weight != MIN_VAL) trafficSplitsInRightState = false
|
||||||
trafficSplitsInRightState && element.weight == MIN_VAL
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return trafficSplitsInRightState
|
return trafficSplitsInRightState
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupSMI(
|
export async function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
||||||
kubectl: Kubectl,
|
const deleteList = []
|
||||||
serviceEntityList: any[]
|
|
||||||
): Promise<K8sDeleteObject[]> {
|
|
||||||
const deleteList: K8sDeleteObject[] = []
|
|
||||||
|
|
||||||
serviceEntityList.forEach((serviceObject) => {
|
serviceEntityList.forEach((serviceObject) => {
|
||||||
deleteList.push({
|
deleteList.push({
|
||||||
@@ -205,6 +274,4 @@ export async function cleanupSMI(
|
|||||||
|
|
||||||
// delete all objects
|
// delete all objects
|
||||||
await deleteObjects(kubectl, deleteList)
|
await deleteObjects(kubectl, deleteList)
|
||||||
|
|
||||||
return deleteList
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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'
|
||||||
@@ -302,8 +301,7 @@ 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,19 +10,16 @@ import {Kubectl, Resource} from '../types/kubectl'
|
|||||||
import {deployPodCanary} from './canary/podCanaryHelper'
|
import {deployPodCanary} from './canary/podCanaryHelper'
|
||||||
import {deploySMICanary} from './canary/smiCanaryHelper'
|
import {deploySMICanary} from './canary/smiCanaryHelper'
|
||||||
import {DeploymentConfig} from '../types/deploymentConfig'
|
import {DeploymentConfig} from '../types/deploymentConfig'
|
||||||
import {
|
import {deployBlueGreenService} from './blueGreen/serviceBlueGreenHelper'
|
||||||
deployBlueGreen,
|
import {deployBlueGreenIngress} from './blueGreen/ingressBlueGreenHelper'
|
||||||
deployBlueGreenIngress,
|
import {deployBlueGreenSMI} from './blueGreen/smiBlueGreenHelper'
|
||||||
deployBlueGreenService
|
|
||||||
} from './blueGreen/deploy'
|
|
||||||
import {deployBlueGreenSMI} from './blueGreen/deploy'
|
|
||||||
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
import {DeploymentStrategy} from '../types/deploymentStrategy'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {
|
import {
|
||||||
parseTrafficSplitMethod,
|
parseTrafficSplitMethod,
|
||||||
TrafficSplitMethod
|
TrafficSplitMethod
|
||||||
} from '../types/trafficSplitMethod'
|
} from '../types/trafficSplitMethod'
|
||||||
import {parseRouteStrategy} from '../types/routeStrategy'
|
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
|
||||||
import {ExecOutput} from '@actions/exec'
|
import {ExecOutput} from '@actions/exec'
|
||||||
import {
|
import {
|
||||||
getWorkflowAnnotationKeyLabel,
|
getWorkflowAnnotationKeyLabel,
|
||||||
@@ -61,19 +58,17 @@ export async function deployManifests(
|
|||||||
const routeStrategy = parseRouteStrategy(
|
const routeStrategy = parseRouteStrategy(
|
||||||
core.getInput('route-method', {required: true})
|
core.getInput('route-method', {required: true})
|
||||||
)
|
)
|
||||||
const blueGreenDeployment = await deployBlueGreen(
|
|
||||||
kubectl,
|
const {result, newFilePaths} = await Promise.resolve(
|
||||||
files,
|
(routeStrategy == RouteStrategy.INGRESS &&
|
||||||
routeStrategy
|
deployBlueGreenIngress(kubectl, files)) ||
|
||||||
)
|
(routeStrategy == RouteStrategy.SMI &&
|
||||||
core.debug(
|
deployBlueGreenSMI(kubectl, files)) ||
|
||||||
`objects deployed for ${routeStrategy}: ${JSON.stringify(
|
deployBlueGreenService(kubectl, files)
|
||||||
blueGreenDeployment.objects
|
|
||||||
)} `
|
|
||||||
)
|
)
|
||||||
|
|
||||||
checkForErrors([blueGreenDeployment.deployResult.execResult])
|
checkForErrors([result])
|
||||||
return blueGreenDeployment.deployResult.manifestFiles
|
return newFilePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
case DeploymentStrategy.BASIC: {
|
case DeploymentStrategy.BASIC: {
|
||||||
@@ -147,7 +142,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()
|
const annotationKeyLabel = getWorkflowAnnotationKeyLabel(workflowFilePath)
|
||||||
|
|
||||||
await annotateResources(
|
await annotateResources(
|
||||||
files,
|
files,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export function parseAnnotations(str: string) {
|
|
||||||
if (str == '') {
|
|
||||||
return new Map<string, string>()
|
|
||||||
} else {
|
|
||||||
const annotation = JSON.parse(str)
|
|
||||||
return new Map<string, string>(annotation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {DeployResult} from './deployResult'
|
|
||||||
import {K8sObject, K8sDeleteObject} from './k8sObject'
|
|
||||||
|
|
||||||
export interface BlueGreenDeployment {
|
|
||||||
deployResult: DeployResult
|
|
||||||
objects: K8sObject[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlueGreenManifests {
|
|
||||||
serviceEntityList: K8sObject[]
|
|
||||||
serviceNameMap: Map<string, string>
|
|
||||||
unroutedServiceEntityList: K8sObject[]
|
|
||||||
deploymentEntityList: K8sObject[]
|
|
||||||
ingressEntityList: K8sObject[]
|
|
||||||
otherObjects: K8sObject[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlueGreenRejectResult {
|
|
||||||
deleteResult: K8sDeleteObject[]
|
|
||||||
routeResult: BlueGreenDeployment
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import {ExecOutput} from '@actions/exec'
|
|
||||||
|
|
||||||
export interface DeployResult {
|
|
||||||
execResult: ExecOutput
|
|
||||||
manifestFiles: string[]
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
export interface K8sObject {
|
|
||||||
metadata: {
|
|
||||||
name: string
|
|
||||||
labels: Map<string, string>
|
|
||||||
}
|
|
||||||
kind: string
|
|
||||||
spec: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sServiceObject extends K8sObject {
|
|
||||||
spec: {
|
|
||||||
selector: Map<string, string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sDeleteObject {
|
|
||||||
name: string
|
|
||||||
kind: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface K8sIngress extends K8sObject {
|
|
||||||
spec: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
http: {
|
|
||||||
paths: [
|
|
||||||
{
|
|
||||||
backend: {
|
|
||||||
service: {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrafficSplitObject extends K8sObject {
|
|
||||||
apiVersion: string
|
|
||||||
metadata: {
|
|
||||||
name: string
|
|
||||||
labels: Map<string, string>
|
|
||||||
annotations: Map<string, string>
|
|
||||||
}
|
|
||||||
spec: {
|
|
||||||
service: string
|
|
||||||
backends: TrafficSplitBackend[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrafficSplitBackend {
|
|
||||||
service: string
|
|
||||||
weight: number
|
|
||||||
}
|
|
||||||
+5
-12
@@ -10,25 +10,18 @@ export interface Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Kubectl {
|
export class Kubectl {
|
||||||
protected readonly kubectlPath: string
|
private readonly kubectlPath: string
|
||||||
protected readonly namespace: string
|
private readonly namespace: string
|
||||||
protected readonly ignoreSSLErrors: boolean
|
private 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(
|
||||||
@@ -162,7 +155,7 @@ export class Kubectl {
|
|||||||
return this.execute(['delete', ...args])
|
return this.execute(['delete', ...args])
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async execute(args: string[], silent: boolean = false) {
|
private 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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
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,12 +12,11 @@ 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(7)
|
expect(testSearch).toHaveLength(5)
|
||||||
expectedManifests.forEach((fileName) => {
|
expectedManifests.forEach((fileName) => {
|
||||||
expect(testSearch).toContain(fileName)
|
expect(testSearch).toContain(fileName)
|
||||||
})
|
})
|
||||||
@@ -54,7 +53,7 @@ describe('File utils', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
||||||
).toHaveLength(7)
|
).toHaveLength(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +1,24 @@
|
|||||||
import {cleanLabel} from '../utilities/workflowAnnotationUtils'
|
import {
|
||||||
|
cleanLabel,
|
||||||
|
prefixObjectKeys
|
||||||
|
} from '../utilities/workflowAnnotationUtils'
|
||||||
|
|
||||||
describe('WorkflowAnnotationUtils', () => {
|
describe('WorkflowAnnotationUtils', () => {
|
||||||
|
describe('prefixObjectKeys', () => {
|
||||||
|
it('should prefix an object with a given prefix', () => {
|
||||||
|
const obj = {
|
||||||
|
foo: 'bar',
|
||||||
|
baz: 'qux'
|
||||||
|
}
|
||||||
|
const prefix = 'prefix.'
|
||||||
|
const expected = {
|
||||||
|
'prefix.foo': 'bar',
|
||||||
|
'prefix.baz': 'qux'
|
||||||
|
}
|
||||||
|
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('cleanLabel', () => {
|
describe('cleanLabel', () => {
|
||||||
it('should clean label', () => {
|
it('should clean label', () => {
|
||||||
const alreadyClean = 'alreadyClean'
|
const alreadyClean = 'alreadyClean'
|
||||||
@@ -11,10 +29,5 @@ describe('WorkflowAnnotationUtils', () => {
|
|||||||
)
|
)
|
||||||
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
||||||
})
|
})
|
||||||
it('should remove slashes from label', () => {
|
|
||||||
expect(
|
|
||||||
cleanLabel('Workflow Name / With Slashes / And Spaces')
|
|
||||||
).toEqual('Workflow_Name_-_With_Slashes_-_And_Spaces')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
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,
|
||||||
@@ -24,11 +31,21 @@ export function getWorkflowAnnotations(
|
|||||||
helmChartPaths: deploymentConfig.helmChartFilePaths,
|
helmChartPaths: deploymentConfig.helmChartFilePaths,
|
||||||
provider: 'GitHub'
|
provider: 'GitHub'
|
||||||
}
|
}
|
||||||
return JSON.stringify(annotationObject)
|
const prefixedAnnotationObject = prefixObjectKeys(
|
||||||
|
annotationObject,
|
||||||
|
ANNOTATION_PREFIX
|
||||||
|
)
|
||||||
|
return JSON.stringify(prefixedAnnotationObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkflowAnnotationKeyLabel(): string {
|
export function getWorkflowAnnotationKeyLabel(
|
||||||
return `${ANNOTATION_PREFIX}/k8s-deploy`
|
workflowFilePath: string
|
||||||
|
): string {
|
||||||
|
const hashKey = require('crypto')
|
||||||
|
.createHash('MD5')
|
||||||
|
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
|
||||||
|
.digest('hex')
|
||||||
|
return `githubWorkflow_${hashKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,11 +54,7 @@ export function getWorkflowAnnotationKeyLabel(): string {
|
|||||||
* @returns cleaned label
|
* @returns cleaned label
|
||||||
*/
|
*/
|
||||||
export function cleanLabel(label: string): string {
|
export function cleanLabel(label: string): string {
|
||||||
let removedInvalidChars = label
|
const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '')
|
||||||
.replace(/\s/gi, '_')
|
|
||||||
.replace(/[\/\\\|]/gi, '-')
|
|
||||||
.replace(/[^-A-Za-z0-9_.]/gi, '')
|
|
||||||
|
|
||||||
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
||||||
return regex.exec(removedInvalidChars)[0] || ''
|
return regex.exec(removedInvalidChars)[0] || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: unrouted-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: fake-application
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 80
|
|
||||||
targetPort: 80
|
|
||||||
---
|
|
||||||
kind: TrafficSplit
|
|
||||||
metadata:
|
|
||||||
name: foobar-rollout
|
|
||||||
spec:
|
|
||||||
service: foobar
|
|
||||||
backends:
|
|
||||||
- service: foobar-v1
|
|
||||||
weight: 1000
|
|
||||||
- service: foobar-v2
|
|
||||||
weight: 500
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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