mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-21 10:39:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42f5b49202 |
@@ -13,15 +13,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
@@ -29,7 +34,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -43,4 +48,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
name: release Project
|
||||
name: Create release PR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- CHANGELOG.md
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
description: 'Define release version (ex: v1, v2, v3)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: Azure/action-release-workflows/.github/workflows/release_js_project.yaml@81e6a8ed41ced9d131dea884ecae7b8c6dc4f799
|
||||
release-pr:
|
||||
uses: OliverMKing/javascript-release-workflow/.github/workflows/release-pr.yml@main
|
||||
with:
|
||||
changelogPath: ./CHANGELOG.md
|
||||
release: ${{ github.event.inputs.release }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Setup Minikube
|
||||
uses: medyagh/setup-minikube@latest
|
||||
with:
|
||||
minikube-version: 1.31.2
|
||||
minikube-version: 1.24.0
|
||||
kubernetes-version: 1.22.3
|
||||
driver: 'none'
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Setup Minikube
|
||||
uses: medyagh/setup-minikube@latest
|
||||
with:
|
||||
minikube-version: 1.31.2
|
||||
minikube-version: 1.24.0
|
||||
kubernetes-version: 1.22.3
|
||||
driver: 'none'
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
run-integration-test:
|
||||
name: Run Minikube Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
KUBECONFIG: /home/runner/.kube/config
|
||||
NAMESPACE: test-${{ github.run_id }}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
name: Minikube Integration Tests - resource annotation
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-integration-test:
|
||||
name: Run Minikube Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KUBECONFIG: /home/runner/.kube/config
|
||||
NAMESPACE: test-${{ github.run_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf node_modules/
|
||||
npm install
|
||||
- name: Install ncc
|
||||
run: npm i -g @vercel/ncc
|
||||
- name: Install conntrack
|
||||
run: sudo apt-get install -y conntrack
|
||||
- name: Build
|
||||
run: ncc build src/run.ts -o lib
|
||||
|
||||
- uses: Azure/setup-kubectl@v3
|
||||
name: Install Kubectl
|
||||
|
||||
- id: setup-minikube
|
||||
name: Setup Minikube
|
||||
uses: medyagh/setup-minikube@latest
|
||||
with:
|
||||
minikube-version: 1.24.0
|
||||
kubernetes-version: 1.22.3
|
||||
driver: 'none'
|
||||
timeout-minutes: 3
|
||||
|
||||
- name: Create namespace to run tests
|
||||
run: kubectl create ns ${{ env.NAMESPACE }}
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Cleaning any previously created items
|
||||
run: |
|
||||
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}
|
||||
python test/integration/k8s-deploy-delete.py 'Deployment' 'all' ${{ env.NAMESPACE }}
|
||||
python test/integration/k8s-deploy-delete.py 'Ingress' 'all' ${{ env.NAMESPACE }}
|
||||
|
||||
- name: Executing deploy action for pod with resource annotation enabled by default
|
||||
uses: ./
|
||||
with:
|
||||
namespace: ${{ env.NAMESPACE }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test.yml
|
||||
action: deploy
|
||||
|
||||
- name: Checking if deployments is created with additional resource annotation
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py namespace=${{ env.NAMESPACE }} kind=Deployment name=nginx-deployment containerName=nginx:1.14.2 labels=app:nginx,workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Minikube_Integration_Tests_-_resource_annotation selectorLabels=app:nginx annotations=actions.github.com/k8s-deploy,deployment.kubernetes.io/revision,kubectl.kubernetes.io/last-applied-configuration
|
||||
|
||||
- name: Cleaning previously created deployment
|
||||
run: |
|
||||
python test/integration/k8s-deploy-delete.py 'Deployment' 'all' ${{ env.NAMESPACE }}
|
||||
|
||||
- name: Executing deploy action for pod with resource annotation disabled
|
||||
uses: ./
|
||||
with:
|
||||
namespace: ${{ env.NAMESPACE }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test.yml
|
||||
action: deploy
|
||||
annotate-resources: false
|
||||
|
||||
- name: Checking if deployment is created without additional resource annotation
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py namespace=${{ env.NAMESPACE }} kind=Deployment name=nginx-deployment containerName=nginx:1.14.2 selectorLabels=app:nginx annotations=deployment.kubernetes.io/revision,kubectl.kubernetes.io/last-applied-configuration
|
||||
@@ -11,10 +11,9 @@ on: # rebuild any PRs and main branch changes
|
||||
|
||||
jobs:
|
||||
build: # make sure build/ci works properly
|
||||
name: Run Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v1
|
||||
- run: |
|
||||
npm install
|
||||
npm test
|
||||
|
||||
@@ -2,5 +2,6 @@ node_modules
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
lib/
|
||||
|
||||
coverage/
|
||||
@@ -1,11 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## [v4.10.0] - 2023-10-30
|
||||
|
||||
### Added
|
||||
|
||||
- #287 Make annotating resources optional
|
||||
- #283 Fix “Service” route-method of the Blue-Green strategy with some manifest files
|
||||
- #281 bump codeql to node 16
|
||||
- #279 upgrade codeql
|
||||
- #276 Fixes multiple namespaces bug
|
||||
@@ -113,13 +113,9 @@ Following are the key capabilities of this action:
|
||||
<td>force </br></br>(Optional)</td>
|
||||
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>annotate-resources</br></br>(Optional)</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to annotate the resources or not. If set to false all annotations are skipped completely.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>annotate-namespace</br></br>(Optional)</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to annotate the namespace resources object or not. Ignored when annotate-resources is set to false.</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to annotate the namespace resources object or not</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>skip-tls-verify</br></br>(Optional)</td>
|
||||
|
||||
+1
-5
@@ -59,12 +59,8 @@ inputs:
|
||||
description: 'Github token'
|
||||
default: ${{ github.token }}
|
||||
required: true
|
||||
annotate-resources:
|
||||
description: 'Annotate the resources. If set to false all annotations are skipped completely.'
|
||||
required: false
|
||||
default: true
|
||||
annotate-namespace:
|
||||
description: 'Annotate the target namespace. Ignored when annotate-resources is set to false.'
|
||||
description: 'Annotate the target namespace'
|
||||
required: false
|
||||
default: true
|
||||
private-cluster:
|
||||
|
||||
-24128
File diff suppressed because one or more lines are too long
Generated
+252
-422
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -4,7 +4,7 @@
|
||||
"author": "Deepak Sattiraju",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "npm i ncc && npx ncc build src/run.ts -o lib",
|
||||
"build": "npx ncc build src/run.ts -o lib",
|
||||
"test": "jest",
|
||||
"coverage": "jest --coverage=true",
|
||||
"format": "prettier --write .",
|
||||
@@ -18,15 +18,14 @@
|
||||
"@octokit/core": "^3.5.1",
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@types/minipass": "^3.1.2",
|
||||
"js-yaml": "3.13.1",
|
||||
"ncc": "^0.3.6"
|
||||
"js-yaml": "3.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/js-yaml": "^3.12.7",
|
||||
"@types/node": "^12.20.41",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"jest": "^26.0.0",
|
||||
"ncc": "^0.3.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^26.0.0",
|
||||
"typescript": "3.9.5"
|
||||
|
||||
@@ -56,19 +56,24 @@ export async function deploy(
|
||||
for (const ingressResource of ingressResources) {
|
||||
await kubectl.getResource(
|
||||
KubernetesConstants.DiscoveryAndLoadBalancerResource.INGRESS,
|
||||
ingressResource.name,
|
||||
false,
|
||||
ingressResource.namespace
|
||||
ingressResource.name
|
||||
)
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
// annotate resources
|
||||
core.startGroup('Annotating resources')
|
||||
let allPods
|
||||
try {
|
||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||
} catch (e) {
|
||||
core.debug(`Unable to parse pods: ${e}`)
|
||||
}
|
||||
await annotateAndLabelResources(
|
||||
deployedManifestFiles,
|
||||
kubectl,
|
||||
resourceTypes
|
||||
resourceTypes,
|
||||
allPods
|
||||
)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
+19
-2
@@ -129,13 +129,19 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
|
||||
|
||||
// annotate resources
|
||||
core.startGroup('Annotating resources')
|
||||
let allPods
|
||||
try {
|
||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||
} catch (e) {
|
||||
core.debug(`Unable to parse pods: ${e}`)
|
||||
}
|
||||
const resources: Resource[] = getResources(
|
||||
filesToAnnotate,
|
||||
models.DEPLOYMENT_TYPES.concat([
|
||||
models.DiscoveryAndLoadBalancerResource.SERVICE
|
||||
])
|
||||
)
|
||||
await annotateAndLabelResources(filesToAnnotate, kubectl, resources)
|
||||
await annotateAndLabelResources(filesToAnnotate, kubectl, resources, allPods)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
@@ -213,6 +219,17 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
|
||||
|
||||
// annotate resources
|
||||
core.startGroup('Annotating resources')
|
||||
await annotateAndLabelResources(deployedManifestFiles, kubectl, resources)
|
||||
let allPods
|
||||
try {
|
||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||
} catch (e) {
|
||||
core.debug(`Unable to parse pods: ${e}`)
|
||||
}
|
||||
await annotateAndLabelResources(
|
||||
deployedManifestFiles,
|
||||
kubectl,
|
||||
resources,
|
||||
allPods
|
||||
)
|
||||
core.endGroup()
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ export async function deleteGreenObjects(
|
||||
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
|
||||
return {
|
||||
name: getBlueGreenResourceName(obj.metadata.name, GREEN_SUFFIX),
|
||||
kind: obj.kind,
|
||||
namespace: obj.metadata.namespace
|
||||
kind: obj.kind
|
||||
}
|
||||
})
|
||||
|
||||
@@ -67,25 +66,31 @@ export async function deleteObjects(
|
||||
// other common functions
|
||||
export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
||||
const deploymentEntityList: K8sObject[] = []
|
||||
const serviceEntityList: K8sObject[] = []
|
||||
const routedServiceEntityList: K8sObject[] = []
|
||||
const unroutedServiceEntityList: K8sObject[] = []
|
||||
const ingressEntityList: K8sObject[] = []
|
||||
const otherEntitiesList: K8sObject[] = []
|
||||
const serviceNameMap = new Map<string, string>()
|
||||
|
||||
// Manifest objects per type. All resources should be parsed and
|
||||
// organized before we can check if services are “routed” or not.
|
||||
filePaths.forEach((filePath: string) => {
|
||||
const fileContents = fs.readFileSync(filePath).toString()
|
||||
yaml.safeLoadAll(fileContents, (inputObject) => {
|
||||
if (!!inputObject) {
|
||||
const kind = inputObject.kind
|
||||
const name = inputObject.metadata.name
|
||||
|
||||
if (isDeploymentEntity(kind)) {
|
||||
deploymentEntityList.push(inputObject)
|
||||
} else if (isServiceEntity(kind)) {
|
||||
serviceEntityList.push(inputObject)
|
||||
if (isServiceRouted(inputObject, deploymentEntityList)) {
|
||||
routedServiceEntityList.push(inputObject)
|
||||
serviceNameMap.set(
|
||||
name,
|
||||
getBlueGreenResourceName(name, GREEN_SUFFIX)
|
||||
)
|
||||
} else {
|
||||
unroutedServiceEntityList.push(inputObject)
|
||||
}
|
||||
} else if (isIngressEntity(kind)) {
|
||||
ingressEntityList.push(inputObject)
|
||||
} else {
|
||||
@@ -95,16 +100,6 @@ export function getManifestObjects(filePaths: string[]): BlueGreenManifests {
|
||||
})
|
||||
})
|
||||
|
||||
serviceEntityList.forEach((inputObject: any) => {
|
||||
if (isServiceRouted(inputObject, deploymentEntityList)) {
|
||||
const name = inputObject.metadata.name
|
||||
routedServiceEntityList.push(inputObject)
|
||||
serviceNameMap.set(name, getBlueGreenResourceName(name, GREEN_SUFFIX))
|
||||
} else {
|
||||
unroutedServiceEntityList.push(inputObject)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
serviceEntityList: routedServiceEntityList,
|
||||
serviceNameMap: serviceNameMap,
|
||||
@@ -239,10 +234,9 @@ export function isServiceSelectorSubsetOfMatchLabel(
|
||||
export async function fetchResource(
|
||||
kubectl: Kubectl,
|
||||
kind: string,
|
||||
name: string,
|
||||
namespace?: string
|
||||
name: string
|
||||
): Promise<K8sObject> {
|
||||
const result = await kubectl.getResource(kind, name, false, namespace)
|
||||
const result = await kubectl.getResource(kind, name)
|
||||
if (result == null || !!result.stderr) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -97,8 +97,7 @@ export async function validateIngresses(
|
||||
const existingIngress = await fetchResource(
|
||||
kubectl,
|
||||
inputObject.kind,
|
||||
inputObject.metadata.name,
|
||||
inputObject?.metadata?.namespace
|
||||
inputObject.metadata.name
|
||||
)
|
||||
|
||||
const isValid =
|
||||
|
||||
@@ -31,8 +31,7 @@ export async function validateServicesState(
|
||||
const existingService = await fetchResource(
|
||||
kubectl,
|
||||
serviceObject.kind,
|
||||
serviceObject.metadata.name,
|
||||
serviceObject?.metadata?.namespace
|
||||
serviceObject.metadata.name
|
||||
)
|
||||
|
||||
let isServiceGreen =
|
||||
|
||||
@@ -142,8 +142,7 @@ export async function validateTrafficSplitsState(
|
||||
let trafficSplitObject = await fetchResource(
|
||||
kubectl,
|
||||
TRAFFIC_SPLIT_OBJECT,
|
||||
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX),
|
||||
serviceObject?.metadata?.namespace
|
||||
getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)
|
||||
)
|
||||
core.debug(
|
||||
`ts object extracted was ${JSON.stringify(trafficSplitObject)}`
|
||||
@@ -184,8 +183,7 @@ export async function cleanupSMI(
|
||||
serviceObject.metadata.name,
|
||||
GREEN_SUFFIX
|
||||
),
|
||||
kind: serviceObject.kind,
|
||||
namespace: serviceObject?.metadata?.namespace
|
||||
kind: serviceObject.kind
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -195,13 +195,9 @@ async function cleanUpCanary(
|
||||
files: string[],
|
||||
includeServices: boolean
|
||||
): Promise<string[]> {
|
||||
const deleteObject = async function (
|
||||
kind: string,
|
||||
name: string,
|
||||
namespace: string | undefined
|
||||
) {
|
||||
const deleteObject = async function (kind, name) {
|
||||
try {
|
||||
const result = await kubectl.delete([kind, name], namespace)
|
||||
const result = await kubectl.delete([kind, name])
|
||||
checkForErrors([result])
|
||||
} catch (ex) {
|
||||
// Ignore failures of delete if it doesn't exist
|
||||
@@ -217,7 +213,6 @@ async function cleanUpCanary(
|
||||
for (const inputObject of parsedYaml) {
|
||||
const name = inputObject.metadata.name
|
||||
const kind = inputObject.kind
|
||||
const namespace: string | undefined = inputObject?.metadata?.namespace
|
||||
|
||||
if (
|
||||
isDeploymentEntity(kind) ||
|
||||
@@ -227,8 +222,8 @@ async function cleanUpCanary(
|
||||
const canaryObjectName = getCanaryResourceName(name)
|
||||
const baselineObjectName = getBaselineResourceName(name)
|
||||
|
||||
await deleteObject(kind, canaryObjectName, namespace)
|
||||
await deleteObject(kind, baselineObjectName, namespace)
|
||||
await deleteObject(kind, canaryObjectName)
|
||||
await deleteObject(kind, baselineObjectName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,45 +147,32 @@ export async function checkManifestStability(
|
||||
export async function annotateAndLabelResources(
|
||||
files: string[],
|
||||
kubectl: Kubectl,
|
||||
resourceTypes: Resource[]
|
||||
resourceTypes: Resource[],
|
||||
allPods: any
|
||||
) {
|
||||
const defaultWorkflowFileName = 'k8s-deploy-failed-workflow-annotation'
|
||||
const githubToken = core.getInput('token')
|
||||
let workflowFilePath
|
||||
try {
|
||||
workflowFilePath = await getWorkflowFilePath(githubToken)
|
||||
} catch (ex) {
|
||||
core.warning(`Failed to extract workflow file name: ${ex}`)
|
||||
workflowFilePath = defaultWorkflowFileName
|
||||
}
|
||||
const workflowFilePath = await getWorkflowFilePath(githubToken)
|
||||
|
||||
const deploymentConfig = await getDeploymentConfig()
|
||||
const annotationKeyLabel = getWorkflowAnnotationKeyLabel()
|
||||
|
||||
const shouldAnnotateResources = !(
|
||||
core.getInput('annotate-resources').toLowerCase() === 'false'
|
||||
)
|
||||
|
||||
if (shouldAnnotateResources) {
|
||||
await annotateResources(
|
||||
files,
|
||||
kubectl,
|
||||
resourceTypes,
|
||||
annotationKeyLabel,
|
||||
workflowFilePath,
|
||||
deploymentConfig
|
||||
).catch((err) => core.warning(`Failed to annotate resources: ${err} `))
|
||||
}
|
||||
|
||||
await labelResources(files, kubectl, annotationKeyLabel).catch((err) =>
|
||||
core.warning(`Failed to label resources: ${err}`)
|
||||
await annotateResources(
|
||||
files,
|
||||
kubectl,
|
||||
resourceTypes,
|
||||
allPods,
|
||||
annotationKeyLabel,
|
||||
workflowFilePath,
|
||||
deploymentConfig
|
||||
)
|
||||
await labelResources(files, kubectl, annotationKeyLabel)
|
||||
}
|
||||
|
||||
async function annotateResources(
|
||||
files: string[],
|
||||
kubectl: Kubectl,
|
||||
resourceTypes: Resource[],
|
||||
allPods: any,
|
||||
annotationKey: string,
|
||||
workflowFilePath: string,
|
||||
deploymentConfig: DeploymentConfig
|
||||
@@ -221,21 +208,14 @@ async function annotateResources(
|
||||
)
|
||||
if (annotateNamespace) {
|
||||
annotateResults.push(
|
||||
await kubectl.annotate(
|
||||
'namespace',
|
||||
namespace,
|
||||
annotationKeyValStr,
|
||||
namespace
|
||||
)
|
||||
await kubectl.annotate('namespace', namespace, annotationKeyValStr)
|
||||
)
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const annotateResult = await kubectl.annotateFiles(
|
||||
file,
|
||||
annotationKeyValStr,
|
||||
namespace
|
||||
annotationKeyValStr
|
||||
)
|
||||
annotateResults.push(annotateResult)
|
||||
} catch (e) {
|
||||
@@ -253,8 +233,8 @@ async function annotateResources(
|
||||
kubectl,
|
||||
resource.type,
|
||||
resource.name,
|
||||
resource.namespace,
|
||||
annotationKeyValStr
|
||||
annotationKeyValStr,
|
||||
allPods
|
||||
)
|
||||
).forEach((execResult) => annotateResults.push(execResult))
|
||||
}
|
||||
@@ -278,7 +258,7 @@ async function labelResources(
|
||||
const labelResults = []
|
||||
for (const file of files) {
|
||||
try {
|
||||
const labelResult = await kubectl.labelFiles(file, labels)
|
||||
const labelResult = await kubectl.labelFiles(files, labels)
|
||||
labelResults.push(labelResult)
|
||||
} catch (e) {
|
||||
core.warning(`failed to annotate resource: ${e}`)
|
||||
|
||||
@@ -2,7 +2,6 @@ export interface K8sObject {
|
||||
metadata: {
|
||||
name: string
|
||||
labels: Map<string, string>
|
||||
namespace?: string
|
||||
}
|
||||
kind: string
|
||||
spec: any
|
||||
@@ -17,7 +16,6 @@ export interface K8sServiceObject extends K8sObject {
|
||||
export interface K8sDeleteObject {
|
||||
name: string
|
||||
kind: string
|
||||
namespace?: string
|
||||
}
|
||||
|
||||
export interface K8sIngress extends K8sObject {
|
||||
|
||||
+1
-176
@@ -3,6 +3,7 @@ import * as exec from '@actions/exec'
|
||||
import * as io from '@actions/io'
|
||||
import * as core from '@actions/core'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import {config} from 'process'
|
||||
|
||||
describe('Kubectl path', () => {
|
||||
const version = '1.1'
|
||||
@@ -37,7 +38,6 @@ describe('Kubectl path', () => {
|
||||
const kubectlPath = 'kubectlPath'
|
||||
const testNamespace = 'testNamespace'
|
||||
const defaultNamespace = 'default'
|
||||
const otherNamespace = 'otherns'
|
||||
describe('Kubectl class', () => {
|
||||
describe('default namespace behavior', () => {
|
||||
const kubectl = new Kubectl(kubectlPath, defaultNamespace)
|
||||
@@ -122,26 +122,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// overrided ns
|
||||
const silent = false
|
||||
await kubectl.describe(
|
||||
resourceType,
|
||||
resourceName,
|
||||
silent,
|
||||
otherNamespace
|
||||
)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'describe',
|
||||
resourceType,
|
||||
resourceName,
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent}
|
||||
)
|
||||
})
|
||||
|
||||
it('describes a resource silently', async () => {
|
||||
@@ -160,26 +140,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: true}
|
||||
)
|
||||
|
||||
// overrided ns
|
||||
const silent = false
|
||||
await kubectl.describe(
|
||||
resourceType,
|
||||
resourceName,
|
||||
silent,
|
||||
otherNamespace
|
||||
)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'describe',
|
||||
resourceType,
|
||||
resourceName,
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent}
|
||||
)
|
||||
})
|
||||
|
||||
it('annotates resource', async () => {
|
||||
@@ -205,27 +165,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.annotate(
|
||||
resourceType,
|
||||
resourceName,
|
||||
annotation,
|
||||
otherNamespace
|
||||
)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'annotate',
|
||||
resourceType,
|
||||
resourceName,
|
||||
annotation,
|
||||
'--overwrite',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('annotates files with single file', async () => {
|
||||
@@ -246,22 +185,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.annotateFiles(file, annotation, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'annotate',
|
||||
'-f',
|
||||
file,
|
||||
annotation,
|
||||
'--overwrite',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('annotates files with mulitple files', async () => {
|
||||
@@ -282,22 +205,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.annotateFiles(files, annotation, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'annotate',
|
||||
'-f',
|
||||
files.join(','),
|
||||
annotation,
|
||||
'--overwrite',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('labels files with single file', async () => {
|
||||
@@ -318,21 +225,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
await kubectl.labelFiles(file, labels, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'label',
|
||||
'-f',
|
||||
file,
|
||||
...labels,
|
||||
'--overwrite',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('labels files with multiple files', async () => {
|
||||
@@ -353,21 +245,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
await kubectl.labelFiles(files, labels, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'label',
|
||||
'-f',
|
||||
files.join(','),
|
||||
...labels,
|
||||
'--overwrite',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('gets all pods', async () => {
|
||||
@@ -396,20 +273,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.checkRolloutStatus(resourceType, name, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'rollout',
|
||||
'status',
|
||||
`${resourceType}/${name}`,
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('gets resource', async () => {
|
||||
@@ -428,22 +291,6 @@ describe('Kubectl class', () => {
|
||||
],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
const silent = true
|
||||
await kubectl.getResource(resourceType, name, silent, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
[
|
||||
'get',
|
||||
`${resourceType}/${name}`,
|
||||
'-o',
|
||||
'json',
|
||||
'--namespace',
|
||||
otherNamespace
|
||||
],
|
||||
{silent}
|
||||
)
|
||||
})
|
||||
|
||||
it('executes a command', async () => {
|
||||
@@ -474,14 +321,6 @@ describe('Kubectl class', () => {
|
||||
['delete', arg, '--namespace', testNamespace],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.delete(arg, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
['delete', arg, '--namespace', otherNamespace],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
|
||||
it('deletes with multiple arguments', async () => {
|
||||
@@ -492,14 +331,6 @@ describe('Kubectl class', () => {
|
||||
['delete', ...args, '--namespace', testNamespace],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
// override ns
|
||||
await kubectl.delete(args, otherNamespace)
|
||||
expect(exec.getExecOutput).toBeCalledWith(
|
||||
kubectlPath,
|
||||
['delete', ...args, '--namespace', otherNamespace],
|
||||
{silent: false}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -538,11 +369,5 @@ describe('Kubectl class', () => {
|
||||
[command, '--insecure-skip-tls-verify', '--namespace', testNamespace],
|
||||
{silent: false}
|
||||
)
|
||||
|
||||
const kubectlNoFlags = new Kubectl(kubectlPath)
|
||||
kubectlNoFlags.executeCommand(command)
|
||||
expect(exec.getExecOutput).toBeCalledWith(kubectlPath, [command], {
|
||||
silent: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+34
-60
@@ -3,11 +3,11 @@ import {createInlineArray} from '../utilities/arrayUtils'
|
||||
import * as core from '@actions/core'
|
||||
import * as toolCache from '@actions/tool-cache'
|
||||
import * as io from '@actions/io'
|
||||
import {exec} from 'child_process'
|
||||
|
||||
export interface Resource {
|
||||
name: string
|
||||
type: string
|
||||
namespace?: string
|
||||
}
|
||||
|
||||
export class Kubectl {
|
||||
@@ -20,7 +20,7 @@ export class Kubectl {
|
||||
|
||||
constructor(
|
||||
kubectlPath: string,
|
||||
namespace: string = '',
|
||||
namespace: string = 'default',
|
||||
ignoreSSLErrors: boolean = false,
|
||||
resourceGroup: string = '',
|
||||
name: string = ''
|
||||
@@ -47,7 +47,7 @@ export class Kubectl {
|
||||
]
|
||||
if (force) applyArgs.push('--force')
|
||||
|
||||
return await this.execute(applyArgs.concat(this.getFlags()))
|
||||
return await this.execute(applyArgs)
|
||||
} catch (err) {
|
||||
core.debug('Kubectl apply failed:' + err)
|
||||
}
|
||||
@@ -56,43 +56,27 @@ export class Kubectl {
|
||||
public async describe(
|
||||
resourceType: string,
|
||||
resourceName: string,
|
||||
silent: boolean = false,
|
||||
namespace?: string
|
||||
silent: boolean = false
|
||||
): Promise<ExecOutput> {
|
||||
return await this.execute(
|
||||
['describe', resourceType, resourceName].concat(
|
||||
this.getFlags(namespace)
|
||||
),
|
||||
['describe', resourceType, resourceName],
|
||||
silent
|
||||
)
|
||||
}
|
||||
|
||||
public async getNewReplicaSet(deployment: string, namespace?: string) {
|
||||
const result = await this.describe(
|
||||
'deployment',
|
||||
deployment,
|
||||
true,
|
||||
namespace
|
||||
)
|
||||
public async getNewReplicaSet(deployment: string) {
|
||||
const result = await this.describe('deployment', deployment, true)
|
||||
|
||||
let newReplicaSet = ''
|
||||
if (result?.stdout) {
|
||||
const stdout = result.stdout.split('\n')
|
||||
core.debug('stdout from getNewReplicaSet is ' + JSON.stringify(stdout))
|
||||
stdout.forEach((line: string) => {
|
||||
const newreplicaset = 'newreplicaset'
|
||||
if (line && line.toLowerCase().indexOf(newreplicaset) > -1) {
|
||||
core.debug(
|
||||
`found string of interest for replicaset, line is ${line}`
|
||||
)
|
||||
core.debug(
|
||||
`substring is ${line.substring(newreplicaset.length).trim()}`
|
||||
)
|
||||
if (line && line.toLowerCase().indexOf(newreplicaset) > -1)
|
||||
newReplicaSet = line
|
||||
.substring(newreplicaset.length)
|
||||
.trim()
|
||||
.split(' ')[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,8 +86,7 @@ export class Kubectl {
|
||||
public async annotate(
|
||||
resourceType: string,
|
||||
resourceName: string,
|
||||
annotation: string,
|
||||
namespace?: string
|
||||
annotation: string
|
||||
): Promise<ExecOutput> {
|
||||
const args = [
|
||||
'annotate',
|
||||
@@ -111,14 +94,13 @@ export class Kubectl {
|
||||
resourceName,
|
||||
annotation,
|
||||
'--overwrite'
|
||||
].concat(this.getFlags(namespace))
|
||||
]
|
||||
return await this.execute(args)
|
||||
}
|
||||
|
||||
public async annotateFiles(
|
||||
files: string | string[],
|
||||
annotation: string,
|
||||
namespace?: string
|
||||
annotation: string
|
||||
): Promise<ExecOutput> {
|
||||
const filesToAnnotate = createInlineArray(files)
|
||||
core.debug(`annotating ${filesToAnnotate} with annotation ${annotation}`)
|
||||
@@ -128,14 +110,16 @@ export class Kubectl {
|
||||
filesToAnnotate,
|
||||
annotation,
|
||||
'--overwrite'
|
||||
].concat(this.getFlags(namespace))
|
||||
]
|
||||
core.debug(
|
||||
`sending args from annotate to execute: ${JSON.stringify(args)}`
|
||||
)
|
||||
return await this.execute(args)
|
||||
}
|
||||
|
||||
public async labelFiles(
|
||||
files: string | string[],
|
||||
labels: string[],
|
||||
namespace?: string
|
||||
labels: string[]
|
||||
): Promise<ExecOutput> {
|
||||
const args = [
|
||||
'label',
|
||||
@@ -143,59 +127,51 @@ export class Kubectl {
|
||||
createInlineArray(files),
|
||||
...labels,
|
||||
'--overwrite'
|
||||
].concat(this.getFlags(namespace))
|
||||
]
|
||||
return await this.execute(args)
|
||||
}
|
||||
|
||||
public async getAllPods(): Promise<ExecOutput> {
|
||||
return await this.execute(
|
||||
['get', 'pods', '-o', 'json'].concat(this.getFlags()),
|
||||
true
|
||||
)
|
||||
return await this.execute(['get', 'pods', '-o', 'json'], true)
|
||||
}
|
||||
|
||||
public async checkRolloutStatus(
|
||||
resourceType: string,
|
||||
name: string,
|
||||
namespace?: string
|
||||
name: string
|
||||
): Promise<ExecOutput> {
|
||||
return await this.execute(
|
||||
['rollout', 'status', `${resourceType}/${name}`].concat(
|
||||
this.getFlags(namespace)
|
||||
)
|
||||
)
|
||||
return await this.execute([
|
||||
'rollout',
|
||||
'status',
|
||||
`${resourceType}/${name}`
|
||||
])
|
||||
}
|
||||
|
||||
public async getResource(
|
||||
resourceType: string,
|
||||
name: string,
|
||||
silentFailure: boolean = false,
|
||||
namespace?: string
|
||||
silentFailure: boolean = false
|
||||
): Promise<ExecOutput> {
|
||||
core.debug(
|
||||
'fetching resource of type ' + resourceType + ' and name ' + name
|
||||
)
|
||||
return await this.execute(
|
||||
['get', `${resourceType}/${name}`, '-o', 'json'].concat(
|
||||
this.getFlags(namespace)
|
||||
),
|
||||
['get', `${resourceType}/${name}`, '-o', 'json'],
|
||||
silentFailure
|
||||
)
|
||||
}
|
||||
|
||||
public executeCommand(command: string, args?: string) {
|
||||
if (!command) throw new Error('Command must be defined')
|
||||
const a = args ? [args] : []
|
||||
return this.execute([command, ...a.concat(this.getFlags())])
|
||||
return args ? this.execute([command, args]) : this.execute([command])
|
||||
}
|
||||
|
||||
public delete(args: string | string[], namespace?: string) {
|
||||
if (typeof args === 'string')
|
||||
return this.execute(['delete', args].concat(this.getFlags(namespace)))
|
||||
return this.execute(['delete', ...args.concat(this.getFlags(namespace))])
|
||||
public delete(args: string | string[]) {
|
||||
if (typeof args === 'string') return this.execute(['delete', args])
|
||||
return this.execute(['delete', ...args])
|
||||
}
|
||||
|
||||
protected async execute(args: string[], silent: boolean = false) {
|
||||
args = args.concat(this.getExecuteFlags())
|
||||
core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
|
||||
|
||||
return await getExecOutput(this.kubectlPath, args, {
|
||||
@@ -203,15 +179,13 @@ export class Kubectl {
|
||||
})
|
||||
}
|
||||
|
||||
protected getFlags(namespaceOverride?: string): string[] {
|
||||
protected getExecuteFlags(): string[] {
|
||||
const flags = []
|
||||
if (this.ignoreSSLErrors) {
|
||||
flags.push('--insecure-skip-tls-verify')
|
||||
}
|
||||
|
||||
const ns = namespaceOverride || this.namespace
|
||||
if (ns) {
|
||||
flags.push('--namespace', ns)
|
||||
if (this.namespace) {
|
||||
flags.push('--namespace', this.namespace)
|
||||
}
|
||||
|
||||
return flags
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
import {PrivateKubectl} from './privatekubectl'
|
||||
import * as exec from '@actions/exec'
|
||||
|
||||
describe('Private kubectl', () => {
|
||||
const testString = `kubectl annotate -f test.yml,test2.yml,test3.yml -f test4.yml --filename test5.yml actions.github.com/k8s-deploy={"run":"3498366832","repository":"jaiveerk/k8s-deploy","workflow":"Minikube Integration Tests - private cluster","workflowFileName":"run-integration-tests-private.yml","jobName":"run-integration-test","createdBy":"jaiveerk","runUri":"https://github.com/jaiveerk/k8s-deploy/actions/runs/3498366832","commit":"c63b323186ea1320a31290de6dcc094c06385e75","lastSuccessRunCommit":"NA","branch":"refs/heads/main","deployTimestamp":1668787848577,"dockerfilePaths":{"nginx:1.14.2":""},"manifestsPaths":["https://github.com/jaiveerk/k8s-deploy/blob/c63b323186ea1320a31290de6dcc094c06385e75/test/integration/manifests/test.yml"],"helmChartPaths":[],"provider":"GitHub"} --overwrite --namespace test-3498366832`
|
||||
const mockKube = new PrivateKubectl(
|
||||
'kubectlPath',
|
||||
'namespace',
|
||||
true,
|
||||
'resourceGroup',
|
||||
'resourceName'
|
||||
)
|
||||
const mockKube = new PrivateKubectl('')
|
||||
|
||||
it('should extract filenames correctly', () => {
|
||||
expect(mockKube.extractFilesnames(testString)).toEqual(
|
||||
'test.yml test2.yml test3.yml test4.yml test5.yml'
|
||||
)
|
||||
})
|
||||
|
||||
test('Should throw well defined Error on error from Azure', async () => {
|
||||
const errorMsg = 'An error message'
|
||||
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
|
||||
return {exitCode: 1, stdout: '', stderr: errorMsg}
|
||||
})
|
||||
|
||||
await expect(mockKube.executeCommand('az', 'test')).rejects.toThrow(
|
||||
Error(
|
||||
`Call to private cluster failed. Command: 'kubectl az test --insecure-skip-tls-verify --namespace namespace', errormessage: ${errorMsg}`
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,8 @@ import * as path from 'path'
|
||||
|
||||
export class PrivateKubectl extends Kubectl {
|
||||
protected async execute(args: string[], silent: boolean = false) {
|
||||
args = args.concat(this.getExecuteFlags())
|
||||
|
||||
args.unshift('kubectl')
|
||||
let kubectlCmd = args.join(' ')
|
||||
let addFileFlag = false
|
||||
@@ -73,18 +75,11 @@ export class PrivateKubectl extends Kubectl {
|
||||
runOutput
|
||||
)}`
|
||||
)
|
||||
|
||||
if (runOutput.exitCode !== 0) {
|
||||
throw Error(
|
||||
`Call to private cluster failed. Command: '${kubectlCmd}', errormessage: ${runOutput.stderr}`
|
||||
)
|
||||
}
|
||||
|
||||
const runObj: {logs: string; exitCode: number} = JSON.parse(
|
||||
runOutput.stdout
|
||||
)
|
||||
if (!silent) core.info(runObj.logs)
|
||||
if (runObj.exitCode !== 0) {
|
||||
if (runOutput.exitCode !== 0 && runObj.exitCode !== 0) {
|
||||
throw Error(`failed private cluster Kubectl command: ${kubectlCmd}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import * as core from '@actions/core'
|
||||
import {ExecOutput} from '@actions/exec'
|
||||
import {Kubectl} from '../types/kubectl'
|
||||
|
||||
const NAMESPACE = 'namespace'
|
||||
|
||||
export function checkForErrors(
|
||||
execResults: ExecOutput[],
|
||||
warnIfError?: boolean
|
||||
@@ -32,12 +30,7 @@ export async function getLastSuccessfulRunSha(
|
||||
annotationKey: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const result = await kubectl.getResource(
|
||||
NAMESPACE,
|
||||
namespaceName,
|
||||
false,
|
||||
namespaceName
|
||||
)
|
||||
const result = await kubectl.getResource('namespace', namespaceName)
|
||||
if (result?.stderr) {
|
||||
core.warning(result.stderr)
|
||||
return process.env.GITHUB_SHA
|
||||
@@ -60,23 +53,15 @@ export async function annotateChildPods(
|
||||
kubectl: Kubectl,
|
||||
resourceType: string,
|
||||
resourceName: string,
|
||||
namespace: string | undefined,
|
||||
annotationKeyValStr: string
|
||||
annotationKeyValStr: string,
|
||||
allPods
|
||||
): Promise<ExecOutput[]> {
|
||||
let owner = resourceName
|
||||
if (resourceType.toLowerCase().indexOf('deployment') > -1) {
|
||||
owner = await kubectl.getNewReplicaSet(resourceName, namespace)
|
||||
owner = await kubectl.getNewReplicaSet(resourceName)
|
||||
}
|
||||
|
||||
const commandExecutionResults = []
|
||||
|
||||
let allPods
|
||||
try {
|
||||
allPods = JSON.parse((await kubectl.getAllPods()).stdout)
|
||||
} catch (e) {
|
||||
core.debug(`Unable to parse pods: ${e}`)
|
||||
}
|
||||
|
||||
if (allPods?.items && allPods.items?.length > 0) {
|
||||
allPods.items.forEach((pod) => {
|
||||
const owners = pod?.metadata?.ownerReferences
|
||||
@@ -87,8 +72,7 @@ export async function annotateChildPods(
|
||||
kubectl.annotate(
|
||||
'pod',
|
||||
pod.metadata.name,
|
||||
annotationKeyValStr,
|
||||
namespace
|
||||
annotationKeyValStr
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
@@ -4,9 +4,6 @@ import {Kubectl, Resource} from '../types/kubectl'
|
||||
import {checkForErrors} from './kubectlUtils'
|
||||
import {sleep} from './timeUtils'
|
||||
|
||||
const IS_SILENT = false
|
||||
const POD = 'pod'
|
||||
|
||||
export async function checkManifestStability(
|
||||
kubectl: Kubectl,
|
||||
resources: Resource[]
|
||||
@@ -23,35 +20,24 @@ export async function checkManifestStability(
|
||||
try {
|
||||
const result = await kubectl.checkRolloutStatus(
|
||||
resource.type,
|
||||
resource.name,
|
||||
resource.namespace
|
||||
resource.name
|
||||
)
|
||||
checkForErrors([result])
|
||||
} catch (ex) {
|
||||
core.error(ex)
|
||||
await kubectl.describe(
|
||||
resource.type,
|
||||
resource.name,
|
||||
IS_SILENT,
|
||||
resource.namespace
|
||||
)
|
||||
await kubectl.describe(resource.type, resource.name)
|
||||
rolloutStatusHasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.type == KubernetesConstants.KubernetesWorkload.POD) {
|
||||
try {
|
||||
await checkPodStatus(kubectl, resource)
|
||||
await checkPodStatus(kubectl, resource.name)
|
||||
} catch (ex) {
|
||||
core.warning(
|
||||
`Could not determine pod status: ${JSON.stringify(ex)}`
|
||||
)
|
||||
await kubectl.describe(
|
||||
resource.type,
|
||||
resource.name,
|
||||
IS_SILENT,
|
||||
resource.namespace
|
||||
)
|
||||
await kubectl.describe(resource.type, resource.name)
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -59,11 +45,14 @@ export async function checkManifestStability(
|
||||
KubernetesConstants.DiscoveryAndLoadBalancerResource.SERVICE
|
||||
) {
|
||||
try {
|
||||
const service = await getService(kubectl, resource)
|
||||
const service = await getService(kubectl, resource.name)
|
||||
const {spec, status} = service
|
||||
if (spec.type === KubernetesConstants.ServiceTypes.LOAD_BALANCER) {
|
||||
if (!isLoadBalancerIPAssigned(status)) {
|
||||
await waitForServiceExternalIPAssignment(kubectl, resource)
|
||||
await waitForServiceExternalIPAssignment(
|
||||
kubectl,
|
||||
resource.name
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`ServiceExternalIP ${resource.name} ${status.loadBalancer.ingress[0].ip}`
|
||||
@@ -74,12 +63,7 @@ export async function checkManifestStability(
|
||||
core.warning(
|
||||
`Could not determine service status of: ${resource.name} Error: ${ex}`
|
||||
)
|
||||
await kubectl.describe(
|
||||
resource.type,
|
||||
resource.name,
|
||||
IS_SILENT,
|
||||
resource.namespace
|
||||
)
|
||||
await kubectl.describe(resource.type, resource.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +75,7 @@ export async function checkManifestStability(
|
||||
|
||||
export async function checkPodStatus(
|
||||
kubectl: Kubectl,
|
||||
pod: Resource
|
||||
podName: string
|
||||
): Promise<void> {
|
||||
const sleepTimeout = 10 * 1000 // 10 seconds
|
||||
const iterations = 60 // 60 * 10 seconds timeout = 10 minutes max timeout
|
||||
@@ -101,8 +85,8 @@ export async function checkPodStatus(
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await sleep(sleepTimeout)
|
||||
|
||||
core.debug(`Polling for pod status: ${pod.name}`)
|
||||
podStatus = await getPodStatus(kubectl, pod)
|
||||
core.debug(`Polling for pod status: ${podName}`)
|
||||
podStatus = await getPodStatus(kubectl, podName)
|
||||
|
||||
if (
|
||||
podStatus &&
|
||||
@@ -113,42 +97,37 @@ export async function checkPodStatus(
|
||||
}
|
||||
}
|
||||
|
||||
podStatus = await getPodStatus(kubectl, pod)
|
||||
podStatus = await getPodStatus(kubectl, podName)
|
||||
switch (podStatus.phase) {
|
||||
case 'Succeeded':
|
||||
case 'Running':
|
||||
if (isPodReady(podStatus)) {
|
||||
console.log(`pod/${pod.name} is successfully rolled out`)
|
||||
console.log(`pod/${podName} is successfully rolled out`)
|
||||
} else {
|
||||
kubectlDescribeNeeded = true
|
||||
}
|
||||
break
|
||||
case 'Pending':
|
||||
if (!isPodReady(podStatus)) {
|
||||
core.warning(`pod/${pod.name} rollout status check timed out`)
|
||||
core.warning(`pod/${podName} rollout status check timed out`)
|
||||
kubectlDescribeNeeded = true
|
||||
}
|
||||
break
|
||||
case 'Failed':
|
||||
core.error(`pod/${pod.name} rollout failed`)
|
||||
core.error(`pod/${podName} rollout failed`)
|
||||
kubectlDescribeNeeded = true
|
||||
break
|
||||
default:
|
||||
core.warning(`pod/${pod.name} rollout status: ${podStatus.phase}`)
|
||||
core.warning(`pod/${podName} rollout status: ${podStatus.phase}`)
|
||||
}
|
||||
|
||||
if (kubectlDescribeNeeded) {
|
||||
await kubectl.describe(POD, pod.name, IS_SILENT, pod.namespace)
|
||||
await kubectl.describe('pod', podName)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPodStatus(kubectl: Kubectl, pod: Resource) {
|
||||
const podResult = await kubectl.getResource(
|
||||
POD,
|
||||
pod.name,
|
||||
IS_SILENT,
|
||||
pod.namespace
|
||||
)
|
||||
async function getPodStatus(kubectl: Kubectl, podName: string) {
|
||||
const podResult = await kubectl.getResource('pod', podName)
|
||||
checkForErrors([podResult])
|
||||
|
||||
return JSON.parse(podResult.stdout).status
|
||||
@@ -172,12 +151,10 @@ function isPodReady(podStatus: any): boolean {
|
||||
return allContainersAreReady
|
||||
}
|
||||
|
||||
async function getService(kubectl: Kubectl, service: Resource) {
|
||||
async function getService(kubectl: Kubectl, serviceName) {
|
||||
const serviceResult = await kubectl.getResource(
|
||||
KubernetesConstants.DiscoveryAndLoadBalancerResource.SERVICE,
|
||||
service.name,
|
||||
IS_SILENT,
|
||||
service.namespace
|
||||
serviceName
|
||||
)
|
||||
|
||||
checkForErrors([serviceResult])
|
||||
@@ -186,25 +163,25 @@ async function getService(kubectl: Kubectl, service: Resource) {
|
||||
|
||||
async function waitForServiceExternalIPAssignment(
|
||||
kubectl: Kubectl,
|
||||
service: Resource
|
||||
serviceName: string
|
||||
): Promise<void> {
|
||||
const sleepTimeout = 10 * 1000 // 10 seconds
|
||||
const iterations = 18 // 18 * 10 seconds timeout = 3 minutes max timeout
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
core.info(`Wait for service ip assignment : ${service.name}`)
|
||||
core.info(`Wait for service ip assignment : ${serviceName}`)
|
||||
await sleep(sleepTimeout)
|
||||
|
||||
const status = (await getService(kubectl, service)).status
|
||||
const status = (await getService(kubectl, serviceName)).status
|
||||
if (isLoadBalancerIPAssigned(status)) {
|
||||
core.info(
|
||||
`ServiceExternalIP ${service.name} ${status.loadBalancer.ingress[0].ip}`
|
||||
`ServiceExternalIP ${serviceName} ${status.loadBalancer.ingress[0].ip}`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
core.warning(`Wait for service ip assignment timed out ${service.name}`)
|
||||
core.warning(`Wait for service ip assignment timed out${serviceName}`)
|
||||
}
|
||||
|
||||
function isLoadBalancerIPAssigned(status: any) {
|
||||
|
||||
@@ -280,8 +280,7 @@ export function getResources(
|
||||
) {
|
||||
resources.push({
|
||||
type: inputObject.kind,
|
||||
name: inputObject.metadata.name,
|
||||
namespace: inputObject?.metadata?.namespace
|
||||
name: inputObject.metadata.name
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
cleanLabel,
|
||||
removeInvalidLabelCharacters,
|
||||
VALID_LABEL_REGEX
|
||||
} from '../utilities/workflowAnnotationUtils'
|
||||
import {cleanLabel} from '../utilities/workflowAnnotationUtils'
|
||||
|
||||
describe('WorkflowAnnotationUtils', () => {
|
||||
describe('cleanLabel', () => {
|
||||
@@ -20,14 +16,5 @@ describe('WorkflowAnnotationUtils', () => {
|
||||
cleanLabel('Workflow Name / With Slashes / And Spaces')
|
||||
).toEqual('Workflow_Name_-_With_Slashes_-_And_Spaces')
|
||||
})
|
||||
it('should return a blank string when regex fails (https://github.com/Azure/k8s-deploy/issues/266)', () => {
|
||||
const label = '持续部署'
|
||||
expect(cleanLabel(label)).toEqual('github-workflow-file')
|
||||
|
||||
let removedInvalidChars = removeInvalidLabelCharacters(label)
|
||||
|
||||
const regexResult = VALID_LABEL_REGEX.exec(removedInvalidChars)
|
||||
expect(regexResult).toBe(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,8 +2,6 @@ import {DeploymentConfig} from '../types/deploymentConfig'
|
||||
|
||||
const ANNOTATION_PREFIX = 'actions.github.com'
|
||||
|
||||
export const VALID_LABEL_REGEX = /([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]/
|
||||
|
||||
export function getWorkflowAnnotations(
|
||||
lastSuccessRunSha: string,
|
||||
workflowFilePath: string,
|
||||
@@ -39,17 +37,11 @@ export function getWorkflowAnnotationKeyLabel(): string {
|
||||
* @returns cleaned label
|
||||
*/
|
||||
export function cleanLabel(label: string): string {
|
||||
let removedInvalidChars = removeInvalidLabelCharacters(label)
|
||||
|
||||
const regexResult = VALID_LABEL_REGEX.exec(removedInvalidChars) || [
|
||||
'github-workflow-file'
|
||||
]
|
||||
return regexResult[0]
|
||||
}
|
||||
|
||||
export function removeInvalidLabelCharacters(label: string): string {
|
||||
return label
|
||||
let removedInvalidChars = label
|
||||
.replace(/\s/gi, '_')
|
||||
.replace(/[\/\\\|]/gi, '-')
|
||||
.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] || ''
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ def delete(kind, name, namespace):
|
||||
if (name == "all"):
|
||||
print('kubectl delete --all' + kind + ' -n ' + namespace)
|
||||
deletion = subprocess.Popen(
|
||||
['kubectl', 'delete', kind, '--all', '--namespace', namespace])
|
||||
['kubectl', 'delete', kind, name, '--namespace', namespace])
|
||||
result, err = deletion.communicate()
|
||||
else:
|
||||
print('kubectl delete ' + kind + ' ' + name + ' -n ' + namespace)
|
||||
@@ -21,7 +21,7 @@ def delete(kind, name, namespace):
|
||||
def main():
|
||||
kind = sys.argv[1]
|
||||
name = sys.argv[2]
|
||||
namespace = sys.argv[3]
|
||||
namespace = 'test-' + sys.argv[3]
|
||||
delete(kind, name, namespace)
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ def parseArgs(sysArgs):
|
||||
argsDict[labelsKey] = stringListToDict(
|
||||
argsDict[labelsKey].split(","), ":")
|
||||
|
||||
if annotationsKey in argsDict:
|
||||
argsDict[annotationsKey] = stringListToDict(
|
||||
argsDict[annotationsKey].split(","), ":")
|
||||
|
||||
if selectorLabelsKey in argsDict:
|
||||
argsDict[selectorLabelsKey] = stringListToDict(
|
||||
argsDict[selectorLabelsKey].split(","), ":")
|
||||
@@ -56,9 +60,6 @@ def parseArgs(sysArgs):
|
||||
if ingressServicesKey in argsDict:
|
||||
argsDict[ingressServicesKey] = argsDict[ingressServicesKey].split(",")
|
||||
|
||||
if annotationsKey in argsDict:
|
||||
argsDict[annotationsKey] = argsDict[annotationsKey].split(",")
|
||||
|
||||
return argsDict
|
||||
|
||||
|
||||
@@ -97,14 +98,14 @@ def verifyDeployment(deployment, parsedArgs):
|
||||
return dictMatch, msg
|
||||
|
||||
if annotationsKey in parsedArgs:
|
||||
if len(parsedArgs[annotationsKey]) != len(deployment['metadata']['annotations']):
|
||||
return False, f"expected {len(parsedArgs[annotationsKey])} annotations but found {len(deployment['metadata']['annotations'])}"
|
||||
keysPresent, msg = validateKeyPresence(
|
||||
deployment['metadata']['annotations'], parsedArgs[annotationsKey])
|
||||
if not keysPresent:
|
||||
return keysPresent, msg
|
||||
dictMatch, msg = compareDicts(
|
||||
deployment['metadata']['annotations'], parsedArgs[annotationsKey], annotationsKey)
|
||||
if not dictMatch:
|
||||
return dictMatch, msg
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def verifyService(service, parsedArgs):
|
||||
# test selector labels, labels, annotations
|
||||
if not selectorLabelsKey in parsedArgs:
|
||||
@@ -123,10 +124,10 @@ def verifyService(service, parsedArgs):
|
||||
return dictMatch, msg
|
||||
|
||||
if annotationsKey in parsedArgs:
|
||||
keysPresent, msg = validateKeyPresence(
|
||||
service['metadata']['annotations'], parsedArgs[annotationsKey])
|
||||
if not keysPresent:
|
||||
return keysPresent, msg
|
||||
dictMatch, msg = compareDicts(
|
||||
service['metadata']['annotations'], parsedArgs[annotationsKey], annotationsKey)
|
||||
if not dictMatch:
|
||||
return dictMatch, msg
|
||||
|
||||
return True, ""
|
||||
|
||||
@@ -187,13 +188,6 @@ def compareDicts(actual: dict, expected: dict, paramName=""):
|
||||
|
||||
return True, ""
|
||||
|
||||
def validateKeyPresence(actualDict: dict, expectedKeys: list):
|
||||
actualKeys = actualDict.keys()
|
||||
for key in expectedKeys:
|
||||
if key not in actualKeys:
|
||||
return False, f"expected key {key} not found in actual dict. \n actual dict keys {','.join(actualKeys)}"
|
||||
|
||||
return True, ""
|
||||
|
||||
def main():
|
||||
parsedArgs: dict = parseArgs(sys.argv[1:])
|
||||
|
||||
Reference in New Issue
Block a user