diff --git a/.github/workflows/checkin.yml b/.github/workflows/checkin.yml
index 0f66c870..e517dd95 100644
--- a/.github/workflows/checkin.yml
+++ b/.github/workflows/checkin.yml
@@ -1,4 +1,4 @@
-name: "Build and test Action"
+name: 'Build and test Action'
on: [push, pull_request]
jobs:
@@ -20,5 +20,5 @@ jobs:
- run: npm run test:coverage
- uses: codecov/codecov-action@v2
-
+
- run: npm audit
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index b1d2b8c9..77d59434 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -1,4 +1,4 @@
-name: "Create cluster using KinD"
+name: 'Create cluster using KinD'
on: [pull_request, push]
jobs:
@@ -15,8 +15,12 @@ jobs:
- run: npm run build
- - name: "Run engineerd/setup-kind@${{github.sha}}"
+ - name: 'Run engineerd/setup-kind@${{github.sha}}'
uses: ./
+ with:
+ image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6
+ loadBalancer: true
+ localRegistry: true
- run: kubectl cluster-info
diff --git a/.gitignore b/.gitignore
index e61de3ed..45d8a6f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ node_modules/
__tests__/runner/*
lib/
dist/
-coverage/
\ No newline at end of file
+coverage/
+.vscode/
diff --git a/.prettierrc.json b/.prettierrc.json
index 8f1add61..0835748d 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,4 +1,5 @@
{
"endOfLine": "auto",
+ "printWidth": 100,
"singleQuote": true
-}
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 9545e54d..7289d034 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,6 @@ jobs:
> version 0.6 of Kind. See [this document for a detailed migration
> guide][kind-kubeconfig]
-> Note: GitHub Actions workers come pre-configured with `kubectl`.
-
The following arguments can be configured on the job using the `with` keyword
(see example above). Currently, possible inputs are all the flags for
`kind cluster create`, with the additional version, which sets the Kind version
@@ -55,6 +53,8 @@ Optional inputs:
- `skipClusterLogsExport`: if `"true"`, the action will not export the cluster logs
- `verbosity`: numeric log verbosity, (info = 0, debug = 3, trace = 2147483647) (default `"0"`)
- `quiet`: silence all stderr output (default `"false"`)
+- `loadBalancer`: setup a Metallb load-balancer (default `"false"`)
+- `localRegistry`: setup a local registry on localhost:5000 (default `"false"`)
Example using optional inputs:
@@ -78,6 +78,26 @@ jobs:
echo "environment-kubeconfig:" ${KUBECONFIG}
```
+## Kubectl
+
+GitHub Actions workers come pre-configured with `kubectl` but if the Kubernetes version can be identified from the image
+input or the images of the nodes in the config file, it will be installed in the tool-cache with the right version.
+
+## Load-balancer
+
+When `loadBalancer: true` a load-balancer is created based on
+
+## Local registry
+
+When `localRegistry: true` a local registry is created based on
+It is then available on localhost:5000 as KIND_REGISTRY on the host machine
+
+## Self-hosted agents
+
+When using on a self-hosted agent, an access to GITHUB_API_URL ( by default) and are required for setup-kind to work properly.
+
+NB: The load-balancer needs an access to
+
[kind-kubeconfig]: https://github.com/kubernetes-sigs/kind/issues/1060
[gh-actions-path]:
https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/
diff --git a/__tests__/go.test.ts b/__tests__/go.test.ts
index e083103b..b8b748f9 100644
--- a/__tests__/go.test.ts
+++ b/__tests__/go.test.ts
@@ -1,11 +1,11 @@
-import * as go from '../src/go';
+import { env as goenv } from '../src/go';
describe('checking go env simulation', function () {
it('correctly parse os', () => {
- expect(['windows', 'darwin', 'linux']).toContain(go.goos());
+ expect(['windows', 'darwin', 'linux']).toContain(goenv.GOOS);
});
it('correctly parse arch', () => {
- expect(['amd64', 'arm64']).toContain(go.goarch());
+ expect(['amd64', 'arm64']).toContain(goenv.GOARCH);
});
});
diff --git a/__tests__/kind/main.test.ts b/__tests__/kind/main.test.ts
index 5ad4633a..eca46775 100644
--- a/__tests__/kind/main.test.ts
+++ b/__tests__/kind/main.test.ts
@@ -4,7 +4,6 @@ import { KindMainService } from '../../src/kind/main';
const testEnvVars = {
INPUT_VERBOSITY: '3',
INPUT_QUIET: 'true',
- INPUT_VERSION: 'v0.5.3',
INPUT_CONFIG: 'some-path',
INPUT_IMAGE: 'some-docker-image',
INPUT_NAME: 'some-name',
@@ -30,7 +29,6 @@ describe('checking input parsing', function () {
it('correctly parse input', () => {
const service: KindMainService = KindMainService.getInstance();
- expect(service.version).toEqual(testEnvVars.INPUT_VERSION);
expect(service.configFile).toEqual(testEnvVars.INPUT_CONFIG);
expect(service.image).toEqual(testEnvVars.INPUT_IMAGE);
expect(service.name).toEqual(testEnvVars.INPUT_NAME);
@@ -46,14 +44,13 @@ describe('checking input parsing', function () {
});
it('correctly generates the cluster create command', () => {
- const args: string[] = KindMainService.getInstance().createCommand();
+ const args: string[] = KindMainService.getInstance().createCommand('');
expect(args).toEqual([
'create',
'cluster',
'--verbosity',
testEnvVars.INPUT_VERBOSITY,
'--quiet',
- testEnvVars.INPUT_QUIET,
'--config',
path.normalize('/home/runner/repo/some-path'),
'--image',
diff --git a/__tests__/kind/post.test.ts b/__tests__/kind/post.test.ts
index 45d90f45..6ad21ca2 100644
--- a/__tests__/kind/post.test.ts
+++ b/__tests__/kind/post.test.ts
@@ -45,7 +45,6 @@ describe('checking input parsing', function () {
'--verbosity',
testEnvVars.INPUT_VERBOSITY,
'--quiet',
- testEnvVars.INPUT_QUIET,
'--name',
testEnvVars.INPUT_NAME,
'--kubeconfig',
@@ -54,17 +53,18 @@ describe('checking input parsing', function () {
});
it('correctly generates the cluster export logs command', () => {
- const args: string[] = KindPostService.getInstance().exportLogsCommand();
+ const logsDir = KindPostService.getInstance().kindLogsDir();
+ expect(logsDir).toEqual(
+ path.normalize('/home/runner/work/_temp/1c1900ec-8f4f-5069-a966-1d3072cc9723')
+ );
+ const args: string[] = KindPostService.getInstance().exportLogsCommand(logsDir);
expect(args).toEqual([
'export',
'logs',
- path.normalize(
- '/home/runner/work/_temp/1c1900ec-8f4f-5069-a966-1d3072cc9723'
- ),
+ path.normalize('/home/runner/work/_temp/1c1900ec-8f4f-5069-a966-1d3072cc9723'),
'--verbosity',
testEnvVars.INPUT_VERBOSITY,
'--quiet',
- testEnvVars.INPUT_QUIET,
'--name',
testEnvVars.INPUT_NAME,
]);
diff --git a/__tests__/local-registry.test.ts b/__tests__/local-registry.test.ts
new file mode 100644
index 00000000..56acc50a
--- /dev/null
+++ b/__tests__/local-registry.test.ts
@@ -0,0 +1,23 @@
+import { hasRegistryConfig } from '../src/local-registry';
+
+describe('checking registry validation', function () {
+ it('disable_tcp_service configuration', () => {
+ const configPatch = `[plugins."io.containerd.grpc.v1.cri"]
+ disable_tcp_service = true`;
+ expect(hasRegistryConfig(configPatch)).toBeFalsy();
+ });
+ it('empty configuration', () => {
+ const configPatch = ``;
+ expect(hasRegistryConfig(configPatch)).toBeFalsy();
+ });
+ it('wrong port', () => {
+ const configPatch = `[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5001"]
+ endpoint = ["http://kind-registry:5000"]`;
+ expect(hasRegistryConfig(configPatch)).toBeFalsy();
+ });
+ it('correctly configured', () => {
+ const configPatch = `[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
+ endpoint = ["http://kind-registry:5000"]`;
+ expect(hasRegistryConfig(configPatch)).toBeTruthy();
+ });
+});
diff --git a/__tests__/requirements.test.ts b/__tests__/requirements.test.ts
new file mode 100644
index 00000000..41e874e3
--- /dev/null
+++ b/__tests__/requirements.test.ts
@@ -0,0 +1,31 @@
+import { checkEnvironment } from '../src/requirements';
+
+const testEnvVars = {
+ INPUT_VERSION: 'v0.7.0',
+ GITHUB_JOB: 'kind',
+ GITHUB_WORKSPACE: '/home/runner/repo',
+ RUNNER_OS: 'Linux',
+ RUNNER_ARCH: 'X64',
+ RUNNER_TEMP: '/home/runner/work/_temp',
+ RUNNER_TOOL_CACHE: '/opt/hostedtoolcache',
+};
+
+describe('checking requirements', function () {
+ const originalEnv = process.env;
+ beforeEach(() => {
+ jest.resetModules();
+ process.env = {
+ ...originalEnv,
+ ...testEnvVars,
+ };
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
+ it('required GITHUB_JOB must be defined', async () => {
+ process.env['GITHUB_JOB'] = '';
+ await expect(checkEnvironment()).rejects.toThrow('Expected GITHUB_JOB to be defined');
+ });
+});
diff --git a/action.yml b/action.yml
index c9d33a1e..783036f1 100644
--- a/action.yml
+++ b/action.yml
@@ -1,41 +1,51 @@
-name: "KinD (Kubernetes in Docker) Action"
-description: "Easily run a Kubernetes cluster in your GitHub Action"
-author: "Engineerd"
+name: 'KinD (Kubernetes in Docker) Action'
+description: 'Easily run a Kubernetes cluster in your GitHub Action'
+author: 'Engineerd'
inputs:
version:
- description: "Version of Kind to use (default v0.11.1)"
- default: "v0.11.1"
+ description: 'Version of Kind to use (default v0.11.1)'
+ default: 'v0.11.1'
required: true
config:
- description: "Path (relative to the root of the repository) to a kind config file"
+ description: 'Path (relative to the root of the repository) to a kind config file'
image:
- description: "Node Docker image to use for booting the cluster"
+ description: 'Node Docker image to use for booting the cluster'
name:
- description: "Cluster name (default kind)"
- default: "kind"
+ description: 'Cluster name (default kind)'
+ default: 'kind'
required: true
wait:
- description: "Wait for control plane node to be ready (default 300s)"
- default: "300s"
+ description: 'Wait for control plane node to be ready (default 300s)'
+ default: '300s'
kubeconfig:
- description: "Sets kubeconfig path instead of $KUBECONFIG or $HOME/.kube/config"
+ description: 'Sets kubeconfig path instead of $KUBECONFIG or $HOME/.kube/config'
skipClusterCreation:
- description: "If true, the action will not create a cluster, just acquire the tools"
- default: "false"
+ description: 'If true, the action will not create a cluster, just acquire the tools'
+ default: 'false'
skipClusterDeletion:
- description: "If true, the action will not delete the cluster"
- default: "false"
+ description: 'If true, the action will not delete the cluster'
+ default: 'false'
skipClusterLogsExport:
- description: "If true, the action will not export the cluster logs"
- default: "false"
+ description: 'If true, the action will not export the cluster logs'
+ default: 'false'
verbosity:
- description: "Defines log verbosity with a numeric value, (info = 0, debug = 3, trace = 2147483647)"
- default: "0"
+ description: 'Defines log verbosity with a numeric value, (info = 0, debug = 3, trace = 2147483647)'
+ default: '0'
quiet:
- description: "Silence all stderr output"
- default: "false"
+ description: 'Silence all stderr output'
+ default: 'false'
+ token:
+ description: 'Used to retrieve release informations concerning KinD and Kubernetes from https://api.github.com'
+ default: '${{ github.token }}'
+ required: true
+ loadBalancer:
+ description: 'Setup a Metallb load-balancer'
+ default: 'false'
+ localRegistry:
+ description: 'Setup a local registry on localhost:5000'
+ default: 'false'
runs:
- using: "node12"
- main: "dist/main/index.js"
- post: "dist/post/index.js"
+ using: 'node12'
+ main: 'dist/main/index.js'
+ post: 'dist/post/index.js'
post-if: success()
diff --git a/package-lock.json b/package-lock.json
index dfd988e3..e9d07d99 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,14 +13,19 @@
"@actions/cache": "^1.0.8",
"@actions/core": "^1.6.0",
"@actions/exec": "^1.1.0",
+ "@actions/github": "^5.0.0",
"@actions/glob": "^0.2.0",
"@actions/io": "^1.1.1",
"@actions/tool-cache": "^1.7.1",
+ "@iarna/toml": "^2.2.5",
+ "js-yaml": "^4.1.0",
"semver": "^7.3.5",
"uuid": "^8.3.2"
},
"devDependencies": {
+ "@types/iarna__toml": "^2.0.2",
"@types/jest": "^27.4.0",
+ "@types/js-yaml": "^4.0.5",
"@types/node": "^17.0.8",
"@types/semver": "^7.3.9",
"@types/uuid": "^8.3.4",
@@ -117,6 +122,17 @@
"@actions/io": "^1.0.1"
}
},
+ "node_modules/@actions/github": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz",
+ "integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==",
+ "dependencies": {
+ "@actions/http-client": "^1.0.11",
+ "@octokit/core": "^3.4.0",
+ "@octokit/plugin-paginate-rest": "^2.13.3",
+ "@octokit/plugin-rest-endpoint-methods": "^5.1.1"
+ }
+ },
"node_modules/@actions/glob": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz",
@@ -1041,6 +1057,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
+ "node_modules/@iarna/toml": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
+ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
+ },
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1381,6 +1402,107 @@
"node": ">= 8"
}
},
+ "node_modules/@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz",
+ "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==",
+ "dependencies": {
+ "@octokit/auth-token": "^2.4.4",
+ "@octokit/graphql": "^4.5.8",
+ "@octokit/request": "^5.6.0",
+ "@octokit/request-error": "^2.0.5",
+ "@octokit/types": "^6.0.3",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
+ "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+ "dependencies": {
+ "@octokit/request": "^5.6.0",
+ "@octokit/types": "^6.0.3",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-10.2.2.tgz",
+ "integrity": "sha512-EVcXQ+ZrC04cg17AMg1ofocWMxHDn17cB66ZHgYc0eUwjFtxS0oBzkyw2VqIrHBwVgtfoYrq1WMQfQmMjUwthw=="
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.3.tgz",
+ "integrity": "sha512-kdc65UEsqze/9fCISq6BxLzeB9qf0vKvKojIfzgwf4tEF+Wy6c9dXnPFE6vgpoDFB1Z5Jek5WFVU6vL1w22+Iw==",
+ "dependencies": {
+ "@octokit/types": "^6.28.1"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=2"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "5.10.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.10.4.tgz",
+ "integrity": "sha512-Dh+EAMCYR9RUHwQChH94Skl0lM8Fh99auT8ggck/xTzjJrwVzvsd0YH68oRPqp/HxICzmUjLfaQ9sy1o1sfIiA==",
+ "dependencies": {
+ "@octokit/types": "^6.28.1",
+ "deprecation": "^2.3.1"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=3"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz",
+ "integrity": "sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ==",
+ "dependencies": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.1",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "dependencies": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "6.28.1",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.28.1.tgz",
+ "integrity": "sha512-XlxDoQLFO5JnFZgKVQTYTvXRsQFfr/GwDUU108NJ9R5yFPkA2qXhTJjYuul3vE4eLXP40FA2nysOu2zd6boE+w==",
+ "dependencies": {
+ "@octokit/openapi-types": "^10.2.2"
+ }
+ },
"node_modules/@opentelemetry/api": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz",
@@ -1490,6 +1612,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/iarna__toml": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/iarna__toml/-/iarna__toml-2.0.2.tgz",
+ "integrity": "sha512-Q3obxKhBLVVbEQ8zsAmsQVobAAZhi8dFFFjF0q5xKXiaHvH8IkSxcbM27e46M9feUMieR03SPpmp5CtaNzpdBg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -1524,6 +1655,12 @@
"pretty-format": "^27.0.0"
}
},
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
+ "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -1985,8 +2122,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-union": {
"version": "2.1.0",
@@ -2108,6 +2244,11 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "node_modules/before-after-hook": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
+ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2559,6 +2700,11 @@
"node": ">=0.4.0"
}
},
+ "node_modules/deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -3630,6 +3776,14 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -4345,7 +4499,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -4809,14 +4962,22 @@
"dev": true
},
"node_modules/node-fetch": {
- "version": "2.6.6",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
- "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
}
},
"node_modules/node-fetch/node_modules/tr46": {
@@ -5943,6 +6104,11 @@
"node": ">=4.2.0"
}
},
+ "node_modules/universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -6311,6 +6477,17 @@
"@actions/io": "^1.0.1"
}
},
+ "@actions/github": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz",
+ "integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==",
+ "requires": {
+ "@actions/http-client": "^1.0.11",
+ "@octokit/core": "^3.4.0",
+ "@octokit/plugin-paginate-rest": "^2.13.3",
+ "@octokit/plugin-rest-endpoint-methods": "^5.1.1"
+ }
+ },
"@actions/glob": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz",
@@ -7061,6 +7238,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
+ "@iarna/toml": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
+ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -7330,6 +7512,101 @@
"fastq": "^1.6.0"
}
},
+ "@octokit/auth-token": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
+ "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==",
+ "requires": {
+ "@octokit/types": "^6.0.3"
+ }
+ },
+ "@octokit/core": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz",
+ "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==",
+ "requires": {
+ "@octokit/auth-token": "^2.4.4",
+ "@octokit/graphql": "^4.5.8",
+ "@octokit/request": "^5.6.0",
+ "@octokit/request-error": "^2.0.5",
+ "@octokit/types": "^6.0.3",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/endpoint": {
+ "version": "6.0.12",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz",
+ "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==",
+ "requires": {
+ "@octokit/types": "^6.0.3",
+ "is-plain-object": "^5.0.0",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/graphql": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz",
+ "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==",
+ "requires": {
+ "@octokit/request": "^5.6.0",
+ "@octokit/types": "^6.0.3",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/openapi-types": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-10.2.2.tgz",
+ "integrity": "sha512-EVcXQ+ZrC04cg17AMg1ofocWMxHDn17cB66ZHgYc0eUwjFtxS0oBzkyw2VqIrHBwVgtfoYrq1WMQfQmMjUwthw=="
+ },
+ "@octokit/plugin-paginate-rest": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.3.tgz",
+ "integrity": "sha512-kdc65UEsqze/9fCISq6BxLzeB9qf0vKvKojIfzgwf4tEF+Wy6c9dXnPFE6vgpoDFB1Z5Jek5WFVU6vL1w22+Iw==",
+ "requires": {
+ "@octokit/types": "^6.28.1"
+ }
+ },
+ "@octokit/plugin-rest-endpoint-methods": {
+ "version": "5.10.4",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.10.4.tgz",
+ "integrity": "sha512-Dh+EAMCYR9RUHwQChH94Skl0lM8Fh99auT8ggck/xTzjJrwVzvsd0YH68oRPqp/HxICzmUjLfaQ9sy1o1sfIiA==",
+ "requires": {
+ "@octokit/types": "^6.28.1",
+ "deprecation": "^2.3.1"
+ }
+ },
+ "@octokit/request": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz",
+ "integrity": "sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ==",
+ "requires": {
+ "@octokit/endpoint": "^6.0.1",
+ "@octokit/request-error": "^2.1.0",
+ "@octokit/types": "^6.16.1",
+ "is-plain-object": "^5.0.0",
+ "node-fetch": "^2.6.1",
+ "universal-user-agent": "^6.0.0"
+ }
+ },
+ "@octokit/request-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz",
+ "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==",
+ "requires": {
+ "@octokit/types": "^6.0.3",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ }
+ },
+ "@octokit/types": {
+ "version": "6.28.1",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.28.1.tgz",
+ "integrity": "sha512-XlxDoQLFO5JnFZgKVQTYTvXRsQFfr/GwDUU108NJ9R5yFPkA2qXhTJjYuul3vE4eLXP40FA2nysOu2zd6boE+w==",
+ "requires": {
+ "@octokit/openapi-types": "^10.2.2"
+ }
+ },
"@opentelemetry/api": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.4.tgz",
@@ -7433,6 +7710,15 @@
"@types/node": "*"
}
},
+ "@types/iarna__toml": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/iarna__toml/-/iarna__toml-2.0.2.tgz",
+ "integrity": "sha512-Q3obxKhBLVVbEQ8zsAmsQVobAAZhi8dFFFjF0q5xKXiaHvH8IkSxcbM27e46M9feUMieR03SPpmp5CtaNzpdBg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -7467,6 +7753,12 @@
"pretty-format": "^27.0.0"
}
},
+ "@types/js-yaml": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
+ "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==",
+ "dev": true
+ },
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -7780,8 +8072,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-union": {
"version": "2.1.0",
@@ -7876,6 +8167,11 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "before-after-hook": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
+ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -8225,6 +8521,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
+ "deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
+ },
"detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -8995,6 +9296,11 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
+ "is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
+ },
"is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -9558,7 +9864,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"requires": {
"argparse": "^2.0.1"
}
@@ -9901,9 +10206,9 @@
"dev": true
},
"node-fetch": {
- "version": "2.6.6",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
- "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
},
@@ -10697,6 +11002,11 @@
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
"dev": true
},
+ "universal-user-agent": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
+ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
+ },
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
diff --git a/package.json b/package.json
index ef6ef3ae..76c3e821 100644
--- a/package.json
+++ b/package.json
@@ -35,14 +35,19 @@
"@actions/cache": "^1.0.8",
"@actions/core": "^1.6.0",
"@actions/exec": "^1.1.0",
+ "@actions/github": "^5.0.0",
"@actions/glob": "^0.2.0",
"@actions/io": "^1.1.1",
"@actions/tool-cache": "^1.7.1",
+ "@iarna/toml": "^2.2.5",
+ "js-yaml": "^4.1.0",
"semver": "^7.3.5",
"uuid": "^8.3.2"
},
"devDependencies": {
+ "@types/iarna__toml": "^2.0.2",
"@types/jest": "^27.4.0",
+ "@types/js-yaml": "^4.0.5",
"@types/node": "^17.0.8",
"@types/semver": "^7.3.9",
"@types/uuid": "^8.3.4",
diff --git a/src/cache.ts b/src/cache.ts
index 4168def7..2a4be04d 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -4,40 +4,19 @@ import crypto from 'crypto';
import path from 'path';
import process from 'process';
import * as semver from 'semver';
-import { KIND_TOOL_NAME } from './constants';
+import { KIND_TOOL_NAME, KUBECTL_TOOL_NAME } from './constants';
/**
- * Prefix of the kind cache key
- */
-const KIND_CACHE_KEY_PREFIX = `${process.env['RUNNER_OS']}-${process.env['RUNNER_ARCH']}-setup-kind-`;
-
-/**
- * Parameters used by the cache to save and restore
- */
-export interface CacheParameters {
- /**
- * a list of file paths to restore from the cache
- */
- paths: string[];
- /**
- * An explicit key for restoring the cache
- */
- primaryKey: string;
-}
-
-/**
- * Restores Kind by version, $RUNNER_OS and $RUNNER_ARCH
+ * Restores Kind and Kubectl from cache
* @param version
*/
-export async function restoreKindCache(
- version: string
-): Promise {
- const primaryKey = kindPrimaryKey(version);
- const cachePaths = kindCachePaths(version);
+export async function restoreSetupKindCache(kind_version: string, kubernetes_version: string) {
+ const primaryKey = setupKindPrimaryKey(kind_version, kubernetes_version);
+ const paths = setupKindCachePaths(kind_version, kubernetes_version);
core.debug(`Primary key is ${primaryKey}`);
- const matchedKey = await cache.restoreCache(cachePaths, primaryKey);
+ const matchedKey = await cache.restoreCache(paths, primaryKey);
if (matchedKey) {
core.info(`Cache setup-kind restored from key: ${matchedKey}`);
@@ -45,8 +24,8 @@ export async function restoreKindCache(
core.info('Cache setup-kind is not found');
}
return {
- paths: cachePaths,
- primaryKey: primaryKey,
+ paths,
+ primaryKey,
};
}
/**
@@ -56,14 +35,21 @@ export async function restoreKindCache(
* @param version
* @returns the cache paths
*/
-function kindCachePaths(version: string) {
- return [
- path.join(
- `${process.env['RUNNER_TOOL_CACHE']}`,
- KIND_TOOL_NAME,
- semver.clean(version) || version
- ),
+function setupKindCachePaths(kind_version: string, kubernetes_version: string) {
+ const RUNNER_TOOL_CACHE = process.env['RUNNER_TOOL_CACHE'] || '';
+ const paths = [
+ path.join(RUNNER_TOOL_CACHE, KIND_TOOL_NAME, semver.clean(kind_version) || kind_version),
];
+ if (kubernetes_version !== '') {
+ paths.push(
+ path.join(
+ RUNNER_TOOL_CACHE,
+ KUBECTL_TOOL_NAME,
+ semver.clean(kubernetes_version) || kubernetes_version
+ )
+ );
+ }
+ return paths;
}
/**
@@ -73,22 +59,28 @@ function kindCachePaths(version: string) {
* @param version
* @returns the primary Key
*/
-function kindPrimaryKey(version: string) {
- const hash = crypto
- .createHash('sha256')
- .update(`kind-${version}-${process.platform}-${process.arch}-`)
- .digest('hex');
- return `${KIND_CACHE_KEY_PREFIX}${hash}`;
+function setupKindPrimaryKey(kind_version: string, kubernetes_version: string) {
+ const RUNNER_OS = process.env['RUNNER_OS'] || '';
+ const RUNNER_ARCH = process.env['RUNNER_ARCH'] || '';
+ const SETUP_KIND_CACHE_KEY_PREFIX = `${RUNNER_OS}-${RUNNER_ARCH}-setup-kind-`;
+ const key = JSON.stringify({
+ architecture: process.arch,
+ kind: kind_version,
+ kubernetes: kubernetes_version,
+ platform: process.platform,
+ });
+ const hash = crypto.createHash('sha256').update(key).digest('hex');
+ return `${SETUP_KIND_CACHE_KEY_PREFIX}${hash}`;
}
/**
- * Caches Kind by it's primaryKey
+ * Save Kind and Kubectl in the cache
* @param primaryKey
*/
-export async function saveKindCache(parameters: CacheParameters) {
+export async function saveSetupKindCache(paths: string[], primaryKey: string) {
try {
- await cache.saveCache(parameters.paths, parameters.primaryKey);
- core.info(`Cache setup-kind saved with the key ${parameters.primaryKey}`);
+ await cache.saveCache(paths, primaryKey);
+ core.info(`Cache setup-kind saved with the key ${primaryKey}`);
} catch (err) {
const error = err as Error;
if (error.name === cache.ValidationError.name) {
diff --git a/src/constants.ts b/src/constants.ts
index 5e88fbeb..71c4ae28 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,10 +1,15 @@
+import process from 'process';
+
export enum Input {
Version = 'version',
Verbosity = 'verbosity',
Quiet = 'quiet',
Config = 'config',
Image = 'image',
+ LoadBalancer = 'loadBalancer',
+ LocalRegistry = 'localRegistry',
Name = 'name',
+ Token = 'token',
Wait = 'wait',
KubeConfig = 'kubeconfig',
SkipClusterCreation = 'skipClusterCreation',
@@ -23,6 +28,17 @@ export enum Flag {
KubeConfig = '--kubeconfig',
}
-export const KIND_TOOL_NAME = 'kind';
+export const IS_WINDOWS = process.platform === 'win32';
+
+function executableCommand(command: string) {
+ return IS_WINDOWS ? `${command}.exe` : command;
+}
+export const DOCKER_COMMAND = executableCommand('docker');
+
+export const KIND_COMMAND = executableCommand('kind');
export const KIND_DEFAULT_VERSION = 'v0.11.1';
+export const KIND_TOOL_NAME = 'kind';
+
+export const KUBECTL_COMMAND = executableCommand('kubectl');
+export const KUBECTL_TOOL_NAME = 'kubectl';
diff --git a/src/containerd.d.ts b/src/containerd.d.ts
new file mode 100644
index 00000000..dc9b701d
--- /dev/null
+++ b/src/containerd.d.ts
@@ -0,0 +1,15 @@
+export interface ConfigPatch {
+ plugins: { [key: string]: PluginConfig };
+}
+
+export interface PluginConfig {
+ registry: Registry;
+}
+
+export interface Registry {
+ mirrors: { [key: string]: Mirror };
+}
+
+export interface Mirror {
+ endpoint: string[];
+}
diff --git a/src/docker.ts b/src/docker.ts
new file mode 100644
index 00000000..88ab5d87
--- /dev/null
+++ b/src/docker.ts
@@ -0,0 +1,10 @@
+import * as exec from '@actions/exec';
+import { DOCKER_COMMAND } from './constants';
+
+export async function executeDocker(args: string[], options?: exec.ExecOptions) {
+ return await exec.exec(DOCKER_COMMAND, args, options);
+}
+
+export async function getDockerExecutionOutput(args: string[], options?: exec.ExecOptions) {
+ return await exec.getExecOutput(DOCKER_COMMAND, args, options);
+}
diff --git a/src/go.ts b/src/go.ts
index 1bee0caf..ee7506fb 100644
--- a/src/go.ts
+++ b/src/go.ts
@@ -5,8 +5,15 @@ import process from 'process';
* Simulate the calculation of the goos
* @returns go env GOOS
*/
-export function goos(): string {
- return process.platform == 'win32' ? 'windows' : process.platform;
+function goos(platform: string): string {
+ switch (platform) {
+ case 'sunos':
+ return 'solaris';
+ case 'win32':
+ return 'windows';
+ default:
+ return platform;
+ }
}
/**
@@ -14,27 +21,32 @@ export function goos(): string {
* Based on https://nodejs.org/api/process.html#processarch
* @returns go env GOARCH
*/
-export function goarch(): string {
- const architecture = process.arch;
+function goarch(architecture: string, endianness: string): string {
switch (architecture) {
+ case 'ia32':
+ return '386';
+ case 'x32':
+ return 'amd';
case 'x64':
return 'amd64';
case 'arm':
- if (os.endianness().toLowerCase() === 'be') {
- return 'armbe';
- }
- return architecture;
+ return withEndiannessOrDefault(architecture, endianness, 'be');
case 'arm64':
- if (os.endianness().toLowerCase() === 'be') {
- return 'arm64be';
- }
- return architecture;
+ return withEndiannessOrDefault(architecture, endianness, 'be');
+ case 'mips':
+ return withEndiannessOrDefault(architecture, endianness, 'le');
case 'ppc64':
- if (os.endianness().toLowerCase() === 'le') {
- return 'ppc64le';
- }
- return architecture;
+ return withEndiannessOrDefault(architecture, endianness, 'le');
default:
return architecture;
}
}
+
+function withEndiannessOrDefault(architecture: string, endianness: string, suffix: string): string {
+ return endianness === suffix ? architecture + suffix : architecture;
+}
+
+export const env = {
+ GOARCH: goarch(process.arch, os.endianness().toLowerCase()),
+ GOOS: goos(process.platform),
+};
diff --git a/src/installer.ts b/src/installer.ts
new file mode 100644
index 00000000..da7a1ef7
--- /dev/null
+++ b/src/installer.ts
@@ -0,0 +1,84 @@
+import * as core from '@actions/core';
+import * as exec from '@actions/exec';
+import * as tc from '@actions/tool-cache';
+import * as cache from './cache';
+import {
+ IS_WINDOWS,
+ KIND_COMMAND,
+ KIND_TOOL_NAME,
+ KUBECTL_COMMAND,
+ KUBECTL_TOOL_NAME,
+} from './constants';
+
+export async function installTools(
+ kind: {
+ version: string;
+ url: string;
+ },
+ kubernetes: {
+ version: string;
+ url: string;
+ }
+): Promise {
+ await core.group(
+ `Install kind@${kind.version}${kubernetes.version ? ' and kubectl@' + kubernetes.version : ''}`,
+ async () => {
+ const { paths, primaryKey } = await cache.restoreSetupKindCache(
+ kind.version,
+ kubernetes.version
+ );
+ const kindDownloaded = await installKind(kind.version, kind.url);
+ const kubernetesDownloaded = await installKubernetesTools(kubernetes.version, kubernetes.url);
+ if (kindDownloaded || kubernetesDownloaded) {
+ await cache.saveSetupKindCache(paths, primaryKey);
+ }
+ }
+ );
+}
+
+async function installKind(version: string, url: string): Promise {
+ return await installTool(KIND_COMMAND, KIND_TOOL_NAME, version, url);
+}
+
+async function installKubernetesTools(version: string, url: string): Promise {
+ if (version !== '' && url !== '') {
+ return await installTool(
+ KUBECTL_COMMAND,
+ KUBECTL_TOOL_NAME,
+ version,
+ `${url}/${KUBECTL_COMMAND}`
+ );
+ }
+ return false;
+}
+
+async function downloadTool(
+ command: string,
+ toolName: string,
+ version: string,
+ url: string
+): Promise {
+ core.info(`Downloading ${toolName}@${version} from ${url}`);
+ const downloadPath = await tc.downloadTool(url);
+ if (!IS_WINDOWS) {
+ await exec.exec('chmod', ['+x', downloadPath]);
+ }
+ return await tc.cacheFile(downloadPath, command, toolName, version);
+}
+
+async function installTool(
+ command: string,
+ toolName: string,
+ version: string,
+ url: string
+): Promise {
+ let toolPath: string = tc.find(toolName, version);
+ let downloaded = false;
+ if (toolPath === '') {
+ toolPath = await downloadTool(command, toolName, version, url);
+ downloaded = true;
+ }
+ core.addPath(toolPath);
+ core.info(`The tool ${toolName}@${version} is cached under ${toolPath}`);
+ return downloaded;
+}
diff --git a/src/kind/core.ts b/src/kind/core.ts
index f89cba22..dff60b26 100644
--- a/src/kind/core.ts
+++ b/src/kind/core.ts
@@ -1,8 +1,6 @@
import * as exec from '@actions/exec';
-import process from 'process';
+import { KIND_COMMAND } from '../constants';
-export const KIND_COMMAND = process.platform === 'win32' ? 'kind.exe' : 'kind';
-
-export async function executeKindCommand(args: string[]) {
+export async function executeKind(args: string[]) {
await exec.exec(KIND_COMMAND, args);
}
diff --git a/src/kind/main.ts b/src/kind/main.ts
index 8113d42d..fd57b816 100644
--- a/src/kind/main.ts
+++ b/src/kind/main.ts
@@ -1,22 +1,10 @@
import * as core from '@actions/core';
-import * as exec from '@actions/exec';
-import * as tc from '@actions/tool-cache';
-import { ok } from 'assert';
import path from 'path';
import process from 'process';
-import * as semver from 'semver';
-import * as cache from '../cache';
-import {
- Flag,
- Input,
- KIND_DEFAULT_VERSION,
- KIND_TOOL_NAME,
-} from '../constants';
-import * as go from '../go';
-import { executeKindCommand, KIND_COMMAND } from './core';
+import { Flag, Input } from '../constants';
+import { executeKind } from './core';
export class KindMainService {
- version: string;
configFile: string;
image: string;
name: string;
@@ -27,16 +15,12 @@ export class KindMainService {
quiet: boolean;
private constructor() {
- this.version = core.getInput(Input.Version, { required: true });
- this.checkVersion();
this.configFile = core.getInput(Input.Config);
this.image = core.getInput(Input.Image);
- this.checkImage();
this.name = core.getInput(Input.Name, { required: true });
this.waitDuration = core.getInput(Input.Wait);
this.kubeConfigFile = core.getInput(Input.KubeConfig);
- this.skipClusterCreation =
- core.getInput(Input.SkipClusterCreation) === 'true';
+ this.skipClusterCreation = core.getInput(Input.SkipClusterCreation) === 'true';
this.verbosity = +core.getInput(Input.Verbosity);
this.quiet = core.getInput(Input.Quiet) === 'true';
}
@@ -45,102 +29,41 @@ export class KindMainService {
return new KindMainService();
}
- /**
- * Verify that the version of kind is a valid semver and prints a warning if the kind version used is older than the default for setup-kind
- */
- private checkVersion() {
- const cleanVersion = semver.clean(this.version);
- ok(
- cleanVersion,
- `Input ${Input.Version} expects a valid version like ${KIND_DEFAULT_VERSION}`
- );
- if (semver.lt(this.version, KIND_DEFAULT_VERSION)) {
- core.warning(
- `Kind ${KIND_DEFAULT_VERSION} is available, have you considered using it ? See https://github.com/kubernetes-sigs/kind/releases/tag/${KIND_DEFAULT_VERSION}`
- );
- }
- }
-
- /**
- * Prints a warning if a kindest/node is used without sha256.
- * This follows the recommendation from https://kind.sigs.k8s.io/docs/user/working-offline/#using-a-prebuilt-node-imagenode-image
- */
- private checkImage() {
- if (
- this.image !== '' &&
- this.image.startsWith('kindest/node') &&
- !this.image.includes('@sha256:')
- ) {
- core.warning(
- `Please include the @sha256: image digest from the image in the release notes. You can find available image tags on the release page, https://github.com/kubernetes-sigs/kind/releases/tag/${this.version}`
- );
- }
- }
-
// returns the arguments to pass to `kind create cluster`
- createCommand(): string[] {
+ createCommand(configFile: string): string[] {
const args: string[] = ['create', 'cluster'];
if (this.verbosity > 0) {
args.push(Flag.Verbosity, this.verbosity.toString());
}
if (this.quiet) {
- args.push(Flag.Quiet, this.quiet.toString());
+ args.push(Flag.Quiet);
}
- if (this.configFile != '') {
- args.push(
- Flag.Config,
- path.join(`${process.env['GITHUB_WORKSPACE']}`, this.configFile)
- );
+ if (this.configFile !== '') {
+ args.push(Flag.Config, path.join(process.env['GITHUB_WORKSPACE'] || '', this.configFile));
+ } else if (configFile !== '') {
+ args.push(Flag.Config, configFile);
}
- if (this.image != '') {
+ if (this.image !== '') {
args.push(Flag.Image, this.image);
}
- if (this.name != '') {
+ if (this.name !== '') {
args.push(Flag.Name, this.name);
}
- if (this.waitDuration != '') {
+ if (this.waitDuration !== '') {
args.push(Flag.Wait, this.waitDuration);
}
- if (this.kubeConfigFile != '') {
+ if (this.kubeConfigFile !== '') {
args.push(Flag.KubeConfig, this.kubeConfigFile);
}
return args;
}
- // this action should always be run from a Linux worker
- private async downloadKind(): Promise {
- const url = `https://github.com/kubernetes-sigs/kind/releases/download/${
- this.version
- }/kind-${go.goos()}-${go.goarch()}`;
- console.log('downloading kind from ' + url);
- const downloadPath = await tc.downloadTool(url);
- if (process.platform !== 'win32') {
- await exec.exec('chmod', ['+x', downloadPath]);
- }
- const toolPath: string = await tc.cacheFile(
- downloadPath,
- KIND_COMMAND,
- KIND_TOOL_NAME,
- this.version
- );
- core.debug(`kind is cached under ${toolPath}`);
- return toolPath;
- }
-
- async installKind(): Promise {
- const parameters = await cache.restoreKindCache(this.version);
- let toolPath: string = tc.find(KIND_TOOL_NAME, this.version);
- if (toolPath === '') {
- toolPath = await this.downloadKind();
- await cache.saveKindCache(parameters);
- }
- return toolPath;
- }
-
- async createCluster() {
+ async createCluster(configFile: string) {
if (this.skipClusterCreation) {
return;
}
- await executeKindCommand(this.createCommand());
+ await core.group(`Create cluster "${this.name}"`, async () => {
+ await executeKind(this.createCommand(configFile));
+ });
}
}
diff --git a/src/kind/post.ts b/src/kind/post.ts
index 201c6e13..42b476fb 100644
--- a/src/kind/post.ts
+++ b/src/kind/post.ts
@@ -5,7 +5,7 @@ import path from 'path';
import process from 'process';
import { v5 as uuidv5 } from 'uuid';
import { Flag, Input, KIND_TOOL_NAME } from '../constants';
-import { executeKindCommand } from './core';
+import { executeKind } from './core';
export class KindPostService {
name: string;
@@ -18,10 +18,8 @@ export class KindPostService {
private constructor() {
this.name = core.getInput(Input.Name);
this.kubeConfigFile = core.getInput(Input.KubeConfig);
- this.skipClusterDeletion =
- core.getInput(Input.SkipClusterDeletion) === 'true';
- this.skipClusterLogsExport =
- core.getInput(Input.SkipClusterLogsExport) === 'true';
+ this.skipClusterDeletion = core.getInput(Input.SkipClusterDeletion) === 'true';
+ this.skipClusterLogsExport = core.getInput(Input.SkipClusterLogsExport) === 'true';
this.verbosity = +core.getInput(Input.Verbosity);
this.quiet = core.getInput(Input.Quiet) === 'true';
}
@@ -37,7 +35,7 @@ export class KindPostService {
args.push(Flag.Verbosity, this.verbosity.toString());
}
if (this.quiet) {
- args.push(Flag.Quiet, this.quiet.toString());
+ args.push(Flag.Quiet);
}
if (this.name != '') {
args.push(Flag.Name, this.name);
@@ -49,13 +47,13 @@ export class KindPostService {
}
// returns the arguments to pass to `kind export logs`
- exportLogsCommand(): string[] {
- const args: string[] = ['export', 'logs', this.kindLogsDir()];
+ exportLogsCommand(logsDir: string): string[] {
+ const args: string[] = ['export', 'logs', logsDir];
if (this.verbosity > 0) {
args.push(Flag.Verbosity, this.verbosity.toString());
}
if (this.quiet) {
- args.push(Flag.Quiet, this.quiet.toString());
+ args.push(Flag.Quiet);
}
if (this.name != '') {
args.push(Flag.Name, this.name);
@@ -63,23 +61,17 @@ export class KindPostService {
return args;
}
- private kindLogsDir(): string {
+ kindLogsDir(): string {
const dirs: string[] = [KIND_TOOL_NAME];
if (this.name != '') {
dirs.push(this.name);
}
dirs.push('logs');
- return path.join(
- `${process.env['RUNNER_TEMP']}`,
- uuidv5(dirs.join('/'), uuidv5.URL)
- );
+ return path.join(process.env['RUNNER_TEMP'] || '', uuidv5(dirs.join('/'), uuidv5.URL));
}
private artifactName(): string {
- const artifactArgs: string[] = [
- `${process.env['GITHUB_JOB']}`,
- KIND_TOOL_NAME,
- ];
+ const artifactArgs: string[] = [process.env['GITHUB_JOB'] || '', KIND_TOOL_NAME];
if (this.name != '') {
artifactArgs.push(this.name);
}
@@ -87,35 +79,34 @@ export class KindPostService {
return artifactArgs.join('-');
}
- async uploadKindLogs() {
+ async uploadKindLogs(rootDirectory: string) {
const artifactClient = artifact.create();
- const rootDirectory = this.kindLogsDir();
const pattern = rootDirectory + '/**/*';
const globber = await glob.create(pattern);
const files = await globber.glob();
const options = {
continueOnError: true,
};
- await artifactClient.uploadArtifact(
- this.artifactName(),
- files,
- rootDirectory,
- options
- );
+ await artifactClient.uploadArtifact(this.artifactName(), files, rootDirectory, options);
}
async deleteCluster() {
if (this.skipClusterDeletion) {
return;
}
- await executeKindCommand(this.deleteCommand());
+ await core.group(`Delete cluster "${this.name}"`, async () => {
+ await executeKind(this.deleteCommand());
+ });
}
async exportClusterLogs() {
if (this.skipClusterLogsExport) {
return;
}
- await executeKindCommand(this.exportLogsCommand());
- await this.uploadKindLogs();
+ await core.group(`Export logs for cluster "${this.name}"`, async () => {
+ const logsDir = this.kindLogsDir();
+ await executeKind(this.exportLogsCommand(logsDir));
+ await this.uploadKindLogs(logsDir);
+ });
}
}
diff --git a/src/kubectl.ts b/src/kubectl.ts
new file mode 100644
index 00000000..31729869
--- /dev/null
+++ b/src/kubectl.ts
@@ -0,0 +1,49 @@
+import * as core from '@actions/core';
+import * as exec from '@actions/exec';
+import path from 'path';
+import { v5 as uuidv5 } from 'uuid';
+import { Input, KUBECTL_COMMAND, KUBECTL_TOOL_NAME } from './constants';
+import { ConfigMap } from './kubernetes';
+import { write } from './yaml/helper';
+
+export async function executeKubectl(args: string[]) {
+ await exec.exec(KUBECTL_COMMAND, args);
+}
+
+export async function apply(file: string) {
+ const args: string[] = ['apply', '-f', file];
+ await executeKubectl(args);
+}
+
+export async function applyConfigMap(configMap: ConfigMap, fileName: string) {
+ const dirs: string[] = [KUBECTL_TOOL_NAME, core.getInput(Input.Name)];
+ const dir = path.join(process.env['RUNNER_TEMP'] || '', uuidv5(dirs.join('/'), uuidv5.URL));
+ const file = write(dir, fileName, configMap);
+ await apply(file);
+}
+
+export async function createMemberlistSecret(namespace: string) {
+ const args: string[] = [
+ 'create',
+ 'secret',
+ 'generic',
+ '-n',
+ namespace,
+ 'memberlist',
+ '--from-literal=secretkey="$(openssl rand -base64 128)"',
+ ];
+ await executeKubectl(args);
+}
+
+export async function waitForPodReady(namespace: string) {
+ const args: string[] = [
+ 'wait',
+ '-n',
+ namespace,
+ 'pod',
+ '--all',
+ '--for=condition=ready',
+ '--timeout=240s',
+ ];
+ await executeKubectl(args);
+}
diff --git a/src/kubernetes.d.ts b/src/kubernetes.d.ts
new file mode 100644
index 00000000..429410cb
--- /dev/null
+++ b/src/kubernetes.d.ts
@@ -0,0 +1,26 @@
+export interface Cluster {
+ kind: string;
+ apiVersion: string;
+ name?: string;
+ nodes?: Node[];
+ kubeadmConfigPatches?: string[];
+ containerdConfigPatches?: string[];
+}
+
+export interface ConfigMap {
+ apiVersion: string;
+ kind: string;
+ metadata: Metadata;
+ data: { [key: string]: string };
+}
+
+export interface Metadata {
+ namespace: string;
+ name: string;
+}
+
+export interface Node {
+ role: string;
+ image: string;
+ kubeadmConfigPatches?: string[];
+}
diff --git a/src/load-balancer.ts b/src/load-balancer.ts
new file mode 100644
index 00000000..92d0901d
--- /dev/null
+++ b/src/load-balancer.ts
@@ -0,0 +1,101 @@
+import * as core from '@actions/core';
+import * as yaml from 'js-yaml';
+import { Input } from './constants';
+import { getDockerExecutionOutput } from './docker';
+import * as kubectl from './kubectl';
+import { ConfigMap } from './kubernetes';
+
+const METALLB_DEFAULT_VERSION = 'v0.12.1';
+
+const METALLB_SYSTEM = 'metallb-system';
+
+async function createMemberlistSecrets() {
+ await core.group('Create the memberlist secrets', async () => {
+ await kubectl.createMemberlistSecret(METALLB_SYSTEM);
+ });
+}
+
+async function createNamespace(version: string) {
+ await core.group(`Create the metallb@${version} namespace`, async () => {
+ await kubectl.apply(
+ `https://raw.githubusercontent.com/metallb/metallb/${version}/manifests/namespace.yaml`
+ );
+ });
+}
+
+async function applyManifest(version: string) {
+ await core.group(`Apply metallb@${version} manifest`, async () => {
+ await kubectl.apply(
+ `https://raw.githubusercontent.com/metallb/metallb/${version}/manifests/metallb.yaml`
+ );
+ });
+}
+
+export async function setUpLoadBalancer() {
+ if (hasLoadBalancer()) {
+ const version = METALLB_DEFAULT_VERSION;
+ await createNamespace(version);
+ await createMemberlistSecrets();
+ await applyManifest(version);
+ await waitForPods();
+ await setupAddressPool();
+ }
+}
+
+async function waitForPods() {
+ await core.group('Wait for metallb pods to have a status of Running', async () => {
+ await kubectl.waitForPodReady(METALLB_SYSTEM);
+ });
+}
+
+async function getIPAddresses() {
+ const args: string[] = [
+ 'network',
+ 'inspect',
+ '-f',
+ "'{{(index .IPAM.Config 0).Subnet}}'",
+ 'kind',
+ ];
+ const { stdout } = await getDockerExecutionOutput(args, { silent: true });
+ const bytes = stdout.replace(/'/g, '').split('.');
+ return [`${bytes[0]}.${bytes[1]}.255.200-${bytes[0]}.${bytes[1]}.255.250`];
+}
+
+export async function setupAddressPool() {
+ await core.group('Setup address pool used by load-balancers', async () => {
+ const addresses = await getIPAddresses();
+ const configMap: ConfigMap = {
+ apiVersion: 'v1',
+ kind: 'ConfigMap',
+ metadata: {
+ namespace: METALLB_SYSTEM,
+ name: 'config',
+ },
+ data: {
+ config: yaml.dump({
+ 'address-pools': [
+ {
+ name: 'default',
+ protocol: 'layer2',
+ addresses: addresses,
+ },
+ ],
+ }),
+ },
+ };
+ await kubectl.applyConfigMap(configMap, 'metallb-configmap.yaml');
+ });
+}
+
+function hasLoadBalancer() {
+ if (core.getInput(Input.LoadBalancer) == 'true') {
+ if (core.getInput(Input.SkipClusterCreation) == 'true') {
+ core.warning(
+ "The load-balancer requires the cluster to exists. It's configuration will be skipped"
+ );
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
diff --git a/src/local-registry.ts b/src/local-registry.ts
new file mode 100644
index 00000000..8b02460b
--- /dev/null
+++ b/src/local-registry.ts
@@ -0,0 +1,138 @@
+import * as core from '@actions/core';
+import * as TOML from '@iarna/toml';
+import * as yaml from 'js-yaml';
+import path from 'path';
+import { v5 as uuidv5 } from 'uuid';
+import { Input, KIND_TOOL_NAME } from './constants';
+import { ConfigPatch } from './containerd';
+import { executeDocker } from './docker';
+import * as kubectl from './kubectl';
+import { Cluster, ConfigMap } from './kubernetes';
+import { write } from './yaml/helper';
+
+export const REGISTRY_NAME = 'kind-registry';
+export const REGISTRY_HOST = 'localhost';
+export const REGISTRY_PORT = '5000';
+export const KIND_REGISTRY = `${REGISTRY_HOST}:${REGISTRY_PORT}`;
+const REGISTRY_IMAGE = 'registry:2';
+
+export async function initRegistrySetup() {
+ if (core.getInput(Input.LocalRegistry) === 'true') {
+ await createRegistryUnlessAlreadyExists();
+ return createKindConfig();
+ }
+ return '';
+}
+
+async function createRegistryUnlessAlreadyExists() {
+ if (!(await registryAlreadyExists())) {
+ await createRegistry();
+ }
+ core.exportVariable('KIND_REGISTRY', KIND_REGISTRY);
+}
+
+async function connectRegistryToClusterNetwork() {
+ await core.group(`Connect ${REGISTRY_NAME} to the kind network`, async () => {
+ const args = ['network', 'connect', 'kind', REGISTRY_NAME];
+ await executeDocker(args);
+ });
+}
+
+async function createRegistry() {
+ await core.group(
+ `Create ${REGISTRY_NAME} at ${KIND_REGISTRY} with ${REGISTRY_IMAGE}`,
+ async () => {
+ const args = [
+ 'run',
+ '-d',
+ '--restart',
+ 'always',
+ '-p',
+ `${REGISTRY_PORT}:5000`,
+ '--name',
+ REGISTRY_NAME,
+ REGISTRY_IMAGE,
+ ];
+ await executeDocker(args);
+ }
+ );
+}
+
+function createKindConfig() {
+ if (core.getInput(Input.Config) === '') {
+ const cluster: Cluster = {
+ kind: 'Cluster',
+ apiVersion: 'kind.x-k8s.io/v1alpha4',
+ containerdConfigPatches: [
+ `[plugins."io.containerd.grpc.v1.cri".registry.mirrors."${KIND_REGISTRY}"]
+ endpoint = ["http://${REGISTRY_NAME}:5000"]`,
+ ],
+ };
+ const dirs: string[] = [KIND_TOOL_NAME, core.getInput(Input.Name)];
+ const dir = path.join(process.env['RUNNER_TEMP'] || '', uuidv5(dirs.join('/'), uuidv5.URL));
+ return write(dir, 'kind-config.yaml', cluster);
+ }
+ return '';
+}
+
+async function registryAlreadyExists() {
+ const args = ['inspect', '-f', "'{{.State.Running}}'", REGISTRY_NAME];
+ const exitCode = await executeDocker(args, {
+ ignoreReturnCode: true,
+ silent: true,
+ });
+ return exitCode === 0;
+}
+
+export async function finishRegistrySetup() {
+ if (core.getInput(Input.LocalRegistry) === 'true') {
+ await connectRegistryToClusterNetwork();
+ await documentRegistry();
+ }
+}
+
+async function documentRegistry() {
+ await core.group(`Document ${REGISTRY_NAME}`, async () => {
+ const configMap: ConfigMap = {
+ apiVersion: 'v1',
+ kind: 'ConfigMap',
+ metadata: {
+ name: 'local-registry-hosting',
+ namespace: 'kube-public',
+ },
+ data: {
+ 'localRegistryHosting.v1': yaml.dump(
+ {
+ host: KIND_REGISTRY,
+ help: 'https://kind.sigs.k8s.io/docs/user/local-registry/',
+ },
+ {
+ quotingType: '"',
+ forceQuotes: true,
+ }
+ ),
+ },
+ };
+ await kubectl.applyConfigMap(configMap, 'local-registry-configmap.yaml');
+ });
+}
+
+function parseConfigPatch(configPatch: string) {
+ return JSON.parse(JSON.stringify(TOML.parse(configPatch))) as ConfigPatch;
+}
+
+export function hasRegistryConfig(configPatch: string) {
+ const config: ConfigPatch = parseConfigPatch(configPatch);
+ return (
+ config &&
+ config.plugins &&
+ config.plugins['io.containerd.grpc.v1.cri'] &&
+ config.plugins['io.containerd.grpc.v1.cri'].registry &&
+ config.plugins['io.containerd.grpc.v1.cri'].registry.mirrors &&
+ config.plugins['io.containerd.grpc.v1.cri'].registry.mirrors[KIND_REGISTRY] &&
+ config.plugins['io.containerd.grpc.v1.cri'].registry.mirrors[KIND_REGISTRY].endpoint &&
+ config.plugins['io.containerd.grpc.v1.cri'].registry.mirrors[KIND_REGISTRY].endpoint.includes(
+ `http://${REGISTRY_NAME}:5000`
+ )
+ );
+}
diff --git a/src/main.ts b/src/main.ts
index f7bf5cb1..04e9d06f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,41 +1,19 @@
import * as core from '@actions/core';
-import * as io from '@actions/io';
-import { ok } from 'assert';
-import * as go from './go';
+import { installTools } from './installer';
import { KindMainService } from './kind/main';
+import { setUpLoadBalancer } from './load-balancer';
+import { finishRegistrySetup, initRegistrySetup } from './local-registry';
+import { checkEnvironment } from './requirements';
async function run() {
- try {
- checkEnvironment();
- const service: KindMainService = KindMainService.getInstance();
- const toolPath: string = await service.installKind();
- core.addPath(toolPath);
- await service.createCluster();
- } catch (error) {
- core.setFailed((error as Error).message);
- }
+ const { kind, kubernetes } = await checkEnvironment();
+ await installTools(kind, kubernetes);
+ const configFile = await initRegistrySetup();
+ await KindMainService.getInstance().createCluster(configFile);
+ await finishRegistrySetup();
+ await setUpLoadBalancer();
}
-function checkEnvironment() {
- const supportedPlatforms: string[] = ['linux/amd64', 'linux/arm64'];
- const platform = `${go.goos()}/${go.goarch()}`;
- ok(
- supportedPlatforms.includes(platform),
- `Platform "${platform}" is not supported`
- );
- const requiredVariables = [
- 'GITHUB_JOB',
- 'GITHUB_WORKSPACE',
- 'RUNNER_ARCH',
- 'RUNNER_OS',
- 'RUNNER_TEMP',
- 'RUNNER_TOOL_CACHE',
- ];
- requiredVariables.forEach((variable) => {
- ok(`${process.env[variable]}`, `Expected ${variable} to be defined`);
- });
- const docker = io.which('docker', false);
- ok(docker, 'Docker is required for kind use');
-}
-
-run();
+run().catch((error) => {
+ core.setFailed((error as Error).message);
+});
diff --git a/src/post-local-registry.ts b/src/post-local-registry.ts
new file mode 100644
index 00000000..d27225eb
--- /dev/null
+++ b/src/post-local-registry.ts
@@ -0,0 +1,13 @@
+import * as core from '@actions/core';
+import { Input } from './constants';
+import { executeDocker } from './docker';
+import { REGISTRY_NAME } from './local-registry';
+
+export async function removeRegistry() {
+ if (core.getInput(Input.LocalRegistry) === 'true') {
+ await core.group(`Delete ${REGISTRY_NAME}`, async () => {
+ const args = ['rm', '--force', REGISTRY_NAME];
+ await executeDocker(args);
+ });
+ }
+}
diff --git a/src/post.ts b/src/post.ts
index 4c4de83c..d691ba6d 100644
--- a/src/post.ts
+++ b/src/post.ts
@@ -1,14 +1,14 @@
import * as core from '@actions/core';
import { KindPostService } from './kind/post';
+import { removeRegistry } from './post-local-registry';
async function run() {
- try {
- const service: KindPostService = KindPostService.getInstance();
- await service.exportClusterLogs();
- await service.deleteCluster();
- } catch (error) {
- core.setFailed((error as Error).message);
- }
+ const service: KindPostService = KindPostService.getInstance();
+ await service.exportClusterLogs();
+ await service.deleteCluster();
+ await removeRegistry();
}
-run();
+run().catch((error) => {
+ core.setFailed((error as Error).message);
+});
diff --git a/src/requirements.ts b/src/requirements.ts
new file mode 100644
index 00000000..4ae4879d
--- /dev/null
+++ b/src/requirements.ts
@@ -0,0 +1,278 @@
+import * as core from '@actions/core';
+import * as github from '@actions/github';
+import * as io from '@actions/io';
+import { ok } from 'assert';
+import path from 'path';
+import * as semver from 'semver';
+import { Input, KIND_DEFAULT_VERSION } from './constants';
+import { env as goenv } from './go';
+import { Cluster } from './kubernetes';
+import { hasRegistryConfig, KIND_REGISTRY, REGISTRY_NAME } from './local-registry';
+import { read } from './yaml/helper';
+
+export async function checkEnvironment() {
+ checkVariables();
+ await checkDocker();
+ const { platform, kind } = await checkPlatform();
+ const kubernetes = await getKubernetes(platform, kind.version);
+ checkKindConfig();
+ return {
+ kind,
+ kubernetes,
+ };
+}
+
+async function getKubernetes(platform: string, kind_version: string) {
+ const image = core.getInput(Input.Image);
+ checkImageForVersion(image, kind_version);
+ let version = parseKubernetesVersion(image);
+ let url = '';
+
+ if (version === '') {
+ const kindConfig = core.getInput(Input.Config);
+ if (kindConfig !== '') {
+ version = parseKubernetesVersionFromConfig(kindConfig, kind_version);
+ }
+ }
+
+ if (version !== '') {
+ await checkKubernetesVersion(version);
+ url = `https://storage.googleapis.com/kubernetes-release/release/${version}/bin/${platform}`;
+ }
+ return {
+ version,
+ url,
+ };
+}
+
+function parseKubernetesVersionFromConfig(kindConfig: string, kind_version: string) {
+ let version = '';
+ const doc = parseKindConfig(kindConfig);
+ if (doc.nodes) {
+ const versions: string[] = doc.nodes
+ .map((node) => {
+ const image = node.image || '';
+ checkImageForVersion(image, kind_version, { file: kindConfig });
+ return parseKubernetesVersion(image);
+ })
+ .filter((value, index, self) => value && self.indexOf(value) === index)
+ .sort()
+ .reverse();
+ if (versions.length >= 1) {
+ version = versions[0];
+ if (versions.length > 1) {
+ core.warning(
+ `There are multiple versions of Kubernetes, ${version} will be used to configure kubectl`,
+ { file: kindConfig }
+ );
+ }
+ }
+ }
+ return version;
+}
+
+function parseKindConfig(kindConfig: string) {
+ const kindConfigPath = path.join(`${process.env['GITHUB_WORKSPACE'] || ''}`, kindConfig);
+ const doc = read(kindConfigPath) as Cluster;
+ ok(doc.kind === 'Cluster', `The config file ${kindConfig} must be of kind Cluster`);
+ return doc;
+}
+
+function parseKubernetesVersion(image: string) {
+ let version = '';
+ if (image && image.startsWith('kindest/node')) {
+ version = image.split('@')[0].split(':')[1];
+ }
+ return version;
+}
+
+function getOctokit() {
+ const token = core.getInput(Input.Token, { required: true });
+ return github.getOctokit(token, {
+ userAgent: 'engineerd/setup-kind',
+ });
+}
+
+async function checkKubernetesVersion(version: string) {
+ const octokit = getOctokit();
+ const KUBERNETES = 'kubernetes';
+ const { status } = await octokit.rest.repos.getReleaseByTag({
+ owner: KUBERNETES,
+ repo: KUBERNETES,
+ tag: version,
+ });
+ ok(status === 200, `Kubernetes ${version} doesn't exists`);
+}
+
+/**
+ * Check that the platform allows KinD installation with engineerd/setup-kind
+ * @returns
+ */
+async function checkPlatform() {
+ const platform = `${goenv.GOOS}/${goenv.GOARCH}`;
+ const kind = await ensureKindSupportsPlatform(platform);
+ ensureSetupKindSupportsPlatform(platform);
+ return {
+ platform,
+ kind,
+ };
+}
+
+/**
+ * Check that KinD supports the actual platform
+ */
+async function ensureKindSupportsPlatform(platform: string) {
+ const { platforms, version } = await findVersionAndSupportedPlatforms();
+ ok(
+ platforms[platform],
+ `sigs.k8s.io/kind@${version} doesn't support platform ${platform} but ${Object.getOwnPropertyNames(
+ platforms
+ )
+ .sort()
+ .join(' and ')}`
+ );
+ return {
+ version: version,
+ url: platforms[platform],
+ };
+}
+
+/**
+ * Finds supported platforms by version by calling api.github.com
+ * @param inputVersion
+ * @returns
+ */
+async function getReleaseByInputVersion(inputVersion: string) {
+ const octokit = getOctokit();
+ const KUBERNETES_SIGS = 'kubernetes-sigs';
+ const KIND = 'kind';
+ if (inputVersion === 'latest') {
+ const { data } = await octokit.rest.repos.getLatestRelease({
+ owner: KUBERNETES_SIGS,
+ repo: KIND,
+ });
+ return {
+ assets: data.assets,
+ version: data.tag_name,
+ };
+ } else {
+ checkVersion(inputVersion);
+ const { data } = await octokit.rest.repos.getReleaseByTag({
+ owner: KUBERNETES_SIGS,
+ repo: KIND,
+ tag: inputVersion,
+ });
+ return {
+ assets: data.assets,
+ version: data.tag_name,
+ };
+ }
+}
+
+/**
+ * Finds supported platforms by version by calling api.github.com
+ * @returns
+ */
+async function findVersionAndSupportedPlatforms() {
+ const inputVersion = core.getInput(Input.Version, { required: true });
+ const { assets, version } = await getReleaseByInputVersion(inputVersion);
+ const platforms = assets.reduce(
+ (total: { [key: string]: string }, asset: { name: string; browser_download_url: string }) => {
+ const parts = asset.name.split('-');
+ total[`${parts[1]}/${parts[2]}`] = asset.browser_download_url;
+ return total;
+ },
+ {}
+ );
+ return { platforms, version };
+}
+
+/**
+ * Check actually supported platforms by engineerd/setup-kind
+ * @param platform
+ */
+function ensureSetupKindSupportsPlatform(platform: string) {
+ const platforms: string[] = ['linux/amd64', 'linux/arm64'];
+ if (!platforms.includes(platform)) {
+ core.warning(
+ `engineerd/setup-kind doesn't support platform ${platform} but ${platforms.join(' and ')}`
+ );
+ }
+}
+
+/**
+ * Check required variables
+ */
+function checkVariables() {
+ [
+ 'GITHUB_JOB',
+ 'GITHUB_WORKSPACE',
+ 'RUNNER_ARCH',
+ 'RUNNER_OS',
+ 'RUNNER_TEMP',
+ 'RUNNER_TOOL_CACHE',
+ ].forEach((variable) => {
+ ok(process.env[variable] || '', `Expected ${variable} to be defined`);
+ });
+}
+
+/**
+ * Check that Docker is installed on the server
+ */
+async function checkDocker() {
+ const docker = await io.which('docker', false);
+ ok(docker, 'Docker is required for kind use');
+}
+
+/**
+ * Verify that the version of kind is a valid semver and prints a warning if the kind version used is older than the default for setup-kind
+ */
+function checkVersion(version: string) {
+ ok(
+ semver.clean(version),
+ `Input ${Input.Version} expects a valid version like ${KIND_DEFAULT_VERSION}`
+ );
+ if (semver.lt(version, KIND_DEFAULT_VERSION)) {
+ core.warning(
+ `sigs.k8s.io/kind@${KIND_DEFAULT_VERSION} is available, have you considered using it ? See https://github.com/kubernetes-sigs/kind/releases/tag/${KIND_DEFAULT_VERSION}`
+ );
+ }
+}
+
+/**
+ * Prints a warning if a kindest/node is used without sha256.
+ * This follows the recommendation from https://kind.sigs.k8s.io/docs/user/working-offline/#using-a-prebuilt-node-imagenode-image
+ */
+function checkImageForVersion(
+ image: string,
+ kind_version: string,
+ annotationProperties: core.AnnotationProperties = {}
+) {
+ if (image && image.startsWith('kindest/node') && !image.includes('@sha256:')) {
+ core.warning(
+ `Please include the @sha256: image digest for ${image} from the image in the release notes. You can find available image tags on the release page, https://github.com/kubernetes-sigs/kind/releases/tag/${kind_version}`,
+ annotationProperties
+ );
+ }
+}
+
+/**
+ * Prints a warning if the local registry configuration is missing in the kind config file
+ */
+function checkKindConfig() {
+ const kindConfigFile = core.getInput(Input.Config);
+ if (core.getInput(Input.LocalRegistry) === 'true' && kindConfigFile !== '') {
+ const kindConfig = parseKindConfig(kindConfigFile);
+ if (
+ !kindConfig.containerdConfigPatches ||
+ !kindConfig.containerdConfigPatches.find((value) => hasRegistryConfig(value))
+ ) {
+ core.warning(
+ `Please provide the following configuration in containerdConfigPatches:
+ [plugins."io.containerd.grpc.v1.cri".registry.mirrors."${KIND_REGISTRY}"]
+ endpoint = ["https://${REGISTRY_NAME}:5000"]`,
+ { file: kindConfigFile }
+ );
+ }
+ }
+}
diff --git a/src/yaml/helper.ts b/src/yaml/helper.ts
new file mode 100644
index 00000000..75605e8c
--- /dev/null
+++ b/src/yaml/helper.ts
@@ -0,0 +1,18 @@
+import fs from 'fs';
+import * as yaml from 'js-yaml';
+import path from 'path';
+
+const UTF8_ENCODING = 'utf8';
+
+export function write(dir: string, fileName: string, content: unknown) {
+ const file = path.join(dir, fileName);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ fs.writeFileSync(file, yaml.dump(content), UTF8_ENCODING);
+ return file;
+}
+
+export function read(path: string) {
+ return yaml.load(fs.readFileSync(path, UTF8_ENCODING));
+}