Compare commits

...

20 Commits

Author SHA1 Message Date
Thomas Boop
709bd6113d Update releaseVersion 2022-06-22 12:07:32 -04:00
Thomas Boop
d2c6a4e4bc 294.0 release notes (#1963)
* 293.1 release notes

* let make it a minor version bump
2022-06-22 11:57:10 -04:00
Stefan Ruvceski
d11bd3d8be Created env var for forcing node12 actions to run on node16 (#1913)
* Created env var for forcing node12 actions to run on node16

* get value of hostContext environment variable

* changing location of forced node version check

* small code refactoring

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* more of small code refactoring

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* refactoring of conditions for getting internal node version

* changing expected value for node version env var

* Adding empty line between two methods

* Created method GetNodeVersion

* GetNodeVersion from function to inline call and PR fixes

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-06-22 11:13:28 -04:00
Tingluo Huang
761785620f Support pass runner JitConfig as arg. (#1925) 2022-06-22 10:59:29 -04:00
Ferenc Hammerl
416771d4b1 Fix PrependPath format to be array instead of a concatenated string (#1948)
* Fix prependPath format
2022-06-22 09:11:17 -04:00
Tatyana Kostromskaya
9499f477a2 Add retry logic around getting job messages from broker (#1939)
* Jsut simple solution without additional funcs

* Delete old comment

* resolve

* Refactor retry function, make it more common

* Make retry function generic, get rid of extra params

* delete extra using

* Add cancellation token and limit of attempts

* Add some additional logging

* Rework condition

* replace to do..while

* return `while (true)` to simplify code structure

* Add other cancelling token, add TODO comment
2022-06-21 16:12:07 +02:00
Ferenc Hammerl
6bc6d475f9 No longer trace 'ex' twice, only further up in the callstack (#1949) 2022-06-16 16:36:54 -04:00
Tuukka Lahti
ca2b1bc6d5 Update dependencies list to support Ubuntu 22.04 (#1946) 2022-06-15 12:37:02 -04:00
Ferenc Hammerl
591f8c3510 Runner container hooks Beta (#1853)
* Added ability to run Dockerfile.SUFFIX ContainerAction

* Extracted IsDockerFile method

* reformatted, moved from index to Last()

* extracted IsDockerfile to DockerUtil with L0

* added check for IsDockerfile to account for docker://

* updated test to clearly show path/dockerfile:tag

* fail if Data.Image is not Dockerfile or docker://[image]

* Setup noops for JobPrepare and JobCleanup hooks

* Add container jobstarted and jobcomplete hooks

* Run 'index.js' instead of specific command hooks

* Call jobprepare with command arg

* Use right command name (hardcoded)

Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com>

* Invoke hooks with arguments

* Add PrepareJob hook to work with jobcontainers

Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com>

* Rename methods

* Use new hookcontainer to run prep and clean hooks

* Get path from ENV

* Use enums

* Use IOUtils.cs

* Move container files to folder

* Move namespaces

* Store "state" between hooks

* Remove stdin stream in containerstephosts

* Update Constants.cs

* Throw if stdin fails

* Cleanup obvious nullrefs and unused vars

* Cleanup containerhook directory

* Call step exec hook

* Fix windows build

* Remove hook from hookContainer

* Rename file

* More renamings

* Add TODOs

* Fix env name

* Fix missing imports

* Fix imports

* Run script step on jobcontainer

* Enable feature if env is set

* Update ContainerHookManager.cs

* Update ContainerHookManager.cs

* Hooks allowed to work even when context isn't returned

* Custom hooks enabled flag and additional null checks

* New line at the end of the FeatureFlagManager.cs

* Code refactoring

* Supported just in time container building or pulling

* Try mock-build for osx

* Build all platforms

* Run mock on self-hosted

* Remove GITHUB prefix

* Use ContainerHooksPath instead of CustomHooksPath

* Null checks simplified

* Code refactoring

* Changing condition for image builing/pulling

* Code refactoring

* TODO comment removed

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Call container step if FF is on

* Rename run script function

* Use JToken instead of dynamic

* Add TODO

* Small refactoring + renames + TODOs

* Throw on DetermineNodeRuntimeVersion

* Fix formatting

* Add run-container-step

* Supported nodeJS in Alpine containers

* Renamed Alpine to IsAlpine in HookResponse

* Method for checking platform for alpine container

* Added container hooks feature flag check

* Update IsHookFeatureEnabled with new params

* Rename featureflag method

* Finish rename

* Set collection null values to empty arrays when JSON serialising them

* Disable FF until we merge

* Update src/Runner.Worker/Container/ContainerHooks/HookContainer.cs

* Fix method name

* Change hookargs to superclass from interface

* Using only Path.Combine in GenerateResponsePath

* fix merge error

* EntryPointArgs changed to list of args instead of one args string

* Changed List to IEnumerable for EntryPointArgs and MountVolumes

* Get ContainerRuntimePath for JobContainers from hooks

* Read ContainerEnv from response file

* Port mappings saved after creating services

* Support case when responseFile doesn't exist

* Check if response file exists

* Logging in ExecuteHookScript

* Save hook state after all 4 hooks

* Code refactoring

* Remove TODO

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Remove second TODO

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Removing container env changes

* Removing containerEnv and dockerManager

* Delete mock-build.yml

* Update IOUtil.cs

* Add comment about containerhooks

* Fix merge mistake

* Remove solved todo

* Determine which shell to use for hooks scenario

* Overload for method ExecuteHookScript with prependPath as arg

* Adding HostContext to the GetDefaultShellForScript call

* prependPath as a mandatory parameter

* Improve logging for hooks

* Small changes in logging

* Allow null for ContainerEntryPointArgs

* Changed log messages

* Skip setting EntryPoint and EntryPointArgs if hooks are enabled

* Throw if IsAlpine is null in PrepareJob

* Code refactoring - added GetAndValidateResponse method

* Code refactoring

* Changes in exception message

* Only save hookState if returned

* Use FF from server

* Empty line

* Code refactoring

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Send null instead of string empty

* Remove TODO

* Code refactoring and some small changes

* Allow Globals to be null to pass L0

* Fix setup in StepHostL0

* Throw exception earlier if response file doesn't exist and prepare_job hook is running

* Refactoring GetResponse method

* Changing exception message if response file is not found

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Chaning exception message if isAlpine is null for prepare_job hook

* Rename hook folder

* Fail if compatible hookfile not found

* Use .Value instead of casting bool? to bool

* Format spacing

* Formatting

* User user and system mvs

* Use variables instead of entire context in featuremanager

* Update stepTelemetry if step uses containerhooks

* Restore  import

* Remove unneccessary field from HookContainer

* Refactor response context and portmappings

* Force allow hooks if FF is on

* Code refactoring

* Revert deleting usings

* Better hookContainer defaults and use correct portmapping list

* Make GetDefaultShellForScript a  HostContext extension method

* Generic hookresponse

* Code refactoring, unnecessary properties removed - HookContainer moved to the HookInput.cs

* Remove empty line

* Code refactoring and better exception handling

* code refactor, removing unnecessary props

* Move hookstate to global ContainerHookState

* Trace exception before we throw it for not losing information

* Fix for null ref exception in GetResponse

* Adding additional check for null response in prepareJob hook

* Refactoring GetResponse with additional check

* Update error messages

* Ports in ResponseContainer changed from IList to IDictionary

* Fix port format

* Include dockerfile

* Send null Registry obj if there's nothing in it

* Minor formatting

* Check if hookIndexPath exists relocated to the ContainerHookManager

* Code refactoring - ValidateHookExecutable added to the ContainerHookManager

* check if ContainerHooksPath when AllowRunnerContainerHooks is on

* Submit JSON telemetry instead of boolean

* Prefix step hooks with "run"

* Rename FeatureManager

* Fix flipped condition

* Unify js shell path getter with ps1 and sh getter

* Validate on run, not on instantiation of manager

* Cleanup ExecuteAsync methods

* Handle exception in executeHookScript

* Better exception types

* Remove comment

* Simplify boolean

* Allow jobs without jobContainer to run

* Use JObject instead of JToken

* Use correct Response type

* Format class to move cleanupJobAsync to the end of public methods

* Rename HookIndexPath to HookScriptPath

* Refactor methods into expression bodies

* Fix args class hierarchy

* Fix argument order

* Formatting

* Fix nullref and don't swallow stacktrace

* Whilelist HookArgs

* Use FF in FeatureManager

* Update src/Runner.Worker/ContainerOperationProvider.cs

Co-authored-by: Tingluo Huang <tingluohuang@github.com>

* Update src/Runner.Worker/ActionRunner.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Update src/Runner.Worker/ActionRunner.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Only mount well known dirs to job containers

* Get trace from hostcontext

* Use hook execution for setting telemetry

Co-authored-by: Nikola Jokic <nikola.jokic@akvelon.com>
Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com>
Co-authored-by: Nikola Jokic <97525037+nikola-jokic@users.noreply.github.com>
Co-authored-by: Stefan Ruvceski <stefan.ruvceski@akvelon.com>
Co-authored-by: ruvceskistefan <96768603+ruvceskistefan@users.noreply.github.com>
Co-authored-by: Thomas Boop <thboop@github.com>
Co-authored-by: stefanruvceski <ruvceskistefan@github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-06-10 13:51:20 +00:00
Ferenc Hammerl
ac7b34a071 Release 2.293.0 (#1940)
* Update releaseNote.md

* 2.293.0 Release

* Update releaseNote.md

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-06-10 15:42:51 +02:00
Thomas Boop
0d1e6fd57b Add ADR for Container Hooks (#1891)
* Add ADR for Container Hooks

* Rename 0000-container-hooks.md to 1891-container-hooks.md

* Update 1891-container-hooks.md

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Update docs/adrs/1891-container-hooks.md

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2022-06-10 09:35:30 -04:00
Ferenc Hammerl
9623a44c2f Allow admins to fail jobs without container (#1895)
* Allow admins to fail jobs without container

* Make method static

* Update src/Runner.Common/Constants.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Update src/Runner.Worker/JobExtension.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Update src/Runner.Worker/JobExtension.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Rename env

* Add test for throwing when no container but required

* Update src/Runner.Worker/JobExtension.cs

* Update src/Test/L0/Worker/JobExtensionL0.cs

* Update src/Runner.Common/Constants.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-06-09 17:17:11 -04:00
Rob Herley
b2e2aa68c8 remove job summary feature flag (#1936) 2022-06-09 15:49:04 -04:00
eric sciple
a9ce6b92c4 Allow redirect get message call to broker (#1935) 2022-06-09 18:36:55 +00:00
eric sciple
a1bf8401d7 Handle message from broker (#1934) 2022-06-09 14:07:44 -04:00
eric sciple
a7152f1370 server wrapper for pulling full job message (#1933) 2022-06-09 17:50:52 +00:00
eric sciple
af285115e7 http client updates for broker flow (#1931) 2022-06-09 12:46:08 -04:00
Ferenc Hammerl
0431b6fd40 Revert bash and shell -e filePath escape (#1932)
It generated invalid arguments for `Process()` when the `bash` command itself was an argument as well, for example:

```
            _proc.StartInfo.FileName = "/usr/bin/docker";
            _proc.StartInfo.Arguments = "exec -i --workdir /__w/container-hook-e2e/container-hook-e2e 47105c66144d8809d9fa2bce9a58ea0564cd14def0ae7952cd6231fba3576db1 sh -e '/__w/_temp/fd086560-cb92-4f3b-a99c-35a6b7b1bbdb.sh'";
```
2022-06-09 14:37:08 +02:00
Nikola Jokic
c3d5449146 Job hook provider now sets shell name for script handler (#1826)
* Job hook provider now sets shell name for script handler

* fixed script handler and job hook provider to work with the name without fail

* returned used import by osx

* fixed order of imports

* added quotes around resolved script path allowing space in script path

* added quotes around bash and sh _defaultArguments

* Changed double quotes to single quotes in sh -e

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* Changed double quotes to single quotes in bash

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2022-06-08 13:54:23 +02:00
Tingluo Huang
9c5300b5b2 Handle HostedRunnerShutdownMessage from service to shutdown hosted runner faster. (#1922) 2022-06-02 13:14:50 -04:00
44 changed files with 1756 additions and 264 deletions

View File

@@ -0,0 +1,596 @@
# ADR 0000: Container Hooks
**Date**: 2022-05-12
**Status**: Accepted
# Background
[Job Hooks](https://github.com/actions/runner/blob/main/docs/adrs/1751-runner-job-hooks.md) have given users the ability to customize how their self hosted runners run a job.
Users also want the ability to customize how they run containers during the scope of the job, rather then being locked into the docker implementation we have in the runner. They may want to use podman, kubernetes, or even change the docker commands we run.
We should give them that option, and publish examples how how they can create their own hooks.
# Guiding Principles
- **Extensibility** is the focus, we need to make sure we are flexible enough to cover current and future scenarios, even at the cost of making it harder to utilize these hooks
- Args should map **directly** to yaml values provided by the user.
- For example, the current runner overrides `HOME`, we can do that in the hook, but we shouldn't pass that hook as an ENV with the other env's the user has set, as that is not user input, it is how the runner invokes containers
## Interface
- You will set the variable `ACTIONS_RUNNER_CONTAINER_HOOK=/Users/foo/runner/hooks.js` which is the entrypoint to your hook handler.
- There is no partial opt in, you must handle every hook
- We will pass a command and some args via `stdin`
- An exit code of 0 is a success, every other exit code is a failure
- We will support the same runner commands we support in [Job Hooks](https://github.com/actions/runner/blob/main/docs/adrs/1751-runner-job-hooks.md)
- On timeout, we will send a sigint to your process. If you fail to terminate within a reasonable amount of time, we will send a sigkill, and eventually kill the process tree.
An example input looks like
```json
{
"command": "job_cleanup",
"responseFile": "/users/thboop/runner/_work/{guid}.json",
"args": {},
"state":
{
"id": "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480"
}
}
```
`command` is the command we expect you to invoke
`responseFile` is the file you need to write your output to, if the command has output
`args` are the specific arguments the command needs
`state` is a json blog you can pass around to maintain your state, this is covered in more details below.
### Writing responses to a file
All text written to stdout or stderr should appear in the job or step logs. With that in mind, we support a few ways to actually return data:
1. Wrapping the json in some unique tag and processing it like we do commands
2. Writing to a file
For 1, users typically view logging information as a safe action, so we worry someone accidentialy logging unsantized information and causing unexpected or un-secure behavior. We eventually plan to move off of stdout/stderr style commands in favor of a runner cli.
Investing in this area doesn't make a lot of sense at this time.
While writing to a file to communicate isn't the most ideal pattern, its an existing pattern in the runner and serves us well, so lets reuse it.
### Output
Your output must be correctly formatted json. An example output looks like:
```
{
"state": {},
"context"
{
"container" :
{
"id": "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480"
"network": "github_network_53269bd575974817b43f4733536b200c"
}
"services": {
"redis": {
"id": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105",
"ports": {
"8080": "8080"
},
"network": "github_network_53269bd575974817b43f4733536b200c"
}
}
"alpine: true,
}
```
`state` is a unique field any command can return. If it is not empty, we will store the state for you and pass it into all future commands. You can overwrite it by having the next hook invoked return a unique state.
Other fields are dependent upon the command being run.
### Versioning
We will not version these hooks at launch. If needed, we can always major version split these hooks in the future. We will ship in Beta to allow for breaking changes for a few months.
### The Job Context
The [job context](https://docs.github.com/en/actions/learn-github-actions/contexts#example-contents-of-the-job-context) currently has a variety of fields that correspond to containers. We should consider allowing hooks to populate new fields in the job context. That is out of scope for this original release however.
## Hooks
Hooks are to be implemented at a very high level, and map to actions the runner does, rather then specific docker actions like `docker build` or `docker create`. By mapping to runner actions, we create a very extensible framework that is flexible enough to solve any user concerns in the future. By providing first party implementations, we give users easy starting points to customize specific hooks (like `docker build`) without having to write full blown solutions.
The other would be to provide hooks that mirror every docker call we make, and expose more hooks to help support k8s users, with the expectation that users may have to no-op on multiple hooks if they don't correspond to our use case.
Why we don't want to go that way
- It feels clunky, users need to understand which hooks they need to implement and which they can ignore, which isn't a great UX
- It doesn't scale well, I don't want to build a solution where we may need to add more hooks, by mapping to runner actions, updating hooks is a painful experience for users
- Its overwhelming, its easier to tell users to build 4 hooks and track data themselves, rather then 16 hooks where the runner needs certain information and then needs to provide that information back into each hook. If we expose `Container Create`, you need to return the container you created, then we do `container run` which uses that container. If we just give you an image and say create and run this container, you don't need to store the container id in the runner, and it maps better to k8s scenarios where we don't really have container ids.
### Prepare_job hook
The `prepare_job` hook is called when a job is started. We pass in any job or service containers the job has. We expect that you:
- Prune anything from previous jobs if needed
- Create a network if needed
- Pull the job and service containers
- Start the job container
- Start the service containers
- Write to the response file some information we need
- Required: if the container is alpine, otherwise x64
- Optional: any context fields you want to set on the job context, otherwise they will be unavailable for users to use
- Return 0 when the health checks have succeeded and the job/service containers are started
This hook will **always** be called if you have container hooks enabled, even if no service or job containers exist in the job. This allows you to fail the job or implement a default job container if you want to and no job container has been provided.
<details>
<summary>Example Input</summary>
<br>
```
{
"command": "prepare_job",
"responseFile": "/users/thboop/runner/_work/{guid}.json",
"state": {},
"args":
{
"jobContainer": {
"image": "node:14.16",
"workingDirectory": "/__w/thboop-test2/thboop-test2",
"createOptions": "--cpus 1",
"environmentVariables": {
"NODE_ENV": "development"
},
"userMountVolumes:[
{
"sourceVolumePath": "my_docker_volume",
"targetVolumePath": "/volume_mount",
"readOnly": false
},
],
"mountVolumes": [
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work",
"targetVolumePath": "/__w",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/externals",
"targetVolumePath": "/__e",
"readOnly": true
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp",
"targetVolumePath": "/__w/_temp",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_actions",
"targetVolumePath": "/__w/_actions",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_tool",
"targetVolumePath": "/__w/_tool",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_home",
"targetVolumePath": "/github/home",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_workflow",
"targetVolumePath": "/github/workflow",
"readOnly": false
}
],
"registry": {
"username": "foo",
"password": "bar",
"serverUrl": "https://index.docker.io/v1"
},
"portMappings": [ "8080:80/tcp", "8080:80/udp" ]
},
"services": [
{
"contextName": "redis",
"image": "redis",
"createOptions": "--cpus 1",
"environmentVariables": {},
"mountVolumes": [],
"portMappings": [ "8080:80/tcp", "8080:80/udp" ]
"registry": {
"username": "foo",
"password": "bar",
"serverUrl": "https://index.docker.io/v1"
}
}
]
}
}
```
</details>
<details>
<summary>Field Descriptions</summary>
<br>
```
Arg Fields:
jobContainer: **Optional** An Object containing information about the specified job container
"image": **Required** A string containing the docker image
"workingDirectory": **Required** A string containing the absolute path of the working directory
"createOptions": **Optional** The optional create options specified in the [YAML](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container#example-running-a-job-within-a-container)
"environmentVariables": **Optional** A map of key value env's to set
"userMountVolumes: ** Optional** an array of user mount volumes set in the [YAML](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container#example-running-a-job-within-a-container)
"sourceVolumePath": **Required** The source path to the volume to be mounted into the docker container
"targetVolumePath": **Required** The target path to the volume to be mounted into the docker container
"readOnly": false **Required** whether or not the mount should be read only
"mountVolumes": **Required** an array of mounts to mount into the container, same fields as above
"sourceVolumePath": **Required** The source path to the volume to be mounted into the docker container
"targetVolumePath": **Required** The target path to the volume to be mounted into the docker container
"readOnly": false **Required** whether or not the mount should be read only
"registry" **Optional** docker registry credentials to use when using a private container registry
"username": **Optional** the username
"password": **Optional** the password
"serverUrl": **Optional** the registry url
"portMappings": **Optional** an array of source:target ports to map into the container
"services": an array of service containers to spin up
"contextName": **Required** the name of the service in the Job context
"image": **Required** A string containing the docker image
"createOptions": **Optional** The optional create options specified in the [YAML](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container#example-running-a-job-within-a-container)
"environmentVariables": **Optional** A map of key value env's to set
"mountVolumes": **Required** an array of mounts to mount into the container, same fields as above
"sourceVolumePath": **Required** The source path to the volume to be mounted into the docker container
"targetVolumePath": **Required** The target path to the volume to be mounted into the docker container
"readOnly": false **Required** whether or not the mount should be read only
"registry" **Optional** docker registry credentials to use when using a private container registry
"username": **Optional** the username
"password": **Optional** the password
"serverUrl": **Optional** the registry url
"portMappings": **Optional** an array of source:target ports to map into the container
```
</details>
<details>
<summary>Example Output</summary>
<br>
```
{
"state":
{
"network": "github_network_53269bd575974817b43f4733536b200c",
"jobContainer" : "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480",
"serviceContainers":
{
"redis": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105"
}
},
"context"
{
"container" :
{
"id": "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480"
"network": "github_network_53269bd575974817b43f4733536b200c"
}
"services": {
"redis": {
"id": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105",
"ports": {
"8080": "8080"
},
"network": "github_network_53269bd575974817b43f4733536b200c"
}
}
"alpine: true,
}
```
</details>
### Cleanup Job
The `cleanup_job` hook is called at the end of a job and expects you to:
- Stop any running service or job containers (or the equiavalent pod)
- Stop the network (if one exists)
- Delete any job or service containers (or the equiavalent pod)
- Delete the network (if one exists)
- Cleanup anything else that was created for the run
Its input looks like
<details>
<summary>Example Input</summary>
<br>
```
"command": "cleanup_job",
"responseFile": null,
"state":
{
"network": "github_network_53269bd575974817b43f4733536b200c",
"jobContainer" : "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480",
"serviceContainers":
{
"redis": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105"
}
}
"args": {}
```
</details>
No args are provided.
No output is expected.
### Run Container Step
The `run_container_step` is called once per container action in your job and expects you to:
- Pull or build the required container (or fail if you cannot)
- Run the container action and return the exit code of the container
- Stream any step logs output to stdout and stderr
- Cleanup the container after it executes
<details>
<summary>Example Input for Image</summary>
<br>
```
"command": "run_container_step",
"responseFile": null,
"state":
{
"network": "github_network_53269bd575974817b43f4733536b200c",
"jobContainer" : "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480",
"serviceContainers":
{
"redis": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105"
}
}
"args":
{
"image": "node:14.16",
"dockerfile": null,
"entryPointArgs": ["-f", "/dev/null"],
"entryPoint": "tail",
"workingDirectory": "/__w/thboop-test2/thboop-test2",
"createOptions": "--cpus 1",
"environmentVariables": {
"NODE_ENV": "development"
},
"prependPath":["/foo/bar", "bar/foo"]
"userMountVolumes:[
{
"sourceVolumePath": "my_docker_volume",
"targetVolumePath": "/volume_mount",
"readOnly": false
},
],
"mountVolumes": [
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work",
"targetVolumePath": "/__w",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/externals",
"targetVolumePath": "/__e",
"readOnly": true
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp",
"targetVolumePath": "/__w/_temp",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_actions",
"targetVolumePath": "/__w/_actions",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_tool",
"targetVolumePath": "/__w/_tool",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_home",
"targetVolumePath": "/github/home",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_workflow",
"targetVolumePath": "/github/workflow",
"readOnly": false
}
],
"registry": null,
"portMappings": { "80": "801" }
},
```
</details>
<details>
<summary>Example Input for dockerfile</summary>
<br>
```
"command": "run_container_step",
"responseFile": null,
"state":
{
"network": "github_network_53269bd575974817b43f4733536b200c",
"jobContainer" : "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480",
"services":
{
"redis": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105"
}
}
"args":
{
"image": null,
"dockerfile": /__w/_actions/foo/dockerfile,
"entryPointArgs": ["hello world"],
"entryPoint": "echo",
"workingDirectory": "/__w/thboop-test2/thboop-test2",
"createOptions": "--cpus 1",
"environmentVariables": {
"NODE_ENV": "development"
},
"prependPath":["/foo/bar", "bar/foo"]
"userMountVolumes:[
{
"sourceVolumePath": "my_docker_volume",
"targetVolumePath": "/volume_mount",
"readOnly": false
},
],
"mountVolumes": [
{
"sourceVolumePath": "my_docker_volume",
"targetVolumePath": "/volume_mount",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work",
"targetVolumePath": "/__w",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/externals",
"targetVolumePath": "/__e",
"readOnly": true
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp",
"targetVolumePath": "/__w/_temp",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_actions",
"targetVolumePath": "/__w/_actions",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_tool",
"targetVolumePath": "/__w/_tool",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_home",
"targetVolumePath": "/github/home",
"readOnly": false
},
{
"sourceVolumePath": "/home/thomas/git/runner/_layout/_work/_temp/_github_workflow",
"targetVolumePath": "/github/workflow",
"readOnly": false
}
],
"registry": null,
"portMappings": [ "8080:80/tcp", "8080:80/udp" ]
},
}
```
</details>
<details>
<summary>Field Descriptions</summary>
<br>
```
Arg Fields:
"image": **Optional** A string containing the docker image. Otherwise a dockerfile must be provided
"dockerfile": **Optional** A string containing the path to the dockerfile, otherwise an image must be provided
"entryPointArgs": **Optional** A list containing the entry point args
"entryPoint": **Optional** The container entry point to use if the default image entrypoint should be overwritten
"workingDirectory": **Required** A string containing the absolute path of the working directory
"createOptions": **Optional** The optional create options specified in the [YAML](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container#example-running-a-job-within-a-container)
"environmentVariables": **Optional** A map of key value env's to set
"prependPath": **Optional** an array of additional paths to prepend to the $PATH variable
"userMountVolumes: ** Optional** an array of user mount volumes set in the [YAML](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container#example-running-a-job-within-a-container)
"sourceVolumePath": **Required** The source path to the volume to be mounted into the docker container
"targetVolumePath": **Required** The target path to the volume to be mounted into the docker container
"readOnly": false **Required** whether or not the mount should be read only
"mountVolumes": **Required** an array of mounts to mount into the container, same fields as above
"sourceVolumePath": **Required** The source path to the volume to be mounted into the docker container
"targetVolumePath": **Required** The target path to the volume to be mounted into the docker container
"readOnly": false **Required** whether or not the mount should be read only
"registry" **Optional** docker registry credentials to use when using a private container registry
"username": **Optional** the username
"password": **Optional** the password
"serverUrl": **Optional** the registry url
"portMappings": **Optional** an array of source:target ports to map into the container
```
</details>
No output is expected
Currently we build all container actions at the start of the job. By doing it during the hook, we move this to just in time building for hooks. We could expose a hook to build/pull a container action, and have those called at the start of a job, but doing so would require hook authors to track the build containers in the state, which could be painful.
### Run Script Step
The `run_script_step` expects you to:
- Invoke the provided script inside the job container and return the exit code
- Stream any step log output to stdout and stderr
<details>
<summary>Example Input</summary>
<br>
```
"command": "run_script_step",
"responseFile": null,
"state":
{
"network": "github_network_53269bd575974817b43f4733536b200c",
"jobContainer" : "82e8219701fe096a35941d869cf8d71af1d943b5d3bdd718850fb87ac3042480",
"serviceContainers":
{
"redis": "60972d9aa486605e66b0dad4abb638dc3d9116f566579e418166eedb8abb9105"
}
}
"args":
{
"entryPointArgs": ["-e", "/runner/temp/abc123.sh"],
"entryPoint": "bash",
"environmentVariables": {
"NODE_ENV": "development"
},
"prependPath": ["/foo/bar", "bar/foo"],
"workingDirectory": "/__w/thboop-test2/thboop-test2"
}
```
</details>
<details>
<summary>Field Descriptions</summary>
<br>
```
Arg Fields:
"entryPointArgs": **Optional** A list containing the entry point args
"entryPoint": **Optional** The container entry point to use if the default image entrypoint should be overwritten
"prependPath": **Optional** an array of additional paths to prepend to the $PATH variable
"workingDirectory": **Required** A string containing the absolute path of the working directory
"environmentVariables": **Optional** A map of key value env's to set
```
</details>
No output is expected
## Limitations
- We will only support linux on launch
- Hooks are set by the runner admin, and thus are only supported on self hosted runners
## Consequences
- We support non docker scenarios for self hosted runners and allow customers to customize their docker invocations
- We ship/maintain docs on docker hooks and an open source repo with examples
- We support these hooks and add enough telemetry to be able to troubleshoot support issues as they come in.

View File

@@ -34,7 +34,7 @@ The `installdependencies.sh` script should install all required dependencies on
Debian based OS (Debian, Ubuntu, Linux Mint) Debian based OS (Debian, Ubuntu, Linux Mint)
- liblttng-ust0 - liblttng-ust1 or liblttng-ust0
- libkrb5-3 - libkrb5-3
- zlib1g - zlib1g
- libssl1.1, libssl1.0.2 or libssl1.0.0 - libssl1.1, libssl1.0.2 or libssl1.0.0

View File

@@ -1,11 +1,11 @@
## Features ## Features
- Added a pre-release package for the `macOS-arm64` architecture - Added support for a JIT runner config (#1925)
- Note that this packages is pre-release status and may not work with all existing actions - Added `ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION` env option to force actions to run on a specific node version (#1913)
## Bugs ## Bugs
- Fixed an issue where live console logs would fail to close (#1903) - Fixed a bug where container hooks passed in path as a string rather then an array of strings (#1948)
## Misc ## Misc
- Minor cleanup of error messages when running container hooks (#1949)
## Windows x64 ## Windows x64
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows. We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows.

View File

@@ -1 +1 @@
<Update to ./src/runnerversion when creating release> 2.294.0

View File

@@ -66,7 +66,7 @@ then
fi fi
fi fi
$apt_get update && $apt_get install -y liblttng-ust0 libkrb5-3 zlib1g $apt_get update && $apt_get install -y libkrb5-3 zlib1g
if [ $? -ne 0 ] if [ $? -ne 0 ]
then then
echo "'$apt_get' failed with exit code '$?'" echo "'$apt_get' failed with exit code '$?'"
@@ -94,6 +94,14 @@ then
fi fi
} }
apt_get_with_fallbacks liblttng-ust1 liblttng-ust0
if [ $? -ne 0 ]
then
echo "'$apt_get' failed with exit code '$?'"
print_errormessage
exit 1
fi
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$ apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
if [ $? -ne 0 ] if [ $? -ne 0 ]
then then

View File

@@ -90,6 +90,7 @@ namespace GitHub.Runner.Common
public static class Args public static class Args
{ {
public static readonly string Auth = "auth"; public static readonly string Auth = "auth";
public static readonly string JitConfig = "jitconfig";
public static readonly string Labels = "labels"; public static readonly string Labels = "labels";
public static readonly string MonitorSocketAddress = "monitorsocketaddress"; public static readonly string MonitorSocketAddress = "monitorsocketaddress";
public static readonly string Name = "name"; public static readonly string Name = "name";
@@ -151,6 +152,7 @@ namespace GitHub.Runner.Common
public static readonly string DiskSpaceWarning = "runner.diskspace.warning"; public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action"; public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action";
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate"; public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
} }
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry"; public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
@@ -196,6 +198,7 @@ namespace GitHub.Runner.Common
{ {
public static readonly string JobStartedStepName = "Set up runner"; public static readonly string JobStartedStepName = "Set up runner";
public static readonly string JobCompletedStepName = "Complete runner"; public static readonly string JobCompletedStepName = "Complete runner";
public static readonly string ContainerHooksPath = "ACTIONS_RUNNER_CONTAINER_HOOKS";
} }
public static class Path public static class Path
@@ -227,6 +230,7 @@ namespace GitHub.Runner.Common
// //
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS"; public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS"; public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
public static readonly string RequireJobContainer = "ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER";
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG"; public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG"; public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
public static readonly string AllowActionsUseUnsecureNodeVersion = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION"; public static readonly string AllowActionsUseUnsecureNodeVersion = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
@@ -238,6 +242,7 @@ namespace GitHub.Runner.Common
// Set this env var to "node12" to downgrade the node version for internal functions (e.g hashfiles). This does NOT affect the version of node actions. // Set this env var to "node12" to downgrade the node version for internal functions (e.g hashfiles). This does NOT affect the version of node actions.
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION"; public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
} }
public static class System public static class System

View File

@@ -13,6 +13,7 @@ using System.Runtime.Loader;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using GitHub.DistributedTask.Logging; using GitHub.DistributedTask.Logging;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common namespace GitHub.Runner.Common
@@ -641,6 +642,31 @@ namespace GitHub.Runner.Common
var handlerFactory = context.GetService<IHttpClientHandlerFactory>(); var handlerFactory = context.GetService<IHttpClientHandlerFactory>();
return handlerFactory.CreateClientHandler(context.WebProxy); return handlerFactory.CreateClientHandler(context.WebProxy);
} }
public static string GetDefaultShellForScript(this IHostContext hostContext, string path, string prependPath)
{
var trace = hostContext.GetTrace(nameof(GetDefaultShellForScript));
switch (Path.GetExtension(path))
{
case ".sh":
// use 'sh' args but prefer bash
if (WhichUtil.Which("bash", false, trace, prependPath) != null)
{
return "bash";
}
return "sh";
case ".ps1":
if (WhichUtil.Which("pwsh", false, trace, prependPath) != null)
{
return "pwsh";
}
return "powershell";
case ".js":
return Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}") + " {0}";
default:
throw new ArgumentException($"{path} is not a valid path to a script. Make sure it ends in '.sh', '.ps1' or '.js'.");
}
}
} }
public enum ShutdownReason public enum ShutdownReason

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.WebApi;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(RunServer))]
public interface IRunServer : IRunnerService
{
Task ConnectAsync(Uri serverUrl, VssCredentials credentials);
Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken token);
}
public sealed class RunServer : RunnerService, IRunServer
{
private bool _hasConnection;
private VssConnection _connection;
private TaskAgentHttpClient _taskAgentClient;
public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials)
{
_connection = await EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100));
_taskAgentClient = _connection.GetClient<TaskAgentHttpClient>();
_hasConnection = true;
}
private async Task<VssConnection> EstablishVssConnection(Uri serverUrl, VssCredentials credentials, TimeSpan timeout)
{
Trace.Info($"EstablishVssConnection");
Trace.Info($"Establish connection with {timeout.TotalSeconds} seconds timeout.");
int attemptCount = 5;
while (attemptCount-- > 0)
{
var connection = VssUtil.CreateConnection(serverUrl, credentials, timeout: timeout);
try
{
await connection.ConnectAsync();
return connection;
}
catch (Exception ex) when (attemptCount > 0)
{
Trace.Info($"Catch exception during connect. {attemptCount} attempt left.");
Trace.Error(ex);
await HostContext.Delay(TimeSpan.FromMilliseconds(100), CancellationToken.None);
}
}
// should never reach here.
throw new InvalidOperationException(nameof(EstablishVssConnection));
}
private void CheckConnection()
{
if (!_hasConnection)
{
throw new InvalidOperationException($"SetConnection");
}
}
public Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken cancellationToken)
{
CheckConnection();
var jobMessage = RetryRequest<AgentJobRequestMessage>(async () =>
{
return await _taskAgentClient.GetJobMessageAsync(id, cancellationToken);
}, cancellationToken);
return jobMessage;
}
private async Task<T> RetryRequest<T>(Func<Task<T>> func,
CancellationToken cancellationToken,
int maxRetryAttemptsCount = 5
)
{
var retryCount = 0;
while (true)
{
retryCount++;
cancellationToken.ThrowIfCancellationRequested();
try
{
return await func();
}
// TODO: Add handling of non-retriable exceptions: https://github.com/github/actions-broker/issues/122
catch (Exception ex) when (retryCount < maxRetryAttemptsCount)
{
Trace.Error("Catch exception during get full job message");
Trace.Error(ex);
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
Trace.Warning($"Back off {backOff.TotalSeconds} seconds before next retry. {maxRetryAttemptsCount - retryCount} attempt left.");
await Task.Delay(backOff, cancellationToken);
}
}
}
}
}

View File

@@ -15,8 +15,14 @@ namespace GitHub.Runner.Common.Util
public static string GetInternalNodeVersion() public static string GetInternalNodeVersion()
{ {
var forcedNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion); var forcedInternalNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion);
return !string.IsNullOrEmpty(forcedNodeVersion) && BuiltInNodeVersions.Contains(forcedNodeVersion) ? forcedNodeVersion : _defaultNodeVersion; var isForcedInternalNodeVersion = !string.IsNullOrEmpty(forcedInternalNodeVersion) && BuiltInNodeVersions.Contains(forcedInternalNodeVersion);
if (isForcedInternalNodeVersion)
{
return forcedInternalNodeVersion;
}
return _defaultNodeVersion;
} }
} }
} }

View File

@@ -39,6 +39,7 @@ namespace GitHub.Runner.Listener
Constants.Runner.CommandLine.Flags.RunAsService, Constants.Runner.CommandLine.Flags.RunAsService,
Constants.Runner.CommandLine.Flags.Unattended, Constants.Runner.CommandLine.Flags.Unattended,
Constants.Runner.CommandLine.Args.Auth, Constants.Runner.CommandLine.Args.Auth,
Constants.Runner.CommandLine.Args.JitConfig,
Constants.Runner.CommandLine.Args.Labels, Constants.Runner.CommandLine.Args.Labels,
Constants.Runner.CommandLine.Args.MonitorSocketAddress, Constants.Runner.CommandLine.Args.MonitorSocketAddress,
Constants.Runner.CommandLine.Args.Name, Constants.Runner.CommandLine.Args.Name,
@@ -213,6 +214,12 @@ namespace GitHub.Runner.Listener
validator: Validators.AuthSchemeValidator); validator: Validators.AuthSchemeValidator);
} }
public string GetJitConfig()
{
return GetArg(
name: Constants.Runner.CommandLine.Args.JitConfig);
}
public string GetRunnerName() public string GetRunnerName()
{ {
return GetArgOrPrompt( return GetArgOrPrompt(

View File

@@ -1,18 +1,20 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Listener.Configuration;
using System; using System;
using System.Threading; using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.Services.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Linq;
using GitHub.Runner.Listener.Check; using GitHub.Runner.Listener.Check;
using System.Collections.Generic; using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Sdk;
using GitHub.Services.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Listener namespace GitHub.Runner.Listener
{ {
@@ -192,6 +194,30 @@ namespace GitHub.Runner.Listener
return Constants.Runner.ReturnCode.Success; return Constants.Runner.ReturnCode.Success;
} }
var base64JitConfig = command.GetJitConfig();
if (!string.IsNullOrEmpty(base64JitConfig))
{
try
{
var decodedJitConfig = Encoding.UTF8.GetString(Convert.FromBase64String(base64JitConfig));
var jitConfig = StringUtil.ConvertFromJson<Dictionary<string, string>>(decodedJitConfig);
foreach (var config in jitConfig)
{
var configFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), config.Key);
var configContent = Encoding.UTF8.GetString(Convert.FromBase64String(config.Value));
File.WriteAllText(configFile, configContent, Encoding.UTF8);
File.SetAttributes(configFile, File.GetAttributes(configFile) | FileAttributes.Hidden);
Trace.Info($"Save {configContent.Length} chars to '{configFile}'.");
}
}
catch (Exception ex)
{
Trace.Error(ex);
_term.WriteError(ex.Message);
return Constants.Runner.ReturnCode.TerminatedError;
}
}
RunnerSettings settings = configManager.LoadSettings(); RunnerSettings settings = configManager.LoadSettings();
var store = HostContext.GetService<IConfigurationStore>(); var store = HostContext.GetService<IConfigurationStore>();
@@ -322,6 +348,7 @@ namespace GitHub.Runner.Listener
// Should we try to cleanup ephemeral runners // Should we try to cleanup ephemeral runners
bool runOnceJobCompleted = false; bool runOnceJobCompleted = false;
bool skipSessionDeletion = false;
try try
{ {
var notification = HostContext.GetService<IJobNotification>(); var notification = HostContext.GetService<IJobNotification>();
@@ -457,6 +484,34 @@ namespace GitHub.Runner.Listener
} }
} }
} }
// Broker flow
else if (string.Equals(message.MessageType, JobRequestMessageTypes.RunnerJobRequest, StringComparison.OrdinalIgnoreCase))
{
if (autoUpdateInProgress || runOnceJobReceived)
{
skipMessageDeletion = true;
Trace.Info($"Skip message deletion for job request message '{message.MessageId}'.");
}
else
{
var messageRef = StringUtil.ConvertFromJson<RunnerJobRequestRef>(message.Body);
// Create connection
var credMgr = HostContext.GetService<ICredentialManager>();
var creds = credMgr.LoadCredentials();
var runServer = HostContext.CreateService<IRunServer>();
await runServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
var jobMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
jobDispatcher.Run(jobMessage, runOnce);
if (runOnce)
{
Trace.Info("One time used runner received job message.");
runOnceJobReceived = true;
}
}
}
else if (string.Equals(message.MessageType, JobCancelMessage.MessageType, StringComparison.OrdinalIgnoreCase)) else if (string.Equals(message.MessageType, JobCancelMessage.MessageType, StringComparison.OrdinalIgnoreCase))
{ {
var cancelJobMessage = JsonUtility.FromString<JobCancelMessage>(message.Body); var cancelJobMessage = JsonUtility.FromString<JobCancelMessage>(message.Body);
@@ -468,6 +523,14 @@ namespace GitHub.Runner.Listener
Trace.Info($"Skip message deletion for cancellation message '{message.MessageId}'."); Trace.Info($"Skip message deletion for cancellation message '{message.MessageId}'.");
} }
} }
else if (string.Equals(message.MessageType, Pipelines.HostedRunnerShutdownMessage.MessageType, StringComparison.OrdinalIgnoreCase))
{
var HostedRunnerShutdownMessage = JsonUtility.FromString<Pipelines.HostedRunnerShutdownMessage>(message.Body);
skipMessageDeletion = true;
skipSessionDeletion = true;
Trace.Info($"Service requests the hosted runner to shutdown. Reason: '{HostedRunnerShutdownMessage.Reason}'.");
return Constants.Runner.ReturnCode.Success;
}
else else
{ {
Trace.Error($"Received message {message.MessageId} with unsupported message type {message.MessageType}."); Trace.Error($"Received message {message.MessageId} with unsupported message type {message.MessageType}.");
@@ -501,15 +564,18 @@ namespace GitHub.Runner.Listener
await jobDispatcher.ShutdownAsync(); await jobDispatcher.ShutdownAsync();
} }
try if (!skipSessionDeletion)
{ {
await _listener.DeleteSessionAsync(); try
} {
catch (Exception ex) when (runOnce) await _listener.DeleteSessionAsync();
{ }
// ignore exception during delete session for ephemeral runner since the runner might already be deleted from the server side catch (Exception ex) when (runOnce)
// and the delete session call will ends up with 401. {
Trace.Info($"Ignore any exception during DeleteSession for an ephemeral runner. {ex}"); // ignore exception during delete session for ephemeral runner since the runner might already be deleted from the server side
// and the delete session call will ends up with 401.
Trace.Info($"Ignore any exception during DeleteSession for an ephemeral runner. {ex}");
}
} }
messageQueueLoopTokenSource.Dispose(); messageQueueLoopTokenSource.Dispose();

