Refactor entire project (#39)
- Use more modern TypeScript conventions - Use JavaScript Kubernetes Client - Add unit tests - Add integration tests - Add TypeScript compile verify workflow - Switch codeowners
This commit is contained in:
@ -1,83 +0,0 @@
|
||||
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<string> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
}
|
103
src/kubeconfigs/arc.test.ts
Normal file
103
src/kubeconfigs/arc.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import * as actions from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import { getRequiredInputError } from "../../tests/util";
|
||||
import { getArcKubeconfig, KUBECONFIG_LOCATION } from "./arc";
|
||||
import * as az from "./azCommands";
|
||||
|
||||
describe("Arc kubeconfig", () => {
|
||||
test("it throws error without resource group", async () => {
|
||||
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||
getRequiredInputError("resource-group")
|
||||
);
|
||||
});
|
||||
|
||||
test("it throws error without cluster name", async () => {
|
||||
process.env["INPUT_RESOURCE-GROUP"] = "group";
|
||||
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||
getRequiredInputError("cluster-name")
|
||||
);
|
||||
});
|
||||
|
||||
describe("runs az cli commands", () => {
|
||||
const group = "group";
|
||||
const name = "name";
|
||||
const path = "path";
|
||||
const kubeconfig = "kubeconfig";
|
||||
|
||||
beforeEach(() => {
|
||||
process.env["INPUT_RESOURCE-GROUP"] = group;
|
||||
process.env["INPUT_CLUSTER-NAME"] = name;
|
||||
|
||||
jest.spyOn(io, "which").mockImplementation(async () => path);
|
||||
jest.spyOn(az, "runAzCliCommand").mockImplementation(async () => {});
|
||||
jest
|
||||
.spyOn(az, "runAzKubeconfigCommandBlocking")
|
||||
.mockImplementation(async () => kubeconfig);
|
||||
});
|
||||
|
||||
it("throws an error without method", async () => {
|
||||
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||
getRequiredInputError("method")
|
||||
);
|
||||
});
|
||||
|
||||
describe("service account method", () => {
|
||||
beforeEach(() => {
|
||||
process.env["INPUT_METHOD"] = "service-account";
|
||||
});
|
||||
|
||||
it("throws an error without token", async () => {
|
||||
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||
getRequiredInputError("token")
|
||||
);
|
||||
});
|
||||
|
||||
it("gets the kubeconfig", async () => {
|
||||
const token = "token";
|
||||
process.env["INPUT_TOKEN"] = token;
|
||||
|
||||
expect(await getArcKubeconfig()).toBe(kubeconfig);
|
||||
expect(az.runAzKubeconfigCommandBlocking).toHaveBeenCalledWith(
|
||||
path,
|
||||
[
|
||||
"connectedk8s",
|
||||
"proxy",
|
||||
"-n",
|
||||
name,
|
||||
"-g",
|
||||
group,
|
||||
"--token",
|
||||
token,
|
||||
"-f",
|
||||
KUBECONFIG_LOCATION,
|
||||
],
|
||||
KUBECONFIG_LOCATION
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("service principal method", () => {
|
||||
beforeEach(() => {
|
||||
process.env["INPUT_METHOD"] = "service-principal";
|
||||
});
|
||||
|
||||
it("gets the kubeconfig", async () => {
|
||||
expect(await getArcKubeconfig()).toBe(kubeconfig);
|
||||
expect(az.runAzKubeconfigCommandBlocking).toHaveBeenCalledWith(
|
||||
path,
|
||||
[
|
||||
"connectedk8s",
|
||||
"proxy",
|
||||
"-n",
|
||||
name,
|
||||
"-g",
|
||||
group,
|
||||
"-f",
|
||||
KUBECONFIG_LOCATION,
|
||||
],
|
||||
KUBECONFIG_LOCATION
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
68
src/kubeconfigs/arc.ts
Normal file
68
src/kubeconfigs/arc.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as io from "@actions/io";
|
||||
import { Method, parseMethod } from "../types/method";
|
||||
import * as path from "path";
|
||||
import { runAzCliCommand, runAzKubeconfigCommandBlocking } from "./azCommands";
|
||||
|
||||
const RUNNER_TEMP: string = process.env["RUNNER_TEMP"] || "";
|
||||
export const KUBECONFIG_LOCATION: string = path.join(
|
||||
RUNNER_TEMP,
|
||||
`arc_kubeconfig_${Date.now()}`
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the kubeconfig based on provided method for an Arc Kubernetes cluster
|
||||
* @returns The kubeconfig wrapped in a Promise
|
||||
*/
|
||||
export async function getArcKubeconfig(): Promise<string> {
|
||||
const resourceGroupName = core.getInput("resource-group", { required: true });
|
||||
const clusterName = core.getInput("cluster-name", { required: true });
|
||||
const azPath = await io.which("az", true);
|
||||
|
||||
const method: Method | undefined = parseMethod(
|
||||
core.getInput("method", { required: true })
|
||||
);
|
||||
|
||||
await runAzCliCommand(azPath, ["extension", "add", "-n", "connectedk8s"]);
|
||||
|
||||
switch (method) {
|
||||
case Method.SERVICE_ACCOUNT:
|
||||
const saToken = core.getInput("token", { required: true });
|
||||
return await runAzKubeconfigCommandBlocking(
|
||||
azPath,
|
||||
[
|
||||
"connectedk8s",
|
||||
"proxy",
|
||||
"-n",
|
||||
clusterName,
|
||||
"-g",
|
||||
resourceGroupName,
|
||||
"--token",
|
||||
saToken,
|
||||
"-f",
|
||||
KUBECONFIG_LOCATION,
|
||||
],
|
||||
KUBECONFIG_LOCATION
|
||||
);
|
||||
case Method.SERVICE_PRINCIPAL:
|
||||
return await runAzKubeconfigCommandBlocking(
|
||||
azPath,
|
||||
[
|
||||
"connectedk8s",
|
||||
"proxy",
|
||||
"-n",
|
||||
clusterName,
|
||||
"-g",
|
||||
resourceGroupName,
|
||||
"-f",
|
||||
KUBECONFIG_LOCATION,
|
||||
],
|
||||
KUBECONFIG_LOCATION
|
||||
);
|
||||
case undefined:
|
||||
core.warning("Defaulting to kubeconfig method");
|
||||
case Method.KUBECONFIG:
|
||||
default:
|
||||
throw Error("Kubeconfig method not supported for Arc cluster");
|
||||
}
|
||||
}
|
14
src/kubeconfigs/azCommands.test.ts
Normal file
14
src/kubeconfigs/azCommands.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as actions from "@actions/exec";
|
||||
import { runAzCliCommand } from "./azCommands";
|
||||
|
||||
describe("Az commands", () => {
|
||||
test("it runs an az cli command", async () => {
|
||||
const path = "path";
|
||||
const args = ["args"];
|
||||
|
||||
jest.spyOn(actions, "exec").mockImplementation(async () => 0);
|
||||
|
||||
expect(await runAzCliCommand(path, args));
|
||||
expect(actions.exec).toBeCalledWith(path, args, {});
|
||||
});
|
||||
});
|
44
src/kubeconfigs/azCommands.ts
Normal file
44
src/kubeconfigs/azCommands.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import * as fs from "fs";
|
||||
import { ExecOptions } from "@actions/exec/lib/interfaces";
|
||||
import { exec } from "@actions/exec";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
const AZ_TIMEOUT_SECONDS: number = 120;
|
||||
|
||||
/**
|
||||
* Executes an az cli command
|
||||
* @param azPath The path to the az tool
|
||||
* @param args The arguments to be invoked
|
||||
* @param options Optional options for the command execution
|
||||
*/
|
||||
export async function runAzCliCommand(
|
||||
azPath: string,
|
||||
args: string[],
|
||||
options: ExecOptions = {}
|
||||
) {
|
||||
await exec(azPath, args, options);
|
||||
}
|
||||
/**
|
||||
* Executes an az cli command that will set the kubeconfig
|
||||
* @param azPath The path to the az tool
|
||||
* @param args The arguments to be be invoked
|
||||
* @param kubeconfigPath The path to the kubeconfig that is updated by the command
|
||||
* @returns The contents of the kubeconfig
|
||||
*/
|
||||
export async function runAzKubeconfigCommandBlocking(
|
||||
azPath: string,
|
||||
args: string[],
|
||||
kubeconfigPath: string
|
||||
): Promise<string> {
|
||||
const proc = spawn(azPath, args, {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
});
|
||||
proc.unref();
|
||||
|
||||
await sleep(AZ_TIMEOUT_SECONDS);
|
||||
return fs.readFileSync(kubeconfigPath).toString();
|
||||
}
|
||||
|
||||
const sleep = (seconds: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
128
src/kubeconfigs/default.test.ts
Normal file
128
src/kubeconfigs/default.test.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import * as fs from "fs";
|
||||
import { getRequiredInputError } from "../../tests/util";
|
||||
import { createKubeconfig, getDefaultKubeconfig } from "./default";
|
||||
|
||||
describe("Default kubeconfig", () => {
|
||||
test("it creates a kubeconfig with proper format", () => {
|
||||
const certAuth = "certAuth";
|
||||
const token = "token";
|
||||
const clusterUrl = "clusterUrl";
|
||||
|
||||
const kc = createKubeconfig(certAuth, token, clusterUrl);
|
||||
const expected = JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "Config",
|
||||
clusters: [
|
||||
{
|
||||
cluster: {
|
||||
"certificate-authority-data": certAuth,
|
||||
server: clusterUrl,
|
||||
},
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
user: {
|
||||
token: token,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(kc).toBe(expected);
|
||||
});
|
||||
|
||||
test("it throws error without method", () => {
|
||||
expect(() => getDefaultKubeconfig()).toThrow(
|
||||
getRequiredInputError("method")
|
||||
);
|
||||
});
|
||||
|
||||
describe("default method", () => {
|
||||
beforeEach(() => {
|
||||
process.env["INPUT_METHOD"] = "default";
|
||||
});
|
||||
|
||||
test("it throws error without kubeconfig", () => {
|
||||
expect(() => getDefaultKubeconfig()).toThrow(
|
||||
getRequiredInputError("kubeconfig")
|
||||
);
|
||||
});
|
||||
|
||||
test("it gets default config through kubeconfig input", () => {
|
||||
const kc = "example kc";
|
||||
process.env["INPUT_KUBECONFIG"] = kc;
|
||||
|
||||
expect(getDefaultKubeconfig()).toBe(kc);
|
||||
});
|
||||
});
|
||||
|
||||
test("it defaults to default method", () => {
|
||||
process.env["INPUT_METHOD"] = "unknown";
|
||||
|
||||
const kc = "example kc";
|
||||
process.env["INPUT_KUBECONFIG"] = kc;
|
||||
|
||||
expect(getDefaultKubeconfig()).toBe(kc);
|
||||
});
|
||||
|
||||
test("it defaults to default method from service-principal", () => {
|
||||
process.env["INPUT_METHOD"] = "service-principal";
|
||||
|
||||
const kc = "example kc";
|
||||
process.env["INPUT_KUBECONFIG"] = kc;
|
||||
|
||||
expect(getDefaultKubeconfig()).toBe(kc);
|
||||
});
|
||||
|
||||
describe("service-account method", () => {
|
||||
beforeEach(() => {
|
||||
process.env["INPUT_METHOD"] = "service-account";
|
||||
});
|
||||
|
||||
test("it throws error without cluster url", () => {
|
||||
expect(() => getDefaultKubeconfig()).toThrow(
|
||||
getRequiredInputError("k8s-url")
|
||||
);
|
||||
});
|
||||
|
||||
test("it throws error without k8s secret", () => {
|
||||
process.env["INPUT_K8S-URL"] = "url";
|
||||
|
||||
expect(() => getDefaultKubeconfig()).toThrow(
|
||||
getRequiredInputError("k8s-secret")
|
||||
);
|
||||
});
|
||||
|
||||
test("it gets kubeconfig through service-account", () => {
|
||||
const k8sUrl = "https://testing-dns-4za.hfp.earth.azmk8s.io:443";
|
||||
const token = "ZXlKaGJHY2lPcUpTVXpJMU5pSX=";
|
||||
const cert = "LS0tLS1CRUdJTiBDRWyUSUZJQ";
|
||||
const k8sSecret = fs.readFileSync("tests/sample-secret.yml").toString();
|
||||
|
||||
process.env["INPUT_K8S-URL"] = k8sUrl;
|
||||
process.env["INPUT_K8S-SECRET"] = k8sSecret;
|
||||
|
||||
const expectedConfig = JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "Config",
|
||||
clusters: [
|
||||
{
|
||||
cluster: {
|
||||
"certificate-authority-data": cert,
|
||||
server: k8sUrl,
|
||||
},
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
user: {
|
||||
token: Buffer.from(token, "base64").toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(getDefaultKubeconfig()).toBe(expectedConfig);
|
||||
});
|
||||
});
|
||||
});
|
82
src/kubeconfigs/default.ts
Normal file
82
src/kubeconfigs/default.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as jsyaml from "js-yaml";
|
||||
import { K8sSecret, parseK8sSecret } from "../types/k8sSecret";
|
||||
import { Method, parseMethod } from "../types/method";
|
||||
|
||||
/**
|
||||
* Gets the kubeconfig based on provided method for a default Kubernetes cluster
|
||||
* @returns The kubeconfig
|
||||
*/
|
||||
export function getDefaultKubeconfig(): string {
|
||||
const method: Method | undefined = parseMethod(
|
||||
core.getInput("method", { required: true })
|
||||
);
|
||||
|
||||
switch (method) {
|
||||
case Method.SERVICE_ACCOUNT: {
|
||||
const clusterUrl = core.getInput("k8s-url", { required: true });
|
||||
core.debug(
|
||||
"Found clusterUrl. Creating kubeconfig using certificate and token"
|
||||
);
|
||||
|
||||
const k8sSecret: string = core.getInput("k8s-secret", {
|
||||
required: true,
|
||||
});
|
||||
const parsedK8sSecret: K8sSecret = parseK8sSecret(jsyaml.load(k8sSecret));
|
||||
const certAuth: string = parsedK8sSecret.data["ca.crt"];
|
||||
const token: string = Buffer.from(
|
||||
parsedK8sSecret.data.token,
|
||||
"base64"
|
||||
).toString();
|
||||
|
||||
return createKubeconfig(certAuth, token, clusterUrl);
|
||||
}
|
||||
case Method.SERVICE_PRINCIPAL: {
|
||||
core.warning(
|
||||
"Service Principal method not supported for default cluster type"
|
||||
);
|
||||
}
|
||||
case undefined: {
|
||||
core.warning("Defaulting to kubeconfig method");
|
||||
}
|
||||
default: {
|
||||
core.debug("Setting context using kubeconfig");
|
||||
return core.getInput("kubeconfig", { required: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a kubeconfig and returns the string representation
|
||||
* @param certAuth The certificate authentication of the cluster
|
||||
* @param token The user token
|
||||
* @param clusterUrl The server url of the cluster
|
||||
* @returns The kubeconfig as a string
|
||||
*/
|
||||
export function createKubeconfig(
|
||||
certAuth: string,
|
||||
token: string,
|
||||
clusterUrl: string
|
||||
): string {
|
||||
const kubeconfig = {
|
||||
apiVersion: "v1",
|
||||
kind: "Config",
|
||||
clusters: [
|
||||
{
|
||||
cluster: {
|
||||
"certificate-authority-data": certAuth,
|
||||
server: clusterUrl,
|
||||
},
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
user: {
|
||||
token: token,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return JSON.stringify(kubeconfig);
|
||||
}
|
133
src/login.ts
133
src/login.ts
@ -1,133 +0,0 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as io from '@actions/io';
|
||||
import * as toolCache from '@actions/tool-cache';
|
||||
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';
|
||||
|
||||
export function getKubeconfig(): string {
|
||||
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 account 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.");
|
||||
}
|
||||
}
|
||||
|
||||
export function getExecutableExtension(): string {
|
||||
if (os.type().match(/^Win/)) {
|
||||
return '.exe';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getKubectlPath() {
|
||||
let kubectlPath = await 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;
|
||||
}
|
||||
|
||||
export async function setContext(kubeconfigPath: string) {
|
||||
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 = await getKubectlPath();
|
||||
let toolRunner = new ToolRunner(kubectlPath, ['config', 'use-context', context]);
|
||||
await toolRunner.exec();
|
||||
toolRunner = new ToolRunner(kubectlPath, ['config', 'current-context']);
|
||||
await toolRunner.exec();
|
||||
}
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
let kubeconfig = '';
|
||||
const cluster_type = core.getInput('cluster-type', { required: true });
|
||||
if (cluster_type == 'arc') {
|
||||
try{
|
||||
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);
|
30
src/run.test.ts
Normal file
30
src/run.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { getRequiredInputError } from "../tests/util";
|
||||
import { run } from "./run";
|
||||
import fs from "fs";
|
||||
import * as utils from "./utils";
|
||||
|
||||
describe("Run", () => {
|
||||
it("throws error without cluster type", async () => {
|
||||
await expect(run()).rejects.toThrow(getRequiredInputError("cluster-type"));
|
||||
});
|
||||
|
||||
it("writes kubeconfig and sets context", async () => {
|
||||
const kubeconfig = "kubeconfig";
|
||||
|
||||
process.env["INPUT_CLUSTER-TYPE"] = "default";
|
||||
process.env["RUNNER_TEMP"] = "/sample/path";
|
||||
|
||||
jest
|
||||
.spyOn(utils, "getKubeconfig")
|
||||
.mockImplementation(async () => kubeconfig);
|
||||
jest.spyOn(fs, "writeFileSync").mockImplementation(() => {});
|
||||
jest.spyOn(fs, "chmodSync").mockImplementation(() => {});
|
||||
jest.spyOn(utils, "setContext").mockImplementation(() => kubeconfig);
|
||||
|
||||
expect(await run());
|
||||
expect(utils.getKubeconfig).toHaveBeenCalled();
|
||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||
expect(fs.chmodSync).toHaveBeenCalled();
|
||||
expect(utils.setContext).toHaveBeenCalled();
|
||||
});
|
||||
});
|
36
src/run.ts
Normal file
36
src/run.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { Cluster, parseCluster } from "./types/cluster";
|
||||
import { setContext, getKubeconfig } from "./utils";
|
||||
|
||||
/**
|
||||
* Sets the Kubernetes context based on supplied action inputs
|
||||
*/
|
||||
export async function run() {
|
||||
// get inputs
|
||||
const clusterType: Cluster | undefined = parseCluster(
|
||||
core.getInput("cluster-type", {
|
||||
required: true,
|
||||
})
|
||||
);
|
||||
const runnerTempDirectory: string = process.env["RUNNER_TEMP"];
|
||||
const kubeconfigPath: string = path.join(
|
||||
runnerTempDirectory,
|
||||
`kubeconfig_${Date.now()}`
|
||||
);
|
||||
|
||||
// get kubeconfig and update context
|
||||
const kubeconfig: string = await getKubeconfig(clusterType);
|
||||
const kubeconfigWithContext: string = setContext(kubeconfig);
|
||||
|
||||
// output kubeconfig
|
||||
core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`);
|
||||
fs.writeFileSync(kubeconfigPath, kubeconfigWithContext);
|
||||
fs.chmodSync(kubeconfigPath, "600");
|
||||
core.debug("Setting KUBECONFIG environment variable");
|
||||
core.exportVariable("KUBECONFIG", kubeconfigPath);
|
||||
}
|
||||
|
||||
// Run the application
|
||||
run().catch(core.setFailed);
|
24
src/types/cluster.test.ts
Normal file
24
src/types/cluster.test.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Cluster, parseCluster } from "./cluster";
|
||||
|
||||
describe("Cluster type", () => {
|
||||
test("it has required values", () => {
|
||||
const vals = <any>Object.values(Cluster);
|
||||
expect(vals.includes("arc")).toBe(true);
|
||||
expect(vals.includes("generic")).toBe(true);
|
||||
});
|
||||
|
||||
test("it can parse valid values from a string", () => {
|
||||
expect(parseCluster("arc")).toBe(Cluster.ARC);
|
||||
expect(parseCluster("Arc")).toBe(Cluster.ARC);
|
||||
expect(parseCluster("ARC")).toBe(Cluster.ARC);
|
||||
|
||||
expect(parseCluster("generic")).toBe(Cluster.GENERIC);
|
||||
expect(parseCluster("Generic")).toBe(Cluster.GENERIC);
|
||||
expect(parseCluster("GENERIC")).toBe(Cluster.GENERIC);
|
||||
});
|
||||
|
||||
test("it will return undefined if it can't parse values from a string", () => {
|
||||
expect(parseCluster("invalid")).toBe(undefined);
|
||||
expect(parseCluster("unsupportedType")).toBe(undefined);
|
||||
});
|
||||
});
|
16
src/types/cluster.ts
Normal file
16
src/types/cluster.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export enum Cluster {
|
||||
ARC = "arc",
|
||||
GENERIC = "generic",
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to the Cluster enum
|
||||
* @param str The cluster type (case insensitive)
|
||||
* @returns The Cluster enum or undefined if it can't be parsed
|
||||
*/
|
||||
export const parseCluster = (str: string): Cluster | undefined =>
|
||||
Cluster[
|
||||
Object.keys(Cluster).filter(
|
||||
(k) => Cluster[k].toString().toLowerCase() === str.toLowerCase()
|
||||
)[0] as keyof typeof Cluster
|
||||
];
|
33
src/types/k8sSecret.test.ts
Normal file
33
src/types/k8sSecret.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { parseK8sSecret, K8sSecret } from "./k8sSecret";
|
||||
|
||||
describe("K8sSecret type", () => {
|
||||
describe("Parsing from any", () => {
|
||||
test("it returns a type guarded secret", () => {
|
||||
const secret = { data: { token: "token", "ca.crt": "cert" } };
|
||||
expect(() => parseK8sSecret(secret)).not.toThrow();
|
||||
});
|
||||
|
||||
test("it throws an error when secret not provided", () => {
|
||||
expect(() => parseK8sSecret(undefined)).toThrow();
|
||||
});
|
||||
|
||||
test("it throws an error when there is no data field", () => {
|
||||
const secret = {};
|
||||
expect(() => parseK8sSecret(secret)).toThrow();
|
||||
});
|
||||
|
||||
test("it throws an error when there is no token", () => {
|
||||
const secret = {
|
||||
data: {
|
||||
"ca.crt": "cert",
|
||||
},
|
||||
};
|
||||
expect(() => parseK8sSecret(secret)).toThrow();
|
||||
});
|
||||
|
||||
test("it throws an error when there is no ca.crt field", () => {
|
||||
const secret = { data: { token: "token" } };
|
||||
expect(() => parseK8sSecret(secret)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
25
src/types/k8sSecret.ts
Normal file
25
src/types/k8sSecret.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as util from "util";
|
||||
|
||||
export interface K8sSecret {
|
||||
data: {
|
||||
token: string;
|
||||
"ca.crt": string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if an object does not have all required fields to be a K8sSecret
|
||||
* @param secret
|
||||
* @returns A type guarded K8sSecret
|
||||
*/
|
||||
export function parseK8sSecret(secret: any): K8sSecret {
|
||||
if (!secret) throw Error("K8s secret yaml is invalid");
|
||||
if (!secret.data) throw k8sSecretMissingFieldError("data");
|
||||
if (!secret.data.token) throw k8sSecretMissingFieldError("token");
|
||||
if (!secret.data["ca.crt"]) throw k8sSecretMissingFieldError("ca.crt");
|
||||
|
||||
return secret as K8sSecret;
|
||||
}
|
||||
|
||||
const k8sSecretMissingFieldError = (field: string): Error =>
|
||||
Error(util.format("K8s secret yaml does not contain %s field", field));
|
29
src/types/method.test.ts
Normal file
29
src/types/method.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Method, parseMethod } from "./method";
|
||||
|
||||
describe("Method type", () => {
|
||||
test("it has required values", () => {
|
||||
const vals = <any>Object.values(Method);
|
||||
expect(vals.includes("kubeconfig")).toBe(true);
|
||||
expect(vals.includes("service-account")).toBe(true);
|
||||
expect(vals.includes("service-principal")).toBe(true);
|
||||
});
|
||||
|
||||
test("it can parse valid values from a string", () => {
|
||||
expect(parseMethod("kubeconfig")).toBe(Method.KUBECONFIG);
|
||||
expect(parseMethod("Kubeconfig")).toBe(Method.KUBECONFIG);
|
||||
expect(parseMethod("KUBECONFIG")).toBe(Method.KUBECONFIG);
|
||||
|
||||
expect(parseMethod("service-account")).toBe(Method.SERVICE_ACCOUNT);
|
||||
expect(parseMethod("Service-Account")).toBe(Method.SERVICE_ACCOUNT);
|
||||
expect(parseMethod("SERVICE-ACCOUNT")).toBe(Method.SERVICE_ACCOUNT);
|
||||
|
||||
expect(parseMethod("service-principal")).toBe(Method.SERVICE_PRINCIPAL);
|
||||
expect(parseMethod("Service-Principal")).toBe(Method.SERVICE_PRINCIPAL);
|
||||
expect(parseMethod("SERVICE-PRINCIPAL")).toBe(Method.SERVICE_PRINCIPAL);
|
||||
});
|
||||
|
||||
test("it will return undefined if it can't parse values from a string", () => {
|
||||
expect(parseMethod("invalid")).toBe(undefined);
|
||||
expect(parseMethod("unsupportedType")).toBe(undefined);
|
||||
});
|
||||
});
|
17
src/types/method.ts
Normal file
17
src/types/method.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export enum Method {
|
||||
KUBECONFIG = "kubeconfig",
|
||||
SERVICE_ACCOUNT = "service-account",
|
||||
SERVICE_PRINCIPAL = "service-principal",
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to the Method enum
|
||||
* @param str The method (case insensitive)
|
||||
* @returns The Method enum or undefined if it can't be parsed
|
||||
*/
|
||||
export const parseMethod = (str: string): Method | undefined =>
|
||||
Method[
|
||||
Object.keys(Method).filter(
|
||||
(k) => Method[k].toString().toLowerCase() === str.toLowerCase()
|
||||
)[0] as keyof typeof Method
|
||||
];
|
47
src/utils.test.ts
Normal file
47
src/utils.test.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import fs from "fs";
|
||||
import * as arc from "./kubeconfigs/arc";
|
||||
import * as def from "./kubeconfigs/default";
|
||||
import { Cluster } from "./types/cluster";
|
||||
import { getKubeconfig, setContext } from "./utils";
|
||||
|
||||
describe("Utils", () => {
|
||||
describe("get kubeconfig", () => {
|
||||
test("it gets arc kubeconfig when type is arc", async () => {
|
||||
const arcKubeconfig = "arckubeconfig";
|
||||
jest
|
||||
.spyOn(arc, "getArcKubeconfig")
|
||||
.mockImplementation(async () => arcKubeconfig);
|
||||
|
||||
expect(await getKubeconfig(Cluster.ARC)).toBe(arcKubeconfig);
|
||||
});
|
||||
|
||||
test("it defaults to default kubeconfig", async () => {
|
||||
const defaultKubeconfig = "arckubeconfig";
|
||||
jest
|
||||
.spyOn(def, "getDefaultKubeconfig")
|
||||
.mockImplementation(() => defaultKubeconfig);
|
||||
|
||||
expect(await getKubeconfig(undefined)).toBe(defaultKubeconfig);
|
||||
expect(await getKubeconfig(Cluster.GENERIC)).toBe(defaultKubeconfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("set context", () => {
|
||||
const kc = fs.readFileSync("tests/sample-kubeconfig.yml").toString();
|
||||
|
||||
test("it doesn't change kubeconfig without context", () => {
|
||||
expect(setContext(kc)).toBe(kc);
|
||||
});
|
||||
|
||||
test("it writes the context to the kubeconfig", () => {
|
||||
process.env["INPUT_CONTEXT"] = "example";
|
||||
|
||||
const received = JSON.parse(setContext(kc));
|
||||
const expectedKc = JSON.parse(
|
||||
fs.readFileSync("tests/expected-kubeconfig.json").toString()
|
||||
);
|
||||
|
||||
expect(received).toMatchObject(expectedKc);
|
||||
});
|
||||
});
|
||||
});
|
48
src/utils.ts
Normal file
48
src/utils.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as fs from "fs";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { getDefaultKubeconfig } from "./kubeconfigs/default";
|
||||
import { getArcKubeconfig } from "./kubeconfigs/arc";
|
||||
import { Cluster } from "./types/cluster";
|
||||
|
||||
/**
|
||||
* Gets the kubeconfig based on Kubernetes cluster type
|
||||
* @param type The cluster type for the kubeconfig (defaults to generic)
|
||||
* @returns A promise of the kubeconfig
|
||||
*/
|
||||
export async function getKubeconfig(
|
||||
type: Cluster | undefined
|
||||
): Promise<string> {
|
||||
switch (type) {
|
||||
case Cluster.ARC: {
|
||||
return await getArcKubeconfig();
|
||||
}
|
||||
case undefined: {
|
||||
core.warning("Cluster type not recognized. Defaulting to generic.");
|
||||
}
|
||||
default: {
|
||||
return getDefaultKubeconfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context by updating the kubeconfig
|
||||
* @param kubeconfig The kubeconfig
|
||||
* @returns Updated kubeconfig with the context
|
||||
*/
|
||||
export function setContext(kubeconfig: string): string {
|
||||
const context: string = core.getInput("context");
|
||||
if (!context) {
|
||||
core.debug("Can't set context because context is unspecified.");
|
||||
return kubeconfig;
|
||||
}
|
||||
|
||||
// load current kubeconfig
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromString(kubeconfig);
|
||||
|
||||
// update kubeconfig
|
||||
kc.setCurrentContext(context);
|
||||
return kc.exportConfig();
|
||||
}
|
Reference in New Issue
Block a user