diff --git a/README.md b/README.md index baf652fb..90a350dc 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,17 @@ This action can be used to set cluster context before other actions like [`azure/k8s-deploy`](https://github.com/Azure/k8s-deploy/tree/master), [`azure/k8s-create-secret`](https://github.com/Azure/k8s-create-secret/tree/master) or any kubectl commands (in script) can be run subsequently in the workflow. -There are two approaches for specifying the deployment target: +It is a requirement to use [`azure/login`](https://github.com/Azure/login/tree/master) in your workflow before using this action. + +There are three approaches for specifying the deployment target: - Kubeconfig file provided as input to the action - Service account approach where the secret associated with the service account is provided as input to the action +- Service principal approach(only applicable for arc cluster) where service principal provided with 'creds' is used as input to action -If inputs related to both these approaches are provided, kubeconfig approach related inputs are given precedence. +If inputs related to all these approaches are provided, kubeconfig approach related inputs are given precedence. -In both these approaches it is recommended to store these contents (kubeconfig file content or secret content) in a [secret](https://developer.github.com/actions/managing-workflows/storing-secrets/) which could be referenced later in the action. +In all these approaches it is recommended to store these contents (kubeconfig file content or secret content) in a [secret](https://developer.github.com/actions/managing-workflows/storing-secrets/) which could be referenced later in the action. ## Action inputs @@ -22,7 +25,7 @@ In both these approaches it is recommended to store these contents (kubeconfig f method
Method - (Optional) Acceptable values: kubeconfig/service-account. Default value: kubeconfig + (Optional) Acceptable values: kubeconfig/service-account/service-principal. Default value: kubeconfig kubeconfig
Kubectl config @@ -40,6 +43,22 @@ In both these approaches it is recommended to store these contents (kubeconfig f k8s-secret
Secret (Relevant for service account approach) Secret associated with the service account to be used for deployments + + cluster-type
Type of cluster + Type of cluster. Acceptable values: generic/arc + + + cluster-name
Name of arc cluster + Name of Azure Arc enabled Kubernetes cluster. Required only if cluster-type is 'arc'. + + + resource-group
resource group + Resource group containing the Azure Arc enabled Kubernetes cluster. Required only if cluster-type is 'arc'. + + + token
Service account token + Applicable for 'service-account' method. + ## Example usage @@ -101,6 +120,31 @@ kubectl get serviceAccounts -n -o 'jsonpath={ kubectl get secret -n -o yaml ``` +### Service account approach for arc cluster + +```yaml +- uses: azure/k8s-set-context@v1 + with: + method: service-account + cluster-type: 'arc' + cluster-name: + resource-group: + token: '${{ secrets.SA_TOKEN }}' + id: setcontext +``` + +### Service principal approach for arc cluster + +```yaml +- uses: azure/k8s-set-context@v1 + with: + method: service-principal + cluster-type: 'arc' + cluster-name: + resource-group: + id: setcontext +``` + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/action.yml b/action.yml index 8c656ae2..cf50db3d 100644 --- a/action.yml +++ b/action.yml @@ -2,8 +2,12 @@ name: 'Kubernetes set context' description: 'Kubernetes set context' inputs: # Used for setting the target K8s cluster context which will be used by other actions like azure/k8s-actions/k8s-deploy or azure/k8s-actions/k8s-create-secret + cluster-type: + description: 'Acceptable values: generic or arc' + required: true + default: 'generic' method: - description: 'Acceptable values: kubeconfig or service-account' + description: 'Acceptable values: kubeconfig or service-account or service-principal' required: true default: 'kubeconfig' kubeconfig: @@ -23,9 +27,21 @@ inputs: description: 'Service account secret. Run kubectl get serviceaccounts -o yaml and copy the service-account-secret-name. Copy the ouptut of kubectl get secret -o yaml' required: false default: '' + token: + description: 'Token extracted from the secret of service account (should be base 64 decoded)' + required: false + default: '' + resource-group: + description: 'Azure resource group name' + required: false + default: '' + cluster-name: + description: 'Azure connected cluster name' + required: false + default: '' branding: color: 'green' # optional, decorates the entry in the GitHub Marketplace runs: using: 'node12' - main: 'lib/login.js' + main: 'lib/login.js' \ No newline at end of file diff --git a/lib/arc-login.js b/lib/arc-login.js new file mode 100644 index 00000000..ae57a203 --- /dev/null +++ b/lib/arc-login.js @@ -0,0 +1,92 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = require("@actions/core"); +const path = require("path"); +const child_process_1 = require("child_process"); +const fs = require("fs"); +const io = require("@actions/io"); +const exec = require("@actions/exec"); +var azPath; +const kubeconfig_timeout = 120; //timeout in seconds +function getArcKubeconfig() { + return __awaiter(this, void 0, void 0, function* () { + try { + let method = core.getInput('method'); + if (method != 'service-account' && method != 'service-principal') { + throw Error("Supported methods for arc cluster are 'service-account' and 'service-principal'."); + } + let resourceGroupName = core.getInput('resource-group'); + let clusterName = core.getInput('cluster-name'); + if (!resourceGroupName) { + throw Error("'resourceGroupName' is not passed for arc cluster."); + } + if (!clusterName) { + throw Error("'clusterName' is not passed for arc cluster."); + } + azPath = yield io.which("az", true); + yield executeAzCliCommand(`account show`, false); + try { + yield executeAzCliCommand(`extension remove -n connectedk8s`, false); + } + catch (_a) { + //ignore if this causes an error + } + yield executeAzCliCommand(`extension add -n connectedk8s`, false); + yield executeAzCliCommand(`extension list`, false); + const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated + const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); + if (method == 'service-account') { + let saToken = core.getInput('token'); + if (!saToken) { + throw Error("'saToken' is not passed for 'service-account' method."); + } + console.log("using 'service-account' method for authenticating to arc cluster."); + const proc = child_process_1.spawn(azPath, ['connectedk8s', 'proxy', '-n', clusterName, '-g', resourceGroupName, '-f', kubeconfigPath, '--token', saToken], { + detached: true, + stdio: 'ignore' + }); + proc.unref(); + } + else { + console.log("using 'service-principal' method for authenticating to arc cluster."); + const proc = child_process_1.spawn(azPath, ['connectedk8s', 'proxy', '-n', clusterName, '-g', resourceGroupName, '-f', kubeconfigPath], { + detached: true, + stdio: 'ignore' + }); + proc.unref(); + } + console.log(`Waiting for ${kubeconfig_timeout} seconds for kubeconfig to be merged....`); + yield sleep(kubeconfig_timeout * 1000); //sleeping for 2 minutes to allow kubeconfig to be merged + fs.chmodSync(kubeconfigPath, '600'); + core.exportVariable('KUBECONFIG', kubeconfigPath); + console.log('KUBECONFIG environment variable is set'); + } + catch (ex) { + return Promise.reject(ex); + } + }); +} +exports.getArcKubeconfig = getArcKubeconfig; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +function executeAzCliCommand(command, silent, execOptions = {}, args = []) { + return __awaiter(this, void 0, void 0, function* () { + execOptions.silent = !!silent; + try { + yield exec.exec(`"${azPath}" ${command}`, args, execOptions); + } + catch (error) { + throw new Error(error); + } + }); +} diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 00000000..d19f77ec --- /dev/null +++ b/lib/client.js @@ -0,0 +1,99 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const util = require("util"); +const fs = require("fs"); +const httpClient = require("typed-rest-client/HttpClient"); +const core = require("@actions/core"); +var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {}); +class WebRequest { +} +exports.WebRequest = WebRequest; +class WebResponse { +} +exports.WebResponse = WebResponse; +class WebRequestOptions { +} +exports.WebRequestOptions = WebRequestOptions; +function sendRequest(request, options) { + return __awaiter(this, void 0, void 0, function* () { + let i = 0; + let retryCount = options && options.retryCount ? options.retryCount : 5; + let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; + let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; + let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; + let timeToWait = retryIntervalInSeconds; + while (true) { + try { + if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) { + request.body = fs.createReadStream(request.body["path"]); + } + let response = yield sendRequestInternal(request); + if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); + yield sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + continue; + } + return response; + } + catch (error) { + if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); + yield sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + } + else { + if (error.code) { + core.debug("error code =" + error.code); + } + throw error; + } + } + } + }); +} +exports.sendRequest = sendRequest; +function sleepFor(sleepDurationInSeconds) { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); +} +exports.sleepFor = sleepFor; +function sendRequestInternal(request) { + return __awaiter(this, void 0, void 0, function* () { + core.debug(util.format("[%s]%s", request.method, request.uri)); + var response = yield httpCallbackClient.request(request.method, request.uri, request.body, request.headers); + return yield toWebResponse(response); + }); +} +function toWebResponse(response) { + return __awaiter(this, void 0, void 0, function* () { + var res = new WebResponse(); + if (response) { + res.statusCode = response.message.statusCode; + res.statusMessage = response.message.statusMessage; + res.headers = response.message.headers; + var body = yield response.readBody(); + if (body) { + try { + res.body = JSON.parse(body); + } + catch (error) { + core.debug("Could not parse response: " + JSON.stringify(error)); + core.debug("Response: " + JSON.stringify(res.body)); + res.body = body; + } + } + } + return res; + }); +} diff --git a/lib/login.js b/lib/login.js index c6f5eff4..f7749276 100644 --- a/lib/login.js +++ b/lib/login.js @@ -1,122 +1,135 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.run = void 0; -const core = require("@actions/core"); -const path = require("path"); -const fs = require("fs"); -const io = require("@actions/io"); -const toolCache = require("@actions/tool-cache"); -const os = require("os"); -const toolrunner_1 = require("@actions/exec/lib/toolrunner"); -const jsyaml = require("js-yaml"); -const util = require("util"); -function getKubeconfig() { - const method = core.getInput('method', { required: true }); - if (method == 'kubeconfig') { - const kubeconfig = core.getInput('kubeconfig', { required: true }); - core.debug("Setting context using kubeconfig"); - return kubeconfig; - } - else if (method == 'service-account') { - const clusterUrl = core.getInput('k8s-url', { required: true }); - core.debug("Found clusterUrl, creating kubeconfig using certificate and token"); - let k8sSecret = core.getInput('k8s-secret', { required: true }); - var parsedk8sSecret = jsyaml.safeLoad(k8sSecret); - let kubernetesServiceAccountSecretFieldNotPresent = 'The service acount secret yaml does not contain %s; field. Make sure that its present and try again.'; - if (!parsedk8sSecret) { - throw Error("The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again."); - } - if (!parsedk8sSecret.data) { - throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data")); - } - if (!parsedk8sSecret.data.token) { - throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data.token")); - } - if (!parsedk8sSecret.data["ca.crt"]) { - throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data[ca.crt]")); - } - const certAuth = parsedk8sSecret.data["ca.crt"]; - const token = Buffer.from(parsedk8sSecret.data.token, 'base64').toString(); - const kubeconfigObject = { - "apiVersion": "v1", - "kind": "Config", - "clusters": [ - { - "cluster": { - "certificate-authority-data": certAuth, - "server": clusterUrl - } - } - ], - "users": [ - { - "user": { - "token": token - } - } - ] - }; - return JSON.stringify(kubeconfigObject); - } - else { - throw Error("Invalid method specified. Acceptable values are kubeconfig and service-account."); - } -} -function getExecutableExtension() { - if (os.type().match(/^Win/)) { - return '.exe'; - } - return ''; -} -function getKubectlPath() { - return __awaiter(this, void 0, void 0, function* () { - let kubectlPath = yield io.which('kubectl', false); - if (!kubectlPath) { - const allVersions = toolCache.findAllVersions('kubectl'); - kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : ''; - if (!kubectlPath) { - throw new Error('Kubectl is not installed'); - } - kubectlPath = path.join(kubectlPath, `kubectl${getExecutableExtension()}`); - } - return kubectlPath; - }); -} -function setContext(kubeconfigPath) { - return __awaiter(this, void 0, void 0, function* () { - let context = core.getInput('context'); - if (context) { - //To use kubectl commands, the environment variable KUBECONFIG needs to be set for this step - process.env['KUBECONFIG'] = kubeconfigPath; - const kubectlPath = yield getKubectlPath(); - let toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'use-context', context]); - yield toolRunner.exec(); - toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'current-context']); - yield toolRunner.exec(); - } - }); -} -function run() { - return __awaiter(this, void 0, void 0, function* () { - let kubeconfig = getKubeconfig(); - const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated - const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); - core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`); - fs.writeFileSync(kubeconfigPath, kubeconfig); - fs.chmodSync(kubeconfigPath, '600'); - core.exportVariable('KUBECONFIG', kubeconfigPath); - console.log('KUBECONFIG environment variable is set'); - yield setContext(kubeconfigPath); - }); -} -exports.run = run; -run().catch(core.setFailed); +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = require("@actions/core"); +const path = require("path"); +const fs = require("fs"); +const io = require("@actions/io"); +const toolCache = require("@actions/tool-cache"); +const os = require("os"); +const toolrunner_1 = require("@actions/exec/lib/toolrunner"); +const jsyaml = require("js-yaml"); +const util = require("util"); +const arc_login_1 = require("./arc-login"); +function getKubeconfig() { + const method = core.getInput('method', { required: true }); + if (method == 'kubeconfig') { + const kubeconfig = core.getInput('kubeconfig', { required: true }); + core.debug("Setting context using kubeconfig"); + return kubeconfig; + } + else if (method == 'service-account') { + const clusterUrl = core.getInput('k8s-url', { required: true }); + core.debug("Found clusterUrl, creating kubeconfig using certificate and token"); + let k8sSecret = core.getInput('k8s-secret', { required: true }); + var parsedk8sSecret = jsyaml.safeLoad(k8sSecret); + let kubernetesServiceAccountSecretFieldNotPresent = 'The service acount secret yaml does not contain %s; field. Make sure that its present and try again.'; + if (!parsedk8sSecret) { + throw Error("The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again."); + } + if (!parsedk8sSecret.data) { + throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data")); + } + if (!parsedk8sSecret.data.token) { + throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data.token")); + } + if (!parsedk8sSecret.data["ca.crt"]) { + throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data[ca.crt]")); + } + const certAuth = parsedk8sSecret.data["ca.crt"]; + const token = Buffer.from(parsedk8sSecret.data.token, 'base64').toString(); + const kubeconfigObject = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [ + { + "cluster": { + "certificate-authority-data": certAuth, + "server": clusterUrl + } + } + ], + "users": [ + { + "user": { + "token": token + } + } + ] + }; + return JSON.stringify(kubeconfigObject); + } + else { + throw Error("Invalid method specified. Acceptable values are kubeconfig and service-account."); + } +} +function getExecutableExtension() { + if (os.type().match(/^Win/)) { + return '.exe'; + } + return ''; +} +function getKubectlPath() { + return __awaiter(this, void 0, void 0, function* () { + let kubectlPath = yield io.which('kubectl', false); + if (!kubectlPath) { + const allVersions = toolCache.findAllVersions('kubectl'); + kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : ''; + if (!kubectlPath) { + throw new Error('Kubectl is not installed'); + } + kubectlPath = path.join(kubectlPath, `kubectl${getExecutableExtension()}`); + } + return kubectlPath; + }); +} +function setContext(kubeconfigPath) { + return __awaiter(this, void 0, void 0, function* () { + let context = core.getInput('context'); + if (context) { + //To use kubectl commands, the environment variable KUBECONFIG needs to be set for this step + process.env['KUBECONFIG'] = kubeconfigPath; + const kubectlPath = yield getKubectlPath(); + let toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'use-context', context]); + yield toolRunner.exec(); + toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'current-context']); + yield toolRunner.exec(); + } + }); +} +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + let kubeconfig = ''; + const cluster_type = core.getInput('cluster-type', { required: true }); + if (cluster_type == 'arc') { + yield arc_login_1.getArcKubeconfig().catch(ex => { + throw new Error('Error: Could not get the KUBECONFIG for arc cluster: ' + ex); + }); + } + else { + const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated + const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); + kubeconfig = getKubeconfig(); + core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`); + fs.writeFileSync(kubeconfigPath, kubeconfig); + fs.chmodSync(kubeconfigPath, '600'); + core.exportVariable('KUBECONFIG', kubeconfigPath); + console.log('KUBECONFIG environment variable is set'); + yield setContext(kubeconfigPath); + } + } + catch (ex) { + return Promise.reject(ex); + } + }); +} +run().catch(core.setFailed); diff --git a/node_modules/@actions/core/package.json b/node_modules/@actions/core/package.json index f85dacee..57f754d6 100644 --- a/node_modules/@actions/core/package.json +++ b/node_modules/@actions/core/package.json @@ -23,7 +23,7 @@ "_resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", "_shasum": "a78d49f41a4def18e88ce47c2cac615d5694bf09", "_spec": "@actions/core@^1.2.6", - "_where": "/home/n645863/repos/stigok/src/k8s-set-context", + "_where": "D:\\Work\\Actions\\k8s-set-context", "bugs": { "url": "https://github.com/actions/toolkit/issues" }, diff --git a/package.json b/package.json index bc7cb180..6aa01bdb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "main": "lib/run.js", "scripts": { - "build": "tsc --outDir .\\lib\\ --rootDir .\\src\\" + "build": "tsc --outDir ./lib --rootDir ./src" }, "keywords": [ "actions", diff --git a/src/arc-login.ts b/src/arc-login.ts new file mode 100644 index 00000000..88e94303 --- /dev/null +++ b/src/arc-login.ts @@ -0,0 +1,83 @@ +import * as core from '@actions/core'; +import * as path from 'path'; +import {spawn} from 'child_process'; +import * as fs from 'fs'; +import * as io from '@actions/io'; +import * as exec from '@actions/exec'; +var azPath: string; + +const kubeconfig_timeout = 120;//timeout in seconds + +export async function getArcKubeconfig(): Promise { + try { + let method = core.getInput('method'); + if (method != 'service-account' && method != 'service-principal'){ + throw Error("Supported methods for arc cluster are 'service-account' and 'service-principal'."); + } + + let resourceGroupName = core.getInput('resource-group'); + let clusterName = core.getInput('cluster-name'); + if(!resourceGroupName){ + throw Error("'resourceGroupName' is not passed for arc cluster.") + } + if(!clusterName){ + throw Error("'clusterName' is not passed for arc cluster.") + } + azPath = await io.which("az", true); + await executeAzCliCommand(`account show`, false); + try{ + await executeAzCliCommand(`extension remove -n connectedk8s`, false); + } + catch{ + //ignore if this causes an error + } + await executeAzCliCommand(`extension add -n connectedk8s`, false); + await executeAzCliCommand(`extension list`, false); + const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated + const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); + if (method == 'service-account'){ + let saToken = core.getInput('token'); + if(!saToken){ + throw Error("'saToken' is not passed for 'service-account' method.") + } + console.log("using 'service-account' method for authenticating to arc cluster.") + const proc=spawn(azPath,['connectedk8s','proxy','-n',clusterName,'-g',resourceGroupName,'-f',kubeconfigPath,'--token',saToken], { + detached: true, + stdio: 'ignore' + }); + proc.unref(); + } else{ + console.log("using 'service-principal' method for authenticating to arc cluster.") + const proc=spawn(azPath,['connectedk8s','proxy','-n',clusterName,'-g',resourceGroupName,'-f',kubeconfigPath], { + detached: true, + stdio: 'ignore' + }); + proc.unref(); + } + console.log(`Waiting for ${kubeconfig_timeout} seconds for kubeconfig to be merged....`) + await sleep(kubeconfig_timeout*1000) //sleeping for 2 minutes to allow kubeconfig to be merged + fs.chmodSync(kubeconfigPath, '600'); + core.exportVariable('KUBECONFIG', kubeconfigPath); + console.log('KUBECONFIG environment variable is set'); + } catch (ex) { + return Promise.reject(ex); + } +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function executeAzCliCommand( + command: string, + silent?: boolean, + execOptions: any = {}, + args: any = []) { + execOptions.silent = !!silent; + try { + await exec.exec(`"${azPath}" ${command}`, args, execOptions); + } + catch (error) { + throw new Error(error); + } +} \ No newline at end of file diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 00000000..a314d2df --- /dev/null +++ b/src/client.ts @@ -0,0 +1,103 @@ +import util = require("util"); +import fs = require('fs'); +import httpClient = require("typed-rest-client/HttpClient"); +import * as core from '@actions/core'; + +var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {}); + +export class WebRequest { + public method: string; + public uri: string; + // body can be string or ReadableStream + public body: string | NodeJS.ReadableStream; + public headers: any; +} + +export class WebResponse { + public statusCode: number; + public statusMessage: string; + public headers: any; + public body: any; +} + +export class WebRequestOptions { + public retriableErrorCodes?: string[]; + public retryCount?: number; + public retryIntervalInSeconds?: number; + public retriableStatusCodes?: number[]; + public retryRequestTimedout?: boolean; +} + +export async function sendRequest(request: WebRequest, options?: WebRequestOptions): Promise { + let i = 0; + let retryCount = options && options.retryCount ? options.retryCount : 5; + let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2; + let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"]; + let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504]; + let timeToWait: number = retryIntervalInSeconds; + while (true) { + try { + if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) { + request.body = fs.createReadStream(request.body["path"]); + } + + let response: WebResponse = await sendRequestInternal(request); + if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + continue; + } + + return response; + } + catch (error) { + if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) { + core.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message)); + await sleepFor(timeToWait); + timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds; + } + else { + if (error.code) { + core.debug("error code =" + error.code); + } + + throw error; + } + } + } +} + +export function sleepFor(sleepDurationInSeconds: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInSeconds * 1000); + }); +} + +async function sendRequestInternal(request: WebRequest): Promise { + core.debug(util.format("[%s]%s", request.method, request.uri)); + var response: httpClient.HttpClientResponse = await httpCallbackClient.request(request.method, request.uri, request.body, request.headers); + return await toWebResponse(response); +} + +async function toWebResponse(response: httpClient.HttpClientResponse): Promise { + var res = new WebResponse(); + if (response) { + res.statusCode = response.message.statusCode; + res.statusMessage = response.message.statusMessage; + res.headers = response.message.headers; + var body = await response.readBody(); + if (body) { + try { + res.body = JSON.parse(body); + } + catch (error) { + core.debug("Could not parse response: " + JSON.stringify(error)); + core.debug("Response: " + JSON.stringify(res.body)); + res.body = body; + } + } + } + + return res; +} \ No newline at end of file diff --git a/src/login.ts b/src/login.ts index 62026aef..eb44f6e7 100644 --- a/src/login.ts +++ b/src/login.ts @@ -7,18 +7,19 @@ import * as os from 'os'; import { ToolRunner } from "@actions/exec/lib/toolrunner"; import * as jsyaml from 'js-yaml'; import * as util from 'util'; +import { getArcKubeconfig } from './arc-login'; function getKubeconfig(): string { - const method = core.getInput('method', {required: true}); + const method = core.getInput('method', { required: true }); if (method == 'kubeconfig') { - const kubeconfig = core.getInput('kubeconfig', {required : true}); + const kubeconfig = core.getInput('kubeconfig', { required: true }); core.debug("Setting context using kubeconfig"); return kubeconfig; } else if (method == 'service-account') { const clusterUrl = core.getInput('k8s-url', { required: true }); core.debug("Found clusterUrl, creating kubeconfig using certificate and token"); - let k8sSecret = core.getInput('k8s-secret', {required : true}); + let k8sSecret = core.getInput('k8s-secret', { required: true }); var parsedk8sSecret = jsyaml.safeLoad(k8sSecret); let kubernetesServiceAccountSecretFieldNotPresent = 'The service acount secret yaml does not contain %s; field. Make sure that its present and try again.'; if (!parsedk8sSecret) { @@ -102,15 +103,28 @@ async function setContext(kubeconfigPath: string) { } async function run() { - let kubeconfig = getKubeconfig(); - const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated - const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); - core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`); - fs.writeFileSync(kubeconfigPath, kubeconfig); - fs.chmodSync(kubeconfigPath, '600'); - core.exportVariable('KUBECONFIG', kubeconfigPath); - console.log('KUBECONFIG environment variable is set'); - await setContext(kubeconfigPath); + try { + let kubeconfig = ''; + const cluster_type = core.getInput('cluster-type', { required: true }); + if (cluster_type == 'arc') { + await getArcKubeconfig().catch(ex => { + throw new Error('Error: Could not get the KUBECONFIG for arc cluster: ' + ex); + }); + } + else { + const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated + const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`); + kubeconfig = getKubeconfig(); + core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`); + fs.writeFileSync(kubeconfigPath, kubeconfig); + fs.chmodSync(kubeconfigPath, '600'); + core.exportVariable('KUBECONFIG', kubeconfigPath); + console.log('KUBECONFIG environment variable is set'); + await setContext(kubeconfigPath); + } + } catch (ex) { + return Promise.reject(ex); + } } run().catch(core.setFailed); \ No newline at end of file