Compare commits

..

3 Commits

Author SHA1 Message Date
Hariharan Subramanian 6627812069 format check changes 2022-06-27 20:42:08 +00:00
Hariharan Subramanian 9390dc6380 Logging Changes with group 2022-06-27 16:35:32 -04:00
Hariharan Subramanian 8da762e272 Logging changes for deploy 2022-06-27 16:33:31 -04:00
18 changed files with 61 additions and 175 deletions
+6 -12
View File
@@ -43,15 +43,7 @@ Following are the key capabilities of this action:
<tr>
<td>manifests </br></br>(Required)</td>
<td>Path to the manifest files to be used for deployment. These can also be directories containing manifest files, in which case, all manifest files in the referenced directory at every depth will be deployed. Files not ending in .yml or .yaml will be ignored.</td>
</tr>
<tr>
<td>strategy </br></br>(Required)</td>
<td>Acceptable values: basic/canary/blue-green. <br>
Default value: basic
<br>Deployment strategy to be used while applying manifest files on the cluster.
<br>basic - Template is force applied to all pods when deploying to cluster. NOTE: Can only be used with action == deploy
<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
</tr>
</tr>
<tr>
<td>namespace </br></br>(Optional)
<td>Namespace within the cluster to deploy to.</td>
@@ -70,13 +62,15 @@ Following are the key capabilities of this action:
<td>pull-images</br></br>(Optional)</td>
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations</td>
</tr>
<tr>
<td>strategy </br></br>(Optional)</td>
<td>Acceptable values: none/canary/blue-green. <br>
Deployment strategy to be used while applying manifest files on the cluster.<br>none - No deployment strategy is used when deploying.<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
</tr>
<tr>
<td>traffic-split-method </br></br>(Optional)</td>
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
</tr>
<tr>
<td>traffic-split-annotations </br></br>(Optional)</td>
<td>Annotations in the form of key/value pair to be added to TrafficSplit.</td>
<tr>
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
<td>Used to compute the number of replicas of &#39;-baseline&#39; and &#39;-canary&#39; variants of the workloads found in manifest files. For the specified percentage input, if (percentage * numberOfDesirerdReplicas)/100 is not a round number, the floor of this number is used while creating &#39;-baseline&#39; and &#39;-canary&#39;.<br/><br/>For example, if Deployment hello-world was found in the input manifest file with &#39;replicas: 4&#39; and if &#39;strategy: canary&#39; and &#39;percentage: 25&#39; are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The &#39;-baseline&#39; variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the &#39;-canary&#39; variant is created with the image and tag corresponding to the new changes being deployed</td>
+3 -6
View File
@@ -20,9 +20,9 @@ inputs:
required: false
default: true
strategy:
description: 'Deployment strategy to be used. Allowed values are basic, canary and blue-green'
required: true
default: 'basic'
description: 'Deployment strategy to be used. Allowed values are none, canary and blue-green'
required: false
default: 'none'
route-method:
description: 'Route based on service, ingress or SMI for blue-green strategy'
required: false
@@ -35,9 +35,6 @@ inputs:
description: 'Traffic split method to be used. Allowed values are pod and smi'
required: false
default: 'pod'
traffic-split-annotations:
description: 'Annotations in the form of key/value pair to be added to TrafficSplit. Relevant only if deployement strategy is blue-green or canary'
required: false
baseline-and-canary-replicas:
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
required: false
-16
View File
@@ -22,7 +22,6 @@
"@types/jest": "^26.0.0",
"@types/js-yaml": "^3.12.7",
"@types/node": "^12.20.41",
"@vercel/ncc": "^0.34.0",
"jest": "^26.0.0",
"prettier": "2.7.1",
"ts-jest": "^26.0.0",
@@ -1156,15 +1155,6 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
"dev": true
},
"node_modules/@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
"dev": true,
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -7096,12 +7086,6 @@
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
"dev": true
},
"@vercel/ncc": {
"version": "0.34.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
"dev": true
},
"abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
+1 -2
View File
@@ -4,7 +4,7 @@
"author": "Deepak Sattiraju",
"license": "MIT",
"scripts": {
"build": "ncc build src/run.ts -o lib",
"build": "tsc --outDir ./lib --rootDir ./src",
"test": "jest",
"format": "prettier --write .",
"format-check": "prettier --check ."
@@ -23,7 +23,6 @@
"@types/jest": "^26.0.0",
"@types/js-yaml": "^3.12.7",
"@types/node": "^12.20.41",
"@vercel/ncc": "^0.34.0",
"jest": "^26.0.0",
"prettier": "2.7.1",
"ts-jest": "^26.0.0",
+2 -4
View File
@@ -19,8 +19,7 @@ import {parseRouteStrategy} from '../types/routeStrategy'
export async function deploy(
kubectl: Kubectl,
manifestFilePaths: string[],
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
deploymentStrategy: DeploymentStrategy
) {
// update manifests
const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths)
@@ -35,8 +34,7 @@ export async function deploy(
inputManifestFiles,
deploymentStrategy,
kubectl,
trafficSplitMethod,
annotations
trafficSplitMethod
)
core.endGroup()
core.debug('Deployed manifest files: ' + deployedManifestFiles)
+12 -26
View File
@@ -40,15 +40,14 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function promote(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
deploymentStrategy: DeploymentStrategy
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await promoteCanary(kubectl, manifests)
break
case DeploymentStrategy.BLUE_GREEN:
await promoteBlueGreen(kubectl, manifests, annotations)
await promoteBlueGreen(kubectl, manifests)
break
default:
throw Error('Invalid promote deployment strategy')
@@ -66,30 +65,26 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
// 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
core.startGroup('Redirecting traffic to canary deployment')
core.info('Redirecting traffic to canary deployment')
await SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(
kubectl,
manifests
)
core.endGroup()
core.startGroup('Deploying input manifests with SMI canary strategy')
core.info('Deploying input manifests with SMI canary strategy')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.endGroup()
core.startGroup('Redirecting traffic to stable deployment')
core.info('Redirecting traffic to stable deployment')
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
)
core.endGroup()
} else {
core.startGroup('Deploying input manifests')
core.info('Deploying input manifests')
await deploy.deploy(kubectl, manifests, DeploymentStrategy.CANARY)
core.endGroup()
}
core.startGroup('Deleting canary and baseline workloads')
core.info('Deleting canary and baseline workloads')
try {
await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl,
@@ -102,14 +97,9 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
ex
)
}
core.endGroup()
}
async function promoteBlueGreen(
kubectl: Kubectl,
manifests: string[],
annotations: {[key: string]: string} = {}
) {
async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
// update container images and pull secrets
const inputManifestFiles: string[] = updateManifestFiles(manifests)
const manifestObjects: BlueGreenManifests =
@@ -119,7 +109,7 @@ async function promoteBlueGreen(
core.getInput('route-method', {required: true})
)
core.startGroup('Deleting old deployment and making new one')
core.info('Deleting old deployment and making new one')
let result
if (routeStrategy == RouteStrategy.INGRESS) {
result = await promoteBlueGreenIngress(kubectl, manifestObjects)
@@ -128,10 +118,9 @@ async function promoteBlueGreen(
} else {
result = await promoteBlueGreenService(kubectl, manifestObjects)
}
core.endGroup()
// checking stability of newly created deployments
core.startGroup('Checking manifest stability')
core.info('Checking manifest stability')
const deployedManifestFiles = result.newFilePaths
const resources: Resource[] = getResources(
deployedManifestFiles,
@@ -140,9 +129,8 @@ async function promoteBlueGreen(
])
)
await KubernetesManifestUtility.checkManifestStability(kubectl, resources)
core.endGroup()
core.startGroup(
core.info(
'Routing to new deployments and deleting old workloads and services'
)
if (routeStrategy == RouteStrategy.INGRESS) {
@@ -162,8 +150,7 @@ async function promoteBlueGreen(
await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList,
annotations
manifestObjects.serviceEntityList
)
await deleteWorkloadsWithLabel(
kubectl,
@@ -183,5 +170,4 @@ async function promoteBlueGreen(
manifestObjects.deploymentEntityList
)
}
core.endGroup()
}
+7 -15
View File
@@ -15,15 +15,14 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
export async function reject(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy,
annotations: {[key: string]: string} = {}
deploymentStrategy: DeploymentStrategy
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await rejectCanary(kubectl, manifests)
break
case DeploymentStrategy.BLUE_GREEN:
await rejectBlueGreen(kubectl, manifests, annotations)
await rejectBlueGreen(kubectl, manifests)
break
default:
throw 'Invalid delete deployment strategy'
@@ -37,30 +36,24 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
core.getInput('traffic-split-method', {required: true})
)
if (trafficSplitMethod == TrafficSplitMethod.SMI) {
core.startGroup('Rejecting deployment with SMI canary strategy')
core.info('Rejecting deployment with SMI canary strategy')
includeServices = true
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
)
core.endGroup()
}
core.startGroup('Deleting baseline and canary workloads')
core.info('Deleting baseline and canary workloads')
await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl,
manifests,
includeServices
)
core.endGroup()
}
async function rejectBlueGreen(
kubectl: Kubectl,
manifests: string[],
annotations: {[key: string]: string} = {}
) {
core.startGroup('Rejecting deployment with blue green strategy')
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
core.info('Rejecting deployment with blue green strategy')
const routeStrategy = parseRouteStrategy(
core.getInput('route-method', {required: true})
@@ -68,9 +61,8 @@ async function rejectBlueGreen(
if (routeStrategy == RouteStrategy.INGRESS) {
await rejectBlueGreenIngress(kubectl, manifests)
} else if (routeStrategy == RouteStrategy.SMI) {
await rejectBlueGreenSMI(kubectl, manifests, annotations)
await rejectBlueGreenSMI(kubectl, manifests)
} else {
await rejectBlueGreenService(kubectl, manifests)
}
core.endGroup()
}
+3 -7
View File
@@ -6,7 +6,6 @@ import {reject} from './actions/reject'
import {Action, parseAction} from './types/action'
import {parseDeploymentStrategy} from './types/deploymentStrategy'
import {getFilesFromDirectories} from './utilities/fileUtils'
import {parseAnnotations} from './types/annotations'
export async function run() {
// verify kubeconfig is set
@@ -19,9 +18,6 @@ export async function run() {
const action: Action | undefined = parseAction(
core.getInput('action', {required: true})
)
const annotations = parseAnnotations(
core.getInput('annotations', {required: false})
)
const strategy = parseDeploymentStrategy(core.getInput('strategy'))
const manifestsInput = core.getInput('manifests', {required: true})
const manifestFilePaths = manifestsInput
@@ -38,15 +34,15 @@ export async function run() {
// run action
switch (action) {
case Action.DEPLOY: {
await deploy(kubectl, fullManifestFilePaths, strategy, annotations)
await deploy(kubectl, fullManifestFilePaths, strategy)
break
}
case Action.PROMOTE: {
await promote(kubectl, fullManifestFilePaths, strategy, annotations)
await promote(kubectl, fullManifestFilePaths, strategy)
break
}
case Action.REJECT: {
await reject(kubectl, fullManifestFilePaths, strategy, annotations)
await reject(kubectl, fullManifestFilePaths, strategy)
break
}
default: {
@@ -40,8 +40,7 @@ export interface BlueGreenManifests {
export async function routeBlueGreen(
kubectl: Kubectl,
inputManifestFiles: string[],
routeStrategy: RouteStrategy,
annotations: {[key: string]: string} = {}
routeStrategy: RouteStrategy
) {
// sleep for buffer time
const bufferTime: number = parseInt(
@@ -75,8 +74,7 @@ export async function routeBlueGreen(
await routeBlueGreenSMI(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList,
annotations
manifestObjects.serviceEntityList
)
} else {
await routeBlueGreenService(
@@ -23,8 +23,7 @@ const MAX_VAL = 100
export async function deployBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[],
annotations: {[key: string]: string} = {}
filePaths: string[]
) {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@@ -38,7 +37,7 @@ export async function deployBlueGreenSMI(
await kubectl.apply(manifestFiles)
// make extraservices and trafficsplit
await setupSMI(kubectl, manifestObjects.serviceEntityList, annotations)
await setupSMI(kubectl, manifestObjects.serviceEntityList)
// create new deloyments
return await createWorkloadsWithLabel(
@@ -69,18 +68,16 @@ export async function promoteBlueGreenSMI(kubectl: Kubectl, manifestObjects) {
export async function rejectBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[],
annotations: {[key: string]: string} = {}
filePaths: string[]
) {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
// route trafficsplit to stable deployments
// route trafficsplit to stable deploymetns
await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList,
annotations
manifestObjects.serviceEntityList
)
// delete rejected new bluegreen deployments
@@ -94,11 +91,7 @@ export async function rejectBlueGreenSMI(
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
}
export async function setupSMI(
kubectl: Kubectl,
serviceEntityList: any[],
annotations: {[key: string]: string} = {}
) {
export async function setupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
const newObjectsList = []
const trafficObjectList = []
@@ -124,8 +117,7 @@ export async function setupSMI(
createTrafficSplitObject(
kubectl,
inputObject.metadata.name,
NONE_LABEL_VALUE,
annotations
NONE_LABEL_VALUE
)
})
}
@@ -135,8 +127,7 @@ let trafficSplitAPIVersion = ''
async function createTrafficSplitObject(
kubectl: Kubectl,
name: string,
nextLabel: string,
annotations: {[key: string]: string} = {}
nextLabel: string
): Promise<any> {
// cache traffic split api version
if (!trafficSplitAPIVersion)
@@ -154,8 +145,7 @@ async function createTrafficSplitObject(
apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit',
metadata: {
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
annotations: annotations
name: getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
},
spec: {
service: name,
@@ -204,16 +194,14 @@ export function getSMIServiceResource(
export async function routeBlueGreenSMI(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[],
annotations: {[key: string]: string} = {}
serviceEntityList: any[]
) {
for (const serviceObject of serviceEntityList) {
// route trafficsplit to given label
await createTrafficSplitObject(
kubectl,
serviceObject.metadata.name,
nextLabel,
annotations
nextLabel
)
}
}
@@ -288,8 +288,7 @@ async function getTrafficSplitObject(
name: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number,
annotations: {[key: string]: string} = {}
canaryWeight: number
): Promise<string> {
// cached version
if (!trafficSplitAPIVersion) {
@@ -302,8 +301,7 @@ async function getTrafficSplitObject(
apiVersion: trafficSplitAPIVersion,
kind: 'TrafficSplit',
metadata: {
name: getTrafficSplitResourceName(name),
annotations: annotations
name: getTrafficSplitResourceName(name)
},
spec: {
backends: [
+10 -13
View File
@@ -23,8 +23,7 @@ import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
import {ExecOutput} from '@actions/exec'
import {
getWorkflowAnnotationKeyLabel,
getWorkflowAnnotations,
cleanLabel
getWorkflowAnnotations
} from '../utilities/workflowAnnotationUtils'
import {
annotateChildPods,
@@ -41,8 +40,7 @@ export async function deployManifests(
files: string[],
deploymentStrategy: DeploymentStrategy,
kubectl: Kubectl,
trafficSplitMethod: TrafficSplitMethod,
annotations: {[key: string]: string} = {}
trafficSplitMethod: TrafficSplitMethod
): Promise<string[]> {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: {
@@ -64,7 +62,7 @@ export async function deployManifests(
(routeStrategy == RouteStrategy.INGRESS &&
deployBlueGreenIngress(kubectl, files)) ||
(routeStrategy == RouteStrategy.SMI &&
deployBlueGreenSMI(kubectl, files, annotations)) ||
deployBlueGreenSMI(kubectl, files)) ||
deployBlueGreenService(kubectl, files)
)
@@ -72,7 +70,10 @@ export async function deployManifests(
return newFilePaths
}
case DeploymentStrategy.BASIC: {
case undefined: {
core.warning('Deployment strategy is not recognized.')
}
default: {
const trafficSplitMethod = parseTrafficSplitMethod(
core.getInput('traffic-split-method', {required: true})
)
@@ -93,10 +94,6 @@ export async function deployManifests(
return files
}
default: {
throw new Error('Deployment strategy is not recognized.')
}
}
}
@@ -216,10 +213,10 @@ async function labelResources(
label: string
) {
const labels = [
`workflowFriendlyName=${cleanLabel(
normalizeWorkflowStrLabel(process.env.GITHUB_WORKFLOW)
`workflowFriendlyName=${normalizeWorkflowStrLabel(
process.env.GITHUB_WORKFLOW
)}`,
`workflow=${cleanLabel(label)}`
`workflow=${label}`
]
checkForErrors([await kubectl.labelFiles(files, labels)], true)
-8
View File
@@ -1,8 +0,0 @@
export function parseAnnotations(str: string) {
if (str == '') {
return {}
} else {
const annotaion = JSON.parse(str)
return new Map(annotaion)
}
}
-1
View File
@@ -5,7 +5,6 @@ describe('Deployment strategy type', () => {
const vals = <any>Object.values(DeploymentStrategy)
expect(vals.includes('canary')).toBe(true)
expect(vals.includes('blue-green')).toBe(true)
expect(vals.includes('basic')).toBe(true)
})
test('it can parse valid values from a string', () => {
-1
View File
@@ -1,5 +1,4 @@
export enum DeploymentStrategy {
BASIC = 'basic',
CANARY = 'canary',
BLUE_GREEN = 'blue-green'
}
+1 -6
View File
@@ -23,12 +23,7 @@ export async function getDeploymentConfig(): Promise<DeploymentConfig> {
)
}
let imageNames = core.getInput('images').split('\n') || []
imageNames.forEach((element) => {
if (element === null || '/^s+$/') {
imageNames.pop()
}
})
const imageNames = core.getInput('images').split('\n') || []
const imageDockerfilePathMap: {[id: string]: string} = {}
const pullImages = !(core.getInput('pull-images').toLowerCase() === 'false')
+1 -16
View File
@@ -1,7 +1,4 @@
import {
cleanLabel,
prefixObjectKeys
} from '../utilities/workflowAnnotationUtils'
import {prefixObjectKeys} from '../utilities/workflowAnnotationUtils'
describe('WorkflowAnnotationUtils', () => {
describe('prefixObjectKeys', () => {
@@ -18,16 +15,4 @@ describe('WorkflowAnnotationUtils', () => {
expect(prefixObjectKeys(obj, prefix)).toEqual(expected)
})
})
describe('cleanLabel', () => {
it('should clean label', () => {
const alreadyClean = 'alreadyClean'
expect(cleanLabel(alreadyClean)).toEqual(alreadyClean)
expect(cleanLabel('.startInvalid')).toEqual('startInvalid')
expect(cleanLabel('with%S0ME&invalid#chars')).toEqual(
'withS0MEinvalidchars'
)
expect(cleanLabel('with⚒️emoji')).toEqual('withemoji')
})
})
})
-11
View File
@@ -47,14 +47,3 @@ export function getWorkflowAnnotationKeyLabel(
.digest('hex')
return `githubWorkflow_${hashKey}`
}
/**
* Cleans label to match valid kubernetes label specification by removing invalid characters
* @param label
* @returns cleaned label
*/
export function cleanLabel(label: string): string {
const removedInvalidChars = label.replace(/[^-A-Za-z0-9_.]/gi, '')
const regex = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
return regex.exec(removedInvalidChars)[0] || ''
}