mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-22 03:09:27 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef850829ef | |||
| c78473ff1f | |||
| bd41735c15 | |||
| fe047348a8 | |||
| 531cfdcc3d | |||
| 0b5795551a | |||
| bb0278db72 | |||
| 71e93a71d4 | |||
| 19d66d6bdb | |||
| 72a09f4051 | |||
| a17f35ba63 | |||
| 7b11ddb1d5 | |||
| ecec5912ba |
@@ -0,0 +1,28 @@
|
||||
name: Bug Report
|
||||
description: File a bug report, we will respond to this thread with any questions.
|
||||
title: 'Bug: '
|
||||
labels: ['bug', 'triage']
|
||||
assignees: '@Azure/aks-atlanta'
|
||||
body:
|
||||
- type: input
|
||||
id: What-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Tell us what happened and how is it different form the expected?
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: Runner
|
||||
attributes:
|
||||
label: Runner
|
||||
description: What runner are you using?
|
||||
placeholder: Mention the runner info (self-hosted, operating system)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: Logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Run in debug mode for the most verbose logs. Please feel free to attach a screenshot of the logs
|
||||
render: shell
|
||||
@@ -0,0 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: GitHub Action "aks-set-context" Support
|
||||
url: https://github.com/Azure/aks-set-context
|
||||
security: https://github.com/Azure/aks-set-context/blob/main/SECURITY.md
|
||||
about: Please ask and answer questions here.
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Feature Request
|
||||
description: File a Feature Request form, we will respond to this thread with any questions.
|
||||
title: 'Feature Request: '
|
||||
labels: ['Feature']
|
||||
assignees: '@Azure/aks-atlanta'
|
||||
body:
|
||||
- type: input
|
||||
id: Feature request
|
||||
attributes:
|
||||
label: Feature request
|
||||
description: Provide example functionality and links to relevant docs
|
||||
validations:
|
||||
required: true
|
||||
@@ -43,7 +43,15 @@ Following are the key capabilities of this action:
|
||||
<tr>
|
||||
<td>manifests </br></br>(Required)</td>
|
||||
<td>Path to the manifest files to be used for deployment. These can also be directories containing manifest files, in which case, all manifest files in the referenced directory at every depth will be deployed. Files not ending in .yml or .yaml will be ignored.</td>
|
||||
</tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>strategy </br></br>(Required)</td>
|
||||
<td>Acceptable values: basic/canary/blue-green. <br>
|
||||
Default value: basic
|
||||
<br>Deployment strategy to be used while applying manifest files on the cluster.
|
||||
<br>basic - Template is force applied to all pods when deploying to cluster. NOTE: Can only be used with action == deploy
|
||||
<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>namespace </br></br>(Optional)
|
||||
<td>Namespace within the cluster to deploy to.</td>
|
||||
@@ -62,15 +70,13 @@ Following are the key capabilities of this action:
|
||||
<td>pull-images</br></br>(Optional)</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>strategy </br></br>(Optional)</td>
|
||||
<td>Acceptable values: none/canary/blue-green. <br>
|
||||
Deployment strategy to be used while applying manifest files on the cluster.<br>none - No deployment strategy is used when deploying.<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>traffic-split-method </br></br>(Optional)</td>
|
||||
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>traffic-split-annotations </br></br>(Optional)</td>
|
||||
<td>Annotations in the form of key/value pair to be added to TrafficSplit.</td>
|
||||
<tr>
|
||||
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
|
||||
<td>Used to compute the number of replicas of '-baseline' and '-canary' variants of the workloads found in manifest files. For the specified percentage input, if (percentage * numberOfDesirerdReplicas)/100 is not a round number, the floor of this number is used while creating '-baseline' and '-canary'.<br/><br/>For example, if Deployment hello-world was found in the input manifest file with 'replicas: 4' and if 'strategy: canary' and 'percentage: 25' are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The '-baseline' variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the '-canary' variant is created with the image and tag corresponding to the new changes being deployed</td>
|
||||
@@ -90,6 +96,10 @@ Following are the key capabilities of this action:
|
||||
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||
<td>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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -116,6 +126,26 @@ Following are the key capabilities of this action:
|
||||
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
|
||||
|
||||
```yaml
|
||||
|
||||
+16
-3
@@ -20,9 +20,9 @@ inputs:
|
||||
required: false
|
||||
default: true
|
||||
strategy:
|
||||
description: 'Deployment strategy to be used. Allowed values are none, canary and blue-green'
|
||||
required: false
|
||||
default: 'none'
|
||||
description: 'Deployment strategy to be used. Allowed values are basic, canary and blue-green'
|
||||
required: true
|
||||
default: 'basic'
|
||||
route-method:
|
||||
description: 'Route based on service, ingress or SMI for blue-green strategy'
|
||||
required: false
|
||||
@@ -35,6 +35,9 @@ inputs:
|
||||
description: 'Traffic split method to be used. Allowed values are pod and smi'
|
||||
required: false
|
||||
default: 'pod'
|
||||
traffic-split-annotations:
|
||||
description: 'Annotations in the form of key/value pair to be added to TrafficSplit. Relevant only if deployement strategy is blue-green or canary'
|
||||
required: false
|
||||
baseline-and-canary-replicas:
|
||||
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
|
||||
required: false
|
||||
@@ -59,6 +62,16 @@ inputs:
|
||||
description: 'Annotate the target namespace'
|
||||
required: false
|
||||
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:
|
||||
color: 'green'
|
||||
|
||||
Generated
+2404
-1972
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -4,7 +4,7 @@
|
||||
"author": "Deepak Sattiraju",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc --outDir ./lib --rootDir ./src",
|
||||
"build": "ncc build src/run.ts -o lib",
|
||||
"test": "jest",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check ."
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/js-yaml": "^3.12.7",
|
||||
"@types/node": "^12.20.41",
|
||||
"jest": "^26.0.0",
|
||||
"prettier": "2.7.1",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^26.0.0",
|
||||
"typescript": "3.9.5"
|
||||
}
|
||||
|
||||
+14
-7
@@ -19,14 +19,15 @@ import {parseRouteStrategy} from '../types/routeStrategy'
|
||||
export async function deploy(
|
||||
kubectl: Kubectl,
|
||||
manifestFilePaths: string[],
|
||||
deploymentStrategy: DeploymentStrategy
|
||||
deploymentStrategy: DeploymentStrategy,
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
// update manifests
|
||||
const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths)
|
||||
core.debug('Input manifest files: ' + inputManifestFiles)
|
||||
|
||||
// deploy manifests
|
||||
core.info('Deploying manifests')
|
||||
core.startGroup('Deploying manifests')
|
||||
const trafficSplitMethod = parseTrafficSplitMethod(
|
||||
core.getInput('traffic-split-method', {required: true})
|
||||
)
|
||||
@@ -34,12 +35,14 @@ export async function deploy(
|
||||
inputManifestFiles,
|
||||
deploymentStrategy,
|
||||
kubectl,
|
||||
trafficSplitMethod
|
||||
trafficSplitMethod,
|
||||
annotations
|
||||
)
|
||||
core.endGroup()
|
||||
core.debug('Deployed manifest files: ' + deployedManifestFiles)
|
||||
|
||||
// check manifest stability
|
||||
core.info('Checking manifest stability')
|
||||
core.startGroup('Checking manifest stability')
|
||||
const resourceTypes: Resource[] = getResources(
|
||||
deployedManifestFiles,
|
||||
models.DEPLOYMENT_TYPES.concat([
|
||||
@@ -47,17 +50,19 @@ export async function deploy(
|
||||
])
|
||||
)
|
||||
await checkManifestStability(kubectl, resourceTypes)
|
||||
core.endGroup()
|
||||
|
||||
if (deploymentStrategy == DeploymentStrategy.BLUE_GREEN) {
|
||||
core.info('Routing 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
|
||||
core.info('Printing ingresses')
|
||||
core.startGroup('Printing ingresses')
|
||||
const ingressResources: Resource[] = getResources(deployedManifestFiles, [
|
||||
KubernetesConstants.DiscoveryAndLoadBalancerResource.INGRESS
|
||||
])
|
||||
@@ -67,9 +72,10 @@ export async function deploy(
|
||||
ingressResource.name
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// annotate resources
|
||||
core.info('Annotating resources')
|
||||
core.startGroup('Annotating resources')
|
||||
let allPods
|
||||
try {
|
||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||
@@ -82,4 +88,5 @@ export async function deploy(
|
||||
resourceTypes,
|
||||
allPods
|
||||
)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
+26
-12
@@ -40,14 +40,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
|
||||
export async function promote(
|
||||
kubectl: Kubectl,
|
||||
manifests: string[],
|
||||
deploymentStrategy: DeploymentStrategy
|
||||
deploymentStrategy: DeploymentStrategy,
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
switch (deploymentStrategy) {
|
||||
case DeploymentStrategy.CANARY:
|
||||
await promoteCanary(kubectl, manifests)
|
||||
break
|
||||
case DeploymentStrategy.BLUE_GREEN:
|
||||
await promoteBlueGreen(kubectl, manifests)
|
||||
await promoteBlueGreen(kubectl, manifests, annotations)
|
||||
break
|
||||
default:
|
||||
throw Error('Invalid promote deployment strategy')
|
||||
@@ -65,26 +66,30 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
|
||||
// In case of SMI traffic split strategy when deployment is promoted, first we will redirect traffic to
|
||||
// canary deployment, then update stable deployment and then redirect traffic to stable deployment
|
||||
core.info('Redirecting traffic to canary deployment')
|
||||
core.startGroup('Redirecting traffic to canary deployment')
|
||||
await SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(
|
||||
kubectl,
|
||||
manifests
|
||||
)
|
||||
core.endGroup()
|
||||
|
||||
core.info('Deploying input manifests with SMI canary strategy')
|
||||
core.startGroup('Deploying input manifests with SMI canary strategy')
|
||||
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
|
||||
core.endGroup()
|
||||
|
||||
core.info('Redirecting traffic to stable deployment')
|
||||
core.startGroup('Redirecting traffic to stable deployment')
|
||||
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
|
||||
kubectl,
|
||||
manifests
|
||||
)
|
||||
core.endGroup()
|
||||
} else {
|
||||
core.info('Deploying input manifests')
|
||||
core.startGroup('Deploying input manifests')
|
||||
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
core.info('Deleting canary and baseline workloads')
|
||||
core.startGroup('Deleting canary and baseline workloads')
|
||||
try {
|
||||
await canaryDeploymentHelper.deleteCanaryDeployment(
|
||||
kubectl,
|
||||
@@ -97,9 +102,14 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
ex
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
async function promoteBlueGreen(
|
||||
kubectl: Kubectl,
|
||||
manifests: string[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
// update container images and pull secrets
|
||||
const inputManifestFiles: string[] = updateManifestFiles(manifests)
|
||||
const manifestObjects: BlueGreenManifests =
|
||||
@@ -109,7 +119,7 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
core.getInput('route-method', {required: true})
|
||||
)
|
||||
|
||||
core.info('Deleting old deployment and making new one')
|
||||
core.startGroup('Deleting old deployment and making new one')
|
||||
let result
|
||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
|
||||
@@ -118,9 +128,10 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
} else {
|
||||
result = await promoteBlueGreenService(kubectl, manifestObjects)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// checking stability of newly created deployments
|
||||
core.info('Checking manifest stability')
|
||||
core.startGroup('Checking manifest stability')
|
||||
const deployedManifestFiles = result.newFilePaths
|
||||
const resources: Resource[] = getResources(
|
||||
deployedManifestFiles,
|
||||
@@ -129,8 +140,9 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
])
|
||||
)
|
||||
await KubernetesManifestUtility.checkManifestStability(kubectl, resources)
|
||||
core.endGroup()
|
||||
|
||||
core.info(
|
||||
core.startGroup(
|
||||
'Routing to new deployments and deleting old workloads and services'
|
||||
)
|
||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||
@@ -150,7 +162,8 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
await routeBlueGreenSMI(
|
||||
kubectl,
|
||||
NONE_LABEL_VALUE,
|
||||
manifestObjects.serviceEntityList
|
||||
manifestObjects.serviceEntityList,
|
||||
annotations
|
||||
)
|
||||
await deleteWorkloadsWithLabel(
|
||||
kubectl,
|
||||
@@ -170,4 +183,5 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
manifestObjects.deploymentEntityList
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
+15
-7
@@ -15,14 +15,15 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
|
||||
export async function reject(
|
||||
kubectl: Kubectl,
|
||||
manifests: string[],
|
||||
deploymentStrategy: DeploymentStrategy
|
||||
deploymentStrategy: DeploymentStrategy,
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
switch (deploymentStrategy) {
|
||||
case DeploymentStrategy.CANARY:
|
||||
await rejectCanary(kubectl, manifests)
|
||||
break
|
||||
case DeploymentStrategy.BLUE_GREEN:
|
||||
await rejectBlueGreen(kubectl, manifests)
|
||||
await rejectBlueGreen(kubectl, manifests, annotations)
|
||||
break
|
||||
default:
|
||||
throw 'Invalid delete deployment strategy'
|
||||
@@ -36,24 +37,30 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
core.getInput('traffic-split-method', {required: true})
|
||||
)
|
||||
if (trafficSplitMethod == TrafficSplitMethod.SMI) {
|
||||
core.info('Rejecting deployment with SMI canary strategy')
|
||||
core.startGroup('Rejecting deployment with SMI canary strategy')
|
||||
includeServices = true
|
||||
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
|
||||
kubectl,
|
||||
manifests
|
||||
)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
core.info('Deleting baseline and canary workloads')
|
||||
core.startGroup('Deleting baseline and canary workloads')
|
||||
await canaryDeploymentHelper.deleteCanaryDeployment(
|
||||
kubectl,
|
||||
manifests,
|
||||
includeServices
|
||||
)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
core.info('Rejecting deployment with blue green strategy')
|
||||
async function rejectBlueGreen(
|
||||
kubectl: Kubectl,
|
||||
manifests: string[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
core.startGroup('Rejecting deployment with blue green strategy')
|
||||
|
||||
const routeStrategy = parseRouteStrategy(
|
||||
core.getInput('route-method', {required: true})
|
||||
@@ -61,8 +68,9 @@ async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
if (routeStrategy == RouteStrategy.INGRESS) {
|
||||
await rejectBlueGreenIngress(kubectl, manifests)
|
||||
} else if (routeStrategy == RouteStrategy.SMI) {
|
||||
await rejectBlueGreenSMI(kubectl, manifests)
|
||||
await rejectBlueGreenSMI(kubectl, manifests, annotations)
|
||||
} else {
|
||||
await rejectBlueGreenService(kubectl, manifests)
|
||||
}
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
+22
-5
@@ -6,6 +6,8 @@ import {reject} from './actions/reject'
|
||||
import {Action, parseAction} from './types/action'
|
||||
import {parseDeploymentStrategy} from './types/deploymentStrategy'
|
||||
import {getFilesFromDirectories} from './utilities/fileUtils'
|
||||
import {PrivateKubectl} from './types/privatekubectl'
|
||||
import {parseAnnotations} from './types/annotations'
|
||||
|
||||
export async function run() {
|
||||
// verify kubeconfig is set
|
||||
@@ -18,6 +20,9 @@ export async function run() {
|
||||
const action: Action | undefined = parseAction(
|
||||
core.getInput('action', {required: true})
|
||||
)
|
||||
const annotations = parseAnnotations(
|
||||
core.getInput('annotations', {required: false})
|
||||
)
|
||||
const strategy = parseDeploymentStrategy(core.getInput('strategy'))
|
||||
const manifestsInput = core.getInput('manifests', {required: true})
|
||||
const manifestFilePaths = manifestsInput
|
||||
@@ -26,23 +31,35 @@ export async function run() {
|
||||
.filter((manifest) => manifest.length > 0) // remove any blanks
|
||||
|
||||
const fullManifestFilePaths = getFilesFromDirectories(manifestFilePaths)
|
||||
// create kubectl
|
||||
const kubectlPath = await getKubectlPath()
|
||||
const namespace = core.getInput('namespace') || 'default'
|
||||
const kubectl = new Kubectl(kubectlPath, namespace, true)
|
||||
const isPrivateCluster =
|
||||
core.getInput('private-cluster').toLowerCase() === 'true'
|
||||
const resourceGroup = core.getInput('resource-group') || ''
|
||||
const resourceName = core.getInput('name') || ''
|
||||
|
||||
const kubectl = isPrivateCluster
|
||||
? new PrivateKubectl(
|
||||
kubectlPath,
|
||||
namespace,
|
||||
true,
|
||||
resourceGroup,
|
||||
resourceName
|
||||
)
|
||||
: new Kubectl(kubectlPath, namespace, true)
|
||||
|
||||
// run action
|
||||
switch (action) {
|
||||
case Action.DEPLOY: {
|
||||
await deploy(kubectl, fullManifestFilePaths, strategy)
|
||||
await deploy(kubectl, fullManifestFilePaths, strategy, annotations)
|
||||
break
|
||||
}
|
||||
case Action.PROMOTE: {
|
||||
await promote(kubectl, fullManifestFilePaths, strategy)
|
||||
await promote(kubectl, fullManifestFilePaths, strategy, annotations)
|
||||
break
|
||||
}
|
||||
case Action.REJECT: {
|
||||
await reject(kubectl, fullManifestFilePaths, strategy)
|
||||
await reject(kubectl, fullManifestFilePaths, strategy, annotations)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
createWorkloadsWithLabel,
|
||||
deleteWorkloadsAndServicesWithLabel,
|
||||
getManifestObjects,
|
||||
getNewBlueGreenObject,
|
||||
GREEN_LABEL_VALUE,
|
||||
isServiceRouted,
|
||||
NONE_LABEL_VALUE
|
||||
} from './blueGreenHelper'
|
||||
import * as bgHelper from './blueGreenHelper'
|
||||
import {Kubectl} from '../../types/kubectl'
|
||||
import * as fileHelper from '../../utilities/fileUtils'
|
||||
|
||||
jest.mock('../../types/kubectl')
|
||||
|
||||
describe('bluegreenhelper functions', () => {
|
||||
let testObjects
|
||||
beforeEach(() => {
|
||||
//@ts-ignore
|
||||
Kubectl.mockClear()
|
||||
testObjects = getManifestObjects(['test/unit/manifests/test-ingress.yml'])
|
||||
|
||||
jest
|
||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
||||
.mockImplementationOnce(() => [''])
|
||||
})
|
||||
|
||||
test('it should parse objects correctly from one file', () => {
|
||||
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
|
||||
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
|
||||
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
|
||||
|
||||
expect(
|
||||
testObjects.deploymentEntityList[0].spec.selector.matchLabels.app
|
||||
).toBe('nginx')
|
||||
})
|
||||
|
||||
test('correctly makes new blue green object', () => {
|
||||
const modifiedDeployment = getNewBlueGreenObject(
|
||||
testObjects.deploymentEntityList[0],
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
//@ts-ignore
|
||||
expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green')
|
||||
//@ts-ignore
|
||||
expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe(
|
||||
'green'
|
||||
)
|
||||
|
||||
const modifiedSvc = getNewBlueGreenObject(
|
||||
testObjects.serviceEntityList[0],
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
//@ts-ignore
|
||||
expect(modifiedSvc.metadata.name).toBe('nginx-service-green')
|
||||
//@ts-ignore
|
||||
expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green')
|
||||
})
|
||||
|
||||
test('correctly makes labeled workloads', () => {
|
||||
const kubectl = new Kubectl('')
|
||||
expect(Kubectl).toBeCalledTimes(1)
|
||||
const cwlResult = createWorkloadsWithLabel(
|
||||
kubectl,
|
||||
testObjects.deploymentEntityList,
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
cwlResult.then((value) => {
|
||||
//@ts-ignore
|
||||
expect(value.newFilePaths[0]).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
test('correctly classifies routed services', () => {
|
||||
expect(
|
||||
isServiceRouted(
|
||||
testObjects.serviceEntityList[0],
|
||||
testObjects.deploymentEntityList
|
||||
)
|
||||
).toBe(true)
|
||||
testObjects.serviceEntityList[0].spec.selector.app = 'fakeapp'
|
||||
expect(
|
||||
isServiceRouted(
|
||||
testObjects.serviceEntityList[0],
|
||||
testObjects.deploymentEntityList
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test('correctly deletes services and workloads according to label', () => {
|
||||
const kubectl = new Kubectl('')
|
||||
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
|
||||
|
||||
let objectsToDelete = deleteWorkloadsAndServicesWithLabel(
|
||||
kubectl,
|
||||
NONE_LABEL_VALUE,
|
||||
testObjects.deploymentEntityList,
|
||||
testObjects.serviceEntityList
|
||||
)
|
||||
objectsToDelete.then((value) => {
|
||||
expect(value).toHaveLength(2)
|
||||
expect(value).toContainEqual
|
||||
;({name: 'nginx-service', kind: 'Service'})
|
||||
expect(value).toContainEqual({
|
||||
name: 'nginx-deployment',
|
||||
kind: 'Deployment'
|
||||
})
|
||||
})
|
||||
|
||||
objectsToDelete = deleteWorkloadsAndServicesWithLabel(
|
||||
kubectl,
|
||||
GREEN_LABEL_VALUE,
|
||||
testObjects.deploymentEntityList,
|
||||
testObjects.serviceEntityList
|
||||
)
|
||||
objectsToDelete.then((value) => {
|
||||
expect(value).toHaveLength(2)
|
||||
expect(value).toContainEqual({
|
||||
name: 'nginx-service-green',
|
||||
kind: 'Service'
|
||||
})
|
||||
expect(value).toContainEqual({
|
||||
name: 'nginx-deployment-green',
|
||||
kind: 'Deployment'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -40,7 +40,8 @@ export interface BlueGreenManifests {
|
||||
export async function routeBlueGreen(
|
||||
kubectl: Kubectl,
|
||||
inputManifestFiles: string[],
|
||||
routeStrategy: RouteStrategy
|
||||
routeStrategy: RouteStrategy,
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
// sleep for buffer time
|
||||
const bufferTime: number = parseInt(
|
||||
@@ -74,7 +75,8 @@ export async function routeBlueGreen(
|
||||
await routeBlueGreenSMI(
|
||||
kubectl,
|
||||
GREEN_LABEL_VALUE,
|
||||
manifestObjects.serviceEntityList
|
||||
manifestObjects.serviceEntityList,
|
||||
annotations
|
||||
)
|
||||
} else {
|
||||
await routeBlueGreenService(
|
||||
@@ -110,6 +112,7 @@ export async function deleteWorkloadsWithLabel(
|
||||
})
|
||||
|
||||
await deleteObjects(kubectl, resourcesToDelete)
|
||||
return resourcesToDelete
|
||||
}
|
||||
|
||||
export async function deleteWorkloadsAndServicesWithLabel(
|
||||
@@ -141,6 +144,7 @@ export async function deleteWorkloadsAndServicesWithLabel(
|
||||
})
|
||||
|
||||
await deleteObjects(kubectl, resourcesToDelete)
|
||||
return resourcesToDelete
|
||||
}
|
||||
|
||||
export async function deleteObjects(kubectl: Kubectl, deleteList: any[]) {
|
||||
@@ -235,9 +239,6 @@ export async function createWorkloadsWithLabel(
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -278,7 +279,7 @@ export function addBlueGreenLabelsAndAnnotations(
|
||||
updateObjectLabels(inputObject, newLabels, false)
|
||||
updateSelectorLabels(inputObject, newLabels, false)
|
||||
|
||||
// updating spec labels if it is a service
|
||||
// updating spec labels if it is not a service
|
||||
if (!isServiceEntity(inputObject.kind)) {
|
||||
updateSpecLabels(inputObject, newLabels, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
|
||||
import {
|
||||
deployBlueGreenIngress,
|
||||
getUpdatedBlueGreenIngress,
|
||||
isIngressRouted,
|
||||
routeBlueGreenIngress
|
||||
} from './ingressBlueGreenHelper'
|
||||
import {Kubectl} from '../../types/kubectl'
|
||||
import * as fileHelper from '../../utilities/fileUtils'
|
||||
|
||||
jest.mock('../../types/kubectl')
|
||||
|
||||
describe('ingress blue green helpers', () => {
|
||||
let testObjects
|
||||
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
|
||||
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
|
||||
|
||||
beforeEach(() => {
|
||||
//@ts-ignore
|
||||
Kubectl.mockClear()
|
||||
testObjects = getManifestObjects(ingressFilepath)
|
||||
jest
|
||||
.spyOn(fileHelper, 'writeObjectsToFile')
|
||||
.mockImplementationOnce(() => [''])
|
||||
})
|
||||
|
||||
test('it should correctly classify ingresses', () => {
|
||||
expect(
|
||||
isIngressRouted(
|
||||
testObjects.ingressEntityList[0],
|
||||
testObjects.serviceNameMap
|
||||
)
|
||||
).toBe(true)
|
||||
testObjects.ingressEntityList[0].spec.rules[0].http.paths = {}
|
||||
expect(
|
||||
isIngressRouted(
|
||||
testObjects.ingressEntityList[0],
|
||||
testObjects.serviceNameMap
|
||||
)
|
||||
).toBe(false)
|
||||
|
||||
expect(
|
||||
isIngressRouted(
|
||||
getManifestObjects(betaFilepath).ingressEntityList[0],
|
||||
testObjects.serviceNameMap
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('it should correctly update ingresses', () => {
|
||||
const updatedIng = getUpdatedBlueGreenIngress(
|
||||
testObjects.ingressEntityList[0],
|
||||
testObjects.serviceNameMap,
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
//@ts-ignore
|
||||
expect(updatedIng.metadata.labels['k8s.deploy.color']).toBe('green')
|
||||
//@ts-ignore
|
||||
expect(updatedIng.spec.rules[0].http.paths[0].backend.service.name).toBe(
|
||||
'nginx-service-green'
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly prepares blue/green ingresses for deployment', () => {
|
||||
const kc = new Kubectl('')
|
||||
const generatedObjects = routeBlueGreenIngress(
|
||||
kc,
|
||||
GREEN_LABEL_VALUE,
|
||||
testObjects.serviceNameMap,
|
||||
testObjects.ingressEntityList
|
||||
)
|
||||
generatedObjects.then((value) => {
|
||||
expect(value).toHaveLength(1)
|
||||
//@ts-ignore
|
||||
expect(value[0].metadata.name).toBe('nginx-ingress')
|
||||
})
|
||||
})
|
||||
test('correctly deploys services', () => {
|
||||
const kc = new Kubectl('')
|
||||
const result = deployBlueGreenIngress(kc, ingressFilepath)
|
||||
|
||||
result.then((value) => {
|
||||
const nol = value.newObjectsList
|
||||
//@ts-ignore
|
||||
expect(nol[0].metadata.name).toBe('nginx-service-green')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from './blueGreenHelper'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
const BACKEND = 'BACKEND'
|
||||
const BACKEND = 'backend'
|
||||
|
||||
export async function deployBlueGreenIngress(
|
||||
kubectl: Kubectl,
|
||||
@@ -24,13 +24,12 @@ export async function deployBlueGreenIngress(
|
||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||
|
||||
// create deployments with green label value
|
||||
const result = createWorkloadsWithLabel(
|
||||
const workloadDeployment = await createWorkloadsWithLabel(
|
||||
kubectl,
|
||||
manifestObjects.deploymentEntityList,
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
|
||||
// create new services and other objects
|
||||
let newObjectsList = []
|
||||
manifestObjects.serviceEntityList.forEach((inputObject) => {
|
||||
const newBlueGreenObject = getNewBlueGreenObject(
|
||||
@@ -46,7 +45,12 @@ export async function deployBlueGreenIngress(
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||
await kubectl.apply(manifestFiles)
|
||||
|
||||
return result
|
||||
core.debug(
|
||||
'new objects after processing services and other objects: \n' +
|
||||
JSON.stringify(newObjectsList)
|
||||
)
|
||||
|
||||
return {workloadDeployment, newObjectsList}
|
||||
}
|
||||
|
||||
export async function promoteBlueGreenIngress(
|
||||
@@ -54,14 +58,13 @@ export async function promoteBlueGreenIngress(
|
||||
manifestObjects
|
||||
) {
|
||||
//checking if anything to promote
|
||||
if (
|
||||
!validateIngressesState(
|
||||
kubectl,
|
||||
manifestObjects.ingressEntityList,
|
||||
manifestObjects.serviceNameMap
|
||||
)
|
||||
) {
|
||||
throw 'Ingress not in promote state'
|
||||
const {areValid, invalidIngresses} = validateIngresses(
|
||||
kubectl,
|
||||
manifestObjects.ingressEntityList,
|
||||
manifestObjects.serviceNameMap
|
||||
)
|
||||
if (!areValid) {
|
||||
throw 'Ingresses are not in promote state' + invalidIngresses.toString()
|
||||
}
|
||||
|
||||
// create stable deployments with new configuration
|
||||
@@ -138,17 +141,18 @@ export async function routeBlueGreenIngress(
|
||||
})
|
||||
}
|
||||
|
||||
core.debug('New objects: ' + JSON.stringify(newObjectsList))
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
|
||||
await kubectl.apply(manifestFiles)
|
||||
return newObjectsList
|
||||
}
|
||||
|
||||
export function validateIngressesState(
|
||||
export function validateIngresses(
|
||||
kubectl: Kubectl,
|
||||
ingressEntityList: any[],
|
||||
serviceNameMap: Map<string, string>
|
||||
): boolean {
|
||||
let areIngressesTargetingNewServices: boolean = true
|
||||
): {areValid: boolean; invalidIngresses: string[]} {
|
||||
let areValid: boolean = true
|
||||
const invalidIngresses = []
|
||||
ingressEntityList.forEach(async (inputObject) => {
|
||||
if (isIngressRouted(inputObject, serviceNameMap)) {
|
||||
//querying existing ingress
|
||||
@@ -158,33 +162,32 @@ export function validateIngressesState(
|
||||
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
|
||||
let isValid =
|
||||
!!existingIngress &&
|
||||
existingIngress?.metadata?.labels[BLUE_GREEN_VERSION_LABEL] ===
|
||||
GREEN_LABEL_VALUE
|
||||
if (!isValid) {
|
||||
invalidIngresses.push(inputObject.metadata.name)
|
||||
}
|
||||
// to be valid, ingress should exist and should be green
|
||||
areValid = areValid && isValid
|
||||
}
|
||||
})
|
||||
|
||||
return areIngressesTargetingNewServices
|
||||
return {areValid, invalidIngresses}
|
||||
}
|
||||
|
||||
function isIngressRouted(
|
||||
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) => {
|
||||
if (key === 'serviceName' && serviceNameMap.has(value)) {
|
||||
isIngressRouted = true
|
||||
}
|
||||
isIngressRouted =
|
||||
isIngressRouted || (key === 'service' && value.hasOwnProperty('name'))
|
||||
isIngressRouted =
|
||||
isIngressRouted || (key === 'serviceName' && serviceNameMap.has(value))
|
||||
|
||||
return value
|
||||
})
|
||||
@@ -206,15 +209,18 @@ export function getUpdatedBlueGreenIngress(
|
||||
addBlueGreenLabelsAndAnnotations(newObject, type)
|
||||
|
||||
// update ingress labels
|
||||
if (inputObject.apiVersion === 'networking.k8s.io/v1beta1') {
|
||||
return updateIngressBackendBetaV1(newObject, serviceNameMap)
|
||||
}
|
||||
return updateIngressBackend(newObject, serviceNameMap)
|
||||
}
|
||||
|
||||
export function updateIngressBackend(
|
||||
export function updateIngressBackendBetaV1(
|
||||
inputObject: any,
|
||||
serviceNameMap: Map<string, string>
|
||||
): any {
|
||||
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||
if (key.toUpperCase() === BACKEND) {
|
||||
if (key.toLowerCase() === BACKEND) {
|
||||
const {serviceName} = value
|
||||
if (serviceNameMap.has(serviceName)) {
|
||||
// update service name with corresponding bluegreen name only if service is provied in given manifests
|
||||
@@ -227,3 +233,20 @@ export function updateIngressBackend(
|
||||
|
||||
return inputObject
|
||||
}
|
||||
|
||||
export function updateIngressBackend(
|
||||
inputObject: any,
|
||||
serviceNameMap: Map<string, string>
|
||||
): any {
|
||||
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||
if (
|
||||
key.toLowerCase() === BACKEND &&
|
||||
serviceNameMap.has(value?.service?.name)
|
||||
) {
|
||||
value.service.name = serviceNameMap.get(value.service.name)
|
||||
}
|
||||
return value
|
||||
})
|
||||
|
||||
return inputObject
|
||||
}
|
||||
|
||||
@@ -19,21 +19,21 @@ export async function deployBlueGreenService(
|
||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||
|
||||
// create deployments with green label value
|
||||
const result = await createWorkloadsWithLabel(
|
||||
const workloadDeployment = 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
|
||||
return {workloadDeployment, newObjectsList}
|
||||
}
|
||||
|
||||
export async function promoteBlueGreenService(
|
||||
@@ -76,7 +76,6 @@ export async function rejectBlueGreenService(
|
||||
manifestObjects.deploymentEntityList
|
||||
)
|
||||
}
|
||||
|
||||
export async function routeBlueGreenService(
|
||||
kubectl: Kubectl,
|
||||
nextLabel: string,
|
||||
|
||||
@@ -23,7 +23,8 @@ const MAX_VAL = 100
|
||||
|
||||
export async function deployBlueGreenSMI(
|
||||
kubectl: Kubectl,
|
||||
filePaths: string[]
|
||||
filePaths: string[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||
@@ -37,14 +38,16 @@ export async function deployBlueGreenSMI(
|
||||
await kubectl.apply(manifestFiles)
|
||||
|
||||
// make extraservices and trafficsplit
|
||||
await setupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||
await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations)
|
||||
|
||||
// create new deloyments
|
||||
return await createWorkloadsWithLabel(
|
||||
const workloadDeployment = await createWorkloadsWithLabel(
|
||||
kubectl,
|
||||
manifestObjects.deploymentEntityList,
|
||||
GREEN_LABEL_VALUE
|
||||
)
|
||||
|
||||
return {workloadDeployment, newObjectsList}
|
||||
}
|
||||
|
||||
export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
|
||||
@@ -68,16 +71,18 @@ export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
|
||||
|
||||
export async function rejectBlueGreenSMI(
|
||||
kubectl: Kubectl,
|
||||
filePaths: string[]
|
||||
filePaths: string[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
|
||||
|
||||
// route trafficsplit to stable deploymetns
|
||||
// route trafficsplit to stable deployments
|
||||
await routeBlueGreenSMI(
|
||||
kubectl,
|
||||
NONE_LABEL_VALUE,
|
||||
manifestObjects.serviceEntityList
|
||||
manifestObjects.serviceEntityList,
|
||||
annotations
|
||||
)
|
||||
|
||||
// delete rejected new bluegreen deployments
|
||||
@@ -91,7 +96,11 @@ export async function rejectBlueGreenSMI(
|
||||
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
|
||||
}
|
||||
|
||||
export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
||||
export async function setupSMI(
|
||||
kubectl: Kubectl,
|
||||
serviceEntityList: any[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
const newObjectsList = []
|
||||
const trafficObjectList = []
|
||||
|
||||
@@ -117,7 +126,8 @@ export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
|
||||
createTrafficSplitObject(
|
||||
kubectl,
|
||||
inputObject.metadata.name,
|
||||
NONE_LABEL_VALUE
|
||||
NONE_LABEL_VALUE,
|
||||
annotations
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -127,7 +137,8 @@ let trafficSplitAPIVersion = ''
|
||||
async function createTrafficSplitObject(
|
||||
kubectl: Kubectl,
|
||||
name: string,
|
||||
nextLabel: string
|
||||
nextLabel: string,
|
||||
annotations: {[key: string]: string} = {}
|
||||
): Promise<any> {
|
||||
// cache traffic split api version
|
||||
if (!trafficSplitAPIVersion)
|
||||
@@ -145,7 +156,8 @@ async function createTrafficSplitObject(
|
||||
apiVersion: trafficSplitAPIVersion,
|
||||
kind: 'TrafficSplit',
|
||||
metadata: {
|
||||
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
||||
annotations: annotations
|
||||
},
|
||||
spec: {
|
||||
service: name,
|
||||
@@ -194,14 +206,16 @@ export function getSMIServiceResource(
|
||||
export async function routeBlueGreenSMI(
|
||||
kubectl: Kubectl,
|
||||
nextLabel: string,
|
||||
serviceEntityList: any[]
|
||||
serviceEntityList: any[],
|
||||
annotations: {[key: string]: string} = {}
|
||||
) {
|
||||
for (const serviceObject of serviceEntityList) {
|
||||
// route trafficsplit to given label
|
||||
await createTrafficSplitObject(
|
||||
kubectl,
|
||||
serviceObject.metadata.name,
|
||||
nextLabel
|
||||
nextLabel,
|
||||
annotations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,8 @@ async function getTrafficSplitObject(
|
||||
name: string,
|
||||
stableWeight: number,
|
||||
baselineWeight: number,
|
||||
canaryWeight: number
|
||||
canaryWeight: number,
|
||||
annotations: {[key: string]: string} = {}
|
||||
): Promise<string> {
|
||||
// cached version
|
||||
if (!trafficSplitAPIVersion) {
|
||||
@@ -301,7 +302,8 @@ async function getTrafficSplitObject(
|
||||
apiVersion: trafficSplitAPIVersion,
|
||||
kind: 'TrafficSplit',
|
||||
metadata: {
|
||||
name: getTrafficSplitResourceName(name)
|
||||
name: getTrafficSplitResourceName(name),
|
||||
annotations: annotations
|
||||
},
|
||||
spec: {
|
||||
backends: [
|
||||
|
||||
@@ -23,7 +23,8 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
|
||||
import {ExecOutput} from '@actions/exec'
|
||||
import {
|
||||
getWorkflowAnnotationKeyLabel,
|
||||
getWorkflowAnnotations
|
||||
getWorkflowAnnotations,
|
||||
cleanLabel
|
||||
} from '../utilities/workflowAnnotationUtils'
|
||||
import {
|
||||
annotateChildPods,
|
||||
@@ -40,7 +41,8 @@ export async function deployManifests(
|
||||
files: string[],
|
||||
deploymentStrategy: DeploymentStrategy,
|
||||
kubectl: Kubectl,
|
||||
trafficSplitMethod: TrafficSplitMethod
|
||||
trafficSplitMethod: TrafficSplitMethod,
|
||||
annotations: {[key: string]: string} = {}
|
||||
): Promise<string[]> {
|
||||
switch (deploymentStrategy) {
|
||||
case DeploymentStrategy.CANARY: {
|
||||
@@ -58,22 +60,19 @@ export async function deployManifests(
|
||||
core.getInput('route-method', {required: true})
|
||||
)
|
||||
|
||||
const {result, newFilePaths} = await Promise.resolve(
|
||||
const {workloadDeployment, newObjectsList} = await Promise.resolve(
|
||||
(routeStrategy == RouteStrategy.INGRESS &&
|
||||
deployBlueGreenIngress(kubectl, files)) ||
|
||||
(routeStrategy == RouteStrategy.SMI &&
|
||||
deployBlueGreenSMI(kubectl, files)) ||
|
||||
deployBlueGreenSMI(kubectl, files, annotations)) ||
|
||||
deployBlueGreenService(kubectl, files)
|
||||
)
|
||||
|
||||
checkForErrors([result])
|
||||
return newFilePaths
|
||||
checkForErrors([workloadDeployment.result])
|
||||
return workloadDeployment.newFilePaths
|
||||
}
|
||||
|
||||
case undefined: {
|
||||
core.warning('Deployment strategy is not recognized.')
|
||||
}
|
||||
default: {
|
||||
case DeploymentStrategy.BASIC: {
|
||||
const trafficSplitMethod = parseTrafficSplitMethod(
|
||||
core.getInput('traffic-split-method', {required: true})
|
||||
)
|
||||
@@ -94,6 +93,10 @@ export async function deployManifests(
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error('Deployment strategy is not recognized.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +143,7 @@ export async function annotateAndLabelResources(
|
||||
const workflowFilePath = await getWorkflowFilePath(githubToken)
|
||||
|
||||
const deploymentConfig = await getDeploymentConfig()
|
||||
const annotationKeyLabel = getWorkflowAnnotationKeyLabel(workflowFilePath)
|
||||
const annotationKeyLabel = getWorkflowAnnotationKeyLabel()
|
||||
|
||||
await annotateResources(
|
||||
files,
|
||||
@@ -213,10 +216,10 @@ async function labelResources(
|
||||
label: string
|
||||
) {
|
||||
const labels = [
|
||||
`workflowFriendlyName=${normalizeWorkflowStrLabel(
|
||||
process.env.GITHUB_WORKFLOW
|
||||
`workflowFriendlyName=${cleanLabel(
|
||||
normalizeWorkflowStrLabel(process.env.GITHUB_WORKFLOW)
|
||||
)}`,
|
||||
`workflow=${label}`
|
||||
`workflow=${cleanLabel(label)}`
|
||||
]
|
||||
|
||||
checkForErrors([await kubectl.labelFiles(files, labels)], true)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function parseAnnotations(str: string) {
|
||||
if (str == '') {
|
||||
return {}
|
||||
} else {
|
||||
const annotaion = JSON.parse(str)
|
||||
return new Map(annotaion)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ describe('Deployment strategy type', () => {
|
||||
const vals = <any>Object.values(DeploymentStrategy)
|
||||
expect(vals.includes('canary')).toBe(true)
|
||||
expect(vals.includes('blue-green')).toBe(true)
|
||||
expect(vals.includes('basic')).toBe(true)
|
||||
})
|
||||
|
||||
test('it can parse valid values from a string', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum DeploymentStrategy {
|
||||
BASIC = 'basic',
|
||||
CANARY = 'canary',
|
||||
BLUE_GREEN = 'blue-green'
|
||||
}
|
||||
|
||||
+12
-5
@@ -10,18 +10,25 @@ export interface Resource {
|
||||
}
|
||||
|
||||
export class Kubectl {
|
||||
private readonly kubectlPath: string
|
||||
private readonly namespace: string
|
||||
private readonly ignoreSSLErrors: boolean
|
||||
protected readonly kubectlPath: string
|
||||
protected readonly namespace: string
|
||||
protected readonly ignoreSSLErrors: boolean
|
||||
protected readonly resourceGroup: string
|
||||
protected readonly name: string
|
||||
protected isPrivateCluster: boolean
|
||||
|
||||
constructor(
|
||||
kubectlPath: string,
|
||||
namespace: string = 'default',
|
||||
ignoreSSLErrors: boolean = false
|
||||
ignoreSSLErrors: boolean = false,
|
||||
resourceGroup: string = '',
|
||||
name: string = ''
|
||||
) {
|
||||
this.kubectlPath = kubectlPath
|
||||
this.ignoreSSLErrors = !!ignoreSSLErrors
|
||||
this.namespace = namespace
|
||||
this.resourceGroup = resourceGroup
|
||||
this.name = name
|
||||
}
|
||||
|
||||
public async apply(
|
||||
@@ -155,7 +162,7 @@ export class Kubectl {
|
||||
return this.execute(['delete', ...args])
|
||||
}
|
||||
|
||||
private async execute(args: string[], silent: boolean = false) {
|
||||
protected async execute(args: string[], silent: boolean = false) {
|
||||
if (this.ignoreSSLErrors) {
|
||||
args.push('--insecure-skip-tls-verify')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import {Kubectl} from './kubectl'
|
||||
import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec'
|
||||
import * as core from '@actions/core'
|
||||
import * as os from 'os'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export class PrivateKubectl extends Kubectl {
|
||||
protected async execute(args: string[], silent: boolean = false) {
|
||||
args.unshift('kubectl')
|
||||
let kubectlCmd = args.join(' ')
|
||||
let addFileFlag = false
|
||||
let eo = <ExecOptions>{silent}
|
||||
|
||||
if (this.containsFilenames(kubectlCmd)) {
|
||||
// For private clusters, files will referenced solely by their basename
|
||||
kubectlCmd = this.replaceFilnamesWithBasenames(kubectlCmd)
|
||||
addFileFlag = true
|
||||
}
|
||||
|
||||
const privateClusterArgs = [
|
||||
'aks',
|
||||
'command',
|
||||
'invoke',
|
||||
'--resource-group',
|
||||
this.resourceGroup,
|
||||
'--name',
|
||||
this.name,
|
||||
'--command',
|
||||
kubectlCmd
|
||||
]
|
||||
|
||||
if (addFileFlag) {
|
||||
const filenames = this.extractFilesnames(kubectlCmd).split(' ')
|
||||
|
||||
const tempDirectory =
|
||||
process.env['runner.tempDirectory'] || os.tmpdir() + '/manifests'
|
||||
eo.cwd = tempDirectory
|
||||
privateClusterArgs.push(...['--file', '.'])
|
||||
|
||||
let filenamesArr = filenames[0].split(',')
|
||||
for (let index = 0; index < filenamesArr.length; index++) {
|
||||
const file = filenamesArr[index]
|
||||
|
||||
if (!file) {
|
||||
continue
|
||||
}
|
||||
this.moveFileToTempManifestDir(file)
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`private cluster Kubectl run with invoke command: ${kubectlCmd}`
|
||||
)
|
||||
return await getExecOutput('az', privateClusterArgs, eo)
|
||||
}
|
||||
|
||||
private replaceFilnamesWithBasenames(kubectlCmd: string) {
|
||||
let exFilenames = this.extractFilesnames(kubectlCmd)
|
||||
let filenames = exFilenames.split(' ')
|
||||
let filenamesArr = filenames[0].split(',')
|
||||
|
||||
for (let index = 0; index < filenamesArr.length; index++) {
|
||||
filenamesArr[index] = path.basename(filenamesArr[index])
|
||||
}
|
||||
|
||||
let baseFilenames = filenamesArr.join()
|
||||
|
||||
let result = kubectlCmd.replace(exFilenames, baseFilenames)
|
||||
return result
|
||||
}
|
||||
|
||||
public extractFilesnames(strToParse: string) {
|
||||
let start = strToParse.indexOf('-filename')
|
||||
let offset = 7
|
||||
|
||||
if (start == -1) {
|
||||
start = strToParse.indexOf('-f')
|
||||
|
||||
if (start == -1) {
|
||||
return ''
|
||||
}
|
||||
offset = 0
|
||||
}
|
||||
|
||||
let temp = strToParse.substring(start + offset)
|
||||
let end = temp.indexOf(' -')
|
||||
|
||||
//End could be case where the -f flag was last, or -f is followed by some additonal flag and it's arguments
|
||||
return temp.substring(3, end == -1 ? temp.length : end).trim()
|
||||
}
|
||||
|
||||
private containsFilenames(str: string) {
|
||||
return str.includes('-f ') || str.includes('filename ')
|
||||
}
|
||||
|
||||
private createTempManifestsDirectory() {
|
||||
const manifestsDir = '/tmp/manifests'
|
||||
if (!fs.existsSync('/tmp/manifests')) {
|
||||
fs.mkdirSync('/tmp/manifests', {recursive: true})
|
||||
}
|
||||
}
|
||||
|
||||
private moveFileToTempManifestDir(file: string) {
|
||||
this.createTempManifestsDirectory()
|
||||
if (!fs.existsSync('/tmp/' + file)) {
|
||||
core.debug(
|
||||
'/tmp/' +
|
||||
file +
|
||||
' does not exist, and therefore cannot be moved to the manifest directory'
|
||||
)
|
||||
}
|
||||
|
||||
fs.copyFile('/tmp/' + file, '/tmp/manifests/' + file, function (err) {
|
||||
if (err) {
|
||||
core.debug(
|
||||
'Could not rename ' +
|
||||
'/tmp/' +
|
||||
file +
|
||||
' to ' +
|
||||
'/tmp/manifests/' +
|
||||
file +
|
||||
' ERROR: ' +
|
||||
err
|
||||
)
|
||||
return
|
||||
}
|
||||
core.debug(
|
||||
"Successfully moved file '" +
|
||||
file +
|
||||
"' from /tmp to /tmp/manifest directory"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,12 @@ describe('File utils', () => {
|
||||
'test/unit/manifests/manifest_test_dir/another_layer/deep-service.yaml',
|
||||
'test/unit/manifests/manifest_test_dir/nested-test-service.yaml',
|
||||
'test/unit/manifests/test-ingress.yml',
|
||||
'test/unit/manifests/test-ingress-new.yml',
|
||||
'test/unit/manifests/test-service.yml'
|
||||
]
|
||||
|
||||
// is there a more efficient way to test equality w random order?
|
||||
expect(testSearch).toHaveLength(5)
|
||||
expect(testSearch).toHaveLength(6)
|
||||
expectedManifests.forEach((fileName) => {
|
||||
expect(testSearch).toContain(fileName)
|
||||
})
|
||||
@@ -53,7 +54,7 @@ describe('File utils', () => {
|
||||
|
||||
expect(
|
||||
getFilesFromDirectories([outerPath, fileAtOuter, innerPath])
|
||||
).toHaveLength(5)
|
||||
).toHaveLength(6)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export function writeManifestToFile(
|
||||
}
|
||||
|
||||
function getManifestFileName(kind: string, name: string) {
|
||||
const filePath = `${kind}_${name}_ ${getCurrentTime().toString()}`
|
||||
const filePath = `${kind}_${name}_${getCurrentTime().toString()}`
|
||||
const tempDirectory = getTempDirectory()
|
||||
return path.join(tempDirectory, path.basename(filePath))
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils'
|
||||
import {cleanLabel} from '../utilities/workflowAnnotationUtils'
|
||||
|
||||
describe('WorkflowAnnotationUtils', () => {
|
||||
describe('prefixObjectKeys', () => {
|
||||
it('should prefix an object with a given prefix', () => {
|
||||
const obj = {
|
||||
foo: 'bar',
|
||||
baz: 'qux'
|
||||
}
|
||||
const prefix = 'prefix.'
|
||||
const expected = {
|
||||
'prefix.foo': 'bar',
|
||||
'prefix.baz': 'qux'
|
||||
}
|
||||
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
|
||||
describe('cleanLabel', () => {
|
||||
it('should clean label', () => {
|
||||
const alreadyClean = 'alreadyClean'
|
||||
expect(cleanLabel(alreadyClean)).toEqual(alreadyClean)
|
||||
expect(cleanLabel('.startInvalid')).toEqual('startInvalid')
|
||||
expect(cleanLabel('with%S0ME&invalid#chars')).toEqual(
|
||||
'withS0MEinvalidchars'
|
||||
)
|
||||
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {DeploymentConfig} from '../types/deploymentConfig'
|
||||
|
||||
const ANNOTATION_PREFIX = 'actions.github.com/'
|
||||
|
||||
export function prefixObjectKeys(obj: any, prefix: string): any {
|
||||
return Object.keys(obj).reduce((newObj, key) => {
|
||||
newObj[prefix + key] = obj[key]
|
||||
return newObj
|
||||
}, {})
|
||||
}
|
||||
const ANNOTATION_PREFIX = 'actions.github.com'
|
||||
|
||||
export function getWorkflowAnnotations(
|
||||
lastSuccessRunSha: string,
|
||||
@@ -31,19 +24,20 @@ export function getWorkflowAnnotations(
|
||||
helmChartPaths: deploymentConfig.helmChartFilePaths,
|
||||
provider: 'GitHub'
|
||||
}
|
||||
const prefixedAnnotationObject = prefixObjectKeys(
|
||||
annotationObject,
|
||||
ANNOTATION_PREFIX
|
||||
)
|
||||
return JSON.stringify(prefixedAnnotationObject)
|
||||
return JSON.stringify(annotationObject)
|
||||
}
|
||||
|
||||
export function getWorkflowAnnotationKeyLabel(
|
||||
workflowFilePath: string
|
||||
): string {
|
||||
const hashKey = require('crypto')
|
||||
.createHash('MD5')
|
||||
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
|
||||
.digest('hex')
|
||||
return `githubWorkflow_${hashKey}`
|
||||
export function getWorkflowAnnotationKeyLabel(): string {
|
||||
return `${ANNOTATION_PREFIX}/k8s-deploy`
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans label to match valid kubernetes label specification by removing invalid characters
|
||||
* @param label
|
||||
* @returns cleaned label
|
||||
*/
|
||||
export function cleanLabel(label: string): string {
|
||||
const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '')
|
||||
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
||||
return regex.exec(removedInvalidChars)[0] || ''
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /testpath
|
||||
backend:
|
||||
service:
|
||||
name: nginx-service
|
||||
port:
|
||||
number: 80
|
||||
Reference in New Issue
Block a user