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:
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);
|
||||
}
|
Reference in New Issue
Block a user