View File

@@ -0,0 +1,13 @@
using System.Runtime.Serialization;
namespace GitHub.Runner.Listener
{
[DataContract]
public sealed class RunnerJobRequestRef
{
[DataMember(Name = "id")]
public string Id { get; set; }
[DataMember(Name = "runner_request_id")]
public string RunnerRequestId { get; set; }
}
}

View File

@@ -424,6 +424,12 @@ namespace GitHub.Runner.Sdk
throw new NotSupportedException($"Unable to validate execute permissions for directory '{directory}'. Exceeded maximum iterations."); throw new NotSupportedException($"Unable to validate execute permissions for directory '{directory}'. Exceeded maximum iterations.");
} }
public static void CreateEmptyFile(string path)
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllText(path, null);
}
/// <summary> /// <summary>
/// Recursively enumerates a directory without following directory reparse points. /// Recursively enumerates a directory without following directory reparse points.
/// </summary> /// </summary>

View File

@@ -57,6 +57,10 @@ namespace GitHub.Runner.Sdk
settings.SendTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(httpRequestTimeoutSeconds, 100), 1200)); settings.SendTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(httpRequestTimeoutSeconds, 100), 1200));
} }
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_ALLOW_REDIRECT")))
{
settings.AllowAutoRedirect = true;
}
// Remove Invariant from the list of accepted languages. // Remove Invariant from the list of accepted languages.
// //

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Sdk namespace GitHub.Runner.Sdk
{ {

View File

@@ -101,38 +101,41 @@ namespace GitHub.Runner.Worker
IEnumerable<Pipelines.ActionStep> actions = steps.OfType<Pipelines.ActionStep>(); IEnumerable<Pipelines.ActionStep> actions = steps.OfType<Pipelines.ActionStep>();
executionContext.Output("Prepare all required actions"); executionContext.Output("Prepare all required actions");
var result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId); var result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId);
if (state.ImagesToPull.Count > 0) if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{ {
foreach (var imageToPull in result.ImagesToPull) if (state.ImagesToPull.Count > 0)
{ {
Trace.Info($"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'"); foreach (var imageToPull in result.ImagesToPull)
containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.PullActionContainerAsync, {
condition: $"{PipelineTemplateConstants.Success}()", Trace.Info($"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'");
displayName: $"Pull {imageToPull.Key}", containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.PullActionContainerAsync,
data: new ContainerSetupInfo(imageToPull.Value, imageToPull.Key))); condition: $"{PipelineTemplateConstants.Success}()",
displayName: $"Pull {imageToPull.Key}",
data: new ContainerSetupInfo(imageToPull.Value, imageToPull.Key)));
}
} }
}
if (result.ImagesToBuild.Count > 0) if (result.ImagesToBuild.Count > 0)
{
foreach (var imageToBuild in result.ImagesToBuild)
{ {
var setupInfo = result.ImagesToBuildInfo[imageToBuild.Key]; foreach (var imageToBuild in result.ImagesToBuild)
Trace.Info($"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'"); {
containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.BuildActionContainerAsync, var setupInfo = result.ImagesToBuildInfo[imageToBuild.Key];
condition: $"{PipelineTemplateConstants.Success}()", Trace.Info($"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'");
displayName: $"Build {setupInfo.ActionRepository}", containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.BuildActionContainerAsync,
data: new ContainerSetupInfo(imageToBuild.Value, setupInfo.Dockerfile, setupInfo.WorkingDirectory))); condition: $"{PipelineTemplateConstants.Success}()",
displayName: $"Build {setupInfo.ActionRepository}",
data: new ContainerSetupInfo(imageToBuild.Value, setupInfo.Dockerfile, setupInfo.WorkingDirectory)));
}
} }
}
#if !OS_LINUX #if !OS_LINUX
if (containerSetupSteps.Count > 0) if (containerSetupSteps.Count > 0)
{ {
executionContext.Output("Container action is only supported on Linux, skip pull and build docker images."); executionContext.Output("Container action is only supported on Linux, skip pull and build docker images.");
containerSetupSteps.Clear(); containerSetupSteps.Clear();
} }
#endif #endif
}
return new PrepareResult(containerSetupSteps, result.PreStepTracker); return new PrepareResult(containerSetupSteps, result.PreStepTracker);
} }

