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:
Oliver King
2021-12-07 13:10:22 -05:00
committed by GitHub
parent 40d584de6d
commit 315a1c1f59
46 changed files with 16578 additions and 6813 deletions

103
src/kubeconfigs/arc.test.ts Normal file
View 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
View 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");
}
}

View 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, {});
});
});

View 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));

View 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);
});
});
});

View 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);
}