mirror of
https://github.com/actions/runner-container-hooks.git
synced 2025-12-18 19:06:44 +00:00
Compare commits
10 Commits
v0.3.2
...
fhammerl/k
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3df7ec55b | ||
|
|
16276a2a22 | ||
|
|
abeebd2a37 | ||
|
|
2eecf2d378 | ||
|
|
0c38d44dbd | ||
|
|
a62b81fc95 | ||
|
|
ae0066ae41 | ||
|
|
6c9241fb0e | ||
|
|
efe66bb99b | ||
|
|
9a50e3a796 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
|
||||||
@@ -13,7 +13,7 @@ You'll need a runner compatible with hooks, a repository with container workflow
|
|||||||
- You'll need a runner compatible with hooks, a repository with container workflows to which you can register the runner and the hooks from this repository.
|
- You'll need a runner compatible with hooks, a repository with container workflows to which you can register the runner and the hooks from this repository.
|
||||||
- See [the runner contributing.md](../../github/CONTRIBUTING.MD) for how to get started with runner development.
|
- See [the runner contributing.md](../../github/CONTRIBUTING.MD) for how to get started with runner development.
|
||||||
- Build your hook using `npm run build`
|
- Build your hook using `npm run build`
|
||||||
- Enable the hooks by setting `ACTIONS_RUNNER_CONTAINER_HOOKS=./packages/{libraryname}/dist/index.js` file generated by [ncc](https://github.com/vercel/ncc)
|
- Enable the hooks by setting `ACTIONS_RUNNER_CONTAINER_HOOK=./packages/{libraryname}/dist/index.js` file generated by [ncc](https://github.com/vercel/ncc)
|
||||||
- Configure your self hosted runner against the a repository you have admin access
|
- Configure your self hosted runner against the a repository you have admin access
|
||||||
- Run a workflow with a container job, for example
|
- Run a workflow with a container job, for example
|
||||||
```
|
```
|
||||||
|
|||||||
64
docs/adrs/0034-build-docker-with-kaniko.md
Normal file
64
docs/adrs/0034-build-docker-with-kaniko.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# ADR 0034: Build container-action Dockerfiles with Kaniko
|
||||||
|
|
||||||
|
**Date**: 2023-01-26
|
||||||
|
|
||||||
|
**Status**: In Progress
|
||||||
|
|
||||||
|
# Background
|
||||||
|
|
||||||
|
[Building Dockerfiles in k8s using Kaniko](https://github.com/actions/runner-container-hooks/issues/23) has been on the radar since the beginning of container hooks.
|
||||||
|
Currently, this is possible in ARC using a [dind/docker-in-docker](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/runner/actions-runner-dind.dockerfile) sidecar container.
|
||||||
|
This container needs to be launched using `--privileged`, which presents a security concern.
|
||||||
|
|
||||||
|
As an alternative tool, a container running [Kaniko](https://github.com/GoogleContainerTools/kaniko) can be used to build these files instead.
|
||||||
|
Kaniko doesn't need to be `--privileged`.
|
||||||
|
Whether using dind/docker-in-docker sidecar or Kaniko, in this ADR I will refer to these containers as '**builder containers**'
|
||||||
|
|
||||||
|
# Guiding Principles
|
||||||
|
- **Security:** running a Kaniko builder container should be possible without the `--privileged` flag
|
||||||
|
- **Feature parity with Docker:** Any 'Dockerfile' that can be built with vanilla Docker should also be possible to build using a Kaniko build container
|
||||||
|
- **Ease of Use:** The customer should be able to build and push Docker images with minimal configuration
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
### User provided registry
|
||||||
|
The user needs to provide a a remote registry (like ghcr.io or dockerhub) and credentials, for the Kaniko builder container to push to and k8s to pull from later. This is the user's responsiblity so that our solution remains lightweight and generic.
|
||||||
|
- Alternatively, a user-managed local Docker Registry within the k8s cluster can of course be used instead
|
||||||
|
|
||||||
|
### Kaniko feature limit
|
||||||
|
Anything Kaniko can't do we'll be by definition unable to help with. Potential incompatibilities / inconsistencies between Docker and Kaniko will naturally be inherited by our solution.
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
The user will set `containerMode:kubernetes`, because this is a change to the behaviour of our k8s hooks
|
||||||
|
|
||||||
|
The user will set two ENVs:
|
||||||
|
- `ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_HOST`: e.g. `ghcr.io/OWNER` or `dockerhandle`.
|
||||||
|
- `ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_SECRET_NAME`: e.g. `docker-secret`: the name of the `k8s` secret resource that allows you to authenticate against the registry with the given handle above
|
||||||
|
|
||||||
|
The workspace is used as the image name.
|
||||||
|
|
||||||
|
The image tag is a random generated string.
|
||||||
|
|
||||||
|
To execute a container-action, we then run a k8s job by loading the image from the specified registry
|
||||||
|
|
||||||
|
## Additional configuration
|
||||||
|
|
||||||
|
Users may want to use different URLs for the registry when pushing and pulling an image as they will be invoked by different machines on different networks.
|
||||||
|
|
||||||
|
- The **Kaniko build container pushes the image** after building is a pod that belongs to the runner pod.
|
||||||
|
- The **kubelet pulls the image** before starting a pod.
|
||||||
|
|
||||||
|
The above two might not resolve all host names 100% the same so it makes sense to allow different push and pull URLs.
|
||||||
|
|
||||||
|
ENVs `ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_HOST_PUSH` and `ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_HOST_PULL` will be preferred if set.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
As an example, a cluster local docker registry could be a long running pod exposed as a service _and_ as a NodePort.
|
||||||
|
|
||||||
|
The Kaniko builder pod would push to `my-local-registry.default.svc.cluster.local:12345/foohandle`. (`ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_HOST_PUSH`)
|
||||||
|
This URL cannot be resolved by the kubelet to pull the image, so we need a secondary URL to pull it - in this case, using the NodePort, this URL is localhost:NODEPORT/foohandle. (`ACTIONS_RUNNER_CONTAINER_HOOKS_K8S_REGISTRY_HOST_PULL)
|
||||||
|
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Users build container-actions with a local Dockerfile in their k8s cluster without a privileged docker builder container
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# ADR 0072: Using Ephemeral Containers
|
|
||||||
|
|
||||||
**Date:** 27 March 2023
|
|
||||||
|
|
||||||
**Status**: Rejected <!--Accepted|Rejected|Superceded|Deprecated-->
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
We are evaluating using Kubernetes [ephemeral containers](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/) as a drop-in replacement for creating pods for [jobs that run in containers](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) and [service containers](https://docs.github.com/en/actions/using-containerized-services/about-service-containers).
|
|
||||||
|
|
||||||
The main motivator behind using ephemeral containers is to eliminate the need for [Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/). Persistent Volume implementations vary depending on the provider and we want to avoid building a dependency on it in order to provide our end-users a consistent experience.
|
|
||||||
|
|
||||||
With ephemeral containers we could leverage [emptyDir volumes](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) which fits our use case better and its behaviour is consistent across providers.
|
|
||||||
|
|
||||||
However, it's important to acknowledge that ephemeral containers were not designed to handle workloads but rather provide a mechanism to inspect running containers for debugging and troubleshooting purposes.
|
|
||||||
|
|
||||||
## Evaluation
|
|
||||||
|
|
||||||
The criteria that we are using to evaluate whether ephemeral containers are fit for purpose are:
|
|
||||||
|
|
||||||
- Networking
|
|
||||||
- Storage
|
|
||||||
- Security
|
|
||||||
- Resource limits
|
|
||||||
- Logs
|
|
||||||
- Customizability
|
|
||||||
|
|
||||||
### Networking
|
|
||||||
|
|
||||||
Ephemeral containers share the networking namespace of the pod they are attached to. This means that ephemeral containers can access the same network interfaces as the pod and can communicate with other containers in the same pod. However, ephemeral containers cannot have ports configured and as such the fields ports, livenessProbe, and readinessProbe are not available [^1][^2]
|
|
||||||
|
|
||||||
In this scenario we have 3 containers in a pod:
|
|
||||||
|
|
||||||
- `runner`: the main container that runs the GitHub Actions job
|
|
||||||
- `debugger`: the first ephemeral container
|
|
||||||
- `debugger2`: the second ephemeral container
|
|
||||||
|
|
||||||
By sequentially opening ports on each of these containers and connecting to them we can demonstrate that the communication flow between the runner and the debuggers is feasible.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>1. Runner -> Debugger communication</summary>
|
|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>2. Debugger -> Runner communication</summary>
|
|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>3. Debugger2 -> Debugger communication</summary>
|
|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Storage
|
|
||||||
|
|
||||||
An emptyDir volume can be successfully mounted (read/write) by the runner as well as the ephemeral containers. This means that ephemeral containers can share data with the runner and other ephemeral containers.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Configuration</summary>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Extracted from the values.yaml for the gha-runner-scale-set helm chart
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: runner
|
|
||||||
image: ghcr.io/actions/actions-runner:latest
|
|
||||||
command: ["/home/runner/run.sh"]
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /workspace
|
|
||||||
name: work-volume
|
|
||||||
volumes:
|
|
||||||
- name: work-volume
|
|
||||||
emptyDir:
|
|
||||||
sizeLimit: 1Gi
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# The API call to the Kubernetes API used to create the ephemeral containers
|
|
||||||
|
|
||||||
POD_NAME="arc-runner-set-6sfwd-runner-k7qq6"
|
|
||||||
NAMESPACE="arc-runners"
|
|
||||||
|
|
||||||
curl -v "https://<IP>:<PORT>/api/v1/namespaces/$NAMESPACE/pods/$POD_NAME/ephemeralcontainers" \
|
|
||||||
-X PATCH \
|
|
||||||
-H 'Content-Type: application/strategic-merge-patch+json' \
|
|
||||||
--cacert <PATH_TO_CACERT> \
|
|
||||||
--cert <PATH_TO_CERT> \
|
|
||||||
--key <PATH_TO_CLIENT_KEY> \
|
|
||||||
-d '
|
|
||||||
{
|
|
||||||
"spec":
|
|
||||||
{
|
|
||||||
"ephemeralContainers":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "debugger",
|
|
||||||
"command": ["sh"],
|
|
||||||
"image": "ghcr.io/actions/actions-runner:latest",
|
|
||||||
"targetContainerName": "runner",
|
|
||||||
"stdin": true,
|
|
||||||
"tty": true,
|
|
||||||
"volumeMounts": [{
|
|
||||||
"mountPath": "/workspace",
|
|
||||||
"name": "work-volume",
|
|
||||||
"readOnly": false
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "debugger2",
|
|
||||||
"command": ["sh"],
|
|
||||||
"image": "ghcr.io/actions/actions-runner:latest",
|
|
||||||
"targetContainerName": "runner",
|
|
||||||
"stdin": true,
|
|
||||||
"tty": true,
|
|
||||||
"volumeMounts": [{
|
|
||||||
"mountPath": "/workspace",
|
|
||||||
"name": "work-volume",
|
|
||||||
"readOnly": false
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>emptyDir volume mount</summary>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
According to the [ephemeral containers API specification](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#ephemeralcontainer-v1-core) the configuration of the `securityContext` field is possible.
|
|
||||||
|
|
||||||
Ephemeral containers share the same network namespace as the pod they are attached to. This means that ephemeral containers can access the same network interfaces as the pod and can communicate with other containers in the same pod.
|
|
||||||
|
|
||||||
It is also possible for ephemeral containers to [share the process namespace](https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/) with the other containers in the pod. This is disabled by default.
|
|
||||||
|
|
||||||
The above could have unpredictable security implications.
|
|
||||||
|
|
||||||
### Resource limits
|
|
||||||
|
|
||||||
Resources are not allowed for ephemeral containers. Ephemeral containers use spare resources already allocated to the pod. [^1] This is a major drawback as it means that ephemeral containers cannot be configured to have resource limits.
|
|
||||||
|
|
||||||
There are no guaranteed resources for ad-hoc troubleshooting. If troubleshooting causes a pod to exceed its resource limit it may be evicted. [^3]
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
Since ephemeral containers can share volumes with the runner container, it's possible to write logs to the same volume and have them available to the runner container.
|
|
||||||
|
|
||||||
### Customizability
|
|
||||||
|
|
||||||
Ephemeral containers can run any image and tag provided, they can be customized to run any arbitrary job. However, it's important to note that the following are not feasible:
|
|
||||||
|
|
||||||
- Lifecycle is not allowed for ephemeral containers
|
|
||||||
- Ephemeral containers will stop when their command exits, such as exiting a shell, and they will not be restarted. Unlike `kubectl exec`, processes in Ephemeral Containers will not receive an `EOF` if their connections are interrupted, so shells won't automatically exit on disconnect. There is no API support for killing or restarting an ephemeral container. The only way to exit the container is to send it an OS signal. [^4]
|
|
||||||
- Probes are not allowed for ephemeral containers.
|
|
||||||
- Ports are not allowed for ephemeral containers.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
While the evaluation shows that ephemeral containers can be used to run jobs in containers, it's important to acknowledge that ephemeral containers were not designed to handle workloads but rather provide a mechanism to inspect running containers for debugging and troubleshooting purposes.
|
|
||||||
|
|
||||||
Given the limitations of ephemeral containers, we decided not to use them outside of their intended purpose.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
Proposal rejected, no further action required. This document will be used as a reference for future discussions.
|
|
||||||
|
|
||||||
[^1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#ephemeralcontainer-v1-core
|
|
||||||
|
|
||||||
[^2]: https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/
|
|
||||||
|
|
||||||
[^3]: https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/277-ephemeral-containers/README.md#notesconstraintscaveats
|
|
||||||
|
|
||||||
[^4]: https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/277-ephemeral-containers/README.md#ephemeral-container-lifecycle
|
|
||||||
BIN
docs/adrs/images/debugger-runner.png
(Stored with Git LFS)
BIN
docs/adrs/images/debugger-runner.png
(Stored with Git LFS)
Binary file not shown.
BIN
docs/adrs/images/debugger2-debugger.png
(Stored with Git LFS)
BIN
docs/adrs/images/debugger2-debugger.png
(Stored with Git LFS)
Binary file not shown.
BIN
docs/adrs/images/emptyDir_volume.png
(Stored with Git LFS)
BIN
docs/adrs/images/emptyDir_volume.png
(Stored with Git LFS)
Binary file not shown.
BIN
docs/adrs/images/runner-debugger.png
(Stored with Git LFS)
BIN
docs/adrs/images/runner-debugger.png
(Stored with Git LFS)
Binary file not shown.
@@ -73,8 +73,6 @@
|
|||||||
"contextName": "redis",
|
"contextName": "redis",
|
||||||
"image": "redis",
|
"image": "redis",
|
||||||
"createOptions": "--cpus 1",
|
"createOptions": "--cpus 1",
|
||||||
"entrypoint": null,
|
|
||||||
"entryPointArgs": [],
|
|
||||||
"environmentVariables": {},
|
"environmentVariables": {},
|
||||||
"userMountVolumes": [
|
"userMountVolumes": [
|
||||||
{
|
{
|
||||||
|
|||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hooks",
|
"name": "hooks",
|
||||||
"version": "0.3.2",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hooks",
|
"name": "hooks",
|
||||||
"version": "0.3.2",
|
"version": "0.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.5.1",
|
"@types/jest": "^27.5.1",
|
||||||
@@ -1800,9 +1800,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@@ -3926,9 +3926,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hooks",
|
"name": "hooks",
|
||||||
"version": "0.3.2",
|
"version": "0.1.3",
|
||||||
"description": "Three projects are included - k8s: a kubernetes hook implementation that spins up pods dynamically to run a job - docker: A hook implementation of the runner's docker implementation - A hook lib, which contains shared typescript definitions and utilities that the other packages consume",
|
"description": "Three projects are included - k8s: a kubernetes hook implementation that spins up pods dynamically to run a job - docker: A hook implementation of the runner's docker implementation - A hook lib, which contains shared typescript definitions and utilities that the other packages consume",
|
||||||
"main": "",
|
"main": "",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
64
packages/docker/package-lock.json
generated
64
packages/docker/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.6.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"hooklib": "file:../hooklib",
|
"hooklib": "file:../hooklib",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1"
|
"@actions/core": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^17.0.23",
|
||||||
@@ -43,12 +43,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^1.0.11"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/exec": {
|
"node_modules/@actions/exec": {
|
||||||
@@ -60,11 +59,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/http-client": {
|
"node_modules/@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/io": {
|
"node_modules/@actions/io": {
|
||||||
@@ -3779,9 +3778,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
@@ -4903,9 +4902,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@@ -5280,12 +5279,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^1.0.11"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/exec": {
|
"@actions/exec": {
|
||||||
@@ -5297,11 +5295,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/http-client": {
|
"@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/io": {
|
"@actions/io": {
|
||||||
@@ -7378,7 +7376,7 @@
|
|||||||
"hooklib": {
|
"hooklib": {
|
||||||
"version": "file:../hooklib",
|
"version": "file:../hooklib",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.6.0",
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^17.0.23",
|
||||||
"@typescript-eslint/parser": "^5.18.0",
|
"@typescript-eslint/parser": "^5.18.0",
|
||||||
"@zeit/ncc": "^0.22.3",
|
"@zeit/ncc": "^0.22.3",
|
||||||
@@ -8176,9 +8174,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"kleur": {
|
"kleur": {
|
||||||
@@ -8985,9 +8983,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.6.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"hooklib": "file:../hooklib",
|
"hooklib": "file:../hooklib",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
|
|||||||
@@ -427,9 +427,6 @@ export async function containerRun(
|
|||||||
dockerArgs.push(args.image)
|
dockerArgs.push(args.image)
|
||||||
if (args.entryPointArgs) {
|
if (args.entryPointArgs) {
|
||||||
for (const entryPointArg of args.entryPointArgs) {
|
for (const entryPointArg of args.entryPointArgs) {
|
||||||
if (!entryPointArg) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dockerArgs.push(entryPointArg)
|
dockerArgs.push(entryPointArg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ import {
|
|||||||
import { checkEnvironment } from './utils'
|
import { checkEnvironment } from './utils'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
|
||||||
checkEnvironment()
|
|
||||||
const input = await getInputFromStdin()
|
const input = await getInputFromStdin()
|
||||||
|
|
||||||
const args = input['args']
|
const args = input['args']
|
||||||
const command = input['command']
|
const command = input['command']
|
||||||
const responseFile = input['responseFile']
|
const responseFile = input['responseFile']
|
||||||
const state = input['state']
|
const state = input['state']
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkEnvironment()
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case Command.PrepareJob:
|
case Command.PrepareJob:
|
||||||
await prepareJob(args as PrepareJobArgs, responseFile)
|
await prepareJob(args as PrepareJobArgs, responseFile)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export async function runDockerCommand(
|
|||||||
args: string[],
|
args: string[],
|
||||||
options?: RunDockerCommandOptions
|
options?: RunDockerCommandOptions
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
options = optionsWithDockerEnvs(options)
|
|
||||||
const pipes = await exec.getExecOutput('docker', args, options)
|
const pipes = await exec.getExecOutput('docker', args, options)
|
||||||
if (pipes.exitCode !== 0) {
|
if (pipes.exitCode !== 0) {
|
||||||
core.error(`Docker failed with exit code ${pipes.exitCode}`)
|
core.error(`Docker failed with exit code ${pipes.exitCode}`)
|
||||||
@@ -25,45 +24,6 @@ export async function runDockerCommand(
|
|||||||
return Promise.resolve(pipes.stdout)
|
return Promise.resolve(pipes.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function optionsWithDockerEnvs(
|
|
||||||
options?: RunDockerCommandOptions
|
|
||||||
): RunDockerCommandOptions | undefined {
|
|
||||||
// From https://docs.docker.com/engine/reference/commandline/cli/#environment-variables
|
|
||||||
const dockerCliEnvs = new Set([
|
|
||||||
'DOCKER_API_VERSION',
|
|
||||||
'DOCKER_CERT_PATH',
|
|
||||||
'DOCKER_CONFIG',
|
|
||||||
'DOCKER_CONTENT_TRUST_SERVER',
|
|
||||||
'DOCKER_CONTENT_TRUST',
|
|
||||||
'DOCKER_CONTEXT',
|
|
||||||
'DOCKER_DEFAULT_PLATFORM',
|
|
||||||
'DOCKER_HIDE_LEGACY_COMMANDS',
|
|
||||||
'DOCKER_HOST',
|
|
||||||
'DOCKER_STACK_ORCHESTRATOR',
|
|
||||||
'DOCKER_TLS_VERIFY',
|
|
||||||
'BUILDKIT_PROGRESS'
|
|
||||||
])
|
|
||||||
const dockerEnvs = {}
|
|
||||||
for (const key in process.env) {
|
|
||||||
if (dockerCliEnvs.has(key)) {
|
|
||||||
dockerEnvs[key] = process.env[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newOptions = {
|
|
||||||
workingDir: options?.workingDir,
|
|
||||||
input: options?.input,
|
|
||||||
env: options?.env || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set docker envs or overwrite provided ones
|
|
||||||
for (const [key, value] of Object.entries(dockerEnvs)) {
|
|
||||||
newOptions.env[key] = value as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitize(val: string): string {
|
export function sanitize(val: string): string {
|
||||||
if (!val || typeof val !== 'string') {
|
if (!val || typeof val !== 'string') {
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { optionsWithDockerEnvs, sanitize } from '../src/utils'
|
import { sanitize } from '../src/utils'
|
||||||
|
|
||||||
describe('Utilities', () => {
|
describe('Utilities', () => {
|
||||||
it('should return sanitized image name', () => {
|
it('should return sanitized image name', () => {
|
||||||
@@ -9,41 +9,4 @@ describe('Utilities', () => {
|
|||||||
const validStr = 'teststr8_one'
|
const validStr = 'teststr8_one'
|
||||||
expect(sanitize(validStr)).toBe(validStr)
|
expect(sanitize(validStr)).toBe(validStr)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with docker options', () => {
|
|
||||||
it('should augment options with docker environment variables', () => {
|
|
||||||
process.env.DOCKER_HOST = 'unix:///run/user/1001/docker.sock'
|
|
||||||
process.env.DOCKER_NOTEXIST = 'notexist'
|
|
||||||
|
|
||||||
const optionDefinitions: any = [
|
|
||||||
undefined,
|
|
||||||
{},
|
|
||||||
{ env: {} },
|
|
||||||
{ env: { DOCKER_HOST: 'unix://var/run/docker.sock' } }
|
|
||||||
]
|
|
||||||
for (const opt of optionDefinitions) {
|
|
||||||
let options = optionsWithDockerEnvs(opt)
|
|
||||||
expect(options).toBeDefined()
|
|
||||||
expect(options?.env).toBeDefined()
|
|
||||||
expect(options?.env?.DOCKER_HOST).toBe(process.env.DOCKER_HOST)
|
|
||||||
expect(options?.env?.DOCKER_NOTEXIST).toBeUndefined()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not overwrite other options', () => {
|
|
||||||
process.env.DOCKER_HOST = 'unix:///run/user/1001/docker.sock'
|
|
||||||
const opt = {
|
|
||||||
workingDir: 'test',
|
|
||||||
input: Buffer.from('test')
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = optionsWithDockerEnvs(opt)
|
|
||||||
expect(options).toBeDefined()
|
|
||||||
expect(options?.workingDir).toBe(opt.workingDir)
|
|
||||||
expect(options?.input).toBe(opt.input)
|
|
||||||
expect(options?.env).toStrictEqual({
|
|
||||||
DOCKER_HOST: process.env.DOCKER_HOST
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
61
packages/hooklib/package-lock.json
generated
61
packages/hooklib/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1"
|
"@actions/core": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.23",
|
"@types/node": "^17.0.23",
|
||||||
@@ -22,20 +22,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/core": {
|
"node_modules/@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^1.0.11"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/http-client": {
|
"node_modules/@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
@@ -1742,9 +1741,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@@ -2486,14 +2485,6 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/v8-compile-cache": {
|
"node_modules/v8-compile-cache": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||||
@@ -2555,20 +2546,19 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz",
|
||||||
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
"integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^1.0.11"
|
||||||
"uuid": "^8.3.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@actions/http-client": {
|
"@actions/http-client": {
|
||||||
"version": "2.0.1",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
||||||
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@eslint/eslintrc": {
|
"@eslint/eslintrc": {
|
||||||
@@ -3789,9 +3779,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@@ -4310,11 +4300,6 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uuid": {
|
|
||||||
"version": "8.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
|
||||||
},
|
|
||||||
"v8-compile-cache": {
|
"v8-compile-cache": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,6 @@
|
|||||||
"typescript": "^4.6.3"
|
"typescript": "^4.6.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1"
|
"@actions/core": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1026
packages/k8s/package-lock.json
generated
1026
packages/k8s/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,10 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.6.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2",
|
"@actions/io": "^1.1.2",
|
||||||
"@kubernetes/client-node": "^0.18.1",
|
"@kubernetes/client-node": "^0.16.3",
|
||||||
"hooklib": "file:../hooklib"
|
"hooklib": "file:../hooklib"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
containerVolumes,
|
containerVolumes,
|
||||||
DEFAULT_CONTAINER_ENTRY_POINT,
|
DEFAULT_CONTAINER_ENTRY_POINT,
|
||||||
DEFAULT_CONTAINER_ENTRY_POINT_ARGS,
|
DEFAULT_CONTAINER_ENTRY_POINT_ARGS,
|
||||||
generateContainerName,
|
|
||||||
PodPhase
|
PodPhase
|
||||||
} from '../k8s/utils'
|
} from '../k8s/utils'
|
||||||
import { JOB_CONTAINER_NAME } from './constants'
|
import { JOB_CONTAINER_NAME } from './constants'
|
||||||
@@ -32,14 +31,14 @@ export async function prepareJob(
|
|||||||
let container: k8s.V1Container | undefined = undefined
|
let container: k8s.V1Container | undefined = undefined
|
||||||
if (args.container?.image) {
|
if (args.container?.image) {
|
||||||
core.debug(`Using image '${args.container.image}' for job image`)
|
core.debug(`Using image '${args.container.image}' for job image`)
|
||||||
container = createContainerSpec(args.container, JOB_CONTAINER_NAME, true)
|
container = createPodSpec(args.container, JOB_CONTAINER_NAME, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
let services: k8s.V1Container[] = []
|
let services: k8s.V1Container[] = []
|
||||||
if (args.services?.length) {
|
if (args.services?.length) {
|
||||||
services = args.services.map(service => {
|
services = args.services.map(service => {
|
||||||
core.debug(`Adding service '${service.image}' to pod definition`)
|
core.debug(`Adding service '${service.image}' to pod definition`)
|
||||||
return createContainerSpec(service, generateContainerName(service.image))
|
return createPodSpec(service, service.image.split(':')[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!container && !services?.length) {
|
if (!container && !services?.length) {
|
||||||
@@ -125,12 +124,14 @@ function generateResponseFile(
|
|||||||
)
|
)
|
||||||
if (serviceContainers?.length) {
|
if (serviceContainers?.length) {
|
||||||
response.context['services'] = serviceContainers.map(c => {
|
response.context['services'] = serviceContainers.map(c => {
|
||||||
|
if (!c.ports) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const ctxPorts: ContextPorts = {}
|
const ctxPorts: ContextPorts = {}
|
||||||
if (c.ports?.length) {
|
|
||||||
for (const port of c.ports) {
|
for (const port of c.ports) {
|
||||||
ctxPorts[port.containerPort] = port.hostPort
|
ctxPorts[port.containerPort] = port.hostPort
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image: c.image,
|
image: c.image,
|
||||||
@@ -152,12 +153,12 @@ async function copyExternalsToRoot(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContainerSpec(
|
function createPodSpec(
|
||||||
container,
|
container,
|
||||||
name: string,
|
name: string,
|
||||||
jobContainer = false
|
jobContainer = false
|
||||||
): k8s.V1Container {
|
): k8s.V1Container {
|
||||||
if (!container.entryPoint && jobContainer) {
|
if (!container.entryPoint) {
|
||||||
container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT
|
container.entryPoint = DEFAULT_CONTAINER_ENTRY_POINT
|
||||||
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
container.entryPointArgs = DEFAULT_CONTAINER_ENTRY_POINT_ARGS
|
||||||
}
|
}
|
||||||
@@ -165,20 +166,14 @@ export function createContainerSpec(
|
|||||||
const podContainer = {
|
const podContainer = {
|
||||||
name,
|
name,
|
||||||
image: container.image,
|
image: container.image,
|
||||||
|
command: [container.entryPoint],
|
||||||
|
args: container.entryPointArgs,
|
||||||
ports: containerPorts(container)
|
ports: containerPorts(container)
|
||||||
} as k8s.V1Container
|
} as k8s.V1Container
|
||||||
if (container.workingDirectory) {
|
if (container.workingDirectory) {
|
||||||
podContainer.workingDir = container.workingDirectory
|
podContainer.workingDir = container.workingDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.entryPoint) {
|
|
||||||
podContainer.command = [container.entryPoint]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container.entryPointArgs?.length > 0) {
|
|
||||||
podContainer.args = container.entryPointArgs
|
|
||||||
}
|
|
||||||
|
|
||||||
podContainer.env = []
|
podContainer.env = []
|
||||||
for (const [key, value] of Object.entries(
|
for (const [key, value] of Object.entries(
|
||||||
container['environmentVariables']
|
container['environmentVariables']
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import {
|
|||||||
import { isAuthPermissionsOK, namespace, requiredPermissions } from './k8s'
|
import { isAuthPermissionsOK, namespace, requiredPermissions } from './k8s'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
|
||||||
const input = await getInputFromStdin()
|
const input = await getInputFromStdin()
|
||||||
|
|
||||||
const args = input['args']
|
const args = input['args']
|
||||||
const command = input['command']
|
const command = input['command']
|
||||||
const responseFile = input['responseFile']
|
const responseFile = input['responseFile']
|
||||||
const state = input['state']
|
const state = input['state']
|
||||||
|
|
||||||
|
let exitCode = 0
|
||||||
|
try {
|
||||||
if (!(await isAuthPermissionsOK())) {
|
if (!(await isAuthPermissionsOK())) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The Service account needs the following permissions ${JSON.stringify(
|
`The Service account needs the following permissions ${JSON.stringify(
|
||||||
@@ -23,28 +25,28 @@ async function run(): Promise<void> {
|
|||||||
)} on the pod resource in the '${namespace()}' namespace. Please contact your self hosted runner administrator.`
|
)} on the pod resource in the '${namespace()}' namespace. Please contact your self hosted runner administrator.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let exitCode = 0
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case Command.PrepareJob:
|
case Command.PrepareJob:
|
||||||
await prepareJob(args as prepareJobArgs, responseFile)
|
await prepareJob(args as prepareJobArgs, responseFile)
|
||||||
return process.exit(0)
|
break
|
||||||
case Command.CleanupJob:
|
case Command.CleanupJob:
|
||||||
await cleanupJob()
|
await cleanupJob()
|
||||||
return process.exit(0)
|
break
|
||||||
case Command.RunScriptStep:
|
case Command.RunScriptStep:
|
||||||
await runScriptStep(args, state, null)
|
await runScriptStep(args, state, null)
|
||||||
return process.exit(0)
|
break
|
||||||
case Command.RunContainerStep:
|
case Command.RunContainerStep:
|
||||||
exitCode = await runContainerStep(args)
|
exitCode = await runContainerStep(args)
|
||||||
return process.exit(exitCode)
|
break
|
||||||
|
case Command.runContainerStep:
|
||||||
default:
|
default:
|
||||||
throw new Error(`Command not recognized: ${command}`)
|
throw new Error(`Command not recognized: ${command}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.error(error as Error)
|
core.error(error as Error)
|
||||||
process.exit(1)
|
exitCode = 1
|
||||||
}
|
}
|
||||||
|
process.exitCode = exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
void run()
|
void run()
|
||||||
|
|||||||
@@ -516,40 +516,27 @@ class BackOffManager {
|
|||||||
export function containerPorts(
|
export function containerPorts(
|
||||||
container: ContainerInfo
|
container: ContainerInfo
|
||||||
): k8s.V1ContainerPort[] {
|
): k8s.V1ContainerPort[] {
|
||||||
|
// 8080:8080/tcp
|
||||||
|
const portFormat = /(\d{1,5})(:(\d{1,5}))?(\/(tcp|udp))?/
|
||||||
|
|
||||||
const ports: k8s.V1ContainerPort[] = []
|
const ports: k8s.V1ContainerPort[] = []
|
||||||
if (!container.portMappings?.length) {
|
|
||||||
return ports
|
|
||||||
}
|
|
||||||
for (const portDefinition of container.portMappings) {
|
for (const portDefinition of container.portMappings) {
|
||||||
const portProtoSplit = portDefinition.split('/')
|
const submatches = portFormat.exec(portDefinition)
|
||||||
if (portProtoSplit.length > 2) {
|
if (!submatches) {
|
||||||
throw new Error(`Unexpected port format: ${portDefinition}`)
|
throw new Error(
|
||||||
|
`Port definition "${portDefinition}" is in incorrect format`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = new k8s.V1ContainerPort()
|
const port = new k8s.V1ContainerPort()
|
||||||
port.protocol =
|
port.hostPort = Number(submatches[1])
|
||||||
portProtoSplit.length === 2 ? portProtoSplit[1].toUpperCase() : 'TCP'
|
if (submatches[3]) {
|
||||||
|
port.containerPort = Number(submatches[3])
|
||||||
const portSplit = portProtoSplit[0].split(':')
|
|
||||||
if (portSplit.length > 2) {
|
|
||||||
throw new Error('ports should have at most one ":" separator')
|
|
||||||
}
|
}
|
||||||
|
if (submatches[5]) {
|
||||||
const parsePort = (p: string): number => {
|
port.protocol = submatches[5].toUpperCase()
|
||||||
const num = Number(p)
|
|
||||||
if (!Number.isInteger(num) || num < 1 || num > 65535) {
|
|
||||||
throw new Error(`invalid container port: ${p}`)
|
|
||||||
}
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
|
|
||||||
if (portSplit.length === 1) {
|
|
||||||
port.containerPort = parsePort(portSplit[0])
|
|
||||||
} else {
|
} else {
|
||||||
port.hostPort = parsePort(portSplit[0])
|
port.protocol = 'TCP'
|
||||||
port.containerPort = parsePort(portSplit[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ports.push(port)
|
ports.push(port)
|
||||||
}
|
}
|
||||||
return ports
|
return ports
|
||||||
|
|||||||
@@ -22,18 +22,16 @@ export function containerVolumes(
|
|||||||
|
|
||||||
const workspacePath = process.env.GITHUB_WORKSPACE as string
|
const workspacePath = process.env.GITHUB_WORKSPACE as string
|
||||||
if (containerAction) {
|
if (containerAction) {
|
||||||
const i = workspacePath.lastIndexOf('_work/')
|
|
||||||
const workspaceRelativePath = workspacePath.slice(i + '_work/'.length)
|
|
||||||
mounts.push(
|
mounts.push(
|
||||||
{
|
{
|
||||||
name: POD_VOLUME_NAME,
|
name: POD_VOLUME_NAME,
|
||||||
mountPath: '/github/workspace',
|
mountPath: '/github/workspace',
|
||||||
subPath: workspaceRelativePath
|
subPath: workspacePath.substring(workspacePath.indexOf('work/') + 1)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: POD_VOLUME_NAME,
|
name: POD_VOLUME_NAME,
|
||||||
mountPath: '/github/file_commands',
|
mountPath: '/github/file_commands',
|
||||||
subPath: '_temp/_runner_file_commands'
|
subPath: workspacePath.substring(workspacePath.indexOf('work/') + 1)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return mounts
|
return mounts
|
||||||
@@ -111,21 +109,11 @@ export function writeEntryPointScript(
|
|||||||
if (environmentVariables && Object.entries(environmentVariables).length) {
|
if (environmentVariables && Object.entries(environmentVariables).length) {
|
||||||
const envBuffer: string[] = []
|
const envBuffer: string[] = []
|
||||||
for (const [key, value] of Object.entries(environmentVariables)) {
|
for (const [key, value] of Object.entries(environmentVariables)) {
|
||||||
if (
|
|
||||||
key.includes(`=`) ||
|
|
||||||
key.includes(`'`) ||
|
|
||||||
key.includes(`"`) ||
|
|
||||||
key.includes(`$`)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`environment key ${key} is invalid - the key must not contain =, $, ', or "`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
envBuffer.push(
|
envBuffer.push(
|
||||||
`"${key}=${value
|
`"${key}=${value
|
||||||
.replace(/\\/g, '\\\\')
|
.replace(/\\/g, '\\\\')
|
||||||
.replace(/"/g, '\\"')
|
.replace(/"/g, '\\"')
|
||||||
.replace(/\$/g, '\\$')}"`
|
.replace(/=/g, '\\=')}"`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
environmentPrefix = `env ${envBuffer.join(' ')} `
|
environmentPrefix = `env ${envBuffer.join(' ')} `
|
||||||
@@ -147,17 +135,6 @@ exec ${environmentPrefix} ${entryPoint} ${
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateContainerName(image: string): string {
|
|
||||||
const nameWithTag = image.split('/').pop()
|
|
||||||
const name = nameWithTag?.split(':').at(0)
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
throw new Error(`Image definition '${image}' is invalid`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PodPhase {
|
export enum PodPhase {
|
||||||
PENDING = 'Pending',
|
PENDING = 'Pending',
|
||||||
RUNNING = 'Running',
|
RUNNING = 'Running',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
getSecretName,
|
getSecretName,
|
||||||
getStepPodName,
|
getStepPodName,
|
||||||
getVolumeClaimName,
|
getVolumeClaimName,
|
||||||
JOB_CONTAINER_NAME,
|
|
||||||
MAX_POD_NAME_LENGTH,
|
MAX_POD_NAME_LENGTH,
|
||||||
RunnerInstanceLabel,
|
RunnerInstanceLabel,
|
||||||
STEP_POD_NAME_SUFFIX_LENGTH
|
STEP_POD_NAME_SUFFIX_LENGTH
|
||||||
@@ -171,12 +170,4 @@ describe('constants', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('const values', () => {
|
|
||||||
it('should have constants set', () => {
|
|
||||||
expect(JOB_CONTAINER_NAME).toBeTruthy()
|
|
||||||
expect(MAX_POD_NAME_LENGTH).toBeGreaterThan(0)
|
|
||||||
expect(STEP_POD_NAME_SUFFIX_LENGTH).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { containerPorts, POD_VOLUME_NAME } from '../src/k8s'
|
import { POD_VOLUME_NAME } from '../src/k8s'
|
||||||
import {
|
import { containerVolumes, writeEntryPointScript } from '../src/k8s/utils'
|
||||||
containerVolumes,
|
|
||||||
generateContainerName,
|
|
||||||
writeEntryPointScript
|
|
||||||
} from '../src/k8s/utils'
|
|
||||||
import { TestHelper } from './test-setup'
|
import { TestHelper } from './test-setup'
|
||||||
|
|
||||||
let testHelper: TestHelper
|
let testHelper: TestHelper
|
||||||
@@ -49,81 +45,6 @@ describe('k8s utils', () => {
|
|||||||
).toThrow()
|
).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw if environment variable name contains double quote', () => {
|
|
||||||
expect(() =>
|
|
||||||
writeEntryPointScript(
|
|
||||||
'/test',
|
|
||||||
'sh',
|
|
||||||
['-e', 'script.sh'],
|
|
||||||
['/prepend/path'],
|
|
||||||
{
|
|
||||||
'SOME"_ENV': 'SOME_VALUE'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw if environment variable name contains =', () => {
|
|
||||||
expect(() =>
|
|
||||||
writeEntryPointScript(
|
|
||||||
'/test',
|
|
||||||
'sh',
|
|
||||||
['-e', 'script.sh'],
|
|
||||||
['/prepend/path'],
|
|
||||||
{
|
|
||||||
'SOME=ENV': 'SOME_VALUE'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw if environment variable name contains single quote', () => {
|
|
||||||
expect(() =>
|
|
||||||
writeEntryPointScript(
|
|
||||||
'/test',
|
|
||||||
'sh',
|
|
||||||
['-e', 'script.sh'],
|
|
||||||
['/prepend/path'],
|
|
||||||
{
|
|
||||||
"SOME'_ENV": 'SOME_VALUE'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw if environment variable name contains dollar', () => {
|
|
||||||
expect(() =>
|
|
||||||
writeEntryPointScript(
|
|
||||||
'/test',
|
|
||||||
'sh',
|
|
||||||
['-e', 'script.sh'],
|
|
||||||
['/prepend/path'],
|
|
||||||
{
|
|
||||||
SOME_$_ENV: 'SOME_VALUE'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should escape double quote, dollar and backslash in environment variable values', () => {
|
|
||||||
const { runnerPath } = writeEntryPointScript(
|
|
||||||
'/test',
|
|
||||||
'sh',
|
|
||||||
['-e', 'script.sh'],
|
|
||||||
['/prepend/path'],
|
|
||||||
{
|
|
||||||
DQUOTE: '"',
|
|
||||||
BACK_SLASH: '\\',
|
|
||||||
DOLLAR: '$'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(fs.existsSync(runnerPath)).toBe(true)
|
|
||||||
const script = fs.readFileSync(runnerPath, 'utf8')
|
|
||||||
expect(script).toContain('"DQUOTE=\\"')
|
|
||||||
expect(script).toContain('"BACK_SLASH=\\\\"')
|
|
||||||
expect(script).toContain('"DOLLAR=\\$"')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return object with containerPath and runnerPath', () => {
|
it('should return object with containerPath and runnerPath', () => {
|
||||||
const { containerPath, runnerPath } = writeEntryPointScript(
|
const { containerPath, runnerPath } = writeEntryPointScript(
|
||||||
'/test',
|
'/test',
|
||||||
@@ -182,22 +103,19 @@ describe('k8s utils', () => {
|
|||||||
|
|
||||||
it('should have container action volumes', () => {
|
it('should have container action volumes', () => {
|
||||||
let volumes = containerVolumes([], true, true)
|
let volumes = containerVolumes([], true, true)
|
||||||
let workspace = volumes.find(e => e.mountPath === '/github/workspace')
|
expect(
|
||||||
let fileCommands = volumes.find(
|
volumes.find(e => e.mountPath === '/github/workspace')
|
||||||
e => e.mountPath === '/github/file_commands'
|
).toBeTruthy()
|
||||||
)
|
expect(
|
||||||
expect(workspace).toBeTruthy()
|
volumes.find(e => e.mountPath === '/github/file_commands')
|
||||||
expect(workspace?.subPath).toBe('repo/repo')
|
).toBeTruthy()
|
||||||
expect(fileCommands).toBeTruthy()
|
|
||||||
expect(fileCommands?.subPath).toBe('_temp/_runner_file_commands')
|
|
||||||
|
|
||||||
volumes = containerVolumes([], false, true)
|
volumes = containerVolumes([], false, true)
|
||||||
workspace = volumes.find(e => e.mountPath === '/github/workspace')
|
expect(
|
||||||
fileCommands = volumes.find(e => e.mountPath === '/github/file_commands')
|
volumes.find(e => e.mountPath === '/github/workspace')
|
||||||
expect(workspace).toBeTruthy()
|
).toBeTruthy()
|
||||||
expect(workspace?.subPath).toBe('repo/repo')
|
expect(
|
||||||
expect(fileCommands).toBeTruthy()
|
volumes.find(e => e.mountPath === '/github/file_commands')
|
||||||
expect(fileCommands?.subPath).toBe('_temp/_runner_file_commands')
|
).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have externals, github home and github workflow mounts if job container', () => {
|
it('should have externals, github home and github workflow mounts if job container', () => {
|
||||||
@@ -231,101 +149,5 @@ describe('k8s utils', () => {
|
|||||||
volumes = containerVolumes([], false, false)
|
volumes = containerVolumes([], false, false)
|
||||||
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
|
expect(volumes.every(e => e.name === POD_VOLUME_NAME)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse container ports', () => {
|
|
||||||
const tt = [
|
|
||||||
{
|
|
||||||
spec: '8080:80',
|
|
||||||
want: {
|
|
||||||
containerPort: 80,
|
|
||||||
hostPort: 8080,
|
|
||||||
protocol: 'TCP'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
spec: '8080:80/udp',
|
|
||||||
want: {
|
|
||||||
containerPort: 80,
|
|
||||||
hostPort: 8080,
|
|
||||||
protocol: 'UDP'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
spec: '8080/udp',
|
|
||||||
want: {
|
|
||||||
containerPort: 8080,
|
|
||||||
hostPort: undefined,
|
|
||||||
protocol: 'UDP'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
spec: '8080',
|
|
||||||
want: {
|
|
||||||
containerPort: 8080,
|
|
||||||
hostPort: undefined,
|
|
||||||
protocol: 'TCP'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const tc of tt) {
|
|
||||||
const got = containerPorts({ portMappings: [tc.spec] })
|
|
||||||
for (const [key, value] of Object.entries(tc.want)) {
|
|
||||||
expect(got[0][key]).toBe(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw when ports are out of range (0, 65536)', () => {
|
|
||||||
expect(() => containerPorts({ portMappings: ['65536'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['0'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['65536/udp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['0/udp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:65536'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['65536:1'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:65536/tcp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['65536:1/tcp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: [':1'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:/tcp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: [':1/tcp'] })).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw on multi ":" splits', () => {
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:1:1'] })).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw on multi "/" splits', () => {
|
|
||||||
expect(() => containerPorts({ portMappings: ['1:1/tcp/udp'] })).toThrow()
|
|
||||||
expect(() => containerPorts({ portMappings: ['1/tcp/udp'] })).toThrow()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('generate container name', () => {
|
|
||||||
it('should return the container name from image string', () => {
|
|
||||||
expect(
|
|
||||||
generateContainerName('public.ecr.aws/localstack/localstack')
|
|
||||||
).toEqual('localstack')
|
|
||||||
expect(
|
|
||||||
generateContainerName(
|
|
||||||
'public.ecr.aws/url/with/multiple/slashes/postgres:latest'
|
|
||||||
)
|
|
||||||
).toEqual('postgres')
|
|
||||||
expect(generateContainerName('postgres')).toEqual('postgres')
|
|
||||||
expect(generateContainerName('postgres:latest')).toEqual('postgres')
|
|
||||||
expect(generateContainerName('localstack/localstack')).toEqual(
|
|
||||||
'localstack'
|
|
||||||
)
|
|
||||||
expect(generateContainerName('localstack/localstack:latest')).toEqual(
|
|
||||||
'localstack'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw on invalid image string', () => {
|
|
||||||
expect(() =>
|
|
||||||
generateContainerName('localstack/localstack/:latest')
|
|
||||||
).toThrow()
|
|
||||||
expect(() => generateContainerName(':latest')).toThrow()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { cleanupJob } from '../src/hooks'
|
import { cleanupJob } from '../src/hooks'
|
||||||
import { createContainerSpec, prepareJob } from '../src/hooks/prepare-job'
|
import { prepareJob } from '../src/hooks/prepare-job'
|
||||||
import { TestHelper } from './test-setup'
|
import { TestHelper } from './test-setup'
|
||||||
import { generateContainerName } from '../src/k8s/utils'
|
|
||||||
import { V1Container } from '@kubernetes/client-node'
|
|
||||||
|
|
||||||
jest.useRealTimers()
|
jest.useRealTimers()
|
||||||
|
|
||||||
@@ -73,27 +71,4 @@ describe('Prepare job', () => {
|
|||||||
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
||||||
).rejects.toThrow()
|
).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not set command + args for service container if not passed in args', async () => {
|
|
||||||
const services = prepareJobData.args.services.map(service => {
|
|
||||||
return createContainerSpec(service, generateContainerName(service.image))
|
|
||||||
}) as [V1Container]
|
|
||||||
|
|
||||||
expect(services[0].command).toBe(undefined)
|
|
||||||
expect(services[0].args).toBe(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test.each([undefined, null, []])(
|
|
||||||
'should not throw exception when portMapping=%p',
|
|
||||||
async pm => {
|
|
||||||
prepareJobData.args.services.forEach(s => {
|
|
||||||
s.portMappings = pm
|
|
||||||
})
|
|
||||||
await prepareJob(prepareJobData.args, prepareJobOutputFilePath)
|
|
||||||
const content = JSON.parse(
|
|
||||||
fs.readFileSync(prepareJobOutputFilePath).toString()
|
|
||||||
)
|
|
||||||
expect(() => content.context.services[0].image).not.toThrow()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -89,28 +89,6 @@ describe('Run script step', () => {
|
|||||||
).resolves.not.toThrow()
|
).resolves.not.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Dollar symbols in environment variables should not be expanded', async () => {
|
|
||||||
runScriptStepDefinition.args.environmentVariables = {
|
|
||||||
VARIABLE1: '$VAR',
|
|
||||||
VARIABLE2: '${VAR}',
|
|
||||||
VARIABLE3: '$(VAR)'
|
|
||||||
}
|
|
||||||
runScriptStepDefinition.args.entryPointArgs = [
|
|
||||||
'-c',
|
|
||||||
'\'if [[ -z "$VARIABLE1" ]]; then exit 1; fi\'',
|
|
||||||
'\'if [[ -z "$VARIABLE2" ]]; then exit 2; fi\'',
|
|
||||||
'\'if [[ -z "$VARIABLE3" ]]; then exit 3; fi\''
|
|
||||||
]
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
runScriptStep(
|
|
||||||
runScriptStepDefinition.args,
|
|
||||||
prepareJobOutputData.state,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have path variable changed in container with prepend path string array', async () => {
|
it('Should have path variable changed in container with prepend path string array', async () => {
|
||||||
runScriptStepDefinition.args.prependPath = ['/some/other/path']
|
runScriptStepDefinition.args.prependPath = ['/some/other/path']
|
||||||
runScriptStepDefinition.args.entryPoint = '/bin/bash'
|
runScriptStepDefinition.args.entryPoint = '/bin/bash'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- ## Features -->
|
## Features
|
||||||
|
|
||||||
## Bugs
|
## Bugs
|
||||||
|
- Fixed an issue where default private registry images did not pull correctly [#25]
|
||||||
|
|
||||||
- Handle `$` symbols in environment variable names and values in k8s [#74]
|
## Misc
|
||||||
|
|
||||||
<!-- ## Misc -->
|
|
||||||
Reference in New Issue
Block a user