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