View File

@@ -158,8 +158,12 @@ namespace GitHub.Runner.Worker
// Setup container stephost for running inside the container. // Setup container stephost for running inside the container.
if (ExecutionContext.Global.Container != null) if (ExecutionContext.Global.Container != null)
{ {
// Make sure required container is already created. // Make sure the required container is already created
ArgUtil.NotNullOrEmpty(ExecutionContext.Global.Container.ContainerId, nameof(ExecutionContext.Global.Container.ContainerId)); // Container hooks do not necessarily set 'ContainerId'
if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
{
ArgUtil.NotNullOrEmpty(ExecutionContext.Global.Container.ContainerId, nameof(ExecutionContext.Global.Container.ContainerId));
}
var containerStepHost = HostContext.CreateService<IContainerStepHost>(); var containerStepHost = HostContext.CreateService<IContainerStepHost>();
containerStepHost.Container = ExecutionContext.Global.Container; containerStepHost.Container = ExecutionContext.Global.Container;
stepHost = containerStepHost; stepHost = containerStepHost;

View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Handlers;
using GitHub.Services.WebApi;
using Newtonsoft.Json.Linq;
namespace GitHub.Runner.Worker.Container.ContainerHooks
{
[ServiceLocator(Default = typeof(ContainerHookManager))]
public interface IContainerHookManager : IRunnerService
{
Task PrepareJobAsync(IExecutionContext context, List<ContainerInfo> containers);
Task RunContainerStepAsync(IExecutionContext context, ContainerInfo container, string dockerFile);
Task RunScriptStepAsync(IExecutionContext context, ContainerInfo container, string workingDirectory, string fileName, string arguments, IDictionary<string, string> environment, string prependPath);
Task CleanupJobAsync(IExecutionContext context, List<ContainerInfo> containers);
string GetContainerHookData();
}
public class ContainerHookManager : RunnerService, IContainerHookManager
{
private const string ResponseFolderName = "_runner_hook_responses";
private string HookScriptPath;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
HookScriptPath = $"{Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath)}";
}
public async Task PrepareJobAsync(IExecutionContext context, List<ContainerInfo> containers)
{
Trace.Entering();
var jobContainer = containers.Where(c => c.IsJobContainer).SingleOrDefault();
var serviceContainers = containers.Where(c => !c.IsJobContainer).ToList();
var input = new HookInput
{
Command = HookCommand.PrepareJob,
ResponseFile = GenerateResponsePath(),
Args = new PrepareJobArgs
{
Container = jobContainer?.GetHookContainer(),
Services = serviceContainers.Select(c => c.GetHookContainer()).ToList(),
}
};
var prependPath = GetPrependPath(context);
var response = await ExecuteHookScript<PrepareJobResponse>(context, input, ActionRunStage.Pre, prependPath);
if (jobContainer != null)
{
jobContainer.IsAlpine = response.IsAlpine.Value;
}
SaveHookState(context, response.State, input);
UpdateJobContext(context, jobContainer, serviceContainers, response);
}
public async Task RunContainerStepAsync(IExecutionContext context, ContainerInfo container, string dockerFile)
{
Trace.Entering();
var hookState = context.Global.ContainerHookState;
var containerStepArgs = new ContainerStepArgs(container);
if (!string.IsNullOrEmpty(dockerFile))
{
containerStepArgs.Dockerfile = dockerFile;
containerStepArgs.Image = null;
}
var input = new HookInput
{
Args = containerStepArgs,
Command = HookCommand.RunContainerStep,
ResponseFile = GenerateResponsePath(),
State = hookState
};
var prependPath = GetPrependPath(context);
var response = await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
if (response == null)
{
return;
}
SaveHookState(context, response.State, input);
}
public async Task RunScriptStepAsync(IExecutionContext context, ContainerInfo container, string workingDirectory, string entryPoint, string entryPointArgs, IDictionary<string, string> environmentVariables, string prependPath)
{
Trace.Entering();
var input = new HookInput
{
Command = HookCommand.RunScriptStep,
ResponseFile = GenerateResponsePath(),
Args = new ScriptStepArgs
{
EntryPointArgs = entryPointArgs.Split(' ').Select(arg => arg.Trim()),
EntryPoint = entryPoint,
EnvironmentVariables = environmentVariables,
PrependPath = context.Global.PrependPath.Reverse<string>(),
WorkingDirectory = workingDirectory,
},
State = context.Global.ContainerHookState
};
var response = await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
if (response == null)
{
return;
}
SaveHookState(context, response.State, input);
}
public async Task CleanupJobAsync(IExecutionContext context, List<ContainerInfo> containers)
{
Trace.Entering();
var input = new HookInput
{
Command = HookCommand.CleanupJob,
ResponseFile = GenerateResponsePath(),
Args = new CleanupJobArgs(),
State = context.Global.ContainerHookState
};
var prependPath = GetPrependPath(context);
await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
}
public string GetContainerHookData()
{
return JsonUtility.ToString(new { HookScriptPath });
}
private async Task<T> ExecuteHookScript<T>(IExecutionContext context, HookInput input, ActionRunStage stage, string prependPath) where T : HookResponse
{
try
{
ValidateHookExecutable();
context.StepTelemetry.ContainerHookData = GetContainerHookData();
var scriptDirectory = Path.GetDirectoryName(HookScriptPath);
var stepHost = HostContext.CreateService<IDefaultStepHost>();
Dictionary<string, string> inputs = new()
{
["standardInInput"] = JsonUtility.ToString(input),
["path"] = HookScriptPath,
["shell"] = HostContext.GetDefaultShellForScript(HookScriptPath, prependPath)
};
var handlerFactory = HostContext.GetService<IHandlerFactory>();
var handler = handlerFactory.Create(
context,
null,
stepHost,
new ScriptActionExecutionData(),
inputs,
environment: new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
context.Global.Variables,
actionDirectory: scriptDirectory,
localActionContainerSetupSteps: null) as ScriptHandler;
handler.PrepareExecution(stage);
IOUtil.CreateEmptyFile(input.ResponseFile);
await handler.RunAsync(stage);
if (handler.ExecutionContext.Result == TaskResult.Failed)
{
throw new Exception($"The hook script at '{HookScriptPath}' running command '{input.Command}' did not execute successfully");
}
var response = GetResponse<T>(input);
return response;
}
catch (Exception ex)
{
throw new Exception($"Executing the custom container implementation failed. Please contact your self hosted runner administrator.", ex);
}
}
private string GenerateResponsePath() => Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), ResponseFolderName, $"{Guid.NewGuid()}.json");
private static string GetPrependPath(IExecutionContext context) => string.Join(Path.PathSeparator.ToString(), context.Global.PrependPath.Reverse<string>());
private void ValidateHookExecutable()
{
if (!string.IsNullOrEmpty(HookScriptPath) && !File.Exists(HookScriptPath))
{
throw new FileNotFoundException($"File not found at '{HookScriptPath}'. Set {Constants.Hooks.ContainerHooksPath} to the path of an existing file.");
}
var supportedHookExtensions = new string[] { ".js", ".sh", ".ps1" };
if (!supportedHookExtensions.Any(extension => HookScriptPath.EndsWith(extension)))
{
throw new ArgumentOutOfRangeException($"Invalid file extension at '{HookScriptPath}'. {Constants.Hooks.ContainerHooksPath} must be a path to a file with one of the following extensions: {string.Join(", ", supportedHookExtensions)}");
}
}
private T GetResponse<T>(HookInput input) where T : HookResponse
{
if (!File.Exists(input.ResponseFile))
{
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' not found.");
if (input.Args.IsRequireAlpineInResponse())
{
throw new Exception($"Response file is required but not found for the hook script at '{HookScriptPath}' running command '{input.Command}'");
}
return null;
}
T response = IOUtil.LoadObject<T>(input.ResponseFile);
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' was processed successfully");
IOUtil.DeleteFile(input.ResponseFile);
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' was deleted successfully");
if (response == null && input.Args.IsRequireAlpineInResponse())
{
throw new Exception($"Response file could not be read at '{HookScriptPath}' running command '{input.Command}'");
}
response?.Validate(input);
return response;
}
private void SaveHookState(IExecutionContext context, JObject hookState, HookInput input)
{
if (hookState == null)
{
Trace.Info($"No 'state' property found in response file for '{input.Command}'. Global variable for 'ContainerHookState' will not be updated.");
return;
}
context.Global.ContainerHookState = hookState;
Trace.Info($"Global variable 'ContainerHookState' updated successfully for '{input.Command}' with data found in 'state' property of the response file.");
}
private void UpdateJobContext(IExecutionContext context, ContainerInfo jobContainer, List<ContainerInfo> serviceContainers, PrepareJobResponse response)
{
if (response.Context == null)
{
Trace.Info($"The response file does not contain a context. The fields 'jobContext.Container' and 'jobContext.Services' will not be set.");
return;
}
var containerId = response.Context.Container?.Id;
if (containerId != null)
{
context.JobContext.Container["id"] = new StringContextData(containerId);
jobContainer.ContainerId = containerId;
}
var containerNetwork = response.Context.Container?.Network;
if (containerNetwork != null)
{
context.JobContext.Container["network"] = new StringContextData(containerNetwork);
jobContainer.ContainerNetwork = containerNetwork;
}
for (var i = 0; i < response.Context.Services.Count; i++)
{
var responseContainerInfo = response.Context.Services[i];
var globalContainerInfo = serviceContainers[i];
globalContainerInfo.ContainerId = responseContainerInfo.Id;
globalContainerInfo.ContainerNetwork = responseContainerInfo.Network;
var service = new DictionaryContextData()
{
["id"] = new StringContextData(responseContainerInfo.Id),
["ports"] = new DictionaryContextData(),
["network"] = new StringContextData(responseContainerInfo.Network)
};
globalContainerInfo.AddPortMappings(responseContainerInfo.Ports);
foreach (var portMapping in responseContainerInfo.Ports)
{
(service["ports"] as DictionaryContextData)[portMapping.Key] = new StringContextData(portMapping.Value);
}
context.JobContext.Services[globalContainerInfo.ContainerNetworkAlias] = service;
}
}
}
}

