mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-26 22:59:27 +08:00
Vidya reddy/prettier code (#203)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user