Skip to content

Commit

Permalink
Resolved issue with Canary deploy (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaiveerk authored Oct 14, 2022
1 parent c8f0502 commit d64c205
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 95 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Following are the key capabilities of this action:
</tr>
<tr>
<td>baseline-and-canary-replicas </br></br> (Optional and relevant only if strategy is canary and traffic-split-method is smi)</td>
<td>The number of baseline and canary replicas. Percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action </br></br><code>&nbsp;&nbsp;&nbsp;&nbsp;strategy: canary<br>&nbsp;&nbsp;&nbsp;&nbsp;trafficSplitMethod: smi<br>&nbsp;&nbsp;&nbsp;&nbsp;percentage: 20<br>&nbsp;&nbsp;&nbsp;&nbsp;baselineAndCanaryReplicas: 1</code></br></br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
<td>The number of baseline and canary replicas. Percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action </br></br><code>&nbsp;&nbsp;&nbsp;&nbsp;strategy: canary<br>&nbsp;&nbsp;&nbsp;&nbsp;trafficSplitMethod: smi<br>&nbsp;&nbsp;&nbsp;&nbsp;percentage: 20<br>&nbsp;&nbsp;&nbsp;&nbsp;baselineAndCanaryReplicas: 1</code></br></br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas each, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
</tr>
<tr>
<td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td>
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ inputs:
baseline-and-canary-replicas:
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
required: false
default: 0
default: ''
percentage:
description: 'Percentage of traffic redirect to canary deployment'
required: false
Expand Down
22 changes: 17 additions & 5 deletions src/actions/promote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import * as deploy from './deploy'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper'
import {
getResources,
updateManifestFiles
Expand Down Expand Up @@ -57,6 +57,8 @@ export async function promote(
async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
let includeServices = false

const manifestFilesForDeployment: string[] = updateManifestFiles(manifests)

const trafficSplitMethod = parseTrafficSplitMethod(
core.getInput('traffic-split-method', {required: true})
)
Expand All @@ -72,8 +74,14 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
)
core.endGroup()

core.startGroup('Deploying input manifests with SMI canary strategy')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.startGroup(
'Deploying input manifests with SMI canary strategy from promote'
)
await SMICanaryDeploymentHelper.deploySMICanary(
manifestFilesForDeployment,
kubectl,
true
)
core.endGroup()

core.startGroup('Redirecting traffic to stable deployment')
Expand All @@ -83,8 +91,12 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
)
core.endGroup()
} else {
core.startGroup('Deploying input manifests')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.startGroup('Deploying input manifests from promote')
await PodCanaryHelper.deployPodCanary(
manifestFilesForDeployment,
kubectl,
true
)
core.endGroup()
}

Expand Down
34 changes: 30 additions & 4 deletions src/strategyHelpers/canary/canaryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Kubectl} from '../../types/kubectl'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import {
isDeploymentEntity,
isServiceEntity,
Expand Down Expand Up @@ -30,7 +31,7 @@ export async function deleteCanaryDeployment(
includeServices: boolean
) {
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
throw new Error('Manifest file not found')
throw new Error('Manifest files for deleting canary deployment not found')
}

await cleanUpCanary(kubectl, manifestFilePaths, includeServices)
Expand All @@ -54,7 +55,7 @@ export function isResourceMarkedAsStable(inputObject: any): boolean {

export function getStableResource(inputObject: any): object {
const replicaCount = specContainsReplicas(inputObject.kind)
? inputObject.metadata.replicas
? inputObject.spec.replicas
: 0

return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE)
Expand All @@ -79,7 +80,12 @@ export async function fetchResource(
kind: string,
name: string
) {
const result = await kubectl.getResource(kind, name)
let result: ExecOutput
try {
result = await kubectl.getResource(kind, name)
} catch (e) {
core.debug(`detected error while fetching resources: ${e}`)
}

if (!result || result?.stderr) {
return null
Expand All @@ -93,7 +99,7 @@ export async function fetchResource(
return resource
} catch (ex) {
core.debug(
`Exception occurred while Parsing ${resource} in JSON object: ${ex}`
`Exception occurred while parsing ${resource} in JSON object: ${ex}`
)
}
}
Expand All @@ -111,6 +117,26 @@ export function getStableResourceName(name: string) {
return name + STABLE_SUFFIX
}

export function getBaselineDeploymentFromStableDeployment(
inputObject: any,
replicaCount: number
): object {
// TODO: REFACTOR TO MAKE EVERYTHING TYPE SAFE
const oldName = inputObject.metadata.name
const newName =
oldName.substring(0, oldName.length - STABLE_SUFFIX.length) +
BASELINE_SUFFIX

const newObject = getNewCanaryObject(
inputObject,
replicaCount,
BASELINE_LABEL_VALUE
) as any
newObject.metadata.name = newName

return newObject
}

function getNewCanaryObject(
inputObject: any,
replicas: number,
Expand Down
54 changes: 22 additions & 32 deletions src/strategyHelpers/canary/podCanaryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity} from '../../types/kubernetesTypes'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'

export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
export async function deployPodCanary(
filePaths: string[],
kubectl: Kubectl,
onlyDeployStable: boolean = false
) {
const newObjectsList = []
const percentage = parseInt(core.getInput('percentage'))
const percentage = parseInt(core.getInput('percentage', {required: true}))

if (percentage < 0 || percentage > 100)
throw Error('Percentage must be between 0 and 100')
Expand All @@ -22,45 +26,30 @@ export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
const name = inputObject.metadata.name
const kind = inputObject.kind

if (isDeploymentEntity(kind)) {
if (!onlyDeployStable && isDeploymentEntity(kind)) {
core.debug('Calculating replica count for canary')
const canaryReplicaCount = calculateReplicaCountForCanary(
inputObject,
percentage
)
core.debug('Replica count is ' + canaryReplicaCount)

// Get stable object
core.debug('Querying stable object')
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
newObjectsList.push(newCanaryObject)

// if there's already a stable object, deploy baseline as well
const stableObject = await canaryDeploymentHelper.fetchResource(
kubectl,
kind,
name
)

if (!stableObject) {
core.debug('Stable object not found. Creating canary object')
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
newObjectsList.push(newCanaryObject)
} else {
if (stableObject) {
core.debug(
'Creating canary and baseline objects. Stable object found: ' +
JSON.stringify(stableObject)
`Stable object found for ${kind} ${name}. Creating baseline objects`
)

const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
core.debug(
'New canary object: ' + JSON.stringify(newCanaryObject)
)

const newBaselineObject =
canaryDeploymentHelper.getNewBaselineResource(
stableObject,
Expand All @@ -69,12 +58,10 @@ export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
core.debug(
'New baseline object: ' + JSON.stringify(newBaselineObject)
)

newObjectsList.push(newCanaryObject)
newObjectsList.push(newBaselineObject)
}
} else {
// update non deployment entity as it is
// deploy non deployment entity or regular deployments for promote as they are
newObjectsList.push(inputObject)
}
}
Expand All @@ -88,7 +75,10 @@ export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
return {result, newFilePaths: manifestFiles}
}

function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
export function calculateReplicaCountForCanary(
inputObject: any,
percentage: number
) {
const inputReplicaCount = getReplicaCount(inputObject)
return Math.round((inputReplicaCount * percentage) / 100)
return Math.max(1, Math.round((inputReplicaCount * percentage) / 100))
}
Loading

0 comments on commit d64c205

Please sign in to comment.