Vidya reddy/prettier code (#203)

This commit is contained in:
Vidya
2022-06-24 13:57:45 -07:00
committed by GitHub
parent 976c5c4981
commit dcd9bc6b1a
71 changed files with 16044 additions and 15876 deletions
+196 -196
View File
@@ -1,196 +1,196 @@
import { Kubectl } from "../../types/kubectl";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as core from "@actions/core";
import {
isDeploymentEntity,
isServiceEntity,
KubernetesWorkload,
} from "../../types/kubernetesTypes";
import * as utils from "../../utilities/manifestUpdateUtils";
import {
updateObjectAnnotations,
updateObjectLabels,
updateSelectorLabels,
} from "../../utilities/manifestUpdateUtils";
import { updateSpecLabels } from "../../utilities/manifestSpecLabelUtils";
import { checkForErrors } from "../../utilities/kubectlUtils";
export const CANARY_VERSION_LABEL = "workflow/version";
const BASELINE_SUFFIX = "-baseline";
export const BASELINE_LABEL_VALUE = "baseline";
const CANARY_SUFFIX = "-canary";
export const CANARY_LABEL_VALUE = "canary";
export const STABLE_SUFFIX = "-stable";
export const STABLE_LABEL_VALUE = "stable";
export async function deleteCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[],
includeServices: boolean
) {
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
throw new Error("Manifest file not found");
}
await cleanUpCanary(kubectl, manifestFilePaths, includeServices);
}
export function markResourceAsStable(inputObject: any): object {
if (isResourceMarkedAsStable(inputObject)) {
return inputObject;
}
const newObject = JSON.parse(JSON.stringify(inputObject));
addCanaryLabelsAndAnnotations(newObject, STABLE_LABEL_VALUE);
return newObject;
}
export function isResourceMarkedAsStable(inputObject: any): boolean {
return (
inputObject?.metadata?.labels[CANARY_VERSION_LABEL] === STABLE_LABEL_VALUE
);
}
export function getStableResource(inputObject: any): object {
const replicaCount = specContainsReplicas(inputObject.kind)
? inputObject.metadata.replicas
: 0;
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE);
}
export function getNewBaselineResource(
stableObject: any,
replicas?: number
): object {
return getNewCanaryObject(stableObject, replicas, BASELINE_LABEL_VALUE);
}
export function getNewCanaryResource(
inputObject: any,
replicas?: number
): object {
return getNewCanaryObject(inputObject, replicas, CANARY_LABEL_VALUE);
}
export async function fetchResource(
kubectl: Kubectl,
kind: string,
name: string
) {
const result = await kubectl.getResource(kind, name);
if (!result || result?.stderr) {
return null;
}
if (result.stdout) {
const resource = JSON.parse(result.stdout);
try {
utils.UnsetClusterSpecificDetails(resource);
return resource;
} catch (ex) {
core.debug(
`Exception occurred while Parsing ${resource} in JSON object: ${ex}`
);
}
}
}
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 getNewCanaryObject(
inputObject: any,
replicas: number,
type: string
): object {
const newObject = JSON.parse(JSON.stringify(inputObject));
// Updating 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
);
}
addCanaryLabelsAndAnnotations(newObject, type);
if (specContainsReplicas(newObject.kind)) {
newObject.spec.replicas = replicas;
}
return newObject;
}
function specContainsReplicas(kind: string) {
return (
kind.toLowerCase() !== KubernetesWorkload.POD.toLowerCase() &&
kind.toLowerCase() !== KubernetesWorkload.DAEMON_SET.toLowerCase() &&
!isServiceEntity(kind)
);
}
function addCanaryLabelsAndAnnotations(inputObject: any, type: string) {
const newLabels = new Map<string, string>();
newLabels[CANARY_VERSION_LABEL] = type;
updateObjectLabels(inputObject, newLabels, false);
updateObjectAnnotations(inputObject, newLabels, false);
updateSelectorLabels(inputObject, newLabels, false);
if (!isServiceEntity(inputObject.kind)) {
updateSpecLabels(inputObject, newLabels, false);
}
}
async function cleanUpCanary(
kubectl: Kubectl,
files: string[],
includeServices: boolean
) {
const deleteObject = async function (kind, name) {
try {
const result = await kubectl.delete([kind, name]);
checkForErrors([result]);
} catch (ex) {
// Ignore failures of delete if it doesn't exist
}
};
for (const filePath of files) {
const fileContents = fs.readFileSync(filePath).toString();
const parsedYaml = yaml.safeLoadAll(fileContents);
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name;
const kind = inputObject.kind;
if (
isDeploymentEntity(kind) ||
(includeServices && isServiceEntity(kind))
) {
const canaryObjectName = getCanaryResourceName(name);
const baselineObjectName = getBaselineResourceName(name);
await deleteObject(kind, canaryObjectName);
await deleteObject(kind, baselineObjectName);
}
}
}
}
import {Kubectl} from '../../types/kubectl'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as core from '@actions/core'
import {
isDeploymentEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes'
import * as utils from '../../utilities/manifestUpdateUtils'
import {
updateObjectAnnotations,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
export const CANARY_VERSION_LABEL = 'workflow/version'
const BASELINE_SUFFIX = '-baseline'
export const BASELINE_LABEL_VALUE = 'baseline'
const CANARY_SUFFIX = '-canary'
export const CANARY_LABEL_VALUE = 'canary'
export const STABLE_SUFFIX = '-stable'
export const STABLE_LABEL_VALUE = 'stable'
export async function deleteCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[],
includeServices: boolean
) {
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
throw new Error('Manifest file not found')
}
await cleanUpCanary(kubectl, manifestFilePaths, includeServices)
}
export function markResourceAsStable(inputObject: any): object {
if (isResourceMarkedAsStable(inputObject)) {
return inputObject
}
const newObject = JSON.parse(JSON.stringify(inputObject))
addCanaryLabelsAndAnnotations(newObject, STABLE_LABEL_VALUE)
return newObject
}
export function isResourceMarkedAsStable(inputObject: any): boolean {
return (
inputObject?.metadata?.labels[CANARY_VERSION_LABEL] === STABLE_LABEL_VALUE
)
}
export function getStableResource(inputObject: any): object {
const replicaCount = specContainsReplicas(inputObject.kind)
? inputObject.metadata.replicas
: 0
return getNewCanaryObject(inputObject, replicaCount, STABLE_LABEL_VALUE)
}
export function getNewBaselineResource(
stableObject: any,
replicas?: number
): object {
return getNewCanaryObject(stableObject, replicas, BASELINE_LABEL_VALUE)
}
export function getNewCanaryResource(
inputObject: any,
replicas?: number
): object {
return getNewCanaryObject(inputObject, replicas, CANARY_LABEL_VALUE)
}
export async function fetchResource(
kubectl: Kubectl,
kind: string,
name: string
) {
const result = await kubectl.getResource(kind, name)
if (!result || result?.stderr) {
return null
}
if (result.stdout) {
const resource = JSON.parse(result.stdout)
try {
utils.UnsetClusterSpecificDetails(resource)
return resource
} catch (ex) {
core.debug(
`Exception occurred while Parsing ${resource} in JSON object: ${ex}`
)
}
}
}
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 getNewCanaryObject(
inputObject: any,
replicas: number,
type: string
): object {
const newObject = JSON.parse(JSON.stringify(inputObject))
// Updating 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
)
}
addCanaryLabelsAndAnnotations(newObject, type)
if (specContainsReplicas(newObject.kind)) {
newObject.spec.replicas = replicas
}
return newObject
}
function specContainsReplicas(kind: string) {
return (
kind.toLowerCase() !== KubernetesWorkload.POD.toLowerCase() &&
kind.toLowerCase() !== KubernetesWorkload.DAEMON_SET.toLowerCase() &&
!isServiceEntity(kind)
)
}
function addCanaryLabelsAndAnnotations(inputObject: any, type: string) {
const newLabels = new Map<string, string>()
newLabels[CANARY_VERSION_LABEL] = type
updateObjectLabels(inputObject, newLabels, false)
updateObjectAnnotations(inputObject, newLabels, false)
updateSelectorLabels(inputObject, newLabels, false)
if (!isServiceEntity(inputObject.kind)) {
updateSpecLabels(inputObject, newLabels, false)
}
}
async function cleanUpCanary(
kubectl: Kubectl,
files: string[],
includeServices: boolean
) {
const deleteObject = async function (kind, name) {
try {
const result = await kubectl.delete([kind, name])
checkForErrors([result])
} catch (ex) {
// Ignore failures of delete if it doesn't exist
}
}
for (const filePath of files) {
const fileContents = fs.readFileSync(filePath).toString()
const parsedYaml = yaml.safeLoadAll(fileContents)
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name
const kind = inputObject.kind
if (
isDeploymentEntity(kind) ||
(includeServices && isServiceEntity(kind))
) {
const canaryObjectName = getCanaryResourceName(name)
const baselineObjectName = getBaselineResourceName(name)
await deleteObject(kind, canaryObjectName)
await deleteObject(kind, baselineObjectName)
}
}
}
}
+94 -90
View File
@@ -1,90 +1,94 @@
import { Kubectl } from "../../types/kubectl";
import * as core from "@actions/core";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as fileHelper from "../../utilities/fileUtils";
import * as canaryDeploymentHelper from "./canaryHelper";
import { isDeploymentEntity } from "../../types/kubernetesTypes";
import { getReplicaCount } from "../../utilities/manifestUpdateUtils";
export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
const newObjectsList = [];
const percentage = parseInt(core.getInput("percentage"));
if (percentage < 0 || percentage > 100)
throw Error("Percentage must be between 0 and 100");
for (const filePath of filePaths) {
const fileContents = fs.readFileSync(filePath).toString();
const parsedYaml = yaml.safeLoadAll(fileContents);
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name;
const kind = inputObject.kind;
if (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 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 {
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 =
canaryDeploymentHelper.getNewBaselineResource(
stableObject,
canaryReplicaCount
);
core.debug(
"New baseline object: " + JSON.stringify(newBaselineObject)
);
newObjectsList.push(newCanaryObject);
newObjectsList.push(newBaselineObject);
}
} else {
// update non deployment entity as it is
newObjectsList.push(inputObject);
}
}
}
core.debug("New objects list: " + JSON.stringify(newObjectsList));
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
const forceDeployment = core.getInput("force").toLowerCase() === "true";
const result = await kubectl.apply(manifestFiles, forceDeployment);
return { result, newFilePaths: manifestFiles };
}
function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
const inputReplicaCount = getReplicaCount(inputObject);
return Math.round((inputReplicaCount * percentage) / 100);
}
import {Kubectl} from '../../types/kubectl'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity} from '../../types/kubernetesTypes'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
export async function deployPodCanary(filePaths: string[], kubectl: Kubectl) {
const newObjectsList = []
const percentage = parseInt(core.getInput('percentage'))
if (percentage < 0 || percentage > 100)
throw Error('Percentage must be between 0 and 100')
for (const filePath of filePaths) {
const fileContents = fs.readFileSync(filePath).toString()
const parsedYaml = yaml.safeLoadAll(fileContents)
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name
const kind = inputObject.kind
if (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 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 {
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 =
canaryDeploymentHelper.getNewBaselineResource(
stableObject,
canaryReplicaCount
)
core.debug(
'New baseline object: ' + JSON.stringify(newBaselineObject)
)
newObjectsList.push(newCanaryObject)
newObjectsList.push(newBaselineObject)
}
} else {
// update non deployment entity as it is
newObjectsList.push(inputObject)
}
}
}
core.debug('New objects list: ' + JSON.stringify(newObjectsList))
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(manifestFiles, forceDeployment)
return {result, newFilePaths: manifestFiles}
}
function calculateReplicaCountForCanary(inputObject: any, percentage: number) {
const inputReplicaCount = getReplicaCount(inputObject)
return Math.round((inputReplicaCount * percentage) / 100)
}
+328 -319
View File
@@ -1,319 +1,328 @@
import { Kubectl } from "../../types/kubectl";
import * as core from "@actions/core";
import * as fs from "fs";
import * as yaml from "js-yaml";
import * as fileHelper from "../../utilities/fileUtils";
import * as kubectlUtils from "../../utilities/trafficSplitUtils";
import * as canaryDeploymentHelper from "./canaryHelper";
import {
isDeploymentEntity,
isServiceEntity,
} from "../../types/kubernetesTypes";
import { checkForErrors } from "../../utilities/kubectlUtils";
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = "-workflow-rollout";
const TRAFFIC_SPLIT_OBJECT = "TrafficSplit";
export async function deploySMICanary(filePaths: string[], kubectl: Kubectl) {
const canaryReplicaCount = parseInt(
core.getInput("baseline-and-canary-replicas")
);
if (canaryReplicaCount < 0 || canaryReplicaCount > 100)
throw Error("Baseline-and-canary-replicas must be between 0 and 100");
const newObjectsList = [];
filePaths.forEach((filePath: string) => {
const fileContents = fs.readFileSync(filePath).toString();
yaml.safeLoadAll(fileContents, (inputObject) => {
const name = inputObject.metadata.name;
const kind = inputObject.kind;
if (isDeploymentEntity(kind)) {
const stableObject = canaryDeploymentHelper.fetchResource(
kubectl,
kind,
name
);
if (!stableObject) {
core.debug("Stable object not found. Creating only canary object");
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
);
newObjectsList.push(newCanaryObject);
} else {
if (!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)) {
throw Error(`StableSpecSelectorNotExist : ${name}`);
}
core.debug(
"Stable object found. Creating canary and baseline objects"
);
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
);
const newBaselineObject =
canaryDeploymentHelper.getNewBaselineResource(
stableObject,
canaryReplicaCount
);
newObjectsList.push(newCanaryObject);
newObjectsList.push(newBaselineObject);
}
} else {
// Update non deployment entity as it is
newObjectsList.push(inputObject);
}
});
});
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList);
const forceDeployment = core.getInput("force").toLowerCase() === "true";
const result = await kubectl.apply(newFilePaths, forceDeployment);
await createCanaryService(kubectl, filePaths);
return { result, newFilePaths };
}
async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
const newObjectsList = [];
const trafficObjectsList = [];
for (const filePath of filePaths) {
const fileContents = fs.readFileSync(filePath).toString();
const parsedYaml = yaml.safeLoadAll(fileContents);
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name;
const kind = inputObject.kind;
if (isServiceEntity(kind)) {
const newCanaryServiceObject =
canaryDeploymentHelper.getNewCanaryResource(inputObject);
newObjectsList.push(newCanaryServiceObject);
const newBaselineServiceObject =
canaryDeploymentHelper.getNewBaselineResource(inputObject);
newObjectsList.push(newBaselineServiceObject);
const stableObject = await canaryDeploymentHelper.fetchResource(
kubectl,
kind,
canaryDeploymentHelper.getStableResourceName(name)
);
if (!stableObject) {
const newStableServiceObject =
canaryDeploymentHelper.getStableResource(inputObject);
newObjectsList.push(newStableServiceObject);
core.debug("Creating the traffic object for service: " + name);
const trafficObject = await createTrafficSplitManifestFile(
kubectl,
name,
0,
0,
1000
);
trafficObjectsList.push(trafficObject);
} else {
let updateTrafficObject = true;
const trafficObject = await canaryDeploymentHelper.fetchResource(
kubectl,
TRAFFIC_SPLIT_OBJECT,
getTrafficSplitResourceName(name)
);
if (trafficObject) {
const trafficJObject = JSON.parse(JSON.stringify(trafficObject));
if (trafficJObject?.spec?.backends) {
trafficJObject.spec.backends.forEach((s) => {
if (
s.service ===
canaryDeploymentHelper.getCanaryResourceName(name) &&
s.weight === "1000m"
) {
core.debug("Update traffic objcet not required");
updateTrafficObject = false;
}
});
}
}
if (updateTrafficObject) {
core.debug(
"Stable service object present so updating the traffic object for service: " +
name
);
trafficObjectsList.push(updateTrafficSplitObject(kubectl, name));
}
}
}
}
}
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
manifestFiles.push(...trafficObjectsList);
const forceDeployment = core.getInput("force").toLowerCase() === "true";
const result = await kubectl.apply(manifestFiles, forceDeployment);
checkForErrors([result]);
}
export async function redirectTrafficToCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
) {
await adjustTraffic(kubectl, manifestFilePaths, 0, 1000);
}
export async function redirectTrafficToStableDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
) {
await adjustTraffic(kubectl, manifestFilePaths, 1000, 0);
}
async function adjustTraffic(
kubectl: Kubectl,
manifestFilePaths: string[],
stableWeight: number,
canaryWeight: number
) {
if (!manifestFilePaths || manifestFilePaths?.length == 0) {
return;
}
const trafficSplitManifests = [];
for (const filePath of manifestFilePaths) {
const fileContents = fs.readFileSync(filePath).toString();
const parsedYaml = yaml.safeLoadAll(fileContents);
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name;
const kind = inputObject.kind;
if (isServiceEntity(kind)) {
trafficSplitManifests.push(
await createTrafficSplitManifestFile(
kubectl,
name,
stableWeight,
0,
canaryWeight
)
);
}
}
}
if (trafficSplitManifests.length <= 0) {
return;
}
const forceDeployment = core.getInput("force").toLowerCase() === "true";
const result = await kubectl.apply(trafficSplitManifests, forceDeployment);
checkForErrors([result]);
}
async function updateTrafficSplitObject(
kubectl: Kubectl,
serviceName: string
): Promise<string> {
const percentage = parseInt(core.getInput("percentage"));
if (percentage < 0 || percentage > 100)
throw Error("Percentage must be between 0 and 100");
const percentageWithMuliplier = percentage * 10;
const baselineAndCanaryWeight = percentageWithMuliplier / 2;
const stableDeploymentWeight = 1000 - percentageWithMuliplier;
core.debug(
"Creating the traffic object with canary weight: " +
baselineAndCanaryWeight +
",baseling weight: " +
baselineAndCanaryWeight +
",stable: " +
stableDeploymentWeight
);
return await createTrafficSplitManifestFile(
kubectl,
serviceName,
stableDeploymentWeight,
baselineAndCanaryWeight,
baselineAndCanaryWeight
);
}
async function createTrafficSplitManifestFile(
kubectl: Kubectl,
serviceName: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
): Promise<string> {
const smiObjectString = await getTrafficSplitObject(
kubectl,
serviceName,
stableWeight,
baselineWeight,
canaryWeight
);
const manifestFile = fileHelper.writeManifestToFile(
smiObjectString,
TRAFFIC_SPLIT_OBJECT,
serviceName
);
if (!manifestFile) {
throw new Error("Unable to create traffic split manifest file");
}
return manifestFile;
}
let trafficSplitAPIVersion = "";
async function getTrafficSplitObject(
kubectl: Kubectl,
name: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
): Promise<string> {
// cached version
if (!trafficSplitAPIVersion) {
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
kubectl
);
}
return JSON.stringify({
apiVersion: trafficSplitAPIVersion,
kind: "TrafficSplit",
metadata: {
name: getTrafficSplitResourceName(name),
},
spec: {
backends: [
{
service: canaryDeploymentHelper.getStableResourceName(name),
weight: stableWeight,
},
{
service: canaryDeploymentHelper.getBaselineResourceName(name),
weight: baselineWeight,
},
{
service: canaryDeploymentHelper.getCanaryResourceName(name),
weight: canaryWeight,
},
],
service: name,
},
});
}
function getTrafficSplitResourceName(name: string) {
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX;
}
import {Kubectl} from '../../types/kubectl'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils'
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
import {checkForErrors} from '../../utilities/kubectlUtils'
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
export async function deploySMICanary(filePaths: string[], kubectl: Kubectl) {
const canaryReplicaCount = parseInt(
core.getInput('baseline-and-canary-replicas')
)
if (canaryReplicaCount < 0 || canaryReplicaCount > 100)
throw Error('Baseline-and-canary-replicas must be between 0 and 100')
const newObjectsList = []
filePaths.forEach((filePath: string) => {
const fileContents = fs.readFileSync(filePath).toString()
yaml.safeLoadAll(fileContents, (inputObject) => {
const name = inputObject.metadata.name
const kind = inputObject.kind
if (isDeploymentEntity(kind)) {
const stableObject = canaryDeploymentHelper.fetchResource(
kubectl,
kind,
name
)
if (!stableObject) {
core.debug(
'Stable object not found. Creating only canary object'
)
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
newObjectsList.push(newCanaryObject)
} else {
if (
!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)
) {
throw Error(`StableSpecSelectorNotExist : ${name}`)
}
core.debug(
'Stable object found. Creating canary and baseline objects'
)
const newCanaryObject =
canaryDeploymentHelper.getNewCanaryResource(
inputObject,
canaryReplicaCount
)
const newBaselineObject =
canaryDeploymentHelper.getNewBaselineResource(
stableObject,
canaryReplicaCount
)
newObjectsList.push(newCanaryObject)
newObjectsList.push(newBaselineObject)
}
} else {
// Update non deployment entity as it is
newObjectsList.push(inputObject)
}
})
})
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(newFilePaths, forceDeployment)
await createCanaryService(kubectl, filePaths)
return {result, newFilePaths}
}
async function createCanaryService(kubectl: Kubectl, filePaths: string[]) {
const newObjectsList = []
const trafficObjectsList = []
for (const filePath of filePaths) {
const fileContents = fs.readFileSync(filePath).toString()
const parsedYaml = yaml.safeLoadAll(fileContents)
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name
const kind = inputObject.kind
if (isServiceEntity(kind)) {
const newCanaryServiceObject =
canaryDeploymentHelper.getNewCanaryResource(inputObject)
newObjectsList.push(newCanaryServiceObject)
const newBaselineServiceObject =
canaryDeploymentHelper.getNewBaselineResource(inputObject)
newObjectsList.push(newBaselineServiceObject)
const stableObject = await canaryDeploymentHelper.fetchResource(
kubectl,
kind,
canaryDeploymentHelper.getStableResourceName(name)
)
if (!stableObject) {
const newStableServiceObject =
canaryDeploymentHelper.getStableResource(inputObject)
newObjectsList.push(newStableServiceObject)
core.debug('Creating the traffic object for service: ' + name)
const trafficObject = await createTrafficSplitManifestFile(
kubectl,
name,
0,
0,
1000
)
trafficObjectsList.push(trafficObject)
} else {
let updateTrafficObject = true
const trafficObject = await canaryDeploymentHelper.fetchResource(
kubectl,
TRAFFIC_SPLIT_OBJECT,
getTrafficSplitResourceName(name)
)
if (trafficObject) {
const trafficJObject = JSON.parse(
JSON.stringify(trafficObject)
)
if (trafficJObject?.spec?.backends) {
trafficJObject.spec.backends.forEach((s) => {
if (
s.service ===
canaryDeploymentHelper.getCanaryResourceName(
name
) &&
s.weight === '1000m'
) {
core.debug('Update traffic objcet not required')
updateTrafficObject = false
}
})
}
}
if (updateTrafficObject) {
core.debug(
'Stable service object present so updating the traffic object for service: ' +
name
)
trafficObjectsList.push(
updateTrafficSplitObject(kubectl, name)
)
}
}
}
}
}
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
manifestFiles.push(...trafficObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(manifestFiles, forceDeployment)
checkForErrors([result])
}
export async function redirectTrafficToCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
) {
await adjustTraffic(kubectl, manifestFilePaths, 0, 1000)
}
export async function redirectTrafficToStableDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
) {
await adjustTraffic(kubectl, manifestFilePaths, 1000, 0)
}
async function adjustTraffic(
kubectl: Kubectl,
manifestFilePaths: string[],
stableWeight: number,
canaryWeight: number
) {
if (!manifestFilePaths || manifestFilePaths?.length == 0) {
return
}
const trafficSplitManifests = []
for (const filePath of manifestFilePaths) {
const fileContents = fs.readFileSync(filePath).toString()
const parsedYaml = yaml.safeLoadAll(fileContents)
for (const inputObject of parsedYaml) {
const name = inputObject.metadata.name
const kind = inputObject.kind
if (isServiceEntity(kind)) {
trafficSplitManifests.push(
await createTrafficSplitManifestFile(
kubectl,
name,
stableWeight,
0,
canaryWeight
)
)
}
}
}
if (trafficSplitManifests.length <= 0) {
return
}
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(trafficSplitManifests, forceDeployment)
checkForErrors([result])
}
async function updateTrafficSplitObject(
kubectl: Kubectl,
serviceName: string
): Promise<string> {
const percentage = parseInt(core.getInput('percentage'))
if (percentage < 0 || percentage > 100)
throw Error('Percentage must be between 0 and 100')
const percentageWithMuliplier = percentage * 10
const baselineAndCanaryWeight = percentageWithMuliplier / 2
const stableDeploymentWeight = 1000 - percentageWithMuliplier
core.debug(
'Creating the traffic object with canary weight: ' +
baselineAndCanaryWeight +
',baseling weight: ' +
baselineAndCanaryWeight +
',stable: ' +
stableDeploymentWeight
)
return await createTrafficSplitManifestFile(
kubectl,
serviceName,
stableDeploymentWeight,
baselineAndCanaryWeight,
baselineAndCanaryWeight
)
}
async function createTrafficSplitManifestFile(
kubectl: Kubectl,
serviceName: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
): Promise<string> {
const smiObjectString = await getTrafficSplitObject(
kubectl,
serviceName,
stableWeight,
baselineWeight,
canaryWeight
)
const manifestFile = fileHelper.writeManifestToFile(
smiObjectString,
TRAFFIC_SPLIT_OBJECT,
serviceName
)
if (!manifestFile) {
throw new Error('Unable to create traffic split manifest file')
}
return manifestFile
}
let trafficSplitAPIVersion = ''
async function getTrafficSplitObject(
kubectl: Kubectl,
name: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
): Promise<string> {
// cached version
if (!trafficSplitAPIVersion) {
trafficSplitAPIVersion = await kubectlUtils.getTrafficSplitAPIVersion(
kubectl
)
}
return JSON.stringify({
apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit',
metadata: {
name: getTrafficSplitResourceName(name)
},
spec: {
backends: [
{
service: canaryDeploymentHelper.getStableResourceName(name),
weight: stableWeight
},
{
service: canaryDeploymentHelper.getBaselineResourceName(name),
weight: baselineWeight
},
{
service: canaryDeploymentHelper.getCanaryResourceName(name),
weight: canaryWeight
}
],
service: name
}
})
}
function getTrafficSplitResourceName(name: string) {
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX
}