diff --git a/Tasks/KubernetesManifestV0/Strings/resources.resjson/en-US/resources.resjson b/Tasks/KubernetesManifestV0/Strings/resources.resjson/en-US/resources.resjson index 633d6905e18b..107b9a836e3f 100644 --- a/Tasks/KubernetesManifestV0/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/KubernetesManifestV0/Strings/resources.resjson/en-US/resources.resjson @@ -11,7 +11,12 @@ "loc.input.help.namespace": "Sets the namespace for the commands by using the –namespace flag. If the namespace is not provided, the commands will run in the default namespace.", "loc.input.label.strategy": "Strategy", "loc.input.help.strategy": "Deployment strategy to be used", + "loc.input.label.trafficSplitMethod": "Traffic split method", + "loc.input.help.trafficSplitMethod": "Traffic split method to be used", "loc.input.label.percentage": "Percentage", + "loc.input.help.percentage": "Percentage of traffic redirect to canary deployment", + "loc.input.label.baselineAndCanaryReplicas": "Baseline and canary replicas", + "loc.input.help.baselineAndCanaryReplicas": "Baseline and canary replicas count", "loc.input.label.manifests": "Manifests", "loc.input.help.manifests": "Manifests to deploy", "loc.input.label.containers": "Containers", @@ -69,7 +74,6 @@ "loc.messages.NullInputObject": "Input object is null.", "loc.messages.ArgumentsInputNotSupplied": "Arguments are not supplied.", "loc.messages.NullInputObjectMetadata": "Input object metadata is null.", - "loc.messages.CanaryDeploymentAlreadyExistErrorMessage": "Canary deployment already exists. Rejecting this deployment.", "loc.messages.InvalidRejectActionDeploymentStrategy": "Reject action works only with strategy: canary", "loc.messages.InvalidPromotetActionDeploymentStrategy": "Promote action works only with strategy: canary", "loc.messages.AllContainersNotInReadyState": "All the containers are not in a ready state.", @@ -78,5 +82,7 @@ "loc.messages.CouldNotDetermineServiceStatus": "Could not determine the service %s status due to the error: %s", "loc.messages.waitForServiceIpAssignment": "Waiting for service %s external IP assignment", "loc.messages.waitForServiceIpAssignmentTimedOut": "Wait for service %s external IP assignment timed out", - "loc.messages.ServiceExternalIP": "service %s external IP is %s" + "loc.messages.ServiceExternalIP": "service %s external IP is %s", + "loc.messages.UnableToCreateTrafficSplitManifestFile": "Unable to create TrafficSplit manifest file.", + "loc.messages.StableSpecSelectorNotExist": "Resource %s not deployed using SMI canary deployment." } \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/Tests/L0.ts b/Tasks/KubernetesManifestV0/Tests/L0.ts index 16a1e04ac015..9093f4a106ea 100644 --- a/Tasks/KubernetesManifestV0/Tests/L0.ts +++ b/Tasks/KubernetesManifestV0/Tests/L0.ts @@ -30,6 +30,8 @@ describe('Kubernetes Manifests Suite', function () { delete process.env[shared.TestEnvVars.namespace]; delete process.env[shared.TestEnvVars.dockerComposeFile]; delete process.env[shared.TestEnvVars.releaseName]; + delete process.env[shared.TestEnvVars.baselineAndCanaryReplicas]; + delete process.env[shared.TestEnvVars.trafficSplitMethod]; delete process.env.RemoveNamespaceFromEndpoint; }); @@ -54,13 +56,13 @@ describe('Kubernetes Manifests Suite', function () { const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); process.env[shared.TestEnvVars.action] = shared.Actions.deploy; process.env[shared.TestEnvVars.strategy] = shared.Strategy.canary; + process.env[shared.TestEnvVars.trafficSplitMethod] = shared.TrafficSplitMethod.pod; process.env[shared.TestEnvVars.percentage] = '30'; process.env[shared.TestEnvVars.isStableDeploymentPresent] = 'true'; process.env[shared.TestEnvVars.isCanaryDeploymentPresent] = 'false'; process.env[shared.TestEnvVars.isBaselineDeploymentPresent] = 'false'; tr.run(); assert(tr.succeeded, 'task should have succeeded'); - assert(tr.stderr.indexOf('"nginx-deployment-canary" not found') != -1, 'Canary deployment is not present'); assert(tr.stdout.indexOf('nginx-deployment-canary created') != -1, 'Canary deployment is created'); assert(tr.stdout.indexOf('nginx-deployment-baseline created') != -1, 'Baseline deployment is created'); assert(tr.stdout.indexOf('deployment "nginx-deployment-canary" successfully rolled out') != -1, 'Canary deployment is successfully rolled out'); @@ -80,7 +82,7 @@ describe('Kubernetes Manifests Suite', function () { process.env[shared.TestEnvVars.isCanaryDeploymentPresent] = 'true'; process.env[shared.TestEnvVars.isBaselineDeploymentPresent] = 'true'; tr.run(); - assert(tr.failed, 'task should have failed'); + assert(tr.succeeded, 'task should have succeeded'); done(); }); diff --git a/Tasks/KubernetesManifestV0/Tests/TestSetup.ts b/Tasks/KubernetesManifestV0/Tests/TestSetup.ts index b9ec77116d0d..97b21ffb64d4 100644 --- a/Tasks/KubernetesManifestV0/Tests/TestSetup.ts +++ b/Tasks/KubernetesManifestV0/Tests/TestSetup.ts @@ -44,6 +44,8 @@ tr.setInput('secretName', process.env[shared.TestEnvVars.secretName] || ''); tr.setInput('secretType', process.env[shared.TestEnvVars.secretType] || ''); tr.setInput('dockerComposeFile', process.env[shared.TestEnvVars.dockerComposeFile] || ''); tr.setInput('kustomizationPath', process.env[shared.TestEnvVars.kustomizationPath] || ''); +tr.setInput('baselineAndCanaryReplicas', process.env[shared.TestEnvVars.baselineAndCanaryReplicas] || '0'); +tr.setInput('trafficSplitMethod', process.env[shared.TestEnvVars.trafficSplitMethod]); process.env.SYSTEM_DEFAULTWORKINGDIRECTORY = testnamespaceWorkingDirectory; process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI = teamFoundationCollectionUri; @@ -340,7 +342,8 @@ tr.registerMock('../utils/FileHelper', { }, getNewUserDirPath: fh.getNewUserDirPath, ensureDirExists: fh.ensureDirExists, - assertFileExists: fh.assertFileExists + assertFileExists: fh.assertFileExists, + writeManifestToFile: fh.writeManifestToFile }); tr.registerMock('uuid/v4', function () { diff --git a/Tasks/KubernetesManifestV0/Tests/TestShared.ts b/Tasks/KubernetesManifestV0/Tests/TestShared.ts index 398cc7f46487..bbcfa44a25a7 100644 --- a/Tasks/KubernetesManifestV0/Tests/TestShared.ts +++ b/Tasks/KubernetesManifestV0/Tests/TestShared.ts @@ -34,7 +34,9 @@ export let TestEnvVars = { endpointAuthorizationType: "__endpointAuthorizationType__", isStableDeploymentPresent: "__isStableDeploymentPresent__", isCanaryDeploymentPresent: "__isCanaryDeploymentPresent__", - isBaselineDeploymentPresent: "__isBaselineDeploymentPresent__" + isBaselineDeploymentPresent: "__isBaselineDeploymentPresent__", + baselineAndCanaryReplicas: "__baselineAndCanaryReplicas__", + trafficSplitMethod: "__trafficSplitMethod__" }; export let OperatingSystems = { @@ -64,6 +66,11 @@ export let Strategy = { none: "none" }; +export let TrafficSplitMethod = { + pod: "pod", + smi: "smi" +}; + export const ManifestFilesPath = path.join(__dirname, 'manifests', 'deployment.yaml'); export const CanaryManifestFilesPath = path.join(__dirname, 'manifests', 'deployment-canary.yaml'); export const BaselineManifestFilesPath = path.join(__dirname, 'manifests', 'deployment-baseline.yaml'); diff --git a/Tasks/KubernetesManifestV0/src/actions/promote.ts b/Tasks/KubernetesManifestV0/src/actions/promote.ts index 90afe80fc63e..cb9ffa6ee071 100644 --- a/Tasks/KubernetesManifestV0/src/actions/promote.ts +++ b/Tasks/KubernetesManifestV0/src/actions/promote.ts @@ -3,26 +3,42 @@ import * as tl from 'azure-pipelines-task-lib/task'; import * as deploymentHelper from '../utils/DeploymentHelper'; import * as canaryDeploymentHelper from '../utils/CanaryDeploymentHelper'; +import * as SMICanaryDeploymentHelper from '../utils/SMICanaryDeploymentHelper'; import * as utils from '../utils/utilities'; import * as TaskInputParameters from '../models/TaskInputParameters'; import { Kubectl } from 'kubernetes-common-v2/kubectl-object-model'; export async function promote(ignoreSslErrors?: boolean) { - const kubectl = new Kubectl(await utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors); - if (canaryDeploymentHelper.isCanaryDeploymentStrategy()) { - tl.debug('Deploying input manifests'); - await deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None'); - tl.debug('Deployment strategy selected is Canary. Deleting canary and baseline workloads.'); - try { - canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests); - } catch (ex) { - tl.warning('Exception occurred while deleting canary and baseline workloads. Exception: ' + ex); - } - } else { + if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) { tl.debug('Strategy is not canary deployment. Invalid request.'); throw (tl.loc('InvalidPromotetActionDeploymentStrategy')); } + + let includeServices = false; + if (canaryDeploymentHelper.isSMICanaryStrategy()) { + includeServices = true; + // 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 + tl.debug('Redirecting traffic to canary deployment'); + SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(kubectl, TaskInputParameters.manifests); + + tl.debug('Deploying input manifests with SMI canary strategy'); + await deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None'); + + tl.debug('Redirecting traffic to stable deployment'); + SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests); + } else { + tl.debug('Deploying input manifests'); + await deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None'); + } + + tl.debug('Deployment strategy selected is Canary. Deleting canary and baseline workloads.'); + try { + canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices); + } catch (ex) { + tl.warning('Exception occurred while deleting canary and baseline workloads. Exception: ' + ex); + } } \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/src/actions/reject.ts b/Tasks/KubernetesManifestV0/src/actions/reject.ts index 85a32fb50cf7..f10d8987f60a 100644 --- a/Tasks/KubernetesManifestV0/src/actions/reject.ts +++ b/Tasks/KubernetesManifestV0/src/actions/reject.ts @@ -1,6 +1,7 @@ 'use strict'; import * as tl from 'azure-pipelines-task-lib/task'; import * as canaryDeploymentHelper from '../utils/CanaryDeploymentHelper'; +import * as SMICanaryDeploymentHelper from '../utils/SMICanaryDeploymentHelper'; import { Kubectl } from 'kubernetes-common-v2/kubectl-object-model'; import * as utils from '../utils/utilities'; import * as TaskInputParameters from '../models/TaskInputParameters'; @@ -8,11 +9,18 @@ import * as TaskInputParameters from '../models/TaskInputParameters'; export async function reject(ignoreSslErrors?: boolean) { const kubectl = new Kubectl(await utils.getKubectl(), TaskInputParameters.namespace, ignoreSslErrors); - if (canaryDeploymentHelper.isCanaryDeploymentStrategy()) { - tl.debug('Deployment strategy selected is Canary. Deleting baseline and canary workloads.'); - canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests); - } else { + if (!canaryDeploymentHelper.isCanaryDeploymentStrategy()) { tl.debug('Strategy is not canary deployment. Invalid request.'); throw (tl.loc('InvalidRejectActionDeploymentStrategy')); } + + let includeServices = false; + if (canaryDeploymentHelper.isSMICanaryStrategy()) { + tl.debug('Reject deployment with SMI canary strategy'); + includeServices = true; + SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests); + } + + tl.debug('Deployment strategy selected is Canary. Deleting baseline and canary workloads.'); + canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices); } \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/src/models/TaskInputParameters.ts b/Tasks/KubernetesManifestV0/src/models/TaskInputParameters.ts index 1a4b69d22a3a..8de7cda59334 100644 --- a/Tasks/KubernetesManifestV0/src/models/TaskInputParameters.ts +++ b/Tasks/KubernetesManifestV0/src/models/TaskInputParameters.ts @@ -8,6 +8,8 @@ export const imagePullSecrets: string[] = tl.getDelimitedInput('imagePullSecrets export const manifests = tl.getDelimitedInput('manifests', '\n'); export const canaryPercentage: string = tl.getInput('percentage'); export const deploymentStrategy: string = tl.getInput('strategy', false); +export const trafficSplitMethod: string = tl.getInput('trafficSplitMethod', false); +export const baselineAndCanaryReplicas: string = tl.getInput('baselineAndCanaryReplicas', true); export const args: string = tl.getInput('arguments', false); export const secretArguments: string = tl.getInput('secretArguments', false) || ''; export const secretType: string = tl.getInput('secretType', false); diff --git a/Tasks/KubernetesManifestV0/src/utils/CanaryDeploymentHelper.ts b/Tasks/KubernetesManifestV0/src/utils/CanaryDeploymentHelper.ts index 9e207d54aadb..fa5fbe18705c 100644 --- a/Tasks/KubernetesManifestV0/src/utils/CanaryDeploymentHelper.ts +++ b/Tasks/KubernetesManifestV0/src/utils/CanaryDeploymentHelper.ts @@ -6,20 +6,22 @@ import * as fs from 'fs'; import * as yaml from 'js-yaml'; import * as TaskInputParameters from '../models/TaskInputParameters'; -import * as fileHelper from '../utils/FileHelper'; -import * as helper from './KubernetesObjectUtility'; +import * as helper from '../utils/KubernetesObjectUtility'; import { KubernetesWorkload } from 'kubernetes-common-v2/kubernetesconstants'; -import { StringComparer, isEqual } from '../utils/StringComparison'; -import * as utils from './utilities'; +import { StringComparer, isEqual } from './StringComparison'; +import * as utils from '../utils/utilities'; export const CANARY_DEPLOYMENT_STRATEGY = 'CANARY'; +export const TRAFFIC_SPLIT_STRATEGY = 'SMI'; +export const CANARY_VERSION_LABEL = 'azure-pipelines/version'; const BASELINE_SUFFIX = '-baseline'; -const BASELINE_LABEL_VALUE = 'baseline'; +export const BASELINE_LABEL_VALUE = 'baseline'; const CANARY_SUFFIX = '-canary'; -const CANARY_LABEL_VALUE = 'canary'; -const CANARY_VERSION_LABEL = 'azure-pipelines/version'; +export const CANARY_LABEL_VALUE = 'canary'; +export const STABLE_SUFFIX = '-stable'; +export const STABLE_LABEL_VALUE = 'stable'; -export function deleteCanaryDeployment(kubectl: Kubectl, manifestFilePaths: string[]) { +export function deleteCanaryDeployment(kubectl: Kubectl, manifestFilePaths: string[], includeServices: boolean) { // get manifest files const inputManifestFiles: string[] = utils.getManifestFiles(manifestFilePaths); @@ -30,7 +32,7 @@ export function deleteCanaryDeployment(kubectl: Kubectl, manifestFilePaths: stri // create delete cmd prefix let argsPrefix: string; - argsPrefix = createCanaryObjectsArgumentString(inputManifestFiles); + argsPrefix = createCanaryObjectsArgumentString(inputManifestFiles, includeServices); // append delete cmd args as suffix (if present) const args = utils.getDeleteCmdArgs(argsPrefix, TaskInputParameters.args); @@ -43,84 +45,45 @@ export function deleteCanaryDeployment(kubectl: Kubectl, manifestFilePaths: stri } } -export function deployCanary(kubectl: Kubectl, filePaths: string[]) { - const newObjectsList = []; - const percentage = parseInt(TaskInputParameters.canaryPercentage); +export function markResourceAsStable(inputObject: any): object { + if (isResourceMarkedAsStable(inputObject)) { + return inputObject; + } - filePaths.forEach((filePath: string) => { - const fileContents = fs.readFileSync(filePath); - yaml.safeLoadAll(fileContents, function (inputObject) { + const newObject = JSON.parse(JSON.stringify(inputObject)); - const name = inputObject.metadata.name; - const kind = inputObject.kind; - if (helper.isDeploymentEntity(kind)) { - const existingCanaryObject = fetchCanaryResource(kubectl, kind, name); - - if (!!existingCanaryObject) { - throw (tl.loc('CanaryDeploymentAlreadyExistErrorMessage')); - } - - tl.debug('Calculating replica count for canary'); - const canaryReplicaCount = calculateReplicaCountForCanary(inputObject, percentage); - tl.debug('Replica count is ' + canaryReplicaCount); - // Get stable object - tl.debug('Querying stable object'); - const stableObject = fetchResource(kubectl, kind, name); - if (!stableObject) { - tl.debug('Stable object not found. Creating only canary object'); - // If stable object not found, create canary deployment. - const newCanaryObject = getNewCanaryResource(inputObject, canaryReplicaCount); - tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); - newObjectsList.push(newCanaryObject); - } else { - tl.debug('Stable object found. Creating canary and baseline objects'); - // If canary object not found, create canary and baseline object. - const newCanaryObject = getNewCanaryResource(inputObject, canaryReplicaCount); - const newBaselineObject = getNewBaselineResource(stableObject, canaryReplicaCount); - tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); - tl.debug('New baseline object is: ' + JSON.stringify(newBaselineObject)); - newObjectsList.push(newCanaryObject); - newObjectsList.push(newBaselineObject); - } - } else { - // Updating non deployment entity as it is. - newObjectsList.push(inputObject); - } - }); - }); + // Adding labels and annotations. + addCanaryLabelsAndAnnotations(newObject, STABLE_LABEL_VALUE); - const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); - const result = kubectl.apply(manifestFiles); - return { 'result': result, 'newFilePaths': manifestFiles }; + tl.debug("Added stable label: " + JSON.stringify(newObject)); + return newObject; } -export function isCanaryDeploymentStrategy() { - const deploymentStrategy = TaskInputParameters.deploymentStrategy; - return deploymentStrategy && deploymentStrategy.toUpperCase() === CANARY_DEPLOYMENT_STRATEGY; +export function isResourceMarkedAsStable(inputObject: any): boolean { + return inputObject && + inputObject.metadata && + inputObject.metadata.labels && + inputObject.metadata.labels[CANARY_VERSION_LABEL] == STABLE_LABEL_VALUE; } -function calculateReplicaCountForCanary(inputObject: any, percentage: number) { - const inputReplicaCount = helper.getReplicaCount(inputObject); - return Math.round((inputReplicaCount * percentage) / 100); +export function getStableResource(inputObject: any): object { + var replicaCount = isSpecContainsReplicas(inputObject.kind) ? inputObject.metadata.replicas : 0; + return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE); } -function getNewBaselineResource(stableObject: any, replicas: number): object { +export function getNewBaselineResource(stableObject: any, replicas?: number): object { return getNewCanaryObject(stableObject, replicas, BASELINE_LABEL_VALUE); } -function getNewCanaryResource(inputObject: any, replicas: number): object { +export function getNewCanaryResource(inputObject: any, replicas?: number): object { return getNewCanaryObject(inputObject, replicas, CANARY_LABEL_VALUE); } -function getCanaryResourceName(name: string) { - return name + CANARY_SUFFIX; -} - -function getBaselineResourceName(name: string) { - return name + BASELINE_SUFFIX; +export function fetchCanaryResource(kubectl: Kubectl, kind: string, name: string): object { + return fetchResource(kubectl, kind, getCanaryResourceName(name)); } -function fetchResource(kubectl: Kubectl, kind: string, name: string): object { +export function fetchResource(kubectl: Kubectl, kind: string, name: string): object { const result = kubectl.getResource(kind, name); if (result == null || !!result.stderr) { @@ -140,6 +103,28 @@ function fetchResource(kubectl: Kubectl, kind: string, name: string): object { return null; } +export function isCanaryDeploymentStrategy() { + const deploymentStrategy = TaskInputParameters.deploymentStrategy; + return deploymentStrategy && deploymentStrategy.toUpperCase() === CANARY_DEPLOYMENT_STRATEGY; +} + +export function isSMICanaryStrategy() { + const deploymentStrategy = TaskInputParameters.trafficSplitMethod; + return isCanaryDeploymentStrategy() && deploymentStrategy && deploymentStrategy.toUpperCase() === TRAFFIC_SPLIT_STRATEGY; +} + +export function getCanaryResourceName(name: string) { + return name + CANARY_SUFFIX; +} + +export function getBaselineResourceName(name: string) { + return name + BASELINE_SUFFIX; +} + +export function getStableResourceName(name: string) { + return name + STABLE_SUFFIX; +} + function UnsetsClusterSpecficDetails(resource: any) { if (resource == null) { @@ -167,30 +152,35 @@ function UnsetsClusterSpecficDetails(resource: any) { } } -function fetchCanaryResource(kubectl: Kubectl, kind: string, name: string): object { - return fetchResource(kubectl, kind, getCanaryResourceName(name)); -} - function getNewCanaryObject(inputObject: any, replicas: number, type: string): object { const newObject = JSON.parse(JSON.stringify(inputObject)); // Updating name - newObject.metadata.name = type === CANARY_LABEL_VALUE ? - getCanaryResourceName(inputObject.metadata.name) : - getBaselineResourceName(inputObject.metadata.name); + if (type === CANARY_LABEL_VALUE) { + newObject.metadata.name = getCanaryResourceName(inputObject.metadata.name) + } else if (type === STABLE_LABEL_VALUE) { + newObject.metadata.name = getStableResourceName(inputObject.metadata.name) + } else { + newObject.metadata.name = getBaselineResourceName(inputObject.metadata.name); + } // Adding labels and annotations. addCanaryLabelsAndAnnotations(newObject, type); // Updating no. of replicas - if (!isEqual(newObject.kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase) && - !isEqual(newObject.kind, KubernetesWorkload.daemonSet, StringComparer.OrdinalIgnoreCase)) { + if (isSpecContainsReplicas(newObject.kind)) { newObject.spec.replicas = replicas; } return newObject; } +function isSpecContainsReplicas(kind: string) { + return !isEqual(kind, KubernetesWorkload.pod, StringComparer.OrdinalIgnoreCase) && + !isEqual(kind, KubernetesWorkload.daemonSet, StringComparer.OrdinalIgnoreCase) && + !helper.isServiceEntity(kind) +} + function addCanaryLabelsAndAnnotations(inputObject: any, type: string) { const newLabels = new Map(); newLabels[CANARY_VERSION_LABEL] = type; @@ -198,10 +188,13 @@ function addCanaryLabelsAndAnnotations(inputObject: any, type: string) { helper.updateObjectLabels(inputObject, newLabels, false); helper.updateObjectAnnotations(inputObject, newLabels, false); helper.updateSelectorLabels(inputObject, newLabels, false); - helper.updateSpecLabels(inputObject, newLabels, false); + + if (!helper.isServiceEntity(inputObject.kind)) { + helper.updateSpecLabels(inputObject, newLabels, false); + } } -function createCanaryObjectsArgumentString(files: string[]) { +function createCanaryObjectsArgumentString(files: string[], includeServices: boolean) { const kindList = new Set(); const nameList = new Set(); @@ -210,7 +203,8 @@ function createCanaryObjectsArgumentString(files: string[]) { yaml.safeLoadAll(fileContents, function (inputObject) { const name = inputObject.metadata.name; const kind = inputObject.kind; - if (helper.isDeploymentEntity(kind)) { + if (helper.isDeploymentEntity(kind) + || (includeServices && helper.isServiceEntity(kind))) { const canaryObjectName = getCanaryResourceName(name); const baselineObjectName = getBaselineResourceName(name); kindList.add(kind); @@ -226,4 +220,4 @@ function createCanaryObjectsArgumentString(files: string[]) { const args = utils.createKubectlArgs(kindList, nameList); return args; -} \ No newline at end of file +} diff --git a/Tasks/KubernetesManifestV0/src/utils/DeploymentHelper.ts b/Tasks/KubernetesManifestV0/src/utils/DeploymentHelper.ts index 0a17b4c598d7..c5fceb4cc85a 100644 --- a/Tasks/KubernetesManifestV0/src/utils/DeploymentHelper.ts +++ b/Tasks/KubernetesManifestV0/src/utils/DeploymentHelper.ts @@ -17,7 +17,9 @@ import { IExecSyncResult } from 'azure-pipelines-task-lib/toolrunner'; import { Kubectl, Resource } from 'kubernetes-common-v2/kubectl-object-model'; import { isEqual, StringComparer } from './StringComparison'; import { getDeploymentMetadata, getPublishDeploymentRequestUrl, isDeploymentEntity, getManifestUrls } from 'kubernetes-common-v2/image-metadata-helper'; -import { WebRequest, WebResponse, sendRequest } from 'utility-common-v2/restutilities'; +import { WebRequest, sendRequest } from 'utility-common-v2/restutilities'; +import { deployPodCanary } from './PodCanaryDeploymentHelper'; +import { deploySMICanary } from './SMICanaryDeploymentHelper'; const publishPipelineMetadata = tl.getVariable("PUBLISH_PIPELINE_METADATA"); @@ -75,16 +77,49 @@ function getManifestFiles(manifestFilePaths: string[]): string[] { function deployManifests(files: string[], kubectl: Kubectl, isCanaryDeploymentStrategy: boolean): string[] { let result; if (isCanaryDeploymentStrategy) { - const canaryDeploymentOutput = canaryDeploymentHelper.deployCanary(kubectl, files); + let canaryDeploymentOutput: any; + if (canaryDeploymentHelper.isSMICanaryStrategy()) { + canaryDeploymentOutput = deploySMICanary(kubectl, files); + } else { + canaryDeploymentOutput = deployPodCanary(kubectl, files); + } result = canaryDeploymentOutput.result; files = canaryDeploymentOutput.newFilePaths; } else { - result = kubectl.apply(files); + if (canaryDeploymentHelper.isSMICanaryStrategy()) { + const updatedManifests = appendStableVersionLabelToResource(files, kubectl); + result = kubectl.apply(updatedManifests); + } + else { + result = kubectl.apply(files); + } } utils.checkForErrors([result]); return files; } +function appendStableVersionLabelToResource(files: string[], kubectl: Kubectl): string[] { + const manifestFiles = []; + const newObjectsList = []; + + files.forEach((filePath: string) => { + const fileContents = fs.readFileSync(filePath); + yaml.safeLoadAll(fileContents, function (inputObject) { + const kind = inputObject.kind; + if (KubernetesObjectUtility.isDeploymentEntity(kind)) { + const updatedObject = canaryDeploymentHelper.markResourceAsStable(inputObject); + newObjectsList.push(updatedObject); + } else { + manifestFiles.push(filePath); + } + }); + }); + + const updatedManifestFiles = fileHelper.writeObjectsToFile(newObjectsList); + manifestFiles.push(...updatedManifestFiles); + return manifestFiles; +} + async function checkManifestStability(kubectl: Kubectl, resources: Resource[]): Promise { await KubernetesManifestUtility.checkManifestStability(kubectl, resources); diff --git a/Tasks/KubernetesManifestV0/src/utils/FileHelper.ts b/Tasks/KubernetesManifestV0/src/utils/FileHelper.ts index 6a7cf025c95c..ce62b4205732 100644 --- a/Tasks/KubernetesManifestV0/src/utils/FileHelper.ts +++ b/Tasks/KubernetesManifestV0/src/utils/FileHelper.ts @@ -47,7 +47,7 @@ export function writeObjectsToFile(inputObjects: any[]): string[] { tl.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString); } } catch (ex) { - tl.debug('Exception occurred while wrting object to file : ' + inputObject + ' . Exception: ' + ex); + tl.debug('Exception occurred while writing object to file : ' + inputObject + ' . Exception: ' + ex); } }); } @@ -55,6 +55,19 @@ export function writeObjectsToFile(inputObjects: any[]): string[] { return newFilePaths; } +export function writeManifestToFile(inputObjectString: string, kind: string, name: string): string { + if (inputObjectString) { + try { + const fileName = getManifestFileName(kind, name); + fs.writeFileSync(path.join(fileName), inputObjectString); + return fileName; + } catch (ex) { + tl.debug('Exception occurred while writing object to file : ' + inputObjectString + ' . Exception: ' + ex); + } + } + return ''; +} + function getManifestFileName(kind: string, name: string) { const filePath = kind + '_' + name + '_' + getCurrentTime().toString(); const tempDirectory = getTempDirectory(); diff --git a/Tasks/KubernetesManifestV0/src/utils/KubernetesObjectUtility.ts b/Tasks/KubernetesManifestV0/src/utils/KubernetesObjectUtility.ts index e6bcc3ff47c8..0c6f5084a2d8 100644 --- a/Tasks/KubernetesManifestV0/src/utils/KubernetesObjectUtility.ts +++ b/Tasks/KubernetesManifestV0/src/utils/KubernetesObjectUtility.ts @@ -26,6 +26,14 @@ export function isWorkloadEntity(kind: string): boolean { }); } +export function isServiceEntity(kind: string): boolean { + if (!kind) { + throw (tl.loc('ResourceKindNotDefined')); + } + + return isEqual("Service", kind, StringComparer.OrdinalIgnoreCase); +} + export function getReplicaCount(inputObject: any): any { if (!inputObject) { throw (tl.loc('NullInputObject')); @@ -289,7 +297,11 @@ function setSpecLabels(inputObject: any, newLabels: any) { function getSpecSelectorLabels(inputObject: any) { if (!!inputObject && !!inputObject.spec && !!inputObject.spec.selector) { - return inputObject.spec.selector.matchLabels; + if (isServiceEntity(inputObject.kind)) { + return inputObject.spec.selector; + } else { + return inputObject.spec.selector.matchLabels; + } } return null; diff --git a/Tasks/KubernetesManifestV0/src/utils/PodCanaryDeploymentHelper.ts b/Tasks/KubernetesManifestV0/src/utils/PodCanaryDeploymentHelper.ts new file mode 100644 index 000000000000..1644034b22fd --- /dev/null +++ b/Tasks/KubernetesManifestV0/src/utils/PodCanaryDeploymentHelper.ts @@ -0,0 +1,62 @@ +'use strict'; + +import { Kubectl } from 'kubernetes-common-v2/kubectl-object-model'; +import * as tl from 'azure-pipelines-task-lib/task'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; + +import * as TaskInputParameters from '../models/TaskInputParameters'; +import * as fileHelper from '../utils/FileHelper'; +import * as helper from '../utils/KubernetesObjectUtility'; +import * as canaryDeploymentHelper from '../utils/CanaryDeploymentHelper'; + +export function deployPodCanary(kubectl: Kubectl, filePaths: string[]) { + const newObjectsList = []; + const percentage = parseInt(TaskInputParameters.canaryPercentage); + + filePaths.forEach((filePath: string) => { + const fileContents = fs.readFileSync(filePath); + yaml.safeLoadAll(fileContents, function (inputObject) { + + const name = inputObject.metadata.name; + const kind = inputObject.kind; + if (helper.isDeploymentEntity(kind)) { + tl.debug('Calculating replica count for canary'); + const canaryReplicaCount = calculateReplicaCountForCanary(inputObject, percentage); + tl.debug('Replica count is ' + canaryReplicaCount); + + // Get stable object + tl.debug('Querying stable object'); + const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name); + if (!stableObject) { + tl.debug('Stable object not found. Creating only canary object'); + // If stable object not found, create canary deployment. + const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount); + tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); + newObjectsList.push(newCanaryObject); + } else { + tl.debug('Stable object found. Creating canary and baseline objects'); + // If canary object not found, create canary and baseline object. + const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount); + const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount); + tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); + tl.debug('New baseline object is: ' + JSON.stringify(newBaselineObject)); + newObjectsList.push(newCanaryObject); + newObjectsList.push(newBaselineObject); + } + } else { + // Updating non deployment entity as it is. + newObjectsList.push(inputObject); + } + }); + }); + + const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); + const result = kubectl.apply(manifestFiles); + return { 'result': result, 'newFilePaths': manifestFiles }; +} + +function calculateReplicaCountForCanary(inputObject: any, percentage: number) { + const inputReplicaCount = helper.getReplicaCount(inputObject); + return Math.round((inputReplicaCount * percentage) / 100); +} \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/src/utils/SMICanaryDeploymentHelper.ts b/Tasks/KubernetesManifestV0/src/utils/SMICanaryDeploymentHelper.ts new file mode 100644 index 000000000000..09c1ca602b3a --- /dev/null +++ b/Tasks/KubernetesManifestV0/src/utils/SMICanaryDeploymentHelper.ts @@ -0,0 +1,231 @@ +'use strict'; + +import { Kubectl } from 'kubernetes-common-v2/kubectl-object-model'; +import * as tl from 'azure-pipelines-task-lib/task'; +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import * as util from 'util'; + +import * as TaskInputParameters from '../models/TaskInputParameters'; +import * as fileHelper from '../utils/FileHelper'; +import * as helper from '../utils/KubernetesObjectUtility'; +import * as utils from '../utils/utilities'; +import * as canaryDeploymentHelper from '../utils/CanaryDeploymentHelper'; + +const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-azure-pipelines-rollout'; +const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'; + +export function deploySMICanary(kubectl: Kubectl, filePaths: string[]) { + const newObjectsList = []; + const canaryReplicaCount = parseInt(TaskInputParameters.baselineAndCanaryReplicas); + tl.debug('Replica count is ' + canaryReplicaCount); + + filePaths.forEach((filePath: string) => { + const fileContents = fs.readFileSync(filePath); + yaml.safeLoadAll(fileContents, function (inputObject) { + const name = inputObject.metadata.name; + const kind = inputObject.kind; + if (helper.isDeploymentEntity(kind)) { + // Get stable object + tl.debug('Querying stable object'); + const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name); + if (!stableObject) { + tl.debug('Stable object not found. Creating only canary object'); + // If stable object not found, create canary deployment. + const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount); + tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); + newObjectsList.push(newCanaryObject); + } else { + if (!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)) { + throw (tl.loc('StableSpecSelectorNotExist', name)); + } + + tl.debug('Stable object found. Creating canary and baseline objects'); + // If canary object not found, create canary and baseline object. + const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount); + const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount); + tl.debug('New canary object is: ' + JSON.stringify(newCanaryObject)); + tl.debug('New baseline object is: ' + JSON.stringify(newBaselineObject)); + newObjectsList.push(newCanaryObject); + newObjectsList.push(newBaselineObject); + } + } else { + // Updating non deployment entity as it is. + newObjectsList.push(inputObject); + } + }); + }); + + const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); + const result = kubectl.apply(manifestFiles); + createCanaryService(kubectl, filePaths); + return { 'result': result, 'newFilePaths': manifestFiles }; +} + +function createCanaryService(kubectl: Kubectl, filePaths: string[]) { + const newObjectsList = []; + const trafficObjectsList = []; + + filePaths.forEach((filePath: string) => { + const fileContents = fs.readFileSync(filePath); + yaml.safeLoadAll(fileContents, function (inputObject) { + + const name = inputObject.metadata.name; + const kind = inputObject.kind; + if (helper.isServiceEntity(kind)) { + const newCanaryServiceObject = canaryDeploymentHelper.getNewCanaryResource(inputObject); + tl.debug('New canary service object is: ' + JSON.stringify(newCanaryServiceObject)); + newObjectsList.push(newCanaryServiceObject); + + const newBaselineServiceObject = canaryDeploymentHelper.getNewBaselineResource(inputObject); + tl.debug('New baseline object is: ' + JSON.stringify(newBaselineServiceObject)); + newObjectsList.push(newBaselineServiceObject); + + tl.debug('Querying for stable service object'); + const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, canaryDeploymentHelper.getStableResourceName(name)); + if (!stableObject) { + const newStableServiceObject = canaryDeploymentHelper.getStableResource(inputObject); + tl.debug('New stable service object is: ' + JSON.stringify(newStableServiceObject)); + newObjectsList.push(newStableServiceObject); + + tl.debug('Creating the traffic object for service: ' + name); + const trafficObject = createTrafficSplitManifestFile(name, 0, 0, 1000); + tl.debug('Creating the traffic object for service: ' + trafficObject); + trafficObjectsList.push(trafficObject); + } else { + let updateTrafficObject = true; + const trafficObject = canaryDeploymentHelper.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, getTrafficSplitResourceName(name)); + if (trafficObject) { + const trafficJObject = JSON.parse(JSON.stringify(trafficObject)); + if (trafficJObject && trafficJObject.spec && trafficJObject.spec.backends) { + trafficJObject.spec.backends.forEach((s) => { + if (s.service === canaryDeploymentHelper.getCanaryResourceName(name) && s.weight === "1000m") { + tl.debug('Update traffic objcet not required'); + updateTrafficObject = false; + } + }) + } + } + + if (updateTrafficObject) { + tl.debug('Stable service object present so updating the traffic object for service: ' + name); + trafficObjectsList.push(updateTrafficSplitObject(name)); + } + } + } + }); + }); + + const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList); + manifestFiles.push(...trafficObjectsList); + const result = kubectl.apply(manifestFiles); + utils.checkForErrors([result]); +} + +export function redirectTrafficToCanaryDeployment(kubectl: Kubectl, manifestFilePaths: string[]) { + adjustTraffic(kubectl, manifestFilePaths, 0, 1000); +} + +export function redirectTrafficToStableDeployment(kubectl: Kubectl, manifestFilePaths: string[]) { + adjustTraffic(kubectl, manifestFilePaths, 1000, 0); +} + +function getNewServiceObject(inputObject: any, type: string): object { + const newObject = JSON.parse(JSON.stringify(inputObject)); + // Updating name + newObject.metadata.name = type === canaryDeploymentHelper.CANARY_LABEL_VALUE ? + canaryDeploymentHelper.getCanaryResourceName(inputObject.metadata.name) : + canaryDeploymentHelper.getBaselineResourceName(inputObject.metadata.name); + + const newLabels = new Map(); + newLabels[canaryDeploymentHelper.CANARY_VERSION_LABEL] = type; + + helper.updateObjectLabels(inputObject, newLabels, false); + helper.updateObjectAnnotations(inputObject, newLabels, false); + helper.updateSelectorLabels(inputObject, newLabels, false); + + return newObject; +} + +function adjustTraffic(kubectl: Kubectl, manifestFilePaths: string[], stableWeight: number, canaryWeight: number) { + // get manifest files + const inputManifestFiles: string[] = utils.getManifestFiles(manifestFilePaths); + + if (inputManifestFiles == null || inputManifestFiles.length == 0) { + return; + } + + const trafficSplitManifests = []; + const serviceObjects = []; + inputManifestFiles.forEach((filePath: string) => { + const fileContents = fs.readFileSync(filePath); + yaml.safeLoadAll(fileContents, function (inputObject) { + const name = inputObject.metadata.name; + const kind = inputObject.kind; + if (helper.isServiceEntity(kind)) { + trafficSplitManifests.push(createTrafficSplitManifestFile(name, stableWeight, 0, canaryWeight)); + serviceObjects.push(name); + } + }); + }); + + if (trafficSplitManifests.length <= 0) { + return; + } + + const result = kubectl.apply(trafficSplitManifests); + tl.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result); + utils.checkForErrors([result]); +} + +function updateTrafficSplitObject(serviceName: string): string { + const percentage = parseInt(TaskInputParameters.canaryPercentage) * 10; + const baselineAndCanaryWeight = percentage / 2; + const stableDeploymentWeight = 1000 - percentage; + tl.debug('Creating the traffic object with canary weight: ' + baselineAndCanaryWeight + ',baseling weight: ' + baselineAndCanaryWeight + ',stable: ' + stableDeploymentWeight); + return createTrafficSplitManifestFile(serviceName, stableDeploymentWeight, baselineAndCanaryWeight, baselineAndCanaryWeight); +} + +function createTrafficSplitManifestFile(serviceName: string, stableWeight: number, baselineWeight: number, canaryWeight: number): string { + const smiObjectString = getTrafficSplitObject(serviceName, stableWeight, baselineWeight, canaryWeight); + const manifestFile = fileHelper.writeManifestToFile(smiObjectString, TRAFFIC_SPLIT_OBJECT, serviceName); + if (!manifestFile) { + throw new Error(tl.loc('UnableToCreateTrafficSplitManifestFile')); + } + + return manifestFile; +} + +function getTrafficSplitObject(name: string, stableWeight: number, baselineWeight: number, canaryWeight: number): string { + const trafficSplitObjectJson = `{ + "apiVersion": "split.smi-spec.io/v1alpha1", + "kind": "TrafficSplit", + "metadata": { + "name": "%s" + }, + "spec": { + "backends": [ + { + "service": "%s", + "weight": "%sm" + }, + { + "service": "%s", + "weight": "%sm" + }, + { + "service": "%s", + "weight": "%sm" + } + ], + "service": "%s" + } + }`; + + const trafficSplitObject = util.format(trafficSplitObjectJson, getTrafficSplitResourceName(name), canaryDeploymentHelper.getStableResourceName(name), stableWeight, canaryDeploymentHelper.getBaselineResourceName(name), baselineWeight, canaryDeploymentHelper.getCanaryResourceName(name), canaryWeight, name); + return trafficSplitObject; +} + +function getTrafficSplitResourceName(name: string) { + return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX; +} \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/task.json b/Tasks/KubernetesManifestV0/task.json index 74f546258a1c..b605b1ad9119 100644 --- a/Tasks/KubernetesManifestV0/task.json +++ b/Tasks/KubernetesManifestV0/task.json @@ -14,7 +14,7 @@ "version": { "Major": 0, "Minor": 160, - "Patch": 2 + "Patch": 3 }, "demands": [], "groups": [], @@ -66,12 +66,25 @@ "helpMarkDown": "Deployment strategy to be used", "visibleRule": "action = deploy || action = promote || action = reject" }, + { + "name": "trafficSplitMethod", + "type": "pickList", + "label": "Traffic split method", + "required": false, + "defaultValue": "pod", + "options": { + "pod": "Pod", + "smi": "SMI" + }, + "helpMarkDown": "Traffic split method to be used", + "visibleRule": "strategy = canary" + }, { "name": "percentage", "type": "string", "label": "Percentage", "required": true, - "helpMarkDown": "", + "helpMarkDown": "Percentage of traffic redirect to canary deployment", "defaultValue": 0, "visibleRule": "strategy = Canary && action = deploy", "validation": { @@ -79,6 +92,19 @@ "message": "Enter valid percentage value i.e between 0 to 100." } }, + { + "name": "baselineAndCanaryReplicas", + "type": "string", + "label": "Baseline and canary replicas", + "required": true, + "helpMarkDown": "Baseline and canary replicas count", + "defaultValue": 0, + "visibleRule": "strategy = Canary && action = deploy && trafficSplitMethod = SMI", + "validation": { + "expression": "isMatch(value, '(^(([0-9]|[1-9][0-9]|100)(\\.\\d{1,2})?)$)','Multiline')", + "message": "Enter valid value i.e between 0 to 100." + } + }, { "name": "manifests", "type": "filePath", @@ -328,15 +354,16 @@ "NullInputObject": "Input object is null.", "ArgumentsInputNotSupplied": "Arguments are not supplied.", "NullInputObjectMetadata": "Input object metadata is null.", - "CanaryDeploymentAlreadyExistErrorMessage": "Canary deployment already exists. Rejecting this deployment.", "InvalidRejectActionDeploymentStrategy": "Reject action works only with strategy: canary", "InvalidPromotetActionDeploymentStrategy": "Promote action works only with strategy: canary", "AllContainersNotInReadyState": "All the containers are not in a ready state.", "CouldNotDeterminePodStatus": "Could not determine the pod's status due to the error: %s", "KubectlShouldBeUpgraded": "kubectl client version equal to v1.14 or higher is required to use kustomize features.", - "CouldNotDetermineServiceStatus": "Could not determine the service %s status due to the error: %s", + "CouldNotDetermineServiceStatus": "Could not determine the service %s status due to the error: %s", "waitForServiceIpAssignment": "Waiting for service %s external IP assignment", "waitForServiceIpAssignmentTimedOut": "Wait for service %s external IP assignment timed out", - "ServiceExternalIP": "service %s external IP is %s" + "ServiceExternalIP": "service %s external IP is %s", + "UnableToCreateTrafficSplitManifestFile": "Unable to create TrafficSplit manifest file.", + "StableSpecSelectorNotExist": "Resource %s not deployed using SMI canary deployment." } } \ No newline at end of file diff --git a/Tasks/KubernetesManifestV0/task.loc.json b/Tasks/KubernetesManifestV0/task.loc.json index e88c78c6a156..8a9d0c13e1a9 100644 --- a/Tasks/KubernetesManifestV0/task.loc.json +++ b/Tasks/KubernetesManifestV0/task.loc.json @@ -14,7 +14,7 @@ "version": { "Major": 0, "Minor": 160, - "Patch": 2 + "Patch": 3 }, "demands": [], "groups": [], @@ -66,12 +66,25 @@ "helpMarkDown": "ms-resource:loc.input.help.strategy", "visibleRule": "action = deploy || action = promote || action = reject" }, + { + "name": "trafficSplitMethod", + "type": "pickList", + "label": "ms-resource:loc.input.label.trafficSplitMethod", + "required": false, + "defaultValue": "pod", + "options": { + "pod": "Pod", + "smi": "SMI" + }, + "helpMarkDown": "ms-resource:loc.input.help.trafficSplitMethod", + "visibleRule": "strategy = canary" + }, { "name": "percentage", "type": "string", "label": "ms-resource:loc.input.label.percentage", "required": true, - "helpMarkDown": "", + "helpMarkDown": "ms-resource:loc.input.help.percentage", "defaultValue": 0, "visibleRule": "strategy = Canary && action = deploy", "validation": { @@ -79,6 +92,19 @@ "message": "Enter valid percentage value i.e between 0 to 100." } }, + { + "name": "baselineAndCanaryReplicas", + "type": "string", + "label": "ms-resource:loc.input.label.baselineAndCanaryReplicas", + "required": true, + "helpMarkDown": "ms-resource:loc.input.help.baselineAndCanaryReplicas", + "defaultValue": 0, + "visibleRule": "strategy = Canary && action = deploy && trafficSplitMethod = SMI", + "validation": { + "expression": "isMatch(value, '(^(([0-9]|[1-9][0-9]|100)(\\.\\d{1,2})?)$)','Multiline')", + "message": "Enter valid value i.e between 0 to 100." + } + }, { "name": "manifests", "type": "filePath", @@ -328,7 +354,6 @@ "NullInputObject": "ms-resource:loc.messages.NullInputObject", "ArgumentsInputNotSupplied": "ms-resource:loc.messages.ArgumentsInputNotSupplied", "NullInputObjectMetadata": "ms-resource:loc.messages.NullInputObjectMetadata", - "CanaryDeploymentAlreadyExistErrorMessage": "ms-resource:loc.messages.CanaryDeploymentAlreadyExistErrorMessage", "InvalidRejectActionDeploymentStrategy": "ms-resource:loc.messages.InvalidRejectActionDeploymentStrategy", "InvalidPromotetActionDeploymentStrategy": "ms-resource:loc.messages.InvalidPromotetActionDeploymentStrategy", "AllContainersNotInReadyState": "ms-resource:loc.messages.AllContainersNotInReadyState", @@ -337,6 +362,8 @@ "CouldNotDetermineServiceStatus": "ms-resource:loc.messages.CouldNotDetermineServiceStatus", "waitForServiceIpAssignment": "ms-resource:loc.messages.waitForServiceIpAssignment", "waitForServiceIpAssignmentTimedOut": "ms-resource:loc.messages.waitForServiceIpAssignmentTimedOut", - "ServiceExternalIP": "ms-resource:loc.messages.ServiceExternalIP" + "ServiceExternalIP": "ms-resource:loc.messages.ServiceExternalIP", + "UnableToCreateTrafficSplitManifestFile": "ms-resource:loc.messages.UnableToCreateTrafficSplitManifestFile", + "StableSpecSelectorNotExist": "ms-resource:loc.messages.StableSpecSelectorNotExist" } } \ No newline at end of file