Revert "Resolved issue with Canary deploy (#247)"

This reverts commit d64c20579673b9a856e5f165548698ccd7fe2ca2.
This commit is contained in:
Oliver King 2022-10-14 12:35:51 -04:00 committed by GitHub
parent d64c205796
commit ca634531d7
7 changed files with 96 additions and 151 deletions

View File

@ -92,7 +92,7 @@ Following are the key capabilities of this action:
</tr> </tr>
<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>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 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> <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>
</tr> </tr>
<tr> <tr>
<td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td> <td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td>

View File

@ -41,7 +41,7 @@ inputs:
baseline-and-canary-replicas: baseline-and-canary-replicas:
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)' description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
required: false required: false
default: '' default: 0
percentage: percentage:
description: 'Percentage of traffic redirect to canary deployment' description: 'Percentage of traffic redirect to canary deployment'
required: false required: false

View File

@ -1,7 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as deploy from './deploy'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper' import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper' import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper'
import { import {
getResources, getResources,
updateManifestFiles updateManifestFiles
@ -57,8 +57,6 @@ export async function promote(
async function promoteCanary(kubectl: Kubectl, manifests: string[]) { async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
let includeServices = false let includeServices = false
const manifestFilesForDeployment: string[] = updateManifestFiles(manifests)
const trafficSplitMethod = parseTrafficSplitMethod( const trafficSplitMethod = parseTrafficSplitMethod(
core.getInput('traffic-split-method', {required: true}) core.getInput('traffic-split-method', {required: true})
) )
@ -74,14 +72,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
) )
core.endGroup() core.endGroup()
core.startGroup( core.startGroup('Deploying input manifests with SMI canary strategy')
'Deploying input manifests with SMI canary strategy from promote' await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
)
await SMICanaryDeploymentHelper.deploySMICanary(
manifestFilesForDeployment,
kubectl,
true
)
core.endGroup() core.endGroup()
core.startGroup('Redirecting traffic to stable deployment') core.startGroup('Redirecting traffic to stable deployment')
@ -91,12 +83,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
) )
core.endGroup() core.endGroup()
} else { } else {
core.startGroup('Deploying input manifests from promote') core.startGroup('Deploying input manifests')
await PodCanaryHelper.deployPodCanary( await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
manifestFilesForDeployment,
kubectl,
true
)
core.endGroup() core.endGroup()
} }

View File