View File

@@ -0,0 +1,113 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System.Linq;
namespace GitHub.Runner.Worker.Container.ContainerHooks
{
public class HookInput
{
public HookCommand Command { get; set; }
public string ResponseFile { get; set; }
public IHookArgs Args { get; set; }
public JObject State { get; set; }
}
[JsonConverter(typeof(StringEnumConverter))]
public enum HookCommand
{
[EnumMember(Value = "prepare_job")]
PrepareJob,
[EnumMember(Value = "cleanup_job")]
CleanupJob,
[EnumMember(Value = "run_script_step")]
RunScriptStep,
[EnumMember(Value = "run_container_step")]
RunContainerStep,
}
public interface IHookArgs
{
bool IsRequireAlpineInResponse();
}
public class PrepareJobArgs : IHookArgs
{
public HookContainer Container { get; set; }
public IList<HookContainer> Services { get; set; }
public bool IsRequireAlpineInResponse() => Container != null;
}
public class ScriptStepArgs : IHookArgs
{
public IEnumerable<string> EntryPointArgs { get; set; }
public string EntryPoint { get; set; }
public IDictionary<string, string> EnvironmentVariables { get; set; }
public IEnumerable<string> PrependPath { get; set; }
public string WorkingDirectory { get; set; }
public bool IsRequireAlpineInResponse() => false;
}
public class ContainerStepArgs : HookContainer, IHookArgs
{
public bool IsRequireAlpineInResponse() => false;
public ContainerStepArgs(ContainerInfo container) : base(container) { }
}
public class CleanupJobArgs : IHookArgs
{
public bool IsRequireAlpineInResponse() => false;
}
public class ContainerRegistry
{
public string Username { get; set; }
public string Password { get; set; }
public string ServerUrl { get; set; }
}
public class HookContainer
{
public string Image { get; set; }
public string Dockerfile { get; set; }
public IEnumerable<string> EntryPointArgs { get; set; } = new List<string>();
public string EntryPoint { get; set; }
public string WorkingDirectory { get; set; }
public string CreateOptions { get; private set; }
public ContainerRegistry Registry { get; set; }
public IDictionary<string, string> EnvironmentVariables { get; set; } = new Dictionary<string, string>();
public IEnumerable<string> PortMappings { get; set; } = new List<string>();
public IEnumerable<MountVolume> SystemMountVolumes { get; set; } = new List<MountVolume>();
public IEnumerable<MountVolume> UserMountVolumes { get; set; } = new List<MountVolume>();
public HookContainer() { } // For Json deserializer
public HookContainer(ContainerInfo container)
{
Image = container.ContainerImage;
EntryPointArgs = container.ContainerEntryPointArgs?.Split(' ').Select(arg => arg.Trim()) ?? new List<string>();
EntryPoint = container.ContainerEntryPoint;
WorkingDirectory = container.ContainerWorkDirectory;
CreateOptions = container.ContainerCreateOptions;
if (!string.IsNullOrEmpty(container.RegistryAuthUsername))
{
Registry = new ContainerRegistry
{
Username = container.RegistryAuthUsername,
Password = container.RegistryAuthPassword,
ServerUrl = container.RegistryServer,
};
}
EnvironmentVariables = container.ContainerEnvironmentVariables;
PortMappings = container.UserPortMappings.Select(p => p.Value).ToList();
SystemMountVolumes = container.SystemMountVolumes;
UserMountVolumes = container.UserMountVolumes;
}
}
public static class ContainerInfoExtensions
{
public static HookContainer GetHookContainer(this ContainerInfo containerInfo)
{
return new HookContainer(containerInfo);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
namespace GitHub.Runner.Worker.Container.ContainerHooks
{
public class HookResponse
{
public JObject State { get; set; }
public virtual void Validate(HookInput input) { }
}
public class PrepareJobResponse : HookResponse
{
public ResponseContext Context { get; set; }
public bool? IsAlpine { get; set; }
public override void Validate(HookInput input)
{
bool hasJobContainer = ((PrepareJobArgs)input.Args).Container != null;
if (hasJobContainer && IsAlpine == null)
{
throw new Exception("The property 'isAlpine' is required but was not found in the response file.");
}
}
}
public class ResponseContext
{
public ResponseContainer Container { get; set; }
public IList<ResponseContainer> Services { get; set; } = new List<ResponseContainer>();
}
public class ResponseContainer
{
public string Id { get; set; }
public string Network { get; set; }
public IDictionary<string, string> Ports { get; set; }
}
}

View File

@@ -90,6 +90,7 @@ namespace GitHub.Runner.Worker.Container
public string RegistryAuthUsername { get; set; } public string RegistryAuthUsername { get; set; }
public string RegistryAuthPassword { get; set; } public string RegistryAuthPassword { get; set; }
public bool IsJobContainer { get; set; } public bool IsJobContainer { get; set; }
public bool IsAlpine { get; set; }
public IDictionary<string, string> ContainerEnvironmentVariables public IDictionary<string, string> ContainerEnvironmentVariables
{ {
@@ -232,6 +233,14 @@ namespace GitHub.Runner.Worker.Container
} }
} }
public void AddPortMappings(IDictionary<string, string> portMappings)
{
foreach (var pair in portMappings)
{
PortMappings.Add(new PortMapping(pair.Key, pair.Value));
}
}
public void AddPathTranslateMapping(string hostCommonPath, string containerCommonPath) public void AddPathTranslateMapping(string hostCommonPath, string containerCommonPath)
{ {
_pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath)); _pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath));
@@ -322,6 +331,12 @@ namespace GitHub.Runner.Worker.Container
public class PortMapping public class PortMapping
{ {
public PortMapping(string hostPort, string containerPort)
{
this.HostPort = hostPort;
this.ContainerPort = containerPort;
}
public PortMapping(string hostPort, string containerPort, string protocol) public PortMapping(string hostPort, string containerPort, string protocol)
{ {
this.HostPort = hostPort; this.HostPort = hostPort;

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.ServiceProcess;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -10,8 +9,12 @@ using GitHub.Services.Common;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ContextData;
using Microsoft.Win32;
using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.Runner.Worker.Container.ContainerHooks;
#if OS_WINDOWS // keep win specific imports around even through we don't support containers on win at the moment
using System.ServiceProcess;
using Microsoft.Win32;
#endif
namespace GitHub.Runner.Worker namespace GitHub.Runner.Worker
{ {
@@ -25,11 +28,13 @@ namespace GitHub.Runner.Worker
public class ContainerOperationProvider : RunnerService, IContainerOperationProvider public class ContainerOperationProvider : RunnerService, IContainerOperationProvider
{ {
private IDockerCommandManager _dockerManager; private IDockerCommandManager _dockerManager;
private IContainerHookManager _containerHookManager;
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
base.Initialize(hostContext); base.Initialize(hostContext);
_dockerManager = HostContext.GetService<IDockerCommandManager>(); _dockerManager = HostContext.GetService<IDockerCommandManager>();
_containerHookManager = HostContext.GetService<IContainerHookManager>();
} }
public async Task StartContainersAsync(IExecutionContext executionContext, object data) public async Task StartContainersAsync(IExecutionContext executionContext, object data)
@@ -50,72 +55,15 @@ namespace GitHub.Runner.Worker
executionContext.Debug($"Register post job cleanup for stopping/deleting containers."); executionContext.Debug($"Register post job cleanup for stopping/deleting containers.");
executionContext.RegisterPostJobStep(postJobStep); executionContext.RegisterPostJobStep(postJobStep);
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
// Check whether we are inside a container.
// Our container feature requires to map working directory from host to the container.
// If we are already inside a container, we will not able to find out the real working direcotry path on the host.
#if OS_WINDOWS
#pragma warning disable CA1416
// service CExecSvc is Container Execution Agent.
ServiceController[] scServices = ServiceController.GetServices();
if (scServices.Any(x => String.Equals(x.ServiceName, "cexecsvc", StringComparison.OrdinalIgnoreCase) && x.Status == ServiceControllerStatus.Running))
{ {
throw new NotSupportedException("Container feature is not supported when runner is already running inside container."); // Initialize the containers
} containers.ForEach(container => UpdateRegistryAuthForGitHubToken(executionContext, container));
#pragma warning restore CA1416 containers.Where(container => container.IsJobContainer).ForEach(container => MountWellKnownDirectories(executionContext, container));
#else await _containerHookManager.PrepareJobAsync(executionContext, containers);
var initProcessCgroup = File.ReadLines("/proc/1/cgroup"); return;
if (initProcessCgroup.Any(x => x.IndexOf(":/docker/", StringComparison.OrdinalIgnoreCase) >= 0))
{
throw new NotSupportedException("Container feature is not supported when runner is already running inside container.");
}
#endif
#if OS_WINDOWS
#pragma warning disable CA1416
// Check OS version (Windows server 1803 is required)
object windowsInstallationType = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "InstallationType", defaultValue: null);
ArgUtil.NotNull(windowsInstallationType, nameof(windowsInstallationType));
object windowsReleaseId = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ReleaseId", defaultValue: null);
ArgUtil.NotNull(windowsReleaseId, nameof(windowsReleaseId));
executionContext.Debug($"Current Windows version: '{windowsReleaseId} ({windowsInstallationType})'");
if (int.TryParse(windowsReleaseId.ToString(), out int releaseId))
{
if (!windowsInstallationType.ToString().StartsWith("Server", StringComparison.OrdinalIgnoreCase) || releaseId < 1803)
{
throw new NotSupportedException("Container feature requires Windows Server 1803 or higher.");
}
}
else
{
throw new ArgumentOutOfRangeException(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ReleaseId");
}
#pragma warning restore CA1416
#endif
// Check docker client/server version
executionContext.Output("##[group]Checking docker version");
DockerVersion dockerVersion = await _dockerManager.DockerVersion(executionContext);
executionContext.Output("##[endgroup]");
ArgUtil.NotNull(dockerVersion.ServerVersion, nameof(dockerVersion.ServerVersion));
ArgUtil.NotNull(dockerVersion.ClientVersion, nameof(dockerVersion.ClientVersion));
#if OS_WINDOWS
Version requiredDockerEngineAPIVersion = new Version(1, 30); // Docker-EE version 17.6
#else
Version requiredDockerEngineAPIVersion = new Version(1, 35); // Docker-CE version 17.12
#endif
if (dockerVersion.ServerVersion < requiredDockerEngineAPIVersion)
{
throw new NotSupportedException($"Min required docker engine API server version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') server version is '{dockerVersion.ServerVersion}'");
}
if (dockerVersion.ClientVersion < requiredDockerEngineAPIVersion)
{
throw new NotSupportedException($"Min required docker engine API client version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') client version is '{dockerVersion.ClientVersion}'");
} }
await AssertCompatibleOS(executionContext);
// Clean up containers left by previous runs // Clean up containers left by previous runs
executionContext.Output("##[group]Clean up resources from previous jobs"); executionContext.Output("##[group]Clean up resources from previous jobs");
@@ -166,6 +114,12 @@ namespace GitHub.Runner.Worker
List<ContainerInfo> containers = data as List<ContainerInfo>; List<ContainerInfo> containers = data as List<ContainerInfo>;
ArgUtil.NotNull(containers, nameof(containers)); ArgUtil.NotNull(containers, nameof(containers));
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
await _containerHookManager.CleanupJobAsync(executionContext, containers);
return;
}
foreach (var container in containers) foreach (var container in containers)
{ {
await StopContainerAsync(executionContext, container); await StopContainerAsync(executionContext, container);
@@ -238,35 +192,7 @@ namespace GitHub.Runner.Worker
if (container.IsJobContainer) if (container.IsJobContainer)
{ {
// Configure job container - Mount workspace and tools, set up environment, and start long running process MountWellKnownDirectories(executionContext, container);
var githubContext = executionContext.ExpressionValues["github"] as GitHubContext;
ArgUtil.NotNull(githubContext, nameof(githubContext));
var workingDirectory = githubContext["workspace"] as StringContextData;
ArgUtil.NotNullOrEmpty(workingDirectory, nameof(workingDirectory));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Work), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Work))));
#if OS_WINDOWS
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals))));
#else
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals)), true));
#endif
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Temp), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Temp))));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Actions), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Actions))));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Tools), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Tools))));
var tempHomeDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_home");
Directory.CreateDirectory(tempHomeDirectory);
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
container.ContainerEnvironmentVariables["HOME"] = container.TranslateToContainerPath(tempHomeDirectory);
var tempWorkflowDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_workflow");
Directory.CreateDirectory(tempWorkflowDirectory);
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
container.ContainerWorkDirectory = container.TranslateToContainerPath(workingDirectory);
container.ContainerEntryPoint = "tail";
container.ContainerEntryPointArgs = "\"-f\" \"/dev/null\"";
} }
container.ContainerId = await _dockerManager.DockerCreate(executionContext, container); container.ContainerId = await _dockerManager.DockerCreate(executionContext, container);
@@ -329,6 +255,42 @@ namespace GitHub.Runner.Worker
executionContext.Output("##[endgroup]"); executionContext.Output("##[endgroup]");
} }
private void MountWellKnownDirectories(IExecutionContext executionContext, ContainerInfo container)
{
// Configure job container - Mount workspace and tools, set up environment, and start long running process
var githubContext = executionContext.ExpressionValues["github"] as GitHubContext;
ArgUtil.NotNull(githubContext, nameof(githubContext));
var workingDirectory = githubContext["workspace"] as StringContextData;
ArgUtil.NotNullOrEmpty(workingDirectory, nameof(workingDirectory));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Work), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Work))));
#if OS_WINDOWS
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals))));
#else
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals)), true));
#endif
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Temp), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Temp))));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Actions), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Actions))));
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Tools), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Tools))));
var tempHomeDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_home");
Directory.CreateDirectory(tempHomeDirectory);
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
container.ContainerEnvironmentVariables["HOME"] = container.TranslateToContainerPath(tempHomeDirectory);
var tempWorkflowDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_workflow");
Directory.CreateDirectory(tempWorkflowDirectory);
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
container.ContainerWorkDirectory = container.TranslateToContainerPath(workingDirectory);
if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
container.ContainerEntryPoint = "tail";
container.ContainerEntryPointArgs = "\"-f\" \"/dev/null\"";
}
}
private async Task StopContainerAsync(IExecutionContext executionContext, ContainerInfo container) private async Task StopContainerAsync(IExecutionContext executionContext, ContainerInfo container)
{ {
Trace.Entering(); Trace.Entering();
@@ -337,11 +299,11 @@ namespace GitHub.Runner.Worker
if (!string.IsNullOrEmpty(container.ContainerId)) if (!string.IsNullOrEmpty(container.ContainerId))
{ {
if(!container.IsJobContainer) if (!container.IsJobContainer)
{ {
// Print logs for service container jobs (not the "action" job itself b/c that's already logged). // Print logs for service container jobs (not the "action" job itself b/c that's already logged).
executionContext.Output($"Print service container logs: {container.ContainerDisplayName}"); executionContext.Output($"Print service container logs: {container.ContainerDisplayName}");
int logsExitCode = await _dockerManager.DockerLogs(executionContext, container.ContainerId); int logsExitCode = await _dockerManager.DockerLogs(executionContext, container.ContainerId);
if (logsExitCode != 0) if (logsExitCode != 0)
{ {
@@ -522,5 +484,74 @@ namespace GitHub.Runner.Worker
container.RegistryAuthPassword = executionContext.GetGitHubContext("token"); container.RegistryAuthPassword = executionContext.GetGitHubContext("token");
} }
} }
private async Task AssertCompatibleOS(IExecutionContext executionContext)
{
// Check whether we are inside a container.
// Our container feature requires to map working directory from host to the container.
// If we are already inside a container, we will not able to find out the real working direcotry path on the host.
#if OS_WINDOWS
#pragma warning disable CA1416
// service CExecSvc is Container Execution Agent.
ServiceController[] scServices = ServiceController.GetServices();
if (scServices.Any(x => String.Equals(x.ServiceName, "cexecsvc", StringComparison.OrdinalIgnoreCase) && x.Status == ServiceControllerStatus.Running))
{
throw new NotSupportedException("Container feature is not supported when runner is already running inside container.");
}
#pragma warning restore CA1416
#else
var initProcessCgroup = File.ReadLines("/proc/1/cgroup");
if (initProcessCgroup.Any(x => x.IndexOf(":/docker/", StringComparison.OrdinalIgnoreCase) >= 0))
{
throw new NotSupportedException("Container feature is not supported when runner is already running inside container.");
}
#endif
#if OS_WINDOWS
#pragma warning disable CA1416
// Check OS version (Windows server 1803 is required)
object windowsInstallationType = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "InstallationType", defaultValue: null);
ArgUtil.NotNull(windowsInstallationType, nameof(windowsInstallationType));
object windowsReleaseId = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ReleaseId", defaultValue: null);
ArgUtil.NotNull(windowsReleaseId, nameof(windowsReleaseId));
executionContext.Debug($"Current Windows version: '{windowsReleaseId} ({windowsInstallationType})'");
if (int.TryParse(windowsReleaseId.ToString(), out int releaseId))
{
if (!windowsInstallationType.ToString().StartsWith("Server", StringComparison.OrdinalIgnoreCase) || releaseId < 1803)
{
throw new NotSupportedException("Container feature requires Windows Server 1803 or higher.");
}
}
else
{
throw new ArgumentOutOfRangeException(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ReleaseId");
}
#pragma warning restore CA1416
#endif
// Check docker client/server version
executionContext.Output("##[group]Checking docker version");
DockerVersion dockerVersion = await _dockerManager.DockerVersion(executionContext);
executionContext.Output("##[endgroup]");
ArgUtil.NotNull(dockerVersion.ServerVersion, nameof(dockerVersion.ServerVersion));
ArgUtil.NotNull(dockerVersion.ClientVersion, nameof(dockerVersion.ClientVersion));
#if OS_WINDOWS
Version requiredDockerEngineAPIVersion = new Version(1, 30); // Docker-EE version 17.6
#else
Version requiredDockerEngineAPIVersion = new Version(1, 35); // Docker-CE version 17.12
#endif
if (dockerVersion.ServerVersion < requiredDockerEngineAPIVersion)
{
throw new NotSupportedException($"Min required docker engine API server version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') server version is '{dockerVersion.ServerVersion}'");
}
if (dockerVersion.ClientVersion < requiredDockerEngineAPIVersion)
{
throw new NotSupportedException($"Min required docker engine API client version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') client version is '{dockerVersion.ClientVersion}'");
}
}
} }
} }

