mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-22 03:09:27 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef850829ef | |||
| c78473ff1f | |||
| bd41735c15 | |||
| fe047348a8 | |||
| 531cfdcc3d | |||
| 0b5795551a | |||
| bb0278db72 | |||
| 71e93a71d4 | |||
| 19d66d6bdb | |||
| 72a09f4051 | |||
| a17f35ba63 |
@@ -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
|
||||
@@ -74,6 +74,9 @@ Following are the key capabilities of this action:
|
||||
<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>
|
||||
@@ -93,6 +96,10 @@ Following are the key capabilities of this action:
|
||||
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||
<td>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>
|
||||
@@ -119,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
|
||||
|
||||
+13
@@ -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"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ 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)
|
||||
@@ -34,7 +35,8 @@ export async function deploy(
|
||||
inputManifestFiles,
|
||||
deploymentStrategy,
|
||||
kubectl,
|
||||
trafficSplitMethod
|
||||
trafficSplitMethod,
|
||||
annotations
|
||||
)
|
||||
core.endGroup()
|
||||
core.debug('Deployed manifest files: ' + deployedManifestFiles)
|
||||
|
||||
+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,16 +60,16 @@ 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 DeploymentStrategy.BASIC: {
|
||||
@@ -141,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,
|
||||
@@ -214,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)
|
||||
}
|
||||
}
|
||||
+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