@ -2,7 +2,6 @@ import {Kubectl} from '../../types/kubectl'
import * as fs from 'fs' import * as fs from 'fs'
import * as yaml from 'js-yaml' import * as yaml from 'js-yaml'
import * as core from '@actions/core' import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import { import {
isDeploymentEntity, isDeploymentEntity,
isServiceEntity, isServiceEntity,
@ -31,7 +30,7 @@ export async function deleteCanaryDeployment(
includeServices: boolean includeServices: boolean
) { ) {
if (manifestFilePaths == null || manifestFilePaths.length == 0) { if (manifestFilePaths == null || manifestFilePaths.length == 0) {
throw new Error('Manifest files for deleting canary deployment not found') throw new Error('Manifest file not found')
} }
await cleanUpCanary(kubectl, manifestFilePaths, includeServices) await cleanUpCanary(kubectl, manifestFilePaths, includeServices)
@ -55,7 +54,7 @@ export function isResourceMarkedAsStable(inputObject: any): boolean {
export function getStableResource(inputObject: any): object { export function getStableResource(inputObject: any): object {
const replicaCount = specContainsReplicas(inputObject.kind) const replicaCount = specContainsReplicas(inputObject.kind)
? inputObject.spec.replicas ? inputObject.metadata.replicas
: 0 : 0
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE) return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE)
@ -80,12 +79,7 @@ export async function fetchResource(
kind: string, kind: string,
name: string name: string
) { ) {
let result: ExecOutput const result = await kubectl.getResource(kind, name)
try {
result = await kubectl.getResource(kind, name)
} catch (e) {
core.debug(`detected error while fetching resources: ${e}`)
}
if (!result || result?.stderr) { if (!result || result?.stderr) {
return null return null
@ -99,7 +93,7 @@ export async function fetchResource(
return resource return resource
} catch (ex) { } catch (ex) {
core.debug( core.debug(
`Exception occurred while parsing ${resource} in JSON object: ${ex}` `Exception occurred while Parsing ${resource} in JSON object: ${ex}`
) )
} }
} }
@ -117,26 +111,6 @@ export function getStableResourceName(name: string) {
return name + STABLE_SUFFIX 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( function getNewCanaryObject(
inputObject: any, inputObject: any,
replicas: number, replicas: number,

View File

@ -8,13 +8,9 @@ import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity} from '../../types/kubernetesTypes' import {isDeploymentEntity} from '../../types/kubernetesTypes'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils' import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
export async function deployPodCanary( export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
filePaths: string[],
kubectl: Kubectl,
onlyDeployStable: boolean = false
) {
const newObjectsList = [] const newObjectsList = []
const percentage = parseInt(core.getInput('percentage', {required: true})) const percentage = parseInt(core.getInput('percentage'))
if (percentage < 0 || percentage > 100) if (percentage < 0 || percentage > 100)
throw Error('Percentage must be between 0 and 100') throw Error('Percentage must be between 0 and 100')
@ -26,7 +22,7 @@ export async function deployPodCanary(
const name = inputObject.metadata.name const name = inputObject.metadata.name
const kind = inputObject.kind const kind = inputObject.kind
if (!onlyDeployStable && isDeploymentEntity(kind)) { if (isDeploymentEntity(kind)) {
core.debug('Calculating replica count for canary') core.debug('Calculating replica count for canary')
const canaryReplicaCount = calculateReplicaCountForCanary( const canaryReplicaCount = calculateReplicaCountForCanary(
inputObject, inputObject,
@ -34,22 +30,37 @@ export async function deployPodCanary(
) )
core.debug('Replica count is ' + canaryReplicaCount) core.debug('Replica count is ' + canaryReplicaCount)
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource( // Get stable object
inputObject, core.debug('Querying stable object')
canaryReplicaCount
)
newObjectsList.push(newCanaryObject)
// if there's already a stable object, deploy baseline as well
const stableObject = await canaryDeploymentHelper.fetchResource( const stableObject = await canaryDeploymentHelper.fetchResource(
kubectl, kubectl,
kind, kind,
name name
) )
if (stableObject) {
core.debug( if (!stableObject) {
`Stable object found for ${kind} ${name}. Creating baseline objects` core.debug('Stable object not found. Creating canary object')
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
) )
newObjectsList.push(newCanaryObject)
} else {
core.debug(
'Creating canary and baseline objects. Stable object found: ' +
JSON.stringify(stableObject)
)
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
core.debug(
'New canary object: ' + JSON.stringify(newCanaryObject)
)
const newBaselineObject = const newBaselineObject =
canaryDeploymentHelper.getNewBaselineResource( canaryDeploymentHelper.getNewBaselineResource(
stableObject, stableObject,
@ -58,10 +69,12 @@ export async function deployPodCanary(
core.debug( core.debug(
'New baseline object: ' + JSON.stringify(newBaselineObject) 'New baseline object: ' + JSON.stringify(newBaselineObject)
) )
newObjectsList.push(newCanaryObject)
newObjectsList.push(newBaselineObject) newObjectsList.push(newBaselineObject)
} }
} else { } else {
// deploy non deployment entity or regular deployments for promote as they are // update non deployment entity as it is
newObjectsList.push(inputObject) newObjectsList.push(inputObject)
} }
} }
@ -75,10 +88,7 @@ export async function deployPodCanary(
return {result, newFilePaths: manifestFiles} return {result, newFilePaths: manifestFiles}
} }
export function calculateReplicaCountForCanary( function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
inputObject: any,
percentage: number
) {
const inputReplicaCount = getReplicaCount(inputObject) const inputReplicaCount = getReplicaCount(inputObject)
return Math.max(1, Math.round((inputReplicaCount * percentage) / 100)) return Math.round((inputReplicaCount * percentage) / 100)
} }

View File

@ -6,7 +6,6 @@ import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils' import * as fileHelper from '../../utilities/fileUtils'
import * as kubectlUtils from '../../utilities/trafficSplitUtils' import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import * as canaryDeploymentHelper from './canaryHelper' import * as canaryDeploymentHelper from './canaryHelper'
import * as podCanaryHelper from './podCanaryHelper'
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes' import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
import {checkForErrors} from '../../utilities/kubectlUtils' import {checkForErrors} from '../../utilities/kubectlUtils'
import {inputAnnotations} from '../../inputUtils' import {inputAnnotations} from '../../inputUtils'
@ -14,86 +13,67 @@ import {inputAnnotations} from '../../inputUtils'
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout' const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit' const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
export async function deploySMICanary( export async function deploySMICanary(filePaths: string[], kubectl: Kubectl) {
filePaths: string[], const canaryReplicaCount = parseInt(
kubectl: Kubectl, core.getInput('baseline-and-canary-replicas')
onlyDeployStable: boolean = false
) {
const canaryReplicasInput = core.getInput('baseline-and-canary-replicas')
let canaryReplicaCount
let calculateReplicas = true
if (canaryReplicasInput !== '') {
canaryReplicaCount = parseInt(canaryReplicasInput)
calculateReplicas = false
core.debug(
`read replica count ${canaryReplicaCount} from input: ${canaryReplicasInput}`
) )
} if (canaryReplicaCount < 0 || canaryReplicaCount > 100)
if (canaryReplicaCount < 0 && canaryReplicaCount > 100)
throw Error('Baseline-and-canary-replicas must be between 0 and 100') throw Error('Baseline-and-canary-replicas must be between 0 and 100')
const newObjectsList = [] const newObjectsList = []
for await (const filePath of filePaths) { filePaths.forEach((filePath: string) => {
const fileContents = fs.readFileSync(filePath).toString() const fileContents = fs.readFileSync(filePath).toString()
const inputObjects = yaml.safeLoadAll(fileContents) yaml.safeLoadAll(fileContents, (inputObject) => {
for (const inputObject of inputObjects) {
const name = inputObject.metadata.name const name = inputObject.metadata.name
const kind = inputObject.kind const kind = inputObject.kind
if (!onlyDeployStable && isDeploymentEntity(kind)) { if (isDeploymentEntity(kind)) {
if (calculateReplicas) { const stableObject = canaryDeploymentHelper.fetchResource(
// calculate for each object kubectl,
const percentage = parseInt( kind,
core.getInput('percentage', {required: true}) name
) )
canaryReplicaCount =
podCanaryHelper.calculateReplicaCountForCanary(
inputObject,
percentage
)
core.debug(`calculated replica count ${canaryReplicaCount}`)
}
core.debug('Creating canary object') if (!stableObject) {
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource( core.debug(
'Stable object not found. Creating only canary object'
)
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject, inputObject,
canaryReplicaCount canaryReplicaCount
) )
newObjectsList.push(newCanaryObject) newObjectsList.push(newCanaryObject)
} else {
if (
!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)
) {
throw Error(`StableSpecSelectorNotExist : ${name}`)
}
const stableObject = await canaryDeploymentHelper.fetchResource(
kubectl,
kind,
canaryDeploymentHelper.getStableResourceName(name)
)
if (stableObject) {
core.debug( core.debug(
`Stable object found for ${kind} ${name}. Creating baseline objects` 'Stable object found. Creating canary and baseline objects'
)
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
) )
const newBaselineObject = const newBaselineObject =
canaryDeploymentHelper.getBaselineDeploymentFromStableDeployment( canaryDeploymentHelper.getNewBaselineResource(
stableObject, stableObject,
canaryReplicaCount canaryReplicaCount
) )
newObjectsList.push(newCanaryObject)
newObjectsList.push(newBaselineObject) newObjectsList.push(newBaselineObject)
} }
} else if (isDeploymentEntity(kind)) {
core.debug(
`creating stable deployment with ${inputObject.spec.replicas} replicas`
)
const stableDeployment =
canaryDeploymentHelper.getStableResource(inputObject)
newObjectsList.push(stableDeployment)
} else { } else {
// Update non deployment entity or stable deployment as it is // Update non deployment entity as it is
newObjectsList.push(inputObject) newObjectsList.push(inputObject)
} }
} })
} })
core.debug(
`deploying canary objects with SMI: \n ${JSON.stringify(newObjectsList)}`
)
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList) const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true' const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(newFilePaths, forceDeployment) const result = await kubectl.apply(newFilePaths, forceDeployment)
@ -103,7 +83,7 @@ export async function deploySMICanary(
async function createCanaryService(kubectl: Kubectl, filePaths: string[]) { async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
const newObjectsList = [] const newObjectsList = []
const trafficObjectsList: string[] = [] const trafficObjectsList = []
for (const filePath of filePaths) { for (const filePath of filePaths) {
const fileContents = fs.readFileSync(filePath).toString() const fileContents = fs.readFileSync(filePath).toString()
@ -113,7 +93,6 @@ async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
const kind = inputObject.kind const kind = inputObject.kind
if (isServiceEntity(kind)) { if (isServiceEntity(kind)) {
core.debug(`Creating services for ${kind} ${name}`)
const newCanaryServiceObject = const newCanaryServiceObject =
canaryDeploymentHelper.getNewCanaryResource(inputObject) canaryDeploymentHelper.getNewCanaryResource(inputObject)
newObjectsList.push(newCanaryServiceObject) newObjectsList.push(newCanaryServiceObject)
@ -176,7 +155,7 @@ async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
name name
) )
trafficObjectsList.push( trafficObjectsList.push(
await updateTrafficSplitObject(kubectl, name) updateTrafficSplitObject(kubectl, name)
) )
} }
} }
@ -251,7 +230,7 @@ async function updateTrafficSplitObject(
kubectl: Kubectl, kubectl: Kubectl,
serviceName: string serviceName: string
): Promise<string> { ): Promise<string> {
const percentage = parseInt(core.getInput('percentage', {required: true})) const percentage = parseInt(core.getInput('percentage'))
if (percentage < 0 || percentage > 100) if (percentage < 0 || percentage > 100)
throw Error('Percentage must be between 0 and 100') throw Error('Percentage must be between 0 and 100')
@ -262,9 +241,9 @@ async function updateTrafficSplitObject(
core.debug( core.debug(
'Creating the traffic object with canary weight: ' + 'Creating the traffic object with canary weight: ' +
baselineAndCanaryWeight + baselineAndCanaryWeight +
', baseline weight: ' + ',baseling weight: ' +
baselineAndCanaryWeight + baselineAndCanaryWeight +
', stable weight: ' + ',stable: ' +
stableDeploymentWeight stableDeploymentWeight
) )
return await createTrafficSplitManifestFile( return await createTrafficSplitManifestFile(

View File

@ -3,7 +3,6 @@ import {createInlineArray} from '../utilities/arrayUtils'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as toolCache from '@actions/tool-cache' import * as toolCache from '@actions/tool-cache'
import * as io from '@actions/io' import * as io from '@actions/io'
import {exec} from 'child_process'
export interface Resource { export interface Resource {
name: string name: string
@ -143,16 +142,14 @@ export class Kubectl {
public async getResource( public async getResource(
resourceType: string, resourceType: string,
name: string, name: string
silentFailure: boolean = false
): Promise<ExecOutput> { ): Promise<ExecOutput> {
core.debug( return await this.execute([
'fetching resource of type ' + resourceType + ' and name ' + name 'get',
) `${resourceType}/${name}`,
return await this.execute( '-o',
['get', `${resourceType}/${name}`, '-o', 'json'], 'json'
silentFailure ])
)
} }
public executeCommand(command: string, args?: string) { public executeCommand(command: string, args?: string) {
@ -173,10 +170,7 @@ export class Kubectl {
args = args.concat(['--namespace', this.namespace]) args = args.concat(['--namespace', this.namespace])
} }
core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`) core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
return await getExecOutput(this.kubectlPath, args, {silent})
return await getExecOutput(this.kubectlPath, args, {
silent
})
} }
} }