View File

@@ -1226,7 +1226,7 @@ namespace GitHub.Runner.Worker
var value = dict[key].ToString(); var value = dict[key].ToString();
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))
{ {
dict[key] = new StringContextData(stepHost.ResolvePathForStepHost(value)); dict[key] = new StringContextData(stepHost.ResolvePathForStepHost(context, value));
} }
} }
else if (dict[key] is DictionaryContextData) else if (dict[key] is DictionaryContextData)

View File

@@ -0,0 +1,15 @@
using System;
using GitHub.Runner.Common;
namespace GitHub.Runner.Worker
{
public class FeatureManager
{
public static bool IsContainerHooksEnabled(Variables variables)
{
var isContainerHookFeatureFlagSet = variables?.GetBoolean(Constants.Runner.Features.AllowRunnerContainerHooks) ?? false;
var isContainerHooksPathSet = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath));
return isContainerHookFeatureFlagSet && isContainerHooksPathSet;
}
}
}

View File

@@ -275,12 +275,6 @@ namespace GitHub.Runner.Worker
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container) public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
{ {
if (!context.Global.Variables.GetBoolean("DistributedTask.UploadStepSummary") ?? true)
{
Trace.Info("Step Summary is disabled; skipping attachment upload");
return;
}
if (String.IsNullOrEmpty(filePath) || !File.Exists(filePath)) if (String.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{ {
Trace.Info($"Step Summary file ({filePath}) does not exist; skipping attachment upload"); Trace.Info($"Step Summary file ({filePath}) does not exist; skipping attachment upload");

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker.Container; using GitHub.Runner.Worker.Container;
using Newtonsoft.Json.Linq;
namespace GitHub.Runner.Worker namespace GitHub.Runner.Worker
{ {
@@ -22,5 +23,6 @@ namespace GitHub.Runner.Worker
public StepsContext StepsContext { get; set; } public StepsContext StepsContext { get; set; }
public Variables Variables { get; set; } public Variables Variables { get; set; }
public bool WriteDebug { get; set; } public bool WriteDebug { get; set; }
public JObject ContainerHookState { get; set; }
} }
} }

View File

@@ -11,6 +11,8 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Runner.Worker.Expressions; using GitHub.Runner.Worker.Expressions;
using Pipelines = GitHub.DistributedTask.Pipelines; using Pipelines = GitHub.DistributedTask.Pipelines;

View File

@@ -8,6 +8,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container; using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using Pipelines = GitHub.DistributedTask.Pipelines; using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker.Handlers namespace GitHub.Runner.Worker.Handlers
@@ -38,6 +39,8 @@ namespace GitHub.Runner.Worker.Handlers
AddInputsToEnvironment(); AddInputsToEnvironment();
var dockerManager = HostContext.GetService<IDockerCommandManager>(); var dockerManager = HostContext.GetService<IDockerCommandManager>();
var containerHookManager = HostContext.GetService<IContainerHookManager>();
string dockerFile = null;
// container image haven't built/pull // container image haven't built/pull
if (Data.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase)) if (Data.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
@@ -47,26 +50,28 @@ namespace GitHub.Runner.Worker.Handlers
else if (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile")) else if (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile"))
{ {
// ensure docker file exist // ensure docker file exist
var dockerFile = Path.Combine(ActionDirectory, Data.Image); dockerFile = Path.Combine(ActionDirectory, Data.Image);
ArgUtil.File(dockerFile, nameof(Data.Image)); ArgUtil.File(dockerFile, nameof(Data.Image));
if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
ExecutionContext.Output($"##[group]Building docker image");
ExecutionContext.Output($"Dockerfile for action: '{dockerFile}'.");
var imageName = $"{dockerManager.DockerInstanceLabel}:{ExecutionContext.Id.ToString("N")}";
var buildExitCode = await dockerManager.DockerBuild(
ExecutionContext,
ExecutionContext.GetGitHubContext("workspace"),
dockerFile,
Directory.GetParent(dockerFile).FullName,
imageName);
ExecutionContext.Output("##[endgroup]");
if (buildExitCode != 0)
{ {
throw new InvalidOperationException($"Docker build failed with exit code {buildExitCode}"); ExecutionContext.Output($"##[group]Building docker image");
} ExecutionContext.Output($"Dockerfile for action: '{dockerFile}'.");
var imageName = $"{dockerManager.DockerInstanceLabel}:{ExecutionContext.Id.ToString("N")}";
var buildExitCode = await dockerManager.DockerBuild(
ExecutionContext,
ExecutionContext.GetGitHubContext("workspace"),
dockerFile,
Directory.GetParent(dockerFile).FullName,
imageName);
ExecutionContext.Output("##[endgroup]");
Data.Image = imageName; if (buildExitCode != 0)
{
throw new InvalidOperationException($"Docker build failed with exit code {buildExitCode}");
}
Data.Image = imageName;
}
} }
string type = Action.Type == Pipelines.ActionSourceType.Repository ? "Dockerfile" : "DockerHub"; string type = Action.Type == Pipelines.ActionSourceType.Repository ? "Dockerfile" : "DockerHub";
@@ -220,14 +225,21 @@ namespace GitHub.Runner.Worker.Handlers
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value); container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
} }
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager, container)) if (FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
{ {
var runExitCode = await dockerManager.DockerRun(ExecutionContext, container, stdoutManager.OnDataReceived, stderrManager.OnDataReceived); await containerHookManager.RunContainerStepAsync(ExecutionContext, container, dockerFile);
ExecutionContext.Debug($"Docker Action run completed with exit code {runExitCode}"); }
if (runExitCode != 0) else
{
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
{ {
ExecutionContext.Result = TaskResult.Failed; var runExitCode = await dockerManager.DockerRun(ExecutionContext, container, stdoutManager.OnDataReceived, stderrManager.OnDataReceived);
ExecutionContext.Debug($"Docker Action run completed with exit code {runExitCode}");
if (runExitCode != 0)
{
ExecutionContext.Result = TaskResult.Failed;
}
} }
} }
#endif #endif

View File

@@ -8,6 +8,9 @@ using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
namespace GitHub.Runner.Worker.Handlers namespace GitHub.Runner.Worker.Handlers
{ {
@@ -102,6 +105,12 @@ namespace GitHub.Runner.Worker.Handlers
Data.NodeVersion = "node16"; Data.NodeVersion = "node16";
} }
#endif #endif
string forcedNodeVersion = System.Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedActionsNodeVersion);
if (forcedNodeVersion == "node16" && Data.NodeVersion != "node16")
{
Data.NodeVersion = "node16";
}
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion); var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}"); string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
@@ -109,7 +118,7 @@ namespace GitHub.Runner.Worker.Handlers
// 1) Wrap the script file path in double quotes. // 1) Wrap the script file path in double quotes.
// 2) Escape double quotes within the script file path. Double-quote is a valid // 2) Escape double quotes within the script file path. Double-quote is a valid
// file name character on Linux. // file name character on Linux.
string arguments = StepHost.ResolvePathForStepHost(StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\"""))); string arguments = StepHost.ResolvePathForStepHost(ExecutionContext, StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
#if OS_WINDOWS #if OS_WINDOWS
// It appears that node.exe outputs UTF8 when not in TTY mode. // It appears that node.exe outputs UTF8 when not in TTY mode.
@@ -142,14 +151,16 @@ namespace GitHub.Runner.Worker.Handlers
// Execute the process. Exit code 0 should always be returned. // Execute the process. Exit code 0 should always be returned.
// A non-zero exit code indicates infrastructural failure. // A non-zero exit code indicates infrastructural failure.
// Task failure should be communicated over STDOUT using ## commands. // Task failure should be communicated over STDOUT using ## commands.
Task<int> step = StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory), Task<int> step = StepHost.ExecuteAsync(ExecutionContext,
fileName: StepHost.ResolvePathForStepHost(file), workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
fileName: StepHost.ResolvePathForStepHost(ExecutionContext, file),
arguments: arguments, arguments: arguments,
environment: Environment, environment: Environment,
requireExitCodeZero: false, requireExitCodeZero: false,
outputEncoding: outputEncoding, outputEncoding: outputEncoding,
killProcessOnCancel: false, killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding, inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
standardInInput: null,
cancellationToken: ExecutionContext.CancellationToken); cancellationToken: ExecutionContext.CancellationToken);
// Wait for either the node exit or force finish through ##vso command // Wait for either the node exit or force finish through ##vso command

View File

@@ -8,6 +8,8 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using Pipelines = GitHub.DistributedTask.Pipelines; using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker.Handlers namespace GitHub.Runner.Worker.Handlers
@@ -202,14 +204,32 @@ namespace GitHub.Runner.Worker.Handlers
} }
else else
{ {
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell); // For these shells, we want to use system binaries
shellCommand = parsed.shellCommand; var systemShells = new string[] { "bash", "sh", "powershell", "pwsh" };
// For non-ContainerStepHost, the command must be located on the host by Which if (!IsActionStep && systemShells.Contains(shell))
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{ {
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); shellCommand = shell;
commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath);
if (shell == "bash")
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat("sh");
}
else
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shell);
}
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
// For non-ContainerStepHost, the command must be located on the host by Which
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
} }
} }
@@ -229,7 +249,7 @@ namespace GitHub.Runner.Worker.Handlers
{ {
// We do not not the full path until we know what shell is being used, so that we can determine the file extension // We do not not the full path until we know what shell is being used, so that we can determine the file extension
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}"); scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
resolvedScriptPath = $"{StepHost.ResolvePathForStepHost(scriptFilePath).Replace("\"", "\\\"")}"; resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
} }
else else
{ {
@@ -300,6 +320,7 @@ namespace GitHub.Runner.Worker.Handlers
ExecutionContext.Debug($"{fileName} {arguments}"); ExecutionContext.Debug($"{fileName} {arguments}");
Inputs.TryGetValue("standardInInput", out var standardInInput);
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager)) using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager)) using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
{ {
@@ -307,7 +328,8 @@ namespace GitHub.Runner.Worker.Handlers
StepHost.ErrorDataReceived += stderrManager.OnDataReceived; StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
// Execute // Execute
int exitCode = await StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory), int exitCode = await StepHost.ExecuteAsync(ExecutionContext,
workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
fileName: fileName, fileName: fileName,
arguments: arguments, arguments: arguments,
environment: Environment, environment: Environment,
@@ -315,6 +337,7 @@ namespace GitHub.Runner.Worker.Handlers
outputEncoding: null, outputEncoding: null,
killProcessOnCancel: false, killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding, inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
standardInInput: standardInInput,
cancellationToken: ExecutionContext.CancellationToken); cancellationToken: ExecutionContext.CancellationToken);
// Error // Error

View File

@@ -3,10 +3,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
namespace GitHub.Runner.Worker.Handlers namespace GitHub.Runner.Worker.Handlers
{ {
internal class ScriptHandlerHelpers internal static class ScriptHandlerHelpers
{ {
private static readonly Dictionary<string, string> _defaultArguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) private static readonly Dictionary<string, string> _defaultArguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{ {
@@ -81,22 +83,5 @@ namespace GitHub.Runner.Worker.Handlers
throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}"); throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}");
} }
} }
internal static string GetDefaultShellForScript(string path, Common.Tracing trace, string prependPath)
{
var format = "{0} {1}";
switch (Path.GetExtension(path))
{
case ".sh":
// use 'sh' args but prefer bash
var pathToShell = WhichUtil.Which("bash", false, trace, prependPath) ?? WhichUtil.Which("sh", true, trace, prependPath);
return string.Format(format, pathToShell, _defaultArguments["sh"]);
case ".ps1":
var pathToPowershell = WhichUtil.Which("pwsh", false, trace, prependPath) ?? WhichUtil.Which("powershell", true, trace, prependPath);
return string.Format(format, pathToPowershell, _defaultArguments["powershell"]);
default:
throw new ArgumentException($"{path} is not a valid path to a script. Make sure it ends in '.sh' or '.ps1'.");
}
}
} }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using GitHub.DistributedTask.Pipelines.ContextData;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -7,7 +8,9 @@ using GitHub.Runner.Worker.Container;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using System.Linq; using System.Linq;
using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.Runner.Worker.Container.ContainerHooks;
using System.IO;
using System.Threading.Channels;
namespace GitHub.Runner.Worker.Handlers namespace GitHub.Runner.Worker.Handlers
{ {
@@ -16,11 +19,12 @@ namespace GitHub.Runner.Worker.Handlers
event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived; event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived; event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
string ResolvePathForStepHost(string path); string ResolvePathForStepHost(IExecutionContext executionContext, string path);
Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion); Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion);
Task<int> ExecuteAsync(string workingDirectory, Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName, string fileName,
string arguments, string arguments,
IDictionary<string, string> environment, IDictionary<string, string> environment,
@@ -28,6 +32,7 @@ namespace GitHub.Runner.Worker.Handlers
Encoding outputEncoding, Encoding outputEncoding,
bool killProcessOnCancel, bool killProcessOnCancel,
bool inheritConsoleHandler, bool inheritConsoleHandler,
string standardInInput,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }
@@ -48,7 +53,7 @@ namespace GitHub.Runner.Worker.Handlers
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived; public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived; public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
public string ResolvePathForStepHost(string path) public string ResolvePathForStepHost(IExecutionContext executionContext, string path)
{ {
return path; return path;
} }
@@ -58,7 +63,8 @@ namespace GitHub.Runner.Worker.Handlers
return Task.FromResult<string>(preferredVersion); return Task.FromResult<string>(preferredVersion);
} }
public async Task<int> ExecuteAsync(string workingDirectory, public async Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName, string fileName,
string arguments, string arguments,
IDictionary<string, string> environment, IDictionary<string, string> environment,
@@ -66,10 +72,17 @@ namespace GitHub.Runner.Worker.Handlers
Encoding outputEncoding, Encoding outputEncoding,
bool killProcessOnCancel, bool killProcessOnCancel,
bool inheritConsoleHandler, bool inheritConsoleHandler,
string standardInInput,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using (var processInvoker = HostContext.CreateService<IProcessInvoker>()) using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{ {
Channel<string> redirectStandardIn = null;
if (standardInInput != null)
{
redirectStandardIn = Channel.CreateUnbounded<string>(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = true });
redirectStandardIn.Writer.TryWrite(standardInInput);
}
processInvoker.OutputDataReceived += OutputDataReceived; processInvoker.OutputDataReceived += OutputDataReceived;
processInvoker.ErrorDataReceived += ErrorDataReceived; processInvoker.ErrorDataReceived += ErrorDataReceived;
@@ -80,7 +93,7 @@ namespace GitHub.Runner.Worker.Handlers
requireExitCodeZero: requireExitCodeZero, requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding, outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel, killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: null, redirectStandardIn: redirectStandardIn,
inheritConsoleHandler: inheritConsoleHandler, inheritConsoleHandler: inheritConsoleHandler,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
} }
@@ -94,11 +107,15 @@ namespace GitHub.Runner.Worker.Handlers
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived; public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived; public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
public string ResolvePathForStepHost(string path) public string ResolvePathForStepHost(IExecutionContext executionContext, string path)
{ {
// make sure container exist. // make sure container exist.
ArgUtil.NotNull(Container, nameof(Container)); ArgUtil.NotNull(Container, nameof(Container));
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId)); if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global?.Variables))
{
// TODO: Remove nullcheck with executionContext.Global? by setting up ExecutionContext.Global at GitHub.Runner.Common.Tests.Worker.ExecutionContextL0.GetExpressionValues_ContainerStepHost
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
}
// remove double quotes around the path // remove double quotes around the path
path = path.Trim('\"'); path = path.Trim('\"');
@@ -120,6 +137,19 @@ namespace GitHub.Runner.Worker.Handlers
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion) public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{ {
// Optimistically use the default
string nodeExternal = preferredVersion;
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
if (Container.IsAlpine)
{
nodeExternal = CheckPlatformForAlpineContainer(executionContext, preferredVersion);
}
executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}");
return nodeExternal;
}
// Best effort to determine a compatible node runtime // Best effort to determine a compatible node runtime
// There may be more variation in which libraries are linked than just musl/glibc, // There may be more variation in which libraries are linked than just musl/glibc,
// so determine based on known distribtutions instead // so determine based on known distribtutions instead
@@ -128,7 +158,6 @@ namespace GitHub.Runner.Worker.Handlers
var output = new List<string>(); var output = new List<string>();
var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output); var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output);
string nodeExternal;
if (execExitCode == 0) if (execExitCode == 0)
{ {
foreach (var line in output) foreach (var line in output)
@@ -136,26 +165,17 @@ namespace GitHub.Runner.Worker.Handlers
executionContext.Debug(line); executionContext.Debug(line);
if (line.ToLower().Contains("alpine")) if (line.ToLower().Contains("alpine"))
{ {
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64)) nodeExternal = CheckPlatformForAlpineContainer(executionContext, preferredVersion);
{
var os = Constants.Runner.Platform.ToString();
var arch = Constants.Runner.PlatformArchitecture.ToString();
var msg = $"JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected {os} {arch}";
throw new NotSupportedException(msg);
}
nodeExternal = $"{preferredVersion}_alpine";
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
return nodeExternal; return nodeExternal;
} }
} }
} }
// Optimistically use the default
nodeExternal = preferredVersion;
executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}"); executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}");
return nodeExternal; return nodeExternal;
} }
public async Task<int> ExecuteAsync(string workingDirectory, public async Task<int> ExecuteAsync(IExecutionContext context,
string workingDirectory,
string fileName, string fileName,
string arguments, string arguments,
IDictionary<string, string> environment, IDictionary<string, string> environment,
@@ -163,12 +183,25 @@ namespace GitHub.Runner.Worker.Handlers
Encoding outputEncoding, Encoding outputEncoding,
bool killProcessOnCancel, bool killProcessOnCancel,
bool inheritConsoleHandler, bool inheritConsoleHandler,
string standardInInput,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// make sure container exist.
ArgUtil.NotNull(Container, nameof(Container)); ArgUtil.NotNull(Container, nameof(Container));
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId)); var containerHookManager = HostContext.GetService<IContainerHookManager>();
if (FeatureManager.IsContainerHooksEnabled(context.Global.Variables))
{
TranslateToContainerPath(environment);
await containerHookManager.RunScriptStepAsync(context,
Container,
workingDirectory,
fileName,
arguments,
environment,
PrependPath);
return (int)(context.Result ?? 0);
}
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
var dockerManager = HostContext.GetService<IDockerCommandManager>(); var dockerManager = HostContext.GetService<IDockerCommandManager>();
string dockerClientPath = dockerManager.DockerPath; string dockerClientPath = dockerManager.DockerPath;
@@ -202,12 +235,7 @@ namespace GitHub.Runner.Worker.Handlers
dockerCommandArgs.Add(arguments); dockerCommandArgs.Add(arguments);
string dockerCommandArgstring = string.Join(" ", dockerCommandArgs); string dockerCommandArgstring = string.Join(" ", dockerCommandArgs);
TranslateToContainerPath(environment);
// make sure all env are using container path
foreach (var envKey in environment.Keys.ToList())
{
environment[envKey] = this.Container.TranslateToContainerPath(environment[envKey]);
}
using (var processInvoker = HostContext.CreateService<IProcessInvoker>()) using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{ {
@@ -221,7 +249,6 @@ namespace GitHub.Runner.Worker.Handlers
// Let .NET choose the default. // Let .NET choose the default.
outputEncoding = null; outputEncoding = null;
#endif #endif
return await processInvoker.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work), return await processInvoker.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
fileName: dockerClientPath, fileName: dockerClientPath,
arguments: dockerCommandArgstring, arguments: dockerCommandArgstring,
@@ -234,5 +261,28 @@ namespace GitHub.Runner.Worker.Handlers
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
} }
} }
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
{
string nodeExternal = preferredVersion;
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
{
var os = Constants.Runner.Platform.ToString();
var arch = Constants.Runner.PlatformArchitecture.ToString();
var msg = $"JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected {os} {arch}";
throw new NotSupportedException(msg);
}
nodeExternal = $"{preferredVersion}_alpine";
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
return nodeExternal;
}
private void TranslateToContainerPath(IDictionary<string, string> environment)
{
foreach (var envKey in environment.Keys.ToList())
{
environment[envKey] = this.Container.TranslateToContainerPath(environment[envKey]);
}
}
} }
} }

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using GitHub.DistributedTask.Expressions2; using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
@@ -206,6 +207,7 @@ namespace GitHub.Runner.Worker
// Evaluate the job container // Evaluate the job container
context.Debug("Evaluating job container"); context.Debug("Evaluating job container");
var container = templateEvaluator.EvaluateJobContainer(message.JobContainer, jobContext.ExpressionValues, jobContext.ExpressionFunctions); var container = templateEvaluator.EvaluateJobContainer(message.JobContainer, jobContext.ExpressionValues, jobContext.ExpressionFunctions);
ValidateJobContainer(container);
if (container != null) if (container != null)
{ {
jobContext.Global.Container = new Container.ContainerInfo(HostContext, container); jobContext.Global.Container = new Container.ContainerInfo(HostContext, container);
@@ -672,5 +674,13 @@ namespace GitHub.Runner.Worker
Trace.Info($"Total accessible running process: {snapshot.Count}."); Trace.Info($"Total accessible running process: {snapshot.Count}.");
return snapshot; return snapshot;
} }
private static void ValidateJobContainer(JobContainer container)
{
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Actions.RequireJobContainer)) && container == null)
{
throw new ArgumentException("Jobs without a job container are forbidden on this runner, please add a 'container:' to your job or contact your self-hosted runner administrator.");
}
}
} }
} }

View File

@@ -18,8 +18,8 @@ namespace GitHub.Runner.Worker
public class JobHookData public class JobHookData
{ {
public string Path {get; private set;} public string Path { get; private set; }
public ActionRunStage Stage {get; private set;} public ActionRunStage Stage { get; private set; }
public JobHookData(ActionRunStage stage, string path) public JobHookData(ActionRunStage stage, string path)
{ {
@@ -60,7 +60,7 @@ namespace GitHub.Runner.Worker
Dictionary<string, string> inputs = new() Dictionary<string, string> inputs = new()
{ {
["path"] = hookData.Path, ["path"] = hookData.Path,
["shell"] = ScriptHandlerHelpers.GetDefaultShellForScript(hookData.Path, Trace, prependPath) ["shell"] = HostContext.GetDefaultShellForScript(hookData.Path, prependPath)
}; };
// Create the handler // Create the handler

View File

@@ -0,0 +1,39 @@
using System;
using System.Runtime.Serialization;
using GitHub.Services.WebApi;
using Newtonsoft.Json;
namespace GitHub.DistributedTask.Pipelines
{
[DataContract]
public sealed class HostedRunnerShutdownMessage
{
public static readonly String MessageType = "RunnerShutdown";
[JsonConstructor]
internal HostedRunnerShutdownMessage()
{
}
public HostedRunnerShutdownMessage(String reason)
{
this.Reason = reason;
}
[DataMember]
public String Reason
{
get;
private set;
}
public WebApi.TaskAgentMessage GetAgentMessage()
{
return new WebApi.TaskAgentMessage
{
Body = JsonUtility.ToString(this),
MessageType = HostedRunnerShutdownMessage.MessageType,
};
}
}
}

View File

@@ -56,5 +56,8 @@ namespace GitHub.DistributedTask.WebApi
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public int? ExecutionTimeInSeconds { get; set; } public int? ExecutionTimeInSeconds { get; set; }
[DataMember(EmitDefaultValue = false)]
public string ContainerHookData { get; set; }
} }
} }

View File

@@ -5,5 +5,6 @@ namespace GitHub.DistributedTask.WebApi
public static class JobRequestMessageTypes public static class JobRequestMessageTypes
{ {
public const String PipelineAgentJobRequest = "PipelineAgentJobRequest"; public const String PipelineAgentJobRequest = "PipelineAgentJobRequest";
public const String RunnerJobRequest = "RunnerJobRequest";
} }
} }

View File

@@ -141,6 +141,24 @@ namespace GitHub.DistributedTask.WebApi
return ReplaceAgentAsync(poolId, agent.Id, agent, userState, cancellationToken); return ReplaceAgentAsync(poolId, agent.Id, agent, userState, cancellationToken);
} }
public Task<Pipelines.AgentJobRequestMessage> GetJobMessageAsync(
string messageId,
object userState = null,
CancellationToken cancellationToken = default)
{
HttpMethod httpMethod = new HttpMethod("GET");
Guid locationId = new Guid("25adab70-1379-4186-be8e-b643061ebe3a");
object routeValues = new { messageId = messageId };
return SendAsync<Pipelines.AgentJobRequestMessage>(
httpMethod,
locationId,
routeValues: routeValues,
version: new ApiResourceVersion(6.0, 1),
userState: userState,
cancellationToken: cancellationToken);
}
protected Task<T> SendAsync<T>( protected Task<T> SendAsync<T>(
HttpMethod method, HttpMethod method,
Guid locationId, Guid locationId,

View File

@@ -2,6 +2,7 @@
using GitHub.Runner.Listener.Check; using GitHub.Runner.Listener.Check;
using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Worker; using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Runner.Worker.Handlers; using GitHub.Runner.Worker.Handlers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -68,7 +69,8 @@ namespace GitHub.Runner.Common.Tests
typeof(IStep), typeof(IStep),
typeof(IStepHost), typeof(IStepHost),
typeof(IDiagnosticLogManager), typeof(IDiagnosticLogManager),
typeof(IEnvironmentContextData) typeof(IEnvironmentContextData),
typeof(IHookArgs),
}; };
Validate( Validate(
assembly: typeof(IStepsRunner).GetTypeInfo().Assembly, assembly: typeof(IStepsRunner).GetTypeInfo().Assembly,

View File

@@ -25,23 +25,6 @@ namespace GitHub.Runner.Common.Tests.Worker
private CreateStepSummaryCommand _createStepCommand; private CreateStepSummaryCommand _createStepCommand;
private ITraceWriter _trace; private ITraceWriter _trace;
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_FeatureDisabled()
{
using (var hostContext = Setup(featureFlagState: "false"))
{
var stepSummaryFile = Path.Combine(_rootDirectory, "feature-off");
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
_jobExecutionContext.Complete();
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
Assert.Equal(0, _issues.Count);
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
@@ -199,7 +182,7 @@ namespace GitHub.Runner.Common.Tests.Worker
File.WriteAllText(path, contentStr, encoding); File.WriteAllText(path, contentStr, encoding);
} }
private TestHostContext Setup([CallerMemberName] string name = "", string featureFlagState = "true") private TestHostContext Setup([CallerMemberName] string name = "")
{ {
var hostContext = new TestHostContext(this, name); var hostContext = new TestHostContext(this, name);
@@ -241,7 +224,6 @@ namespace GitHub.Runner.Common.Tests.Worker
_variables = new Variables(hostContext, new Dictionary<string, VariableValue> _variables = new Variables(hostContext, new Dictionary<string, VariableValue>
{ {
{ "MySecretName", new VariableValue("My secret value", true) }, { "MySecretName", new VariableValue("My secret value", true) },
{ "DistributedTask.UploadStepSummary", featureFlagState },
}); });
// Directory for test data // Directory for test data

View File

@@ -100,7 +100,6 @@ namespace GitHub.Runner.Common.Tests.Worker
{ {
using (TestHostContext hc = CreateTestContext()) using (TestHostContext hc = CreateTestContext())
{ {
// Arrange: Create a job request message. // Arrange: Create a job request message.
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference(); TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
TimelineReference timeline = new TimelineReference(); TimelineReference timeline = new TimelineReference();

View File

@@ -211,6 +211,24 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task JobExtensionBuildFailsWithoutContainerIfRequired()
{
Environment.SetEnvironmentVariable(Constants.Variables.Actions.RequireJobContainer, "true");
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>() { new JobExtensionRunner(null, "", "prepare1", null), new JobExtensionRunner(null, "", "prepare2", null) }, new Dictionary<Guid, IActionRunner>())));
await Assert.ThrowsAsync<ArgumentException>(() => jobExtension.InitializeJob(_jobEc, _message));
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]

View File

@@ -10,6 +10,7 @@ using GitHub.Runner.Worker.Container;
using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ContextData;
using System.Linq; using System.Linq;
using GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker namespace GitHub.Runner.Common.Tests.Worker
{ {
@@ -24,6 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_ec = new Mock<IExecutionContext>(); _ec = new Mock<IExecutionContext>();
_ec.SetupAllProperties(); _ec.SetupAllProperties();
_ec.Setup(x => x.Global).Returns(new GlobalContext { WriteDebug = true }); _ec.Setup(x => x.Global).Returns(new GlobalContext { WriteDebug = true });
_ec.Object.Global.Variables = new Variables(hc, new Dictionary<string, VariableValue>());
var trace = hc.GetTrace(); var trace = hc.GetTrace();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); }); _ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); });

View File

@@ -1 +1 @@
2.292.0 2.294.0