From 58ba3f066551e5c78a4b8ebec9f51a5de2b769af Mon Sep 17 00:00:00 2001 From: Jaiveer Katariya <35347859+jaiveerk@users.noreply.github.com> Date: Mon, 21 Nov 2022 10:30:35 -0500 Subject: [PATCH] new commit with all changes (#258) --- .../workflows/run-integration-tests-basic.yml | 2 +- .../run-integration-tests-private.yml | 81 +++++++++++++++++++ action.yml | 1 + src/types/kubectl.test.ts | 11 --- src/types/kubectl.ts | 9 ++- src/types/privatekubectl.test.ts | 12 +++ src/types/privatekubectl.ts | 56 +++++++++---- test/integration/k8s-deploy-test.py | 29 +++++-- 8 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/run-integration-tests-private.yml create mode 100644 src/types/privatekubectl.test.ts diff --git a/.github/workflows/run-integration-tests-basic.yml b/.github/workflows/run-integration-tests-basic.yml index f78208fb1..43b0aaa01 100644 --- a/.github/workflows/run-integration-tests-basic.yml +++ b/.github/workflows/run-integration-tests-basic.yml @@ -64,7 +64,7 @@ jobs: test/integration/manifests/test.yml action: deploy - - name: Checking if deployments and services were created with canary labels and original tag + - name: Checking if deployments and services were created 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_-_basic selectorLabels=app:nginx python test/integration/k8s-deploy-test.py namespace=${{ env.NAMESPACE }} kind=Service name=nginx-service labels=workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Minikube_Integration_Tests_-_basic selectorLabels=app:nginx diff --git a/.github/workflows/run-integration-tests-private.yml b/.github/workflows/run-integration-tests-private.yml new file mode 100644 index 000000000..bb40438f5 --- /dev/null +++ b/.github/workflows/run-integration-tests-private.yml @@ -0,0 +1,81 @@ +name: Cluster Integration Tests - private cluster +on: + pull_request: + branches: + - 'releases/*' + push: + branches: + - main + 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 }} + permissions: + contents: read + id-token: write + 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: Build + run: ncc build src/run.ts -o lib + - name: Azure login + uses: azure/login@v1.4.3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - uses: Azure/setup-kubectl@v3 + name: Install Kubectl + + - name: Create private AKS cluster and set context + run: | + set +x + # create cluster + az group create --location eastus --name ${{ env.NAMESPACE }} + az aks create --name ${{ env.NAMESPACE }} --resource-group ${{ env.NAMESPACE }} --enable-private-cluster --generate-ssh-keys + az aks get-credentials --resource-group ${{ env.NAMESPACE }} --name ${{ env.NAMESPACE }} + + - name: Create namespace to run tests + run: | + az aks command invoke --resource-group ${{ env.NAMESPACE }} --name ${{ env.NAMESPACE }} --command "kubectl create ns ${{ env.NAMESPACE }}" + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.x' + + - name: Executing deploy action for pod + uses: ./ + with: + namespace: ${{ env.NAMESPACE }} + images: nginx:1.14.2 + manifests: | + test/integration/manifests/test.yml + action: deploy + private-cluster: true + resource-group: ${{ env.NAMESPACE }} + name: ${{ env.NAMESPACE }} + + - name: Checking if deployments and services were created + run: | + python test/integration/k8s-deploy-test.py private=${{ env.NAMESPACE }} namespace=${{ env.NAMESPACE }} kind=Deployment name=nginx-deployment containerName=nginx:1.14.2 labels=app:nginx,workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Cluster_Integration_Tests_-_private_cluster selectorLabels=app:nginx + python test/integration/k8s-deploy-test.py private=${{ env.NAMESPACE }} namespace=${{ env.NAMESPACE }} kind=Service name=nginx-service labels=workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Cluster_Integration_Tests_-_private_cluster selectorLabels=app:nginx + + - name: Clean up AKS cluster + if: ${{ always() }} + run: | + echo "deleting AKS cluster and resource group" + az aks delete --yes --resource-group ${{ env.NAMESPACE }} --name ${{ env.NAMESPACE }} + az group delete --resource-group ${{ env.NAMESPACE }} --yes diff --git a/action.yml b/action.yml index 9ec828847..5a6faf1c7 100644 --- a/action.yml +++ b/action.yml @@ -6,6 +6,7 @@ inputs: namespace: description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will run in the default namespace.' required: false + default: default manifests: description: 'Path to the manifest files which will be used for deployment.' required: true diff --git a/src/types/kubectl.test.ts b/src/types/kubectl.test.ts index cf7689153..fd1ac00a0 100644 --- a/src/types/kubectl.test.ts +++ b/src/types/kubectl.test.ts @@ -48,17 +48,6 @@ describe('Kubectl class', () => { return execReturn }) }) - - describe('omits default namespace from commands', () => { - it('executes a command without appending --namespace arg', async () => { - // no args - const command = 'command' - expect(await kubectl.executeCommand(command)).toBe(execReturn) - expect(exec.getExecOutput).toBeCalledWith(kubectlPath, [command], { - silent: false - }) - }) - }) }) describe('with a success exec return in testNamespace', () => { diff --git a/src/types/kubectl.ts b/src/types/kubectl.ts index f97533d9a..b92279086 100644 --- a/src/types/kubectl.ts +++ b/src/types/kubectl.ts @@ -102,13 +102,18 @@ export class Kubectl { files: string | string[], annotation: string ): Promise { + const filesToAnnotate = createInlineArray(files) + core.debug(`annotating ${filesToAnnotate} with annotation ${annotation}`) const args = [ 'annotate', '-f', - createInlineArray(files), + filesToAnnotate, annotation, '--overwrite' ] + core.debug( + `sending args from annotate to execute: ${JSON.stringify(args)}` + ) return await this.execute(args) } @@ -169,7 +174,7 @@ export class Kubectl { if (this.ignoreSSLErrors) { args.push('--insecure-skip-tls-verify') } - if (this.namespace && this.namespace != 'default') { + if (this.namespace) { args = args.concat(['--namespace', this.namespace]) } core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`) diff --git a/src/types/privatekubectl.test.ts b/src/types/privatekubectl.test.ts new file mode 100644 index 000000000..437a21cbf --- /dev/null +++ b/src/types/privatekubectl.test.ts @@ -0,0 +1,12 @@ +import {PrivateKubectl} from './privatekubectl' + +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('') + + it('should extract filenames correctly', () => { + expect(mockKube.extractFilesnames(testString)).toEqual( + 'test.yml test2.yml test3.yml test4.yml test5.yml' + ) + }) +}) diff --git a/src/types/privatekubectl.ts b/src/types/privatekubectl.ts index 390ae3e82..e4f4d383a 100644 --- a/src/types/privatekubectl.ts +++ b/src/types/privatekubectl.ts @@ -1,4 +1,5 @@ import {Kubectl} from './kubectl' +import * as minimist from 'minimist' import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec' import * as core from '@actions/core' import * as os from 'os' @@ -7,6 +8,9 @@ import * as path from 'path' export class PrivateKubectl extends Kubectl { protected async execute(args: string[], silent: boolean = false) { + if (this.namespace) { + args = args.concat(['--namespace', this.namespace]) + } args.unshift('kubectl') let kubectlCmd = args.join(' ') let addFileFlag = false @@ -22,6 +26,13 @@ export class PrivateKubectl extends Kubectl { addFileFlag = true } + if (this.resourceGroup === '') { + throw Error('Resource group must be specified for private cluster') + } + if (this.name === '') { + throw Error('Cluster name must be specified for private cluster') + } + const privateClusterArgs = [ 'aks', 'command', @@ -31,7 +42,7 @@ export class PrivateKubectl extends Kubectl { '--name', this.name, '--command', - kubectlCmd + `${kubectlCmd}` ] if (addFileFlag) { @@ -57,10 +68,13 @@ export class PrivateKubectl extends Kubectl { `private cluster Kubectl run with invoke command: ${kubectlCmd}` ) - const runOutput = await getExecOutput( - 'az', - [...privateClusterArgs, '-o', 'json'], - eo + const allArgs = [...privateClusterArgs, '-o', 'json'] + core.debug(`full form of az command: az ${allArgs.join(' ')}`) + const runOutput = await getExecOutput('az', allArgs, eo) + core.debug( + `from kubectl private cluster command got run output ${JSON.stringify( + runOutput + )}` ) const runObj: {logs: string; exitCode: number} = JSON.parse( runOutput.stdout @@ -93,23 +107,31 @@ export class PrivateKubectl extends Kubectl { } public extractFilesnames(strToParse: string) { - let start = strToParse.indexOf('-filename') - let offset = 7 + const fileNames: string[] = [] + const argv = minimist(strToParse.split(' ')) + const fArg = 'f' + const filenameArg = 'filename' - if (start == -1) { - start = strToParse.indexOf('-f') + fileNames.push(...this.extractFilesFromMinimist(argv, fArg)) + fileNames.push(...this.extractFilesFromMinimist(argv, filenameArg)) - if (start == -1) { - return '' + return fileNames.join(' ') + } + + private extractFilesFromMinimist(argv, arg: string): string[] { + if (!argv[arg]) { + return [] + } + const toReturn: string[] = [] + if (typeof argv[arg] === 'string') { + toReturn.push(...argv[arg].split(',')) + } else { + for (const value of argv[arg] as string[]) { + toReturn.push(...value.split(',')) } - offset = 0 } - let temp = strToParse.substring(start + offset) - let end = temp.indexOf(' -') - - //End could be case where the -f flag was last, or -f is followed by some additonal flag and it's arguments - return temp.substring(3, end == -1 ? temp.length : end).trim() + return toReturn } private containsFilenames(str: string) { diff --git a/test/integration/k8s-deploy-test.py b/test/integration/k8s-deploy-test.py index 83f40c640..7b80babb6 100644 --- a/test/integration/k8s-deploy-test.py +++ b/test/integration/k8s-deploy-test.py @@ -20,6 +20,7 @@ namespaceKey = "namespace" ingressServicesKey = "ingressServices" tsServicesKey = "tsServices" +privateKey = "private" def parseArgs(sysArgs): @@ -197,18 +198,36 @@ def main(): kind = parsedArgs[kindKey] name = parsedArgs[nameKey] namespace = parsedArgs[namespaceKey] - print('kubectl get '+kind+' '+name+' -n '+namespace+' -o json') + cmd = 'kubectl get '+kind + ' '+name+' -n '+namespace+' -o json' + k8s_object = None + azPrefix = "" try: - k8_object = json.load(os.popen('kubectl get '+kind + - ' '+name+' -n '+namespace+' -o json')) + if privateKey in parsedArgs: + uniqueName = parsedArgs[privateKey] + azPrefix = f"az aks command invoke --resource-group {uniqueName} --name {uniqueName} --command " + cmd = azPrefix + "'" + cmd + "'" + outputString = os.popen(cmd).read() + successExit = "exitcode=0" + if successExit not in outputString: + raise ValueError(f"private cluster get failed for {kind} {name}") + + objString = outputString.split(successExit)[1] + k8_object = json.loads(objString) + + else: + k8_object = json.load(os.popen(cmd)) if k8_object == None: raise ValueError(f"{kind} {name} was not found") + except: msg = kind+' '+name+' not created or not found' - foundObjects = json.load( - os.popen('kubectl get '+kind+' -n '+namespace+' -o json')) + getAllObjectsCmd = azPrefix + 'kubectl get '+kind+' -n '+namespace + if not azPrefix == "": + getAllObjectsCmd = azPrefix + "'{getAllObjectsCmd}'" # add extra set of quotes + cmd = + "'" + cmd + "'" + foundObjects = os.popen().read() suffix = f"resources of type {kind}: {foundObjects}" sys.exit(msg + " " + suffix)