diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..70f73c2b060 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, these accounts +# will be requested for review when someone opens a pull request. +* @aws/aws-ecs-agent diff --git a/.github/workflows/gitsecrets.yml b/.github/workflows/gitsecrets.yml index f01997d9c8c..5c106412de0 100644 --- a/.github/workflows/gitsecrets.yml +++ b/.github/workflows/gitsecrets.yml @@ -2,6 +2,7 @@ name: GitSecretsScan on: [push, pull_request] +permissions: read-all jobs: git-secret-check: name: Git Secrets Scan @@ -12,6 +13,8 @@ jobs: path: src/github.com/aws/amazon-ecs-agent - name: Git Secrets Scan Script run: | + # workaround git-secrets requiring the say command: https://github.com/awslabs/git-secrets/pull/221 + ln -s "$(which echo)" /usr/local/bin/say set -ex cd $GITHUB_WORKSPACE git clone https://github.com/awslabs/git-secrets.git && cd git-secrets diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 527fb295a31..13efe56a20f 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,6 +2,7 @@ name: Linux on: [pull_request] +permissions: read-all jobs: unit-tests: name: Linux unit tests @@ -11,12 +12,22 @@ jobs: with: path: src/github.com/aws/amazon-ecs-agent - name: get GO_VERSION + id: get-go-version run: | cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent - echo "GO_VERSION=$(cat GO_VERSION)" >> $GITHUB_ENV + set -eou pipefail + go_version=$(cat -e GO_VERSION) + go_version=${go_version%?} + go_version_length=${#go_version} + go_version_re="^([0-9]+\.){1,2}([0-9]+)$" + if ! [[ $go_version_length -le 10 && $go_version =~ $go_version_re ]] ; then + echo "invalid GO version" + exit 1 + fi + echo "::set-output name=GO_VERSION::$go_version" - uses: actions/setup-go@v2 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ steps.get-go-version.outputs.GO_VERSION }} - uses: actions/checkout@v2 with: submodules: true @@ -28,3 +39,5 @@ jobs: cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent make test-silent make analyze-cover-profile + make test-init + make analyze-cover-profile-init diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 0b5c4a48755..497eee78a38 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,6 +2,7 @@ name: Static Checks on: [push, pull_request] +permissions: read-all jobs: static-check: name: Static Analysis @@ -11,12 +12,22 @@ jobs: with: path: src/github.com/aws/amazon-ecs-agent - name: get GO_VERSION + id: get-go-version run: | cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent - echo "GO_VERSION=$(cat GO_VERSION)" >> $GITHUB_ENV + set -eou pipefail + go_version=$(cat -e GO_VERSION) + go_version=${go_version%?} + go_version_length=${#go_version} + go_version_re="^([0-9]+\.){1,2}([0-9]+)$" + if ! [[ $go_version_length -le 10 && $go_version =~ $go_version_re ]] ; then + echo "invalid GO version" + exit 1 + fi + echo "::set-output name=GO_VERSION::$go_version" - uses: actions/setup-go@v2 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ steps.get-go-version.outputs.GO_VERSION }} - uses: actions/checkout@v2 with: path: src/github.com/aws/amazon-ecs-agent @@ -29,6 +40,42 @@ jobs: make get-deps make static-check + init-check: + name: Static Analysis Init + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + path: src/github.com/aws/amazon-ecs-agent + - name: get GO_VERSION + id: get-go-version + run: | + cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent + set -eou pipefail + go_version=$(cat -e GO_VERSION) + go_version=${go_version%?} + go_version_length=${#go_version} + go_version_re="^([0-9]+\.){1,2}([0-9]+)$" + if ! [[ $go_version_length -le 10 && $go_version =~ $go_version_re ]] ; then + echo "invalid GO version" + exit 1 + fi + echo "::set-output name=GO_VERSION::$go_version" + - uses: actions/setup-go@v2 + with: + go-version: ${{ steps.get-go-version.outputs.GO_VERSION }} + - uses: actions/checkout@v2 + with: + path: src/github.com/aws/amazon-ecs-agent + - name: run static checks + run: | + export GOPATH=$GITHUB_WORKSPACE + export PATH=$PATH:$(go env GOPATH)/bin + export GO111MODULE=auto + cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent + make get-deps-init + make static-check-init + x-platform-build: name: Cross platform build runs-on: ubuntu-latest @@ -37,12 +84,22 @@ jobs: with: path: src/github.com/aws/amazon-ecs-agent - name: get GO_VERSION + id: get-go-version run: | cd $GITHUB_WORKSPACE/src/github.com/aws/amazon-ecs-agent - echo "GO_VERSION=$(cat GO_VERSION)" >> $GITHUB_ENV + set -eou pipefail + go_version=$(cat -e GO_VERSION) + go_version=${go_version%?} + go_version_length=${#go_version} + go_version_re="^([0-9]+\.){1,2}([0-9]+)$" + if ! [[ $go_version_length -le 10 && $go_version =~ $go_version_re ]] ; then + echo "invalid GO version" + exit 1 + fi + echo "::set-output name=GO_VERSION::$go_version" - uses: actions/setup-go@v2 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ steps.get-go-version.outputs.GO_VERSION }} - uses: actions/checkout@v2 with: submodules: true diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 5f96bef035e..a1911b212d5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -2,6 +2,7 @@ name: Windows on: [pull_request] +permissions: read-all jobs: windows-unit-tests: name: Windows unit tests @@ -11,13 +12,21 @@ jobs: with: path: src/github.com/aws/amazon-ecs-agent - name: get GO_VERSION + id: get-go-version run: | cd "$Env:GITHUB_WORKSPACE" cd "src/github.com/aws/amazon-ecs-agent" - echo "GO_VERSION_WINDOWS=$(type GO_VERSION_WINDOWS)" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + $ErrorActionPreference = "Stop" + $go_version_win = $(type GO_VERSION_WINDOWS) + $go_version_re = "^\d+\.{1,2}\d+$" + if (-Not ($go_version_win.Length -le 10 -or $go_version_win -match $go_version_re) ) { + echo "invalid GO version" + exit 1 + } + Write-Output "::set-output name=GO_VERSION_WINDOWS::$go_version_win" - uses: actions/setup-go@v2 with: - go-version: ${{ env.GO_VERSION_WINDOWS }} + go-version: ${{ steps.get-go-version.outputs.GO_VERSION_WINDOWS }} - uses: actions/checkout@v2 with: submodules: true diff --git a/.gitignore b/.gitignore index 6cdbe76d8ad..7c4c961a14a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ _bin/ *.swp *.orig /agent/version/_version.go +/ecs-init/version/version.go .agignore *.sublime-* .DS_Store @@ -20,3 +21,23 @@ _bin/ *.iml cover.out coverprofile.out +/amazon-ecs-init* +/BUILDROOT/ +/x86_64/ +/sources.tar +/ecs-init-* +/ecs.conf +/.deb-done +/.rpm-done +/.srpm-done +/BUILD +/RPMS +/SOURCES +/SRPMS +/ecs-init.spec +/sources.tgz +ecs-agent-*.tar +/ecs.service +*.log +*.DS_Store +.run/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 357fa1a3b8e..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: go -go_import_path: github.com/aws/amazon-ecs-init -sudo: false -go: - - 1.15 - -matrix: - include: - - os: linux - script: - - make get-deps - - make static-check - - make test - - make analyze-cover-profile diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ee0beea92..cd16069888d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,113 @@ # Changelog +## 1.68.1 +* Bug - Update ECS CNI and VPC plugins to fix instances with IMDSv1 disabled [#3531](https://github.com/aws/amazon-ecs-agent/pull/3531) +* Bug - Filter out metricCount=0 and its corresponding metricValue for service connect metric TargetResponseTime [#3537](https://github.com/aws/amazon-ecs-agent/pull/3537) + +## 1.68.0 +* Bug - Add ServiceConnect image to clean-up exclusion list [#3521](https://github.com/aws/amazon-ecs-agent/pull/3521) +* Enhancement: added new agent configuration to specify ephemeral host port range [#3522](https://github.com/aws/amazon-ecs-agent/pull/3522) + +## 1.67.2 +* Bug - Fix the generation of network bindings for Service Connect container [#3513](https://github.com/aws/amazon-ecs-agent/pull/3513) +* Bug - Prevent resetting valid agent state db when IMDS fails on startup [#3509](https://github.com/aws/amazon-ecs-agent/pull/3509) + +## 1.67.1 +* Bug - Read git hash from RELEASE_COMMIT file if possible [#3508](https://github.com/aws/amazon-ecs-agent/pull/3508) + +## 1.67.0 +* Bug - Don't log errors on instances not using GMSA [#3489](https://github.com/aws/amazon-ecs-agent/pull/3489) +* Enhancement - Update packaging Readme files with updated instructions to build init files [#3490](https://github.com/aws/amazon-ecs-agent/pull/3490) +* Bug - Fix unit tests for cgroup v2 [#3491](https://github.com/aws/amazon-ecs-agent/pull/3491) +* Enhancement - Update readme for ECS_SELINUX_CAPABLE to clarify Z-mode mount only and limited support [#3496](https://github.com/aws/amazon-ecs-agent/pull/3496) +* Bug - Fix agent short hash version bug [#3497](https://github.com/aws/amazon-ecs-agent/pull/3497) +* Bug - Use Ubuntu 20.04 for linux GH Unit tests [#3501](https://github.com/aws/amazon-ecs-agent/pull/3501) +* Feature - Container port range mapping [#3506](https://github.com/aws/amazon-ecs-agent/pull/3506) + +## 1.66.2 +* Bug - Add ecs-serviceconnect to CNI and Agent build scripts [#3482](https://github.com/aws/amazon-ecs-agent/pull/3482) +* Bug - add call to update-version.sh to dockerfree-agent-image [#3484](https://github.com/aws/amazon-ecs-agent/pull/3484) + +## 1.66.1 +* Bug - Update ecs agent version short hash to point to built head [#3476](https://github.com/aws/amazon-ecs-agent/pull/3476) +* Bug - Remove CAP_CHOWN [#3480](https://github.com/aws/amazon-ecs-agent/pull/3480) + +## 1.66.0 +* Feature - gMSA on Linux support [#3464](https://github.com/aws/amazon-ecs-agent/pull/3464) +* Enhancement - Restart AppNet Relay on failure [#3469](Restart AppNet Relay on failure) + +## 1.65.1 +* Enhancement - Add grpc vendor dependencies [#3439](https://github.com/aws/amazon-ecs-agent/pull/3439) +* Bug - Workaround git-secrets scan issue: awslabs/git-secrets#221 [#3442](https://github.com/aws/amazon-ecs-agent/pull/3442) + +## 1.65.0 +* Feature - ECS Agent changes to support task scale in protection feature. ECS Agent API Endpoint is also introduced with this feature. This feature allows a user to update and get task protection state of a task from a task container by calling ECS Agent API Endpoint, which protects the task from being terminated in a scale-in event [#3427](https://github.com/aws/amazon-ecs-agent/pull/3427) Github feature request - [#125](https://github.com/aws/containers-roadmap/issues/125) +* Enhancement - Update service connect config validator to validate fields with a global standard, or consumed and proceeded by ECS Agent for service connect [#3424](https://github.com/aws/amazon-ecs-agent/pull/3424) +* Enhancement - ServiceConnect AppNet version handling; init bootstrap; CNI interface name update for service connect [#3436](https://github.com/aws/amazon-ecs-agent/pull/3436) +* Enhancement - Add file watcher for Appnet agent image update for service connect [#3435](https://github.com/aws/amazon-ecs-agent/pull/3435) +* Enhancement - Change method for retrieving Windows network statistics in case of awsvpc network mode for Windows [#3425](https://github.com/aws/amazon-ecs-agent/pull/3425) +* Bug - Fix minor unreachable code caused by t.Fatal [#3372](https://github.com/aws/amazon-ecs-agent/pull/3372) + +## 1.64.0 +* Feature - Add service connect feature. This feature enables ECS Service to be discoverable and ECS will leverage the container port mappings, service name and default application namespace associated with the cluster and the service to register your service for discovery and to enable discovery of dependencies through DNS lookup [#3414](https://github.com/aws/amazon-ecs-agent/pull/3414) +* Bugfix: Bump Go to 1.19.1 for CVE-2022-27664 [#3398](https://github.com/aws/amazon-ecs-agent/pull/3398) + +## 1.63.1 +* Feature - Add VPC ID to TMDE v4 Task Responses. [#3288](https://github.com/aws/amazon-ecs-agent/pull/3288) [#3385](https://github.com/aws/amazon-ecs-agent/pull/3385) +* Feature - Add ServiceName to TMDE v4 Task Responses. [#3362](https://github.com/aws/amazon-ecs-agent/pull/3362) +* Enhancement - Add codeowners file and update token permission to read only for workflow. [#3374](https://github.com/aws/amazon-ecs-agent/pull/3374) +* Enhancement - Dependabot ecs-init fixes. [#3388](https://github.com/aws/amazon-ecs-agent/pull/3388) + +## 1.63.0 +* Feature - Add configurable default profile ECS_ALTERNATE_CREDENTIAL_PROFILE. [#3365](https://github.com/aws/amazon-ecs-agent/pull/3365) +* Enhancement - Update ECS_RESERVED_MEMORY description in README. [#3363](https://github.com/aws/amazon-ecs-agent/pull/3363) +* Enhancement - Update dependencies to include security patches reported by dependabot for agent [#3367](https://github.com/aws/amazon-ecs-agent/pull/3367) +* Enhancement - Update dependencies to include security patches reported by dependabot for ecs-init. [#3277](https://github.com/aws/amazon-ecs-agent/pull/3277) +* Enhancement - Reduce the flakiness of TestExecCommandAgent. [#3355](https://github.com/aws/amazon-ecs-agent/pull/3355) +* Bug - Add appmesh path to agent container image config. [#3378](https://github.com/aws/amazon-ecs-agent/pull/3378) +* Bug - Fix cgroupv2 mem usage calculation to match docker cli. [#3370](https://github.com/aws/amazon-ecs-agent/pull/3370) +* Bug - Fix json syntax in release-config. [#3359](https://github.com/aws/amazon-ecs-agent/pull/3359) +* Bug - Update validation script with more comprehensive set of files. [#3358](https://github.com/aws/amazon-ecs-agent/pull/3358) +* Bug - Update changelog generation to add missing spec file. [#3356](https://github.com/aws/amazon-ecs-agent/pull/3356) +* Bug - Fix format string for ecs-init. [#3282](https://github.com/aws/amazon-ecs-agent/pull/3282) + +## 1.62.2 +* Enhancement - Load ServiceName from ACS Task Payload. [#3342](https://github.com/aws/amazon-ecs-agent/pull/3342) +* Bug - Update healthcheck and ports in dockerfree build. [#3343](https://github.com/aws/amazon-ecs-agent/pull/3343) + +## 1.62.1 +* Bug - Fix an issue with cgroup mount [#3324](https://github.com/aws/amazon-ecs-agent/pull/3324) +* Enhancement - Build changes - Add GitShortSha to config, Add md5, json file creation [#3327](https://github.com/aws/amazon-ecs-agent/pull/3327) + +## 1.62.0 +* Enhancement - Update golang version to 1.18.3 [#3301](https://github.com/aws/amazon-ecs-agent/pull/3301) +* Enhancement - Update windows golang version to 1.18.3 [#3317](https://github.com/aws/amazon-ecs-agent/pull/3317) +* Bugfix - amazon-ecs-cni-plugins: Always run DeleteVeth on cleanup, fixes veth "exchange full" errors [#3311](https://github.com/aws/amazon-ecs-agent/pull/3311) + +## 1.61.3 +* Enhancement - Add command and error logging for FSx file mapping when calling out to PowerShell [#3240](https://github.com/aws/amazon-ecs-agent/pull/3240) +* Enhancement - Update README.md with missing environment variables [#3244](https://github.com/aws/amazon-ecs-agent/pull/3244) + +## 1.61.2 +* Enhancement - Integrate new/updated build targets and processes [#3234](https://github.com/aws/amazon-ecs-agent/pull/3234) +* Enhancement - Trimming task reason to a max of 1024 characters as per Back-end model [#3229](https://github.com/aws/amazon-ecs-agent/pull/3229) +* Enhancement - Add log message when receiving error during cached image inspection [#3216](https://github.com/aws/amazon-ecs-agent/pull/3216) +* Bug - Fix an issue where a task can be stuck in PENDING for ever when container dependencies can never be fulfilled [#3218](https://github.com/aws/amazon-ecs-agent/pull/3218) + +## 1.61.1 +* Enhancement - Remove hard-coded task CPU limit and advertise a new capability ecs.capability.increased-task-cpu-limit [#3197](https://github.com/aws/amazon-ecs-agent/pull/3197) +* Enhancement - Simplify api/task code [#3176](https://github.com/aws/amazon-ecs-agent/pull/3176) +* Enhancement - Remove unused .travis.yml file [#3171](https://github.com/aws/amazon-ecs-agent/pull/3171) +* Bug - Fix potential goroutine leaks [#3170](https://github.com/aws/amazon-ecs-agent/pull/3170) +* Bug - Fix credential rotation issue with ECS-A Windows [#3184](https://github.com/aws/amazon-ecs-agent/pull/3184) +* Bug - Fix Windows base image versions for integration tests [#3179](https://github.com/aws/amazon-ecs-agent/pull/3179) + +## 1.61.0 +* Enhancement - Support for unified cgroups and the systemd cgroup driver [#3127](https://github.com/aws/amazon-ecs-agent/pull/3127) +* Enhancement - Apply minimumCPUShare to both task and container CPU shares [#3156](https://github.com/aws/amazon-ecs-agent/pull/3156) + +## 1.60.1 +* Enhancement - Add dockerfree init build targets [#3149](https://github.com/aws/amazon-ecs-agent/pull/3149) +* Enhancement - Merge ecs-init repo [#3141](https://github.com/aws/amazon-ecs-agent/pull/3141) ## 1.60.0 * Enhancement - Update cgroups library to the latest release [#3126](https://github.com/aws/amazon-ecs-agent/pull/3126) diff --git a/GO_VERSION b/GO_VERSION index ff278344b33..66e2ae6c25c 100644 --- a/GO_VERSION +++ b/GO_VERSION @@ -1 +1 @@ -1.17.5 +1.19.1 diff --git a/GO_VERSION_WINDOWS b/GO_VERSION_WINDOWS index ff278344b33..66e2ae6c25c 100644 --- a/GO_VERSION_WINDOWS +++ b/GO_VERSION_WINDOWS @@ -1 +1 @@ -1.17.5 +1.19.1 diff --git a/INIT_README.md b/INIT_README.md deleted file mode 100644 index 02187364592..00000000000 --- a/INIT_README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Amazon Elastic Container Service RPM - -[![Build Status](https://travis-ci.org/aws/amazon-ecs-init.svg?branch=master)](https://travis-ci.org/aws/amazon-ecs-init) - -The Amazon Elastic Container Service RPM is software developed to support the [Amazon ECS Container -Agent](http://github.com/aws/amazon-ecs-agent). The Amazon ECS RPM is packaged for RPM-based systems that utilize -[Upstart](http://upstart.ubuntu.com) as the init system. - -## Behavior -The upstart script installed by the Amazon ECS RPM runs at the completion of runlevel 3, 4, or 5 as the system starts. -The script will clean up any previous copies of the Amazon ECS Container Agent, and then start a new copy. Logs from -the RPM are available at `/var/log/ecs/ecs-init.log`, while logs from the Amazon ECS Container Agent are available at -`/var/log/ecs/ecs-agent.log`. The Amazon ECS RPM makes the Amazon ECS Container Agent introspection endpoint available -at `http://127.0.0.1:51678/v1`. Configuration for the Amazon ECS Container Agent is read from `/etc/ecs/ecs.config`. -All of the configurations in this file are used as environment variables of the ECS Agent container. Additionally, some -configurations can be used to configure other properties of the ECS Agent container, as described below. - -| Configuration Key | Example Value(s) | Description | Default value | -|:----------------|:----------------------------|:------------|:-----------------------| -| `ECS_AGENT_LABELS` | `{"test.label.1":"value1","test.label.2":"value2"}` | The labels to add to the ECS Agent container. | | - -Additionally, the following environment variable(s) can be used to configure the behavior of the RPM: - -| Environment Variable Name | Example Value(s) | Description | Default value | -|:----------------|:----------------------------|:------------|:-----------------------| -| `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` | <true | false> | By default, the ecs-init service adds an iptable rule to drop non-local packets to localhost if they're not part of an existing forwarded connection or DNAT, and removes the rule upon stop. If `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` is set to true, this rule will not be added/removed. | false | -| `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | <true | false> | By default, the ecs-init service adds an iptable rule to block access to ECS Agent's introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` is set to true, this rule will not be added/removed. | false | -| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is `eth0` | `eth0` | - -The above environment variable(s) can be used in the following way -- On Amazon Linux 1, the flag `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` can be turned on by adding `env ECS_SKIP_LOCALHOST_TRAFFIC_FILTER=true` to /etc/init/ecs.conf. -- On Amazon Linux 2, the flag `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` can be turned on by adding `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER=true` to /etc/ecs/ecs.config. - -## Usage -The upstart script installed by the Amazon Elastic Container Service RPM can be started or stopped with the following commands respectively: - -* `sudo start ecs` -* `sudo stop ecs` - -### Updates -Updates to the Amazon ECS Container Agent should be performed through the Amazon ECS Container Agent. In the case where -an update failed and the Amazon ECS Container Agent is no longer functional, a rollback can be initiated as follows: - -1. `sudo stop ecs` -2. `sudo /usr/libexec/amazon-ecs-init reload-cache` -3. `sudo start ecs` - -## Security disclosures -If you think you’ve found a potential security issue, please do not post it in the Issues. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS security directly](mailto:aws-security@amazon.com). - -## Development - -#### Building the RPM for test - -On your local machine, you can use the docker target to generate an rpm: - -``` -make rpm-in-docker -``` - -This rpm can then be installed in an amazon linux ami: - -``` -# send rpm either through s3 or scp -rpm -i rpm-that-you-built.rpm -sudo systemctl enable ecs -sudo systemctl start ecs -``` - -#### Dev dependencies - -Run `make get-deps` to get dependencies for running tests and generating mocks. - -#### Generating mocks - -Mocks can be generated using the `make generate` Makefile target. **NOTE** that this must be run on a linux machine. - -## License - -The Amazon Elastic Container Service RPM is licensed under the Apache 2.0 License. diff --git a/Makefile b/Makefile index 75c3bca8f7f..68ec22822e9 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ USERID=$(shell id -u) # default value of TARGET_OS TARGET_OS=linux -.PHONY: all gobuild static xplatform-build docker release certs test clean netkitten test-registry benchmark-test gogenerate run-integ-tests pause-container get-cni-sources cni-plugins test-artifacts +.PHONY: all gobuild static xplatform-build docker release certs test clean netkitten test-registry benchmark-test gogenerate run-integ-tests pause-container get-cni-sources cni-plugins test-artifacts release-agent release-agent-internal BUILD_PLATFORM:=$(shell uname -m) ifeq (${BUILD_PLATFORM},aarch64) @@ -39,6 +39,8 @@ all: docker gobuild: ./scripts/build false +gobuild-init-deb: + ./scripts/gobuild.sh debian # create output directories .out-stamp: @@ -47,13 +49,16 @@ gobuild: # Basic go build static: - ./scripts/build + ./scripts/build true "" true true # Cross-platform build target for static checks xplatform-build: GOOS=linux GOARCH=arm64 ./scripts/build true "" false GOOS=windows GOARCH=amd64 ./scripts/build true "" false - GOOS=darwin GOARCH=amd64 ./scripts/build true "" false + # Agent and its dependencies on Go 1.18.x are not compatible with Mac (Darwin). + # Mac is not a supported target platform for Agent, so commenting out + # cross-platform build step for Mac temporarily. + # GOOS=darwin GOARCH=amd64 ./scripts/build true "" false BUILDER_IMAGE="amazon/amazon-ecs-agent-build:make" .builder-image-stamp: scripts/dockerfiles/Dockerfile.build @@ -103,7 +108,7 @@ docker-release: pause-container-release cni-plugins .out-stamp --rm \ "amazon/amazon-ecs-agent-${BUILD}:make" -# Release packages our agent into a "scratch" based dockerfile +# Legacy target : Release packages our agent into a "scratch" based dockerfile release: certs docker-release @./scripts/create-amazon-ecs-scratch @docker build -f scripts/dockerfiles/Dockerfile.release -t "amazon/amazon-ecs-agent:latest" . @@ -119,6 +124,10 @@ gogenerate: go generate -x ./agent/... $(MAKE) goimports +gogenerate-init: + PATH=$(PATH):$(shell pwd)/scripts go generate -x ./ecs-init/... + $(MAKE) goimports + # 'go' may not be on the $PATH for sudo tests GO_EXECUTABLE=$(shell command -v go 2> /dev/null) @@ -140,6 +149,10 @@ test: ${GOTEST} -tags unit -coverprofile cover.out -timeout=60s ./agent/... go tool cover -func cover.out > coverprofile.out +test-init: + go test -count=1 -short -v -coverprofile cover.out ./ecs-init/... + go tool cover -func cover.out > coverprofile-init.out + test-silent: $(eval VERBOSE=) ${GOTEST} -tags unit -coverprofile cover.out -timeout=60s ./agent/... @@ -149,16 +162,22 @@ test-silent: analyze-cover-profile: coverprofile.out ./scripts/analyze-cover-profile +.PHONY: analyze-cover-profile-init +analyze-cover-profile-init: coverprofile-init.out + ./scripts/analyze-cover-profile-init + run-integ-tests: test-registry gremlin container-health-check-image run-sudo-tests ECS_LOGLEVEL=debug ${GOTEST} -tags integration -timeout=30m ./agent/... run-sudo-tests: sudo -E ${GOTEST} -tags sudo -timeout=10m ./agent/... +run-sudo-unit-tests: + sudo -E ${GOTEST} -tags 'sudo_unit' -timeout=60s ./agent/... + benchmark-test: go test -run=XX -bench=. ./agent/... - .PHONY: build-image-for-ecr upload-images replicate-images build-image-for-ecr: netkitten volumes-test image-cleanup-test-images fluentd exec-command-agent-test @@ -179,9 +198,6 @@ pause-container: .out-stamp pause-container-release: pause-container @docker save ${PAUSE_CONTAINER_IMAGE}:${PAUSE_CONTAINER_TAG} > "$(PWD)/out/${PAUSE_CONTAINER_TARBALL}" -# Variable to determine branch/tag of amazon-ecs-cni-plugins -ECS_CNI_REPOSITORY_REVISION=master - # Variable to override cni repository location ECS_CNI_REPOSITORY_SRC_DIR=$(PWD)/amazon-ecs-cni-plugins VPC_CNI_REPOSITORY_SRC_DIR=$(PWD)/amazon-vpc-cni-plugins @@ -212,6 +228,7 @@ build-vpc-cni-plugins: docker run --rm --net=none \ -e GO111MODULE=off \ -e GIT_SHORT_HASH=$(shell cd $(VPC_CNI_REPOSITORY_SRC_DIR) && git rev-parse --short=8 HEAD) \ + -e GIT_TAG=$(shell cd $(VPC_CNI_REPOSITORY_SRC_DIR) && git describe --tags --always --dirty) \ -u "$(USERID)" \ -v "$(PWD)/out/amazon-vpc-cni-plugins:/go/src/github.com/aws/amazon-vpc-cni-plugins/build/${TARGET_OS}_$(GOARCH)" \ -v "$(VPC_CNI_REPOSITORY_SRC_DIR):/go/src/github.com/aws/amazon-vpc-cni-plugins" \ @@ -223,7 +240,28 @@ cni-plugins: get-cni-sources .out-stamp build-ecs-cni-plugins build-vpc-cni-plug mv $(PWD)/out/amazon-vpc-cni-plugins/* $(PWD)/out/cni-plugins @echo "Built all cni plugins successfully." +# dockerfree build process will build the agent container image from scratch +# requires glibc-static -- we precompile amd/arm to the misc/pause-container/pause-image-tar-files/ directory +dockerfree-pause: + ./scripts/build-pause + +dockerfree-certs: + ./scripts/get-host-certs + +dockerfree-cni-plugins: + ./scripts/build-cni-plugins + +# see dockerfree-pause above: assumes that the pre-compiled pause container tar exists +# builds agent image and saves on disk, assumes cni plugins have been pulled +release-agent-internal: dockerfree-certs dockerfree-cni-plugins static + ./scripts/build-agent-image + +# Default Agent target to build. Pulls cni plugins, builds agent image and save it to disk +release-agent: get-cni-sources + $(MAKE) release-agent-internal + +# Legacy target used for building agent artifacts for functional tests .PHONY: codebuild codebuild: .out-stamp $(MAKE) release TARGET_OS="linux" @@ -259,7 +297,6 @@ image-cleanup-test-images: container-health-check-image: $(MAKE) -C misc/container-health $(MFLAGS) - # all .go files in the agent, excluding vendor/, model/ and testutils/ directories, and all *_test.go and *_mocks.go files GOFILES:=$(shell go list -f '{{$$p := .}}{{range $$f := .GoFiles}}{{$$p.Dir}}/{{$$f}} {{end}}' ./agent/... \ | grep -v /testutils/ | grep -v _test\.go$ | grep -v _mocks\.go$ | grep -v /model) @@ -286,47 +323,157 @@ gogenerate-check: gogenerate # check that gogenerate does not generate a diff. git diff --exit-code +.PHONY: gogenerate-check-init +gogenerate-check-init: gogenerate-init + # check that gogenerate does not generate a diff. + git diff --exit-code + .PHONY: static-check static-check: gocyclo govet importcheck gogenerate-check # use default checks of staticcheck tool, except style checks (-ST*) and depracation checks (-SA1019) # depracation checks have been left out for now; removing their warnings requires error handling for newer suggested APIs, changes in function signatures and their usages. # https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck - staticcheck -tests=false -checks "inherit,-ST*,-SA1019,-SA9002" ./agent/... + staticcheck -tests=false -checks "inherit,-ST*,-SA1019,-SA9002,-SA4006" ./agent/... + +.PHONY: static-check-init +static-check-init: gocyclo govet importcheck gogenerate-check-init + # use default checks of staticcheck tool, except style checks (-ST*) + # https://github.com/dominikh/go-tools/tree/master/cmd/staticcheck + staticcheck -tests=false -checks "inherit,-ST*" ./ecs-init/... .PHONY: goimports goimports: goimports -w $(GOFMTFILES) GOPATH=$(shell go env GOPATH) + +install-golang: + ./scripts/install-golang.sh + .get-deps-stamp: - go get golang.org/x/tools/cmd/cover go get github.com/golang/mock/mockgen cd "${GOPATH}/src/github.com/golang/mock/mockgen" && git checkout 1.3.1 && go get ./... && go install ./... && cd - go get golang.org/x/tools/cmd/goimports - GO111MODULE=on go get github.com/fzipp/gocyclo/cmd/gocyclo@v0.3.1 - GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck@v0.2.1 + GO111MODULE=on go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.3.1 + GO111MODULE=on go install honnef.co/go/tools/cmd/staticcheck@v0.3.2 touch .get-deps-stamp get-deps: .get-deps-stamp +get-deps-init: + go get golang.org/x/tools/cover + go get github.com/golang/mock/mockgen + cd "${GOPATH}/src/github.com/golang/mock/mockgen" && git checkout 1.3.1 && go get ./... && go install ./... && cd - + GO111MODULE=on go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.3.1 + go get golang.org/x/tools/cmd/goimports + GO111MODULE=on go install honnef.co/go/tools/cmd/staticcheck@v0.3.2 + +amazon-linux-sources.tgz: + ./scripts/update-version.sh + cp packaging/amazon-linux-ami-integrated/ecs-agent.spec ecs-agent.spec + cp packaging/amazon-linux-ami-integrated/ecs.conf ecs.conf + cp packaging/amazon-linux-ami-integrated/ecs.service ecs.service + cp packaging/amazon-linux-ami-integrated/amazon-ecs-volume-plugin.conf amazon-ecs-volume-plugin.conf + cp packaging/amazon-linux-ami-integrated/amazon-ecs-volume-plugin.service amazon-ecs-volume-plugin.service + cp packaging/amazon-linux-ami-integrated/amazon-ecs-volume-plugin.socket amazon-ecs-volume-plugin.socket + tar -czf ./sources.tgz ecs-init scripts misc agent amazon-ecs-cni-plugins amazon-vpc-cni-plugins agent-container Makefile VERSION RELEASE_COMMIT + +.amazon-linux-rpm-integrated-done: amazon-linux-sources.tgz + test -e SOURCES || ln -s . SOURCES + rpmbuild --define "%_topdir $(PWD)" -bb ecs-agent.spec + find RPMS/ -type f -exec cp {} . \; + touch .amazon-linux-rpm-integrated-done + +amazon-linux-rpm-integrated: .amazon-linux-rpm-integrated-done + +.generic-rpm-integrated-done: get-cni-sources + ./scripts/update-version.sh + cp packaging/generic-rpm-integrated/amazon-ecs-init.spec amazon-ecs-init.spec + cp packaging/generic-rpm-integrated/ecs.service ecs.service + cp packaging/generic-rpm-integrated/amazon-ecs-volume-plugin.service amazon-ecs-volume-plugin.service + cp packaging/generic-rpm-integrated/amazon-ecs-volume-plugin.socket amazon-ecs-volume-plugin.socket + tar -czf ./sources.tgz ecs-init scripts misc agent amazon-ecs-cni-plugins amazon-vpc-cni-plugins agent-container Makefile VERSION GO_VERSION + test -e SOURCES || ln -s . SOURCES + rpmbuild --define "%_topdir $(PWD)" -bb amazon-ecs-init.spec + find RPMS/ -type f -exec cp {} . \; + touch .generic-rpm-integrated-done + +# Build init rpm +generic-rpm-integrated: .generic-rpm-integrated-done + +VERSION = $(shell cat ecs-init/ECSVERSION) + +.generic-deb-integrated-done: get-cni-sources + ./scripts/update-version.sh + mkdir -p BUILDROOT + tar -czf ./amazon-ecs-init_${VERSION}.orig.tar.gz ecs-init scripts README.md + cp -r packaging/generic-deb-integrated/debian Makefile ecs-init scripts misc agent agent-container amazon-ecs-cni-plugins amazon-vpc-cni-plugins README.md VERSION GO_VERSION BUILDROOT + cd BUILDROOT && dpkg-buildpackage -uc -b + touch .generic-deb-integrated-done + +# Build init deb +generic-deb-integrated: .generic-deb-integrated-done + +ARCH:=$(shell uname -m) +ifeq (${ARCH},x86_64) + AGENT_FILENAME=ecs-agent-v${VERSION}.tar +else ifeq (${ARCH},aarch64) + AGENT_FILENAME=ecs-agent-arm64-v${VERSION}.tar +# osx M1 instances +else ifeq (${ARCH},arm64) + AGENT_FILENAME=ecs-agent-arm64-v${VERSION}.tar +endif + clean: + -rm -f misc/certs/host-certs.crt &> /dev/null + -rm -rf misc/pause-container/image/ + -rm -rf misc/pause-container/rootfs/ + -rm -rf misc/plugins/ + -rm -rf out/ + -rm -rf rootfs/ + -$(MAKE) -C $(ECS_CNI_REPOSITORY_SRC_DIR) clean + -rm -f .get-deps-stamp + -rm -f .builder-image-stamp + -rm -f .out-stamp + -rm -f ecs-agent.spec + -rm -rf $(PWD)/bin + -rm -rf cover.out + -rm -rf coverprofile.out + -rm -rf coverprofile-init.out + # ecs-init & rpm cleanup + -rm -f ecs-init.spec + -rm -f amazon-ecs-init.spec + -rm -f ecs.conf + -rm -f ecs.service + -rm -f amazon-ecs-volume-plugin.conf + -rm -f amazon-ecs-volume-plugin.service + -rm -f amazon-ecs-volume-plugin.socket + -rm -rf ./bin + -rm -f ./sources.tgz + -rm -f ./amazon-ecs-init + -rm -f ./ecs-init/ecs-init + -rm -f ./amazon-ecs-init-*.rpm + -rm -f ./ecs-agent-*.tar + -rm -f ./ecs-init-*.src.rpm + -rm -rf ./ecs-init-* + -rm -rf ./BUILDROOT BUILD RPMS SRPMS SOURCES SPECS + -rm -rf ./x86_64 + -rm -f ./amazon-ecs-init_${VERSION}* + -rm -f .srpm-done .rpm-done .generic-rpm-done .generic-deb-integrated-done + -rm -f .deb-done + -rm -f .amazon-linux-rpm-integrated-done + -rm -f .generic-rpm-integrated-done + -rm -f amazon-ecs-volume-plugin + +clean-all: clean + # for our dockerfree builds, we likely don't have docker # ensure docker is running and we can talk to it, abort if not: docker ps > /dev/null -docker rmi $(BUILDER_IMAGE) "amazon/amazon-ecs-agent-cleanbuild:make" -docker rmi $(BUILDER_IMAGE) "amazon/amazon-ecs-agent-cleanbuild-windows:make" - rm -f misc/certs/ca-certificates.crt &> /dev/null - rm -rf out/ - -$(MAKE) -C $(ECS_CNI_REPOSITORY_SRC_DIR) clean -$(MAKE) -C misc/netkitten $(MFLAGS) clean -$(MAKE) -C misc/volumes-test $(MFLAGS) clean -$(MAKE) -C misc/exec-command-agent-test $(MFLAGS) clean -$(MAKE) -C misc/gremlin $(MFLAGS) clean -$(MAKE) -C misc/image-cleanup-test-images $(MFLAGS) clean -$(MAKE) -C misc/container-health $(MFLAGS) clean - -rm -f .get-deps-stamp - -rm -f .builder-image-stamp - -rm -f .out-stamp - -rm -rf $(PWD)/bin - -rm -rf cover.out - -rm -rf coverprofile.out - diff --git a/README.md b/README.md index b7d68e4ec95..83ad6d130fd 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,16 @@ ![Amazon ECS logo](doc/ecs.png "Amazon ECS") -![Build Status](https://github.com/aws/amazon-ecs-agent/workflows/Build/badge.svg?branch=dev) - The Amazon ECS Container Agent is a component of Amazon Elastic Container Service ([Amazon ECS](http://aws.amazon.com/ecs/)) and is responsible for managing containers on behalf of Amazon ECS. +This repository comes with ECS-Init, which is a [systemd](http://www.freedesktop.org/wiki/Software/systemd/) based service to support the Amazon ECS Container Agent and keep it running. It is used for systems that utilize `systemd` as init systems and is packaged as deb or rpm. The source for ECS-Init is available in this repository at `./ecs-init` while the packaging is available at `./packaging`. + ## Usage The best source of information on running this software is the [Amazon ECS documentation](http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_agent.html). -Please note that from Agent version 1.20.0, Minimum required Docker version is 1.9.0, corresponding to Docker API version 1.21. For more information, please visit [Amazon ECS Container Agent Versions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container_agent_versions.html). - ### On the Amazon Linux AMI On the [Amazon Linux AMI](https://aws.amazon.com/amazon-linux-ami/), we provide an installable RPM which can be used via @@ -21,9 +19,11 @@ On the [Amazon Linux AMI](https://aws.amazon.com/amazon-linux-ami/), we provide ### On Other Linux AMIs +[Amazon ECS docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-install.html) provides deb and rpm packages and instructions to install ECS Container Agent on non-Amazon Linux instances. + The Amazon ECS Container Agent may also be run in a Docker container on an EC2 instance with a recent Docker version installed. A Docker image is available in our -[Docker Hub Repository](https://registry.hub.docker.com/u/amazon/amazon-ecs-agent/). +[Docker Hub Repository](https://hub.docker.com/r/amazon/amazon-ecs-agent). ```bash $ # Set up directories the agent uses @@ -109,6 +109,56 @@ PS C:\> $agentVersion = "v1.20.4" PS C:\> Initialize-ECSAgent -Cluster 'windows' -EnableTaskIAMRole -Version $agentVersion ``` +## Build ECS Agent from source + +### Build ECS Agent Image (Linux) + +ECS Agent can also be built locally from source on a linux machine. Use the following steps to build ECS Agent +* Get ECS Agent source +``` +git clone https://github.com/aws/amazon-ecs-agent.git +``` +* Build Agent image using ```release-agent``` make target +``` +make release-agent +``` +This installs the required build dependencies, builds ECS Agent image and saves it at a path ```ecs-agent-v${AGENT_VERSION}.tar```. Load this using +``` +docker load < ecs-agent-v${AGENT_VERSION}.tar +``` +Follow the instructions [above](https://github.com/aws/amazon-ecs-agent#on-other-linux-amis) to continue with the installation + +### Build and run standalone (Linux) + +The Amazon ECS Container Agent may also be run outside of a Docker container as a Go binary. At this time, this is not recommended +for production on Linux, but it can be useful for development or easier integration with your local Go tools. + +The following commands run the agent outside of Docker: + +``` +make gobuild +./out/amazon-ecs-agent +``` + +### Standalone (Windows) + +The Amazon ECS Container Agent may be built by invoking `scripts\build_agent.ps1` + +### Scripts (Windows) + +The following scripts are available to help develop the Amazon ECS Container Agent on Windows: + +* `scripts\run-integ-tests.ps1` - Runs all integration tests in the `engine` and `stats` packages +* `misc\windows-deploy\Install-ECSAgent.ps1` - Install the ECS agent as a Windows service +* `misc\windows-deploy\amazon-ecs-agent.ps1` - Helper script to set up the host and run the agent as a process +* `misc\windows-deploy\user-data.ps1` - Sample user-data that can be used with the Windows Server 2016 with Containers + AMI to run the agent as a process + + +### Build ECS-Init Package (Linux) + +You can also build the ECS-Init packaged as a deb or rpm depending on the linux system you running. Follow instructions at [generic-deb-integrated](https://github.com/aws/amazon-ecs-agent/tree/master/packaging/generic-deb-integrated/debian) and [generic-rpm-integrated](https://github.com/aws/amazon-ecs-agent/tree/master/packaging/generic-rpm-integrated) to build and install ECS Agent packaged with Init deb or rpm packages + ## Advanced Usage The Amazon ECS Container Agent supports a number of configuration options, most of which should be set through @@ -142,12 +192,12 @@ additional details on each available environment variable. | `ECS_POLL_METRICS` | <true | false> | Whether to poll or stream when gathering metrics for tasks. Setting this value to `true` can help reduce the CPU usage of dockerd and containerd on the ECS container instance. See also ECS_POLL_METRICS_WAIT_DURATION for setting the poll interval. | `false` | `false` | | `ECS_POLLING_METRICS_WAIT_DURATION` | 10s | Time to wait between polling for metrics for a task. Not used when ECS_POLL_METRICS is false. Maximum value is 20s and minimum value is 5s. If user sets above maximum it will be set to max, and if below minimum it will be set to min. | 10s | 10s | | `ECS_PULL_DEPENDENT_CONTAINERS_UPFRONT` | <true | false> | Whether to pull images for containers with dependencies before the dependsOn condition has been satisfied. | false | false | -| `ECS_RESERVED_MEMORY` | 32 | Memory, in MiB, to reserve for use by things other than containers managed by Amazon ECS. | 0 | 0 | +| `ECS_RESERVED_MEMORY` | 32 | Reduction, in MiB, of the memory capacity of the instance that is reported to Amazon ECS. Used by Amazon ECS when placing tasks on container instances. This doesn't reserve memory usage on the instance. | 0 | 0 | | `ECS_AVAILABLE_LOGGING_DRIVERS` | `["awslogs","fluentd","gelf","json-file","journald","logentries","splunk","syslog"]` | Which logging drivers are available on the container instance. | `["json-file","none"]` | `["json-file","none"]` | | `ECS_DISABLE_PRIVILEGED` | `true` | Whether launching privileged containers is disabled on the container instance. | `false` | `false` | -| `ECS_SELINUX_CAPABLE` | `true` | Whether SELinux is available on the container instance. | `false` | `false` | +| `ECS_SELINUX_CAPABLE` | `true` | Whether SELinux is available on the container instance. (Limited support; Z-mode mounts only.) | `false` | `false` | | `ECS_APPARMOR_CAPABLE` | `true` | Whether AppArmor is available on the container instance. | `false` | `false` | -| `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION` | 10m | Default time to wait to delete containers for a stopped task (see also `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION_JITTER`). If set to less than 1 minute, the value is ignored. | 3h | 3h | +| `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION` | 10m | Default time to wait to delete containers for a stopped task (see also `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION_JITTER`). If set to less than 1 second, the value is ignored. | 3h | 3h | | `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION_JITTER` | 1h | Jitter value for the task engine cleanup wait duration. When specified, the actual cleanup wait duration time for each task will be the duration specified in `ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION` plus a random duration between 0 and the jitter duration. | blank | blank | | `ECS_CONTAINER_STOP_TIMEOUT` | 10m | Instance scoped configuration for time to wait for the container to exit normally before being forcibly killed. | 30s | 30s | | `ECS_CONTAINER_START_TIMEOUT` | 10m | Timeout before giving up on starting a container. | 3m | 8m | @@ -176,14 +226,15 @@ additional details on each available environment variable. | `ECS_AGENT_HEALTHCHECK_HOST` | `localhost` | Override for the ecs-agent container's healthcheck localhost ip address| `localhost` | `localhost` | | `ECS_ENABLE_CPU_UNBOUNDED_WINDOWS_WORKAROUND` | `true` | When `true`, ECS will allow CPU unbounded(CPU=`0`) tasks to run along with CPU bounded tasks in Windows. | Not applicable | `false` | | `ECS_ENABLE_MEMORY_UNBOUNDED_WINDOWS_WORKAROUND` | `true` | When `true`, ECS will ignore the memory reservation parameter (soft limit) to run along with memory bounded tasks in Windows. To run a memory unbounded task, omit the memory hard limit and set any memory reservation, it will be ignored. | Not applicable | `false` | -| `ECS_TASK_METADATA_RPS_LIMIT` | `100,150` | Comma separated integer values for steady state and burst throttle limits for task metadata endpoint | `40,60` | `40,60` | +| `ECS_TASK_METADATA_RPS_LIMIT` | `100,150` | Comma separated integer values for steady state and burst throttle limits for combined total traffic to task metadata endpoint and agent api endpoint. | `40,60` | `40,60` | | `ECS_SHARED_VOLUME_MATCH_FULL_CONFIG` | `true` | When `true`, ECS Agent will compare name, driver options, and labels to make sure volumes are identical. When `false`, Agent will short circuit shared volume comparison if the names match. This is the default Docker behavior. If a volume is shared across instances, this should be set to `false`. | `false` | `false`| | `ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM` | `ec2_instance` | If `ec2_instance` is specified, existing tags defined on the container instance will be registered to Amazon ECS and will be discoverable using the `ListTagsForResource` API. Using this requires that the IAM role associated with the container instance have the `ec2:DescribeTags` action allowed. | `none` | `none` | | `ECS_CONTAINER_INSTANCE_TAGS` | `{"tag_key": "tag_val"}` | The metadata that you apply to the container instance to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters. If tags also exist on your container instance that are propagated using the `ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM` parameter, those tags will be overwritten by the tags specified using `ECS_CONTAINER_INSTANCE_TAGS`. | `{}` | `{}` | | `ECS_ENABLE_UNTRACKED_IMAGE_CLEANUP` | `true` | Whether to allow the ECS agent to delete containers and images that are not part of ECS tasks. | `false` | `false` | -| `ECS_EXCLUDE_UNTRACKED_IMAGE` | `alpine:latest` | Comma seperated list of `imageName:tag` of images that should not be deleted by the ECS agent if `ECS_ENABLE_UNTRACKED_IMAGE_CLEANUP` is enabled. | | | +| `ECS_EXCLUDE_UNTRACKED_IMAGE` | `alpine:latest` | Comma separated list of `imageName:tag` of images that should not be deleted by the ECS agent if `ECS_ENABLE_UNTRACKED_IMAGE_CLEANUP` is enabled. | | | | `ECS_DISABLE_DOCKER_HEALTH_CHECK` | `false` | Whether to disable the Docker Container health check for the ECS Agent. | `false` | `false` | | `ECS_NVIDIA_RUNTIME` | nvidia | The Nvidia Runtime to be used to pass Nvidia GPU devices to containers. | nvidia | Not Applicable | +| `ECS_ALTERNATE_CREDENTIAL_PROFILE` | default | An alternate credential role/profile name. | default | default | | `ECS_ENABLE_SPOT_INSTANCE_DRAINING` | `true` | Whether to enable Spot Instance draining for the container instance. If true, if the container instance receives a [spot interruption notice](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html), agent will set the instance's status to [DRAINING](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-instance-draining.html), which gracefully shuts down and replaces all tasks running on the instance that are part of a service. It is recommended that this be set to `true` when using spot instances. | `false` | `false` | | `ECS_LOG_ROLLOVER_TYPE` | `size` | `hourly` | Determines whether the container agent logfile will be rotated based on size or hourly. By default, the agent logfile is rotated each hour. | `hourly` | `hourly` | | `ECS_LOG_OUTPUT_FORMAT` | `logfmt` | `json` | Determines the log output format. When the json format is used, each line in the log would be a structured JSON map. | `logfmt` | `logfmt` | @@ -196,6 +247,25 @@ additional details on each available environment variable. | `ECS_ENABLE_RUNTIME_STATS` | `true` | Determines if [pprof](https://pkg.go.dev/net/http/pprof) is enabled for the agent. If enabled, the different profiles can be accessed through the agent's introspection port (e.g. `curl http://localhost:51678/debug/pprof/heap > heap.pprof`). In addition, agent's [runtime stats](https://pkg.go.dev/runtime#ReadMemStats) are logged to `/var/log/ecs/runtime-stats.log` file. | `false` | `false` | | `ECS_EXCLUDE_IPV6_PORTBINDING` | `true` | Determines if agent should exclude IPv6 port binding using default network mode. If enabled, IPv6 port binding will be filtered out, and the response of DescribeTasks API call will not show tasks' IPv6 port bindings, but it is still included in Task metadata endpoint. | `true` | `true` | | `ECS_WARM_POOLS_CHECK` | `true` | Whether to ensure instances going into an [EC2 Auto Scaling group warm pool](https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-warm-pools.html) are prevented from being registered with the cluster. Set to true only if using EC2 Autoscaling | `false` | `false` | +| `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` | `false` | By default, the ecs-init service adds an iptable rule to drop non-local packets to localhost if they're not part of an existing forwarded connection or DNAT, and removes the rule upon stop. If this is set to true, the rule will not be added or removed. | `false` | `false` | +| `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | `true` | By default, the ecs-init service adds an iptable rule to block access to the agent introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If this is set to true, the rule will not be added or removed | `false` | `false` | +| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | The primary network interface name to be used for blocking offhost agent introspection port access | `eth0` | `eth0` | +| `ECS_ENABLE_GPU_SUPPORT` | `true` | Whether you use container instances with GPU support. This parameter is specified for the agent. You must also configure your task definitions for GPU. For more information | `false` | `Not applicable` | +| `HTTP_PROXY` | `10.0.0.131:3128` | The hostname (or IP address) and port number of an HTTP proxy to use for the Amazon ECS agent to connect to the internet. For example, this proxy will be used if your container instances do not have external network access through an Amazon VPC internet gateway or NAT gateway or instance. If this variable is set, you must also set the NO_PROXY variable to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` | +| `NO_PROXY` | | The HTTP traffic that should not be forwarded to the specified HTTP_PROXY. You must specify 169.254.169.254,/var/run/docker.sock to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` | +| `CREDENTIALS_FETCHER_HOST` | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Used to create a connection to the [credentials-fetcher daemon](https://github.com/aws/credentials-fetcher); to support gMSA on Linux. The default is fine for most users, only needs to be modified if user is configuring a custom credentials-fetcher socket path, ie, [CF_UNIX_DOMAIN_SOCKET_DIR](https://github.com/aws/credentials-fetcher#default-environment-variables). | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Not Applicable | +| `CREDENTIALS_FETCHER_SECRET_NAME_FOR_DOMAINLESS_GMSA` | `secretmanager-secretname` | Used to support scaling option for gMSA on Linux [credentials-fetcher daemon](https://github.com/aws/credentials-fetcher). If user is configuring gMSA on a non-domain joined instance, they need to create an Active Directory user with access to retrieve principals for the gMSA account and store it in secrets manager | `secretmanager-secretname` | Not Applicable | +| `ECS_DYNAMIC_HOST_PORT_RANGE` | `100-200` | This specifies the dynamic host port range that the agent uses to assign host ports from, for a container port range mapping. | Defined by `/proc/sys/net/ipv4/ip_local_port_range` | `49152-65535` | + +Additionally, the following environment variable(s) can be used to configure the behavior of the ecs-init service. When using ECS-Init, all env variables, including the ECS Agent variables above, are read from path `/etc/ecs/ecs.config`: +| Environment Variable Name | Example Value(s) | Description | Default value | +|:----------------|:----------------------------|:------------|:-----------------------| +| `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` | <true | false> | By default, the ecs-init service adds an iptable rule to drop non-local packets to localhost if they're not part of an existing forwarded connection or DNAT, and removes the rule upon stop. If `ECS_SKIP_LOCALHOST_TRAFFIC_FILTER` is set to true, this rule will not be added/removed. | false | +| `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | <true | false> | By default, the ecs-init service adds an iptable rule to block access to ECS Agent's introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` is set to true, this rule will not be added/removed. | false | +| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is `eth0` | `eth0` | +| `ECS_AGENT_LABELS` | `{"test.label.1":"value1","test.label.2":"value2"}` | The labels to add to the ECS Agent container. | | + + ### Persistence @@ -212,29 +282,6 @@ The agent also supports the following flags: * ` -loglevel` — Options: `[||||]`. The agent will output on stdout at the given level. This is overridden by the `ECS_LOGLEVEL` environment variable, if present. -## Building and Running from Source - -**Running the Amazon ECS Container Agent outside of Amazon EC2 is not supported.** - -### Docker Image (on Linux) - -The Amazon ECS Container Agent may be built by typing `make` with the [Docker -daemon](https://docs.docker.com/installation/) (v1.5.0) running. - -This produces an image tagged `amazon/ecs-container-agent:make` that -you may run as described above. - -### Standalone (on Linux) - -The Amazon ECS Container Agent may also be run outside of a Docker container as a Go binary. This is not recommended -for production on Linux, but it can be useful for development or easier integration with your local Go tools. - -The following commands run the agent outside of Docker: - -``` -make gobuild -./out/amazon-ecs-agent -``` ### Make Targets (on Linux) @@ -242,7 +289,10 @@ The following targets are available. Each may be run with `make `. | Make Target | Description | |:-----------------------|:------------| -| `release` | *(Default)* Builds the agent within a Docker container and and packages it into a scratch-based image | +| `release-agent` | *(Default Agent build)* Builds Agent fetching required dependencies and saves image .tar to disk| +| `generic-rpm-integrated`| Builds init rpm package and saves .rpm package to disk | +| `generic-deb-integrated`| Builds init deb package and saves .deb package to disk | +| `release` | *(Legacy Agent build)* Builds the agent within a Docker container and packages it into a scratch-based image | | `gobuild` | Runs a normal `go build` of the agent and stores the binary in `./out/amazon-ecs-agent` | | `static` | Runs `go build` to produce a static binary in `./out/amazon-ecs-agent` | | `test` | Runs all unit tests using `go test` | @@ -250,20 +300,6 @@ The following targets are available. Each may be run with `make `. | `run-integ-tests` | Runs all integration tests in the `engine` and `stats` packages | | `clean` | Removes build artifacts. *Note: this does not remove Docker images* | -### Standalone (on Windows) - -The Amazon ECS Container Agent may be built by invoking `scripts\build_agent.ps1` - -### Scripts (on Windows) - -The following scripts are available to help develop the Amazon ECS Container Agent on Windows: - -* `scripts\run-integ-tests.ps1` - Runs all integration tests in the `engine` and `stats` packages -* `misc\windows-deploy\Install-ECSAgent.ps1` - Install the ECS agent as a Windows service -* `misc\windows-deploy\amazon-ecs-agent.ps1` - Helper script to set up the host and run the agent as a process -* `misc\windows-deploy\user-data.ps1` - Sample user-data that can be used with the Windows Server 2016 with Containers - AMI to run the agent as a process - ## Contributing diff --git a/VERSION b/VERSION index 4d5fde5bd16..0944cc489c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.60.0 +1.68.1 diff --git a/agent-container/agent-config.json b/agent-container/agent-config.json new file mode 100644 index 00000000000..c8f925ba6a2 --- /dev/null +++ b/agent-container/agent-config.json @@ -0,0 +1 @@ +{"author":"Amazon Web Services, Inc.","config":{"Cmd":["/agent"],"ArgsEscaped":true},"created":"~~timestamp~~","config":{"ExposedPorts":{"51678/tcp":{},"51679/tcp":{}},"Env":["PATH=/host/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Healthcheck":{"Test":["CMD","/agent","--healthcheck"]}},"history":[{"created":"~~timestamp~~","author":"Amazon Web Services, Inc.","created_by":"°o°","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:~~digest~~"]}} diff --git a/agent-container/agent-image-VERSION b/agent-container/agent-image-VERSION new file mode 100644 index 00000000000..d3827e75a5c --- /dev/null +++ b/agent-container/agent-image-VERSION @@ -0,0 +1 @@ +1.0 diff --git a/agent-container/agent-manifest.json b/agent-container/agent-manifest.json new file mode 100644 index 00000000000..a4dc80a17b9 --- /dev/null +++ b/agent-container/agent-manifest.json @@ -0,0 +1 @@ +[{"Config":"config.json","RepoTags":["amazon/amazon-ecs-agent:latest"],"Layers":["rootfs/layer.tar"]}] diff --git a/agent-container/agent-repositories b/agent-container/agent-repositories new file mode 100644 index 00000000000..91624061cc3 --- /dev/null +++ b/agent-container/agent-repositories @@ -0,0 +1 @@ +{"amazon/amazon-ecs-agent":{"amazon-ecs":"rootfs"}} diff --git a/agent/acs/client/acs_client_test.go b/agent/acs/client/acs_client_test.go index 6e56e84f55f..f316bff8b91 100644 --- a/agent/acs/client/acs_client_test.go +++ b/agent/acs/client/acs_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -347,9 +348,9 @@ func testCS(conn *mock_wsconn.MockWebsocketConn) wsclient.ClientServer { // TODO: replace with gomock func startMockAcsServer(t *testing.T, closeWS <-chan bool) (*httptest.Server, chan<- string, <-chan string, <-chan error, error) { - serverChan := make(chan string) - requestsChan := make(chan string) - errChan := make(chan error) + serverChan := make(chan string, 1) + requestsChan := make(chan string, 1) + errChan := make(chan error, 1) upgrader := websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/agent/acs/client/acs_error_test.go b/agent/acs/client/acs_error_test.go index c8c631040bf..1ba77736245 100644 --- a/agent/acs/client/acs_error_test.go +++ b/agent/acs/client/acs_error_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/acs_handler.go b/agent/acs/handler/acs_handler.go index b52feedba2f..f65ccfaba3a 100644 --- a/agent/acs/handler/acs_handler.go +++ b/agent/acs/handler/acs_handler.go @@ -196,12 +196,10 @@ func NewSession( func (acsSession *session) Start() error { // connectToACS channel is used to indicate the intent to connect to ACS // It's processed by the select loop to connect to ACS - connectToACS := make(chan struct{}) + connectToACS := make(chan struct{}, 1) // This is required to trigger the first connection to ACS. Subsequent // connections are triggered by the handleACSError() method - go func() { - connectToACS <- struct{}{} - }() + connectToACS <- struct{}{} for { select { case <-connectToACS: diff --git a/agent/acs/handler/acs_handler_test.go b/agent/acs/handler/acs_handler_test.go index b086a86a869..ed25e268d55 100644 --- a/agent/acs/handler/acs_handler_test.go +++ b/agent/acs/handler/acs_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/attach_eni_handler_common_test.go b/agent/acs/handler/attach_eni_handler_common_test.go index 6453ebc002a..fddbf3f8e68 100644 --- a/agent/acs/handler/attach_eni_handler_common_test.go +++ b/agent/acs/handler/attach_eni_handler_common_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/attach_instance_eni_handler_test.go b/agent/acs/handler/attach_instance_eni_handler_test.go index 0a51baefaa3..b834eddbde4 100644 --- a/agent/acs/handler/attach_instance_eni_handler_test.go +++ b/agent/acs/handler/attach_instance_eni_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/attach_task_eni_handler_test.go b/agent/acs/handler/attach_task_eni_handler_test.go index fbe240e7441..cdcda4bdad3 100644 --- a/agent/acs/handler/attach_task_eni_handler_test.go +++ b/agent/acs/handler/attach_task_eni_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/heartbeat_handler_test.go b/agent/acs/handler/heartbeat_handler_test.go index ac3a7eb7343..176de61acb1 100644 --- a/agent/acs/handler/heartbeat_handler_test.go +++ b/agent/acs/handler/heartbeat_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/payload_handler_test.go b/agent/acs/handler/payload_handler_test.go index b265efb9c10..4c66b63e494 100644 --- a/agent/acs/handler/payload_handler_test.go +++ b/agent/acs/handler/payload_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -217,9 +218,11 @@ func TestHandlePayloadMessageSaveDataError(t *testing.T) { Arn: "t1", DesiredStatusUnsafe: apitaskstatus.TaskRunning, ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + NetworkMode: apitask.BridgeNetworkMode, } + expectedTask.GetID() // to set the task setIdOnce (sync.Once) property - assert.Equal(t, addedTask, expectedTask, "added task is not expected") + assert.Equal(t, expectedTask, addedTask, "added task is not expected") } func newTestDataClient(t *testing.T) (data.Client, func()) { @@ -277,8 +280,10 @@ func TestHandlePayloadMessageAckedWhenTaskAdded(t *testing.T) { expectedTask := &apitask.Task{ Arn: "t1", ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + NetworkMode: apitask.BridgeNetworkMode, } - assert.Equal(t, addedTask, expectedTask, "received task is not expected") + expectedTask.GetID() // to set the task setIdOnce (sync.Once) property + assert.Equal(t, expectedTask, addedTask, "received task is not expected") } // TestHandlePayloadMessageCredentialsAckedWhenTaskAdded tests if the handler generates @@ -454,8 +459,10 @@ func TestPayloadBufferHandler(t *testing.T) { expectedTask := &apitask.Task{ Arn: taskArn, ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + NetworkMode: apitask.BridgeNetworkMode, } - assert.Equal(t, addedTask, expectedTask, "received task is not expected") + expectedTask.GetID() // to set the task setIdOnce (sync.Once) property + assert.Equal(t, expectedTask, addedTask, "received task is not expected") } // TestPayloadBufferHandlerWithCredentials tests if the async payloadBufferHandler routine @@ -686,8 +693,10 @@ func validateTaskAndCredentials(taskCredentialsAck, expectedCredentialsAckForTas expectedTask := &apitask.Task{ Arn: expectedTaskArn, ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + NetworkMode: apitask.BridgeNetworkMode, } expectedTask.SetCredentialsID(expectedTaskCredentials.CredentialsID) + expectedTask.GetID() // to set the task setIdOnce (sync.Once) property if !reflect.DeepEqual(addedTask, expectedTask) { return fmt.Errorf("Mismatch between expected and added tasks, expected: %v, added: %v", expectedTask, addedTask) diff --git a/agent/acs/handler/refresh_credentials_handler_test.go b/agent/acs/handler/refresh_credentials_handler_test.go index d7fa8f88892..a87df989a20 100644 --- a/agent/acs/handler/refresh_credentials_handler_test.go +++ b/agent/acs/handler/refresh_credentials_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/handler/task_manifest_handler_test.go b/agent/acs/handler/task_manifest_handler_test.go index 545bb46cf5d..cce4b9c24dc 100644 --- a/agent/acs/handler/task_manifest_handler_test.go +++ b/agent/acs/handler/task_manifest_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/acs/model/api/api-2.json b/agent/acs/model/api/api-2.json index 3f873aafe71..beff6ce825d 100644 --- a/agent/acs/model/api/api-2.json +++ b/agent/acs/model/api/api-2.json @@ -20,7 +20,7 @@ }, "input":{"shape":"AttachInstanceNetworkInterfacesMessage"}, "output":{"shape":"AckRequest"}, - "documentation":"AttachNetworkInterface requests that the Agent look for and confirm the attachment of a network interface by the control plane that is not bound to a specific task." + "documentation":"AttachInstanceNetworkInterfaces requests that the Agent look for and confirm the attachment of a network interface by the control plane that is not bound to a specific task." }, "AttachTaskNetworkInterfaces":{ "name":"AttachTaskNetworkInterfaces", @@ -48,7 +48,7 @@ }, "input":{"shape":"HeartbeatMessage"}, "output":{"shape":"HeartbeatAckRequest"}, - "documentation":"Heartbeat is a periodic message between the Agent and ECS backend to keep the connection alive." + "documentation":"Heartbeat is a periodic message that informs the agent all is well." }, "Payload":{ "name":"Payload", @@ -180,6 +180,29 @@ "elasticNetworkInterfaces":{"shape":"ElasticNetworkInterfaceList"} } }, + "Attachment":{ + "type":"structure", + "members":{ + "attachmentArn":{"shape":"String"}, + "attachmentType":{"shape":"String"}, + "attachmentProperties":{"shape":"AttachmentPropertyList"} + } + }, + "AttachmentList":{ + "type":"list", + "member":{"shape":"Attachment"} + }, + "AttachmentProperty":{ + "type":"structure", + "members":{ + "name":{"shape":"String"}, + "value":{"shape":"String"} + } + }, + "AttachmentPropertyList":{ + "type":"list", + "member":{"shape":"AttachmentProperty"} + }, "AuthStrategy":{ "type":"string", "enum":["ExecutionRole"] @@ -595,6 +618,7 @@ "type":"structure", "members":{ "containerPort":{"shape":"Integer"}, + "containerPortRange":{"shape":"String"}, "hostPort":{"shape":"Integer"}, "protocol":{"shape":"TransportProtocol"} } @@ -723,7 +747,10 @@ "pidMode":{"shape":"String"}, "ipcMode":{"shape":"String"}, "proxyConfiguration":{"shape":"ProxyConfiguration"}, - "launchType":{"shape":"String"} + "launchType":{"shape":"String"}, + "serviceName":{"shape":"String"}, + "attachments":{"shape":"AttachmentList"}, + "networkMode":{"shape":"String"} } }, "TaskList":{ diff --git a/agent/acs/model/ecsacs/api.go b/agent/acs/model/ecsacs/api.go index 622d197b96b..ffc34639677 100644 --- a/agent/acs/model/ecsacs/api.go +++ b/agent/acs/model/ecsacs/api.go @@ -285,6 +285,44 @@ func (s AttachTaskNetworkInterfacesOutput) GoString() string { return s.String() } +type Attachment struct { + _ struct{} `type:"structure"` + + AttachmentArn *string `locationName:"attachmentArn" type:"string"` + + AttachmentProperties []*AttachmentProperty `locationName:"attachmentProperties" type:"list"` + + AttachmentType *string `locationName:"attachmentType" type:"string"` +} + +// String returns the string representation +func (s Attachment) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s Attachment) GoString() string { + return s.String() +} + +type AttachmentProperty struct { + _ struct{} `type:"structure"` + + Name *string `locationName:"name" type:"string"` + + Value *string `locationName:"value" type:"string"` +} + +// String returns the string representation +func (s AttachmentProperty) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s AttachmentProperty) GoString() string { + return s.String() +} + type BadRequestException struct { _ struct{} `type:"structure"` RespMetadata protocol.ResponseMetadata `json:"-" xml:"-"` @@ -1354,6 +1392,8 @@ type PortMapping struct { ContainerPort *int64 `locationName:"containerPort" type:"integer"` + ContainerPortRange *string `locationName:"containerPortRange" type:"string"` + HostPort *int64 `locationName:"hostPort" type:"integer"` Protocol *string `locationName:"protocol" type:"string" enum:"TransportProtocol"` @@ -1605,6 +1645,8 @@ type Task struct { Associations []*Association `locationName:"associations" type:"list"` + Attachments []*Attachment `locationName:"attachments" type:"list"` + Containers []*Container `locationName:"containers" type:"list"` Cpu *float64 `locationName:"cpu" type:"double"` @@ -1623,6 +1665,8 @@ type Task struct { Memory *int64 `locationName:"memory" type:"integer"` + NetworkMode *string `locationName:"networkMode" type:"string"` + Overrides *string `locationName:"overrides" type:"string"` PidMode *string `locationName:"pidMode" type:"string"` @@ -1631,6 +1675,8 @@ type Task struct { RoleCredentials *IAMRoleCredentials `locationName:"roleCredentials" type:"structure"` + ServiceName *string `locationName:"serviceName" type:"string"` + TaskDefinitionAccountId *string `locationName:"taskDefinitionAccountId" type:"string"` Version *string `locationName:"version" type:"string"` diff --git a/agent/acs/update_handler/updater_test.go b/agent/acs/update_handler/updater_test.go index adeff4458ef..165534b1346 100644 --- a/agent/acs/update_handler/updater_test.go +++ b/agent/acs/update_handler/updater_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -127,7 +128,7 @@ func TestPerformUpdateWithUpdatesDisabled(t *testing.T) { Reason: ptr("Updates are disabled").(*string), }}) - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), @@ -181,7 +182,7 @@ func TestFullUpdateFlow(t *testing.T) { require.Equal(t, "update-tar-data", writtenFile.String(), "incorrect data written") - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), @@ -249,7 +250,7 @@ func TestUndownloadedUpdate(t *testing.T) { MessageId: ptr("mid").(*string), }}) - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), @@ -307,7 +308,7 @@ func TestDuplicateUpdateMessagesWithSuccess(t *testing.T) { require.Equal(t, "update-tar-data", writtenFile.String(), "incorrect data written") - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), @@ -376,7 +377,7 @@ func TestDuplicateUpdateMessagesWithFailure(t *testing.T) { require.Equal(t, "update-tar-data", writtenFile.String(), "incorrect data written") - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), @@ -447,7 +448,7 @@ func TestNewerUpdateMessages(t *testing.T) { require.Equal(t, "newer-update-tar-data", writtenFile.String(), "incorrect data written") - taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil) + taskEngine := engine.NewTaskEngine(cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil) msg := &ecsacs.PerformUpdateMessage{ ClusterArn: ptr("cluster").(*string), ContainerInstanceArn: ptr("containerInstance").(*string), diff --git a/agent/api/appmesh/appmesh.go b/agent/api/appmesh/appmesh.go index d971b8bd6f9..267b9a7cca7 100644 --- a/agent/api/appmesh/appmesh.go +++ b/agent/api/appmesh/appmesh.go @@ -48,9 +48,9 @@ type AppMesh struct { ProxyEgressPort string // AppPorts is the port number that application is listening on AppPorts []string - // EgressIgnoredIPs is the list of ports for which egress traffic will be ignored + // EgressIgnoredIPs is the list of IPs for which egress traffic will be ignored EgressIgnoredIPs []string - // EgressIgnoredPorts is the list of IPs for which egress traffic will be ignored + // EgressIgnoredPorts is the list of ports for which egress traffic will be ignored EgressIgnoredPorts []string } diff --git a/agent/api/appmesh/appmesh_test.go b/agent/api/appmesh/appmesh_test.go index 846c9036988..9061499609b 100644 --- a/agent/api/appmesh/appmesh_test.go +++ b/agent/api/appmesh/appmesh_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/appnet/client.go b/agent/api/appnet/client.go new file mode 100644 index 00000000000..4adf9fac942 --- /dev/null +++ b/agent/api/appnet/client.go @@ -0,0 +1,54 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package appnet + +import ( + "context" + "fmt" + "net" + "net/http" +) + +type appnetClientCtxKey int + +type client struct { + udsHttpClient http.Client +} + +const ( + udsAddressKey appnetClientCtxKey = iota + unixNetworkName = "unix" +) + +// Client retrieves the singleton Appnet client +func Client() *client { + return &client{ + udsHttpClient: http.Client{ + Transport: &http.Transport{ + DialContext: udsDialContext, + }, + }, + } +} + +func udsDialContext(ctx context.Context, _, _ string) (net.Conn, error) { + udsPath, ok := ctx.Value(udsAddressKey).(string) + if !ok { + return nil, fmt.Errorf("appnet client: Path to appnet admin socket was not a string") + } + if udsPath == "" { + return nil, fmt.Errorf("appnet client: Path to appnet admin socket was blank") + } + return net.Dial(unixNetworkName, udsPath) +} diff --git a/agent/api/appnet/client_linux.go b/agent/api/appnet/client_linux.go new file mode 100644 index 00000000000..847ea5f1a0e --- /dev/null +++ b/agent/api/appnet/client_linux.go @@ -0,0 +1,74 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package appnet + +import ( + "context" + "net/http" + "time" + + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/utils/retry" + "github.com/pkg/errors" + prometheus "github.com/prometheus/client_model/go" +) + +var ( + // Injection point for UTs + oneSecondBackoffNoJitter = retry.NewExponentialBackoff(time.Second, time.Second, 0, 1) +) + +// GetStats invokes Appnet Agent's stats API to retrieve ServiceConnect stats in prometheus format. This function expects +// an Appnet-Agent-hosted HTTP server listening on the UDS path passed in config. +func (cl *client) GetStats(config serviceconnect.RuntimeConfig) (map[string]*prometheus.MetricFamily, error) { + resp, err := cl.performAppnetRequest(http.MethodGet, config.AdminSocketPath, config.StatsRequest) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.Wrapf(err, "received non-OK HTTP status %v from Service Connect stats endpoint", resp.StatusCode) + } + return parseServiceConnectStats(resp.Body) +} + +// DrainInboundConnections invokes Appnet Agent's drain_listeners API which starts draining ServiceConnect inbound connections. +// This function expects an Appnet-agent-hosted HTTP server listening on the UDS path passed in config. +func (cl *client) DrainInboundConnections(config serviceconnect.RuntimeConfig) error { + return retry.RetryNWithBackoff(oneSecondBackoffNoJitter, 3, func() error { + resp, err := cl.performAppnetRequest(http.MethodGet, config.AdminSocketPath, config.DrainRequest) + if err != nil { + logger.Warn("Error invoking Appnet's DrainInboundConnections", logger.Fields{ + "adminSocketPath": config.AdminSocketPath, + field.Error: err, + }) + return err + } + defer resp.Body.Close() + return nil + }) +} + +func (cl *client) performAppnetRequest(method, udsPath, url string) (*http.Response, error) { + ctx := context.WithValue(context.Background(), udsAddressKey, udsPath) + req, _ := http.NewRequestWithContext(ctx, method, url, nil) + httpClient := cl.udsHttpClient + return httpClient.Do(req) +} diff --git a/agent/api/appnet/client_linux_test.go b/agent/api/appnet/client_linux_test.go new file mode 100644 index 00000000000..24b2c4cc1e3 --- /dev/null +++ b/agent/api/appnet/client_linux_test.go @@ -0,0 +1,234 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package appnet + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/gorilla/mux" + prometheus "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" +) + +const ( + testUDSPath = "/tmp/appnet_admin.sock" + testStatsUrl = "http://thingie/stats/are/cool?true&key=value" + testDrainUrl = "http://widget/drain/connections?all_of_them&key=value" +) + +var ( + typeCounter = prometheus.MetricType_COUNTER + typeHistogram = prometheus.MetricType_HISTOGRAM + mockedValidStats = map[string]*prometheus.MetricFamily{ + "MetricFamily1": { + Name: aws.String("MetricFamily1"), + Type: &typeCounter, + Metric: []*prometheus.Metric{ + { + Label: []*prometheus.LabelPair{ + { + Name: aws.String("dimensionA"), + Value: aws.String("value1"), + }, + { + Name: aws.String("dimensionB"), + Value: aws.String("value2"), + }, + }, + Counter: &prometheus.Counter{ + Value: aws.Float64(1), + }, + }, + }, + }, + "MetricFamily2": { + Name: aws.String("MetricFamily2"), + Type: &typeHistogram, + Metric: []*prometheus.Metric{ + { + Label: []*prometheus.LabelPair{ + { + Name: aws.String("dimensionX"), + Value: aws.String("value1"), + }, + { + Name: aws.String("dimensionY"), + Value: aws.String("value2"), + }, + }, + Histogram: &prometheus.Histogram{ + Bucket: []*prometheus.Bucket{ + { + CumulativeCount: aws.Uint64(1), + UpperBound: aws.Float64(0.5), + }, + }, + }, + }, + }, + }, + } +) + +func setupTestUdsServer(t *testing.T, rawResponse string) *httptest.Server { + t.Helper() + r := mux.NewRouter() + r.HandleFunc("/stats/are/cool", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.True(t, r.URL.Query().Has("true")) + assert.Equal(t, "value", r.URL.Query().Get("key")) + fmt.Fprintf(w, "%s", rawResponse) + }) + r.HandleFunc("/drain/connections", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.True(t, r.URL.Query().Has("all_of_them")) + assert.Equal(t, "value", r.URL.Query().Get("key")) + fmt.Fprintf(w, "%s", rawResponse) + }) + ts := httptest.NewUnstartedServer(r) + l, err := net.Listen("unix", testUDSPath) + if err != nil { + t.Fatal("Error setting up test UDS HTTP server", err) + } + ts.Listener.Close() + ts.Listener = l + ts.Start() + return ts +} + +func TestGetStats(t *testing.T) { + for _, tc := range []struct { + name string + udsPath string + rawStats string + expectedResult map[string]*prometheus.MetricFamily + isErrorExpected bool + expectedErrorContains string + }{ + { + name: "happy case with valid counter and histogram metrics", + udsPath: testUDSPath, + rawStats: `# TYPE MetricFamily1 counter + MetricFamily1{dimensionA="value1", dimensionB="value2"} 1 + # TYPE MetricFamily2 histogram + MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1 + `, + expectedResult: mockedValidStats, + isErrorExpected: false, + }, + { + name: "sad case with invalid metrics", + udsPath: testUDSPath, + rawStats: `# TYPE MetricFamily1 counter + bad metric 1 + # TYPE MetricFamily2 histogram + bad metric 2 + `, + expectedResult: nil, + isErrorExpected: true, + expectedErrorContains: "text format parsing error", + }, + { + name: "invalid UDS path", + udsPath: "/this/doesnt/exist.sock", + rawStats: `# TYPE MetricFamily1 counter + MetricFamily1{dimensionA="value1", dimensionB="value2"} 1 + # TYPE MetricFamily2 histogram + MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1 + `, + expectedResult: nil, + isErrorExpected: true, + expectedErrorContains: "dial unix /this/doesnt/exist.sock: connect: no such file or directory", + }, + { + name: "blank UDS path", + udsPath: "", + rawStats: `# TYPE MetricFamily1 counter + MetricFamily1{dimensionA="value1", dimensionB="value2"} 1 + # TYPE MetricFamily2 histogram + MetricFamily2{dimensionX="value1", dimensionY="value2", le="0.5"} 1 + `, + expectedResult: nil, + isErrorExpected: true, + expectedErrorContains: "appnet client: Path to appnet admin socket was blank", + }, + } { + t.Run(tc.name, func(t *testing.T) { + ts := setupTestUdsServer(t, tc.rawStats) + t.Cleanup(func() { + ts.Close() + }) + stats, err := Client().GetStats(serviceconnect.RuntimeConfig{AdminSocketPath: tc.udsPath, StatsRequest: testStatsUrl, DrainRequest: testDrainUrl}) + assert.Equal(t, tc.expectedResult, stats) + if tc.isErrorExpected { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrorContains) + } else { + assert.NoError(t, err) + } + }) + } + +} + +func TestDrainInboundConnections(t *testing.T) { + for _, tc := range []struct { + name string + udsPath string + isErrorExpected bool + expectedErrorContains string + }{ + { + name: "happy case with valid uds path", + udsPath: testUDSPath, + isErrorExpected: false, + }, + { + name: "sad case with invalid uds path", + udsPath: "/this/doesnt/exist.sock", + isErrorExpected: true, + expectedErrorContains: "dial unix /this/doesnt/exist.sock: connect: no such file or directory", + }, + { + name: "sad case with blank uds path", + udsPath: "", + isErrorExpected: true, + expectedErrorContains: "appnet client: Path to appnet admin socket was blank", + }, + } { + t.Run(tc.name, func(t *testing.T) { + ts := setupTestUdsServer(t, "not important") + t.Cleanup(func() { + ts.Close() + }) + err := Client().DrainInboundConnections(serviceconnect.RuntimeConfig{AdminSocketPath: tc.udsPath, StatsRequest: testStatsUrl, DrainRequest: testDrainUrl}) + if tc.isErrorExpected { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErrorContains) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/agent/api/appnet/client_other.go b/agent/api/appnet/client_other.go new file mode 100644 index 00000000000..92f64233dfb --- /dev/null +++ b/agent/api/appnet/client_other.go @@ -0,0 +1,32 @@ +//go:build !linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package appnet + +import ( + "fmt" + + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + + prometheus "github.com/prometheus/client_model/go" +) + +func (cl *client) GetStats(config serviceconnect.RuntimeConfig) (map[string]*prometheus.MetricFamily, error) { + return nil, fmt.Errorf("appnet client: GetStats is not supported in this platform") +} + +func (cl *client) DrainInboundConnections(config serviceconnect.RuntimeConfig) error { + return fmt.Errorf("appnet client: DrainInboundConnections is not supported in this platform") +} diff --git a/agent/api/appnet/stats.go b/agent/api/appnet/stats.go new file mode 100644 index 00000000000..0141067c677 --- /dev/null +++ b/agent/api/appnet/stats.go @@ -0,0 +1,42 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package appnet + +import ( + "io" + + prometheus "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" +) + +// parseServiceConnectStats method parses stats in prometheus format and converts it to prometheus client model. RawStats looks like below +// +// # TYPE MetricFamily1 counter +// MetricFamily1{dimensionA=value1, dimensionB=value2} 1 +// +// # TYPE MetricFamily3 gauge +// MetricFamily3{dimensionA=value1, dimensionB=value2} 3 +// +// # TYPE MetricFamily2 histogram +// MetricFamily2{dimensionX=value1, dimensionY=value2, le=0.5} 1 +// MetricFamily2{dimensionX=value1, dimensionY=value2, le=1} 2 +// MetricFamily2{dimensionX=value1, dimensionY=value2, le=5} 3 +func parseServiceConnectStats(rawStats io.Reader) (map[string]*prometheus.MetricFamily, error) { + var parser expfmt.TextParser + stats, err := parser.TextToMetricFamilies(rawStats) + if err != nil { + return nil, err + } + return stats, nil +} diff --git a/agent/api/container/container.go b/agent/api/container/container.go index 662cca10520..89e92461be0 100644 --- a/agent/api/container/container.go +++ b/agent/api/container/container.go @@ -15,8 +15,10 @@ package container import ( "encoding/json" + "errors" "fmt" "strconv" + "strings" "sync" "time" @@ -64,6 +66,13 @@ const ( // MetadataURIFormat defines the URI format for v4 metadata endpoint MetadataURIFormatV4 = "http://169.254.170.2/v4/%s" + // AgentURIEnvVarName defines the name of the environment variable + // injected into containers that contains the Agent endpoints. + AgentURIEnvVarName = "ECS_AGENT_URI" + + // AgentURIFormat defines the URI format for Agent endpoints + AgentURIFormat = "http://169.254.170.2/api/%s" + // SecretProviderSSM is to show secret provider being SSM SecretProviderSSM = "ssm" @@ -310,6 +319,13 @@ type Container struct { finishedAt time.Time labels map[string]string + + // ContainerHasPortRange is set to true when the container has at least 1 port range requested. + ContainerHasPortRange bool + // ContainerPortSet is a set of singular container ports that don't belong to a containerPortRange request + ContainerPortSet map[int]struct{} + // ContainerPortRangeMap is a map of containerPortRange to its associated hostPortRange + ContainerPortRangeMap map[string]string } type DependsOn struct { @@ -495,11 +511,14 @@ func (c *Container) String() string { // GetSteadyStateStatus returns the steady state status for the container. If // Container.steadyState is not initialized, the default steady state status -// defined by `defaultContainerSteadyStateStatus` is returned. The 'pause' +// defined by `defaultContainerSteadyStateStatus` is returned. In awsvpc, the 'pause' // container's steady state differs from that of other containers, as the // 'pause' container can reach its teady state once networking resources // have been provisioned for it, which is done in the `ContainerResourcesProvisioned` -// state +// state. In bridge mode, pause containers are currently used exclusively for +// supporting service-connect tasks. Those pause containers will have steady state +// status "ContainerRunning" as the actual network provisioning is done by ServiceConnect +// container (aka Appnet agent) func (c *Container) GetSteadyStateStatus() apicontainerstatus.ContainerStatus { if c.SteadyStateStatusUnsafe == nil { return defaultContainerSteadyStateStatus @@ -507,6 +526,16 @@ func (c *Container) GetSteadyStateStatus() apicontainerstatus.ContainerStatus { return *c.SteadyStateStatusUnsafe } +// SetSteadyStateStatusUnsafe allows setting container steady state status after they +// are initially created. +// In bridge mode, this is used by overriding the ServiceConnect container steady +// status to ContainerResourcesProvisioned because it comes with ACS task payload and will +// get ContainerRunning by default during unmarshalling. We need ServiceConnect container +// to provision network resources to support SC bridge mode +func (c *Container) SetSteadyStateStatusUnsafe(steadyState apicontainerstatus.ContainerStatus) { + c.SteadyStateStatusUnsafe = &steadyState +} + // IsKnownSteadyState returns true if the `KnownState` of the container equals // the `steadyState` defined for the container func (c *Container) IsKnownSteadyState() bool { @@ -934,6 +963,22 @@ func (c *Container) InjectV4MetadataEndpoint() { fmt.Sprintf(MetadataURIFormatV4, c.V3EndpointID) } +// InjectV1AgentAPIEndpoint injects the v1 Agent API endpoint into the container +// as an environment variable. +func (c *Container) InjectV1AgentAPIEndpoint() { + c.lock.Lock() + defer c.lock.Unlock() + c.ensureEnvironmentIsInitialized() + c.Environment[AgentURIEnvVarName] = fmt.Sprintf(AgentURIFormat, c.V3EndpointID) +} + +// Initializes Environment Map if it is nil +func (c *Container) ensureEnvironmentIsInitialized() { + if c.Environment == nil { + c.Environment = make(map[string]string) + } +} + // ShouldCreateWithSSMSecret returns true if this container needs to get secret // value from SSM Parameter Store func (c *Container) ShouldCreateWithSSMSecret() bool { @@ -1289,6 +1334,44 @@ func (c *Container) UpdateManagedAgentSentStatus(agentName string, status apicon return false } +// RequiresCredentialSpec checks if container needs a credentialspec resource +func (c *Container) RequiresCredentialSpec() bool { + credSpec, err := c.getCredentialSpec() + if err != nil || credSpec == "" { + return false + } + + return true +} + +// GetCredentialSpec is used to retrieve the current credentialspec resource +func (c *Container) GetCredentialSpec() (string, error) { + return c.getCredentialSpec() +} + +func (c *Container) getCredentialSpec() (string, error) { + c.lock.RLock() + defer c.lock.RUnlock() + + if c.DockerConfig.HostConfig == nil { + return "", errors.New("empty container hostConfig") + } + + hostConfig := &dockercontainer.HostConfig{} + err := json.Unmarshal([]byte(*c.DockerConfig.HostConfig), hostConfig) + if err != nil || len(hostConfig.SecurityOpt) == 0 { + return "", errors.New("unable to obtain security options from container hostConfig") + } + + for _, opt := range hostConfig.SecurityOpt { + if strings.HasPrefix(opt, "credentialspec") { + return opt, nil + } + } + + return "", errors.New("unable to obtain credentialspec") +} + func (c *Container) GetManagedAgentStatus(agentName string) apicontainerstatus.ManagedAgentStatus { c.lock.RLock() defer c.lock.RUnlock() @@ -1324,3 +1407,39 @@ func (c *Container) IsContainerTornDown() bool { defer c.lock.RUnlock() return c.ContainerTornDownUnsafe } + +func (c *Container) SetContainerHasPortRange(containerHasPortRange bool) { + c.lock.Lock() + defer c.lock.Unlock() + c.ContainerHasPortRange = containerHasPortRange +} + +func (c *Container) HasPortRange() bool { + c.lock.RLock() + defer c.lock.RUnlock() + return c.ContainerHasPortRange +} + +func (c *Container) SetContainerPortSet(containerPortSet map[int]struct{}) { + c.lock.Lock() + defer c.lock.Unlock() + c.ContainerPortSet = containerPortSet +} + +func (c *Container) GetContainerPortSet() map[int]struct{} { + c.lock.RLock() + defer c.lock.RUnlock() + return c.ContainerPortSet +} + +func (c *Container) SetContainerPortRangeMap(portRangeMap map[string]string) { + c.lock.Lock() + defer c.lock.Unlock() + c.ContainerPortRangeMap = portRangeMap +} + +func (c *Container) GetContainerPortRangeMap() map[string]string { + c.lock.RLock() + defer c.lock.RUnlock() + return c.ContainerPortRangeMap +} diff --git a/agent/api/container/container_test.go b/agent/api/container/container_test.go index 76d248b1446..bfbfcdcf9ea 100644 --- a/agent/api/container/container_test.go +++ b/agent/api/container/container_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -137,7 +138,7 @@ func TestIsInternal(t *testing.T) { } // TestSetupExecutionRoleFlag tests whether or not the container appropriately -//sets the flag for using execution roles +// sets the flag for using execution roles func TestSetupExecutionRoleFlag(t *testing.T) { testCases := []struct { container *Container @@ -969,3 +970,113 @@ func TestUpdateManagedAgentSentStatus(t *testing.T) { }) } } + +func TestRequiresCredentialSpec(t *testing.T) { + testCases := []struct { + name string + container *Container + expectedOutput bool + }{ + { + name: "hostconfig_nil", + container: &Container{}, + expectedOutput: false, + }, + { + name: "invalid_case", + container: getContainer("invalid"), + expectedOutput: false, + }, + { + name: "empty_sec_opt", + container: getContainer("{\"NetworkMode\":\"bridge\"}"), + expectedOutput: false, + }, + { + name: "missing_credentialspec", + container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"), + expectedOutput: false, + }, + { + name: "valid_credentialspec_file", + container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}"), + expectedOutput: true, + }, + { + name: "valid_credentialspec_s3", + container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}"), + expectedOutput: true, + }, + { + name: "valid_credentialspec_ssm", + container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}"), + expectedOutput: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, tc.container.RequiresCredentialSpec()) + }) + } +} + +func TestGetCredentialSpecErr(t *testing.T) { + testCases := []struct { + name string + container *Container + expectedOutputString string + expectedErrorString string + }{ + { + name: "hostconfig_nil", + container: &Container{}, + expectedOutputString: "", + expectedErrorString: "empty container hostConfig", + }, + { + name: "invalid_case", + container: getContainer("invalid"), + expectedOutputString: "", + expectedErrorString: "unable to obtain security options from container hostConfig", + }, + { + name: "empty_sec_opt", + container: getContainer("{\"NetworkMode\":\"bridge\"}"), + expectedOutputString: "", + expectedErrorString: "unable to obtain security options from container hostConfig", + }, + { + name: "missing_credentialspec", + container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"), + expectedOutputString: "", + expectedErrorString: "unable to obtain credentialspec", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expectedOutputStr, err := tc.container.GetCredentialSpec() + assert.Equal(t, tc.expectedOutputString, expectedOutputStr) + assert.EqualError(t, err, tc.expectedErrorString) + }) + } +} + +func TestGetCredentialSpecHappyPath(t *testing.T) { + c := getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}") + + expectedCredentialSpec := "credentialspec:file://gmsa_gmsa-acct.json" + + credentialspec, err := c.GetCredentialSpec() + assert.NoError(t, err) + assert.EqualValues(t, expectedCredentialSpec, credentialspec) +} + +func getContainer(hostConfig string) *Container { + c := &Container{ + Name: "c", + } + c.DockerConfig.HostConfig = &hostConfig + return c +} diff --git a/agent/api/container/container_unix.go b/agent/api/container/container_unix.go index a105be71284..3bbd4a40815 100644 --- a/agent/api/container/container_unix.go +++ b/agent/api/container/container_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -15,22 +16,8 @@ package container -import ( - "github.com/pkg/errors" -) - const ( // DockerContainerMinimumMemoryInBytes is the minimum amount of // memory to be allocated to a docker container DockerContainerMinimumMemoryInBytes = 4 * 1024 * 1024 // 4MB ) - -// RequiresCredentialSpec checks if container needs a credentialspec resource -func (c *Container) RequiresCredentialSpec() bool { - return false -} - -// GetCredentialSpec is used to retrieve the current credentialspec resource -func (c *Container) GetCredentialSpec() (string, error) { - return "", errors.New("unsupported platform") -} diff --git a/agent/api/container/container_windows.go b/agent/api/container/container_windows.go index 3b81dbb58a6..49b3eeb36a9 100644 --- a/agent/api/container/container_windows.go +++ b/agent/api/container/container_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -15,54 +16,8 @@ package container -import ( - "encoding/json" - "strings" - - dockercontainer "github.com/docker/docker/api/types/container" - "github.com/pkg/errors" -) - const ( // DockerContainerMinimumMemoryInBytes is the minimum amount of // memory to be allocated to a docker container DockerContainerMinimumMemoryInBytes = 256 * 1024 * 1024 // 256MB ) - -// RequiresCredentialSpec checks if container needs a credentialspec resource -func (c *Container) RequiresCredentialSpec() bool { - credSpec, err := c.getCredentialSpec() - if err != nil || credSpec == "" { - return false - } - - return true -} - -// GetCredentialSpec is used to retrieve the current credentialspec resource -func (c *Container) GetCredentialSpec() (string, error) { - return c.getCredentialSpec() -} - -func (c *Container) getCredentialSpec() (string, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - if c.DockerConfig.HostConfig == nil { - return "", errors.New("empty container hostConfig") - } - - hostConfig := &dockercontainer.HostConfig{} - err := json.Unmarshal([]byte(*c.DockerConfig.HostConfig), hostConfig) - if err != nil || len(hostConfig.SecurityOpt) == 0 { - return "", errors.New("unable to obtain security options from container hostConfig") - } - - for _, opt := range hostConfig.SecurityOpt { - if strings.HasPrefix(opt, "credentialspec") { - return opt, nil - } - } - - return "", errors.New("unable to obtain credentialspec") -} diff --git a/agent/api/container/container_windows_test.go b/agent/api/container/container_windows_test.go deleted file mode 100644 index 89aa2f08a43..00000000000 --- a/agent/api/container/container_windows_test.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build windows && unit - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package container - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRequiresCredentialSpec(t *testing.T) { - testCases := []struct { - name string - container *Container - expectedOutput bool - }{ - { - name: "hostconfig_nil", - container: &Container{}, - expectedOutput: false, - }, - { - name: "invalid_case", - container: getContainer("invalid"), - expectedOutput: false, - }, - { - name: "empty_sec_opt", - container: getContainer("{\"NetworkMode\":\"bridge\"}"), - expectedOutput: false, - }, - { - name: "missing_credentialspec", - container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"), - expectedOutput: false, - }, - { - name: "valid_credentialspec_file", - container: getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}"), - expectedOutput: true, - }, - { - name: "valid_credentialspec_s3", - container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::${BucketName}/${ObjectName}\"]}"), - expectedOutput: true, - }, - { - name: "valid_credentialspec_ssm", - container: getContainer("{\"SecurityOpt\": [\"credentialspec:arn:aws:ssm:region:aws_account_id:parameter/parameter_name\"]}"), - expectedOutput: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expectedOutput, tc.container.RequiresCredentialSpec()) - }) - } -} - -func TestGetCredentialSpecErr(t *testing.T) { - testCases := []struct { - name string - container *Container - expectedOutputString string - expectedErrorString string - }{ - { - name: "hostconfig_nil", - container: &Container{}, - expectedOutputString: "", - expectedErrorString: "empty container hostConfig", - }, - { - name: "invalid_case", - container: getContainer("invalid"), - expectedOutputString: "", - expectedErrorString: "unable to obtain security options from container hostConfig", - }, - { - name: "empty_sec_opt", - container: getContainer("{\"NetworkMode\":\"bridge\"}"), - expectedOutputString: "", - expectedErrorString: "unable to obtain security options from container hostConfig", - }, - { - name: "missing_credentialspec", - container: getContainer("{\"SecurityOpt\": [\"invalid-sec-opt\"]}"), - expectedOutputString: "", - expectedErrorString: "unable to obtain credentialspec", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - expectedOutputStr, err := tc.container.GetCredentialSpec() - assert.Equal(t, tc.expectedOutputString, expectedOutputStr) - assert.EqualError(t, err, tc.expectedErrorString) - }) - } -} - -func TestGetCredentialSpecHappyPath(t *testing.T) { - c := getContainer("{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}") - - expectedCredentialSpec := "credentialspec:file://gmsa_gmsa-acct.json" - - credentialspec, err := c.GetCredentialSpec() - assert.NoError(t, err) - assert.EqualValues(t, expectedCredentialSpec, credentialspec) -} - -func getContainer(hostConfig string) *Container { - c := &Container{ - Name: "c", - } - c.DockerConfig.HostConfig = &hostConfig - return c -} diff --git a/agent/api/container/containeroverrides_test.go b/agent/api/container/containeroverrides_test.go index c7479c7dc8a..da40ea00c98 100644 --- a/agent/api/container/containeroverrides_test.go +++ b/agent/api/container/containeroverrides_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/container/containertype.go b/agent/api/container/containertype.go index 9045b872aa8..aa5f9e60498 100644 --- a/agent/api/container/containertype.go +++ b/agent/api/container/containertype.go @@ -35,6 +35,10 @@ const ( // sharing either PID or IPC resource namespaces. Regardless if one or // both flags are used, only 1 of these containers need to be active ContainerNamespacePause + + // ContainerServiceConnectRelay represents the internal container type + // for the relay to share connections to management infrastructure. + ContainerServiceConnectRelay ) // ContainerType represents the type of the internal container created diff --git a/agent/api/container/containertype_test.go b/agent/api/container/containertype_test.go index 41298acfc1c..5681df63a8b 100644 --- a/agent/api/container/containertype_test.go +++ b/agent/api/container/containertype_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/container/port_binding.go b/agent/api/container/port_binding.go index c26f466af9b..8f770d70787 100644 --- a/agent/api/container/port_binding.go +++ b/agent/api/container/port_binding.go @@ -17,6 +17,7 @@ import ( "strconv" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" + "github.com/docker/go-connections/nat" ) @@ -31,6 +32,8 @@ const ( type PortBinding struct { // ContainerPort is the port inside the container ContainerPort uint16 + // ContainerPortRange is a range of ports exposed inside the container + ContainerPortRange string // HostPort is the port exposed on the host HostPort uint16 // BindIP is the IP address to which the port is bound diff --git a/agent/api/container/port_binding_test.go b/agent/api/container/port_binding_test.go index 5648e4f0c8f..722a76d064d 100644 --- a/agent/api/container/port_binding_test.go +++ b/agent/api/container/port_binding_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -20,6 +21,7 @@ import ( "testing" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" + "github.com/docker/go-connections/nat" ) diff --git a/agent/api/container/status/containerstatus_test.go b/agent/api/container/status/containerstatus_test.go index 10cd9b94783..0016f1cf19f 100644 --- a/agent/api/container/status/containerstatus_test.go +++ b/agent/api/container/status/containerstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/container/status/managedagentstatus_test.go b/agent/api/container/status/managedagentstatus_test.go index 151312367a6..332c11d1899 100644 --- a/agent/api/container/status/managedagentstatus_test.go +++ b/agent/api/container/status/managedagentstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/container/transitiondependency_test.go b/agent/api/container/transitiondependency_test.go index 093f8dbc331..e4466988d1f 100644 --- a/agent/api/container/transitiondependency_test.go +++ b/agent/api/container/transitiondependency_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/container/transport_test.go b/agent/api/container/transport_test.go index 5e230054b6b..2f21240ee8d 100644 --- a/agent/api/container/transport_test.go +++ b/agent/api/container/transport_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/ecsclient/client.go b/agent/api/ecsclient/client.go index 2ff65d9fe12..e588722a6b7 100644 --- a/agent/api/ecsclient/client.go +++ b/agent/api/ecsclient/client.go @@ -20,8 +20,6 @@ import ( "strings" "time" - "github.com/aws/amazon-ecs-agent/agent/logger" - "github.com/aws/amazon-ecs-agent/agent/api" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" @@ -30,24 +28,31 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ec2" "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/httpclient" + "github.com/aws/amazon-ecs-agent/agent/logger" "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/cihub/seelog" "github.com/docker/docker/pkg/system" + "github.com/docker/go-connections/nat" ) const ( - ecsMaxImageDigestLength = 255 - ecsMaxReasonLength = 255 - ecsMaxRuntimeIDLength = 255 - pollEndpointCacheTTL = 12 * time.Hour - roundtripTimeout = 5 * time.Second - azAttrName = "ecs.availability-zone" - cpuArchAttrName = "ecs.cpu-architecture" - osTypeAttrName = "ecs.os-type" - osFamilyAttrName = "ecs.os-family" + ecsMaxImageDigestLength = 255 + ecsMaxContainerReasonLength = 255 + ecsMaxTaskReasonLength = 1024 + ecsMaxRuntimeIDLength = 255 + pollEndpointCacheTTL = 12 * time.Hour + azAttrName = "ecs.availability-zone" + cpuArchAttrName = "ecs.cpu-architecture" + osTypeAttrName = "ecs.os-type" + osFamilyAttrName = "ecs.os-family" + RoundtripTimeout = 5 * time.Second + // ecsMaxNetworkBindingsLength is the maximum length of the ecs.NetworkBindings list sent as part of the + // container state change payload. Currently, this is enforced only when containerPortRanges are requested. + ecsMaxNetworkBindingsLength = 100 ) // APIECSClient implements ECSClient @@ -69,7 +74,7 @@ func NewECSClient( var ecsConfig aws.Config ecsConfig.Credentials = credentialProvider ecsConfig.Region = &config.AWSRegion - ecsConfig.HTTPClient = httpclient.New(roundtripTimeout, config.AcceptInsecureCert) + ecsConfig.HTTPClient = httpclient.New(RoundtripTimeout, config.AcceptInsecureCert) if config.APIEndpoint != "" { ecsConfig.Endpoint = &config.APIEndpoint } @@ -404,7 +409,7 @@ func (client *APIECSClient) SubmitTaskStateChange(change api.TaskStateChange) er Cluster: aws.String(client.config.Cluster), Task: aws.String(change.TaskARN), Status: aws.String(status), - Reason: aws.String(change.Reason), + Reason: aws.String(trimString(change.Reason, ecsMaxTaskReasonLength)), PullStartedAt: change.PullStartedAt, PullStoppedAt: change.PullStoppedAt, ExecutionStoppedAt: change.ExecutionStoppedAt, @@ -418,7 +423,12 @@ func (client *APIECSClient) SubmitTaskStateChange(change api.TaskStateChange) er containerEvents := make([]*ecs.ContainerStateChange, len(change.Containers)) for i, containerEvent := range change.Containers { - containerEvents[i] = client.buildContainerStateChangePayload(containerEvent, client.config.ShouldExcludeIPv6PortBinding.Enabled()) + payload, err := client.buildContainerStateChangePayload(containerEvent, client.config.ShouldExcludeIPv6PortBinding.Enabled()) + if err != nil { + seelog.Errorf("Could not submit task state change: [%s]: %v", change.String(), err) + return err + } + containerEvents[i] = payload } req.Containers = containerEvents @@ -449,7 +459,7 @@ func (client *APIECSClient) buildManagedAgentStateChangePayload(change api.Manag } var trimmedReason *string if change.Reason != "" { - trimmedReason = aws.String(trimString(change.Reason, ecsMaxReasonLength)) + trimmedReason = aws.String(trimString(change.Reason, ecsMaxContainerReasonLength)) } return &ecs.ManagedAgentStateChange{ ManagedAgentName: aws.String(change.Name), @@ -459,7 +469,7 @@ func (client *APIECSClient) buildManagedAgentStateChangePayload(change api.Manag } } -func (client *APIECSClient) buildContainerStateChangePayload(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) *ecs.ContainerStateChange { +func (client *APIECSClient) buildContainerStateChangePayload(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) (*ecs.ContainerStateChange, error) { statechange := &ecs.ContainerStateChange{ ContainerName: aws.String(change.ContainerName), } @@ -468,7 +478,7 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe statechange.RuntimeId = aws.String(trimmedRuntimeID) } if change.Reason != "" { - trimmedReason := trimString(change.Reason, ecsMaxReasonLength) + trimmedReason := trimString(change.Reason, ecsMaxContainerReasonLength) statechange.Reason = aws.String(trimmedReason) } if change.ImageDigest != "" { @@ -480,7 +490,7 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe if status != apicontainerstatus.ContainerStopped && status != apicontainerstatus.ContainerRunning { seelog.Warnf("Not submitting unsupported upstream container state %s for container %s in task %s", status.String(), change.ContainerName, change.TaskArn) - return nil + return nil, nil } stat := change.Status.String() if stat == "DEAD" { @@ -493,7 +503,38 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe statechange.ExitCode = aws.Int64(exitCode) } + networkBindings := getNetworkBindings(change, shouldExcludeIPv6PortBinding) + // we enforce a limit on the no. of network bindings for containers with at-least 1 port range requested. + // this limit is enforced by ECS, and we fail early and don't call SubmitContainerStateChange. + if change.Container.HasPortRange() && len(networkBindings) > ecsMaxNetworkBindingsLength { + return nil, fmt.Errorf("no. of network bindings %v is more than the maximum supported no. %v, "+ + "container: %s "+"task: %s", len(networkBindings), ecsMaxNetworkBindingsLength, change.ContainerName, change.TaskArn) + } + statechange.NetworkBindings = networkBindings + + return statechange, nil +} + +// ProtocolBindIP used to store protocol and bindIP information associated to a particular host port +type ProtocolBindIP struct { + protocol string + bindIP string +} + +// getNetworkBindings returns the list of networkingBindings, sent to ECS as part of the container state change payload +func getNetworkBindings(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) []*ecs.NetworkBinding { networkBindings := []*ecs.NetworkBinding{} + // hostPortToProtocolBindIPMap is a map to store protocol and bindIP information associated to host ports + // that belong to a range. This is used in case when there are multiple protocol/bindIP combinations associated to a + // port binding. example: when both IPv4 and IPv6 bindIPs are populated by docker and shouldExcludeIPv6PortBinding is false. + hostPortToProtocolBindIPMap := map[int64][]ProtocolBindIP{} + + // ContainerPortSet consists of singular ports, and ports that belong to a range, but for which we were not able to + // find contiguous host ports and ask docker to pick instead. + containerPortSet := change.Container.GetContainerPortSet() + // each entry in the ContainerPortRangeMap implies that we found a contiguous host port range for the same + containerPortRangeMap := change.Container.GetContainerPortRangeMap() + for _, binding := range change.PortBindings { if binding.BindIP == "::" && shouldExcludeIPv6PortBinding { seelog.Debugf("Exclude IPv6 port binding %v for container %s in task %s", binding, change.ContainerName, change.TaskArn) @@ -505,24 +546,54 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe bindIP := binding.BindIP protocol := binding.Protocol.String() - networkBindings = append(networkBindings, &ecs.NetworkBinding{ - BindIP: aws.String(bindIP), - ContainerPort: aws.Int64(containerPort), - HostPort: aws.Int64(hostPort), - Protocol: aws.String(protocol), - }) + // create network binding for each containerPort that exists in the singular ContainerPortSet + // for container ports that belong to a range, we'll have 1 consolidated network binding for the range + if _, ok := containerPortSet[int(containerPort)]; ok { + networkBindings = append(networkBindings, &ecs.NetworkBinding{ + BindIP: aws.String(bindIP), + ContainerPort: aws.Int64(containerPort), + HostPort: aws.Int64(hostPort), + Protocol: aws.String(protocol), + }) + } else { + // populate hostPortToProtocolBindIPMap – this is used below when we construct network binding for ranges. + hostPortToProtocolBindIPMap[hostPort] = append(hostPortToProtocolBindIPMap[hostPort], + ProtocolBindIP{ + protocol: protocol, + bindIP: bindIP, + }) + } + } + + for containerPortRange, hostPortRange := range containerPortRangeMap { + // we check for protocol and bindIP information associated to any one of the host ports from the hostPortRange, + // all ports belonging to the same range share this information. + hostPort, _, _ := nat.ParsePortRangeToInt(hostPortRange) + if val, ok := hostPortToProtocolBindIPMap[int64(hostPort)]; ok { + for _, v := range val { + networkBindings = append(networkBindings, &ecs.NetworkBinding{ + BindIP: aws.String(v.bindIP), + ContainerPortRange: aws.String(containerPortRange), + HostPortRange: aws.String(hostPortRange), + Protocol: aws.String(v.protocol), + }) + } + } } - statechange.NetworkBindings = networkBindings - return statechange + return networkBindings } func (client *APIECSClient) SubmitContainerStateChange(change api.ContainerStateChange) error { - pl := client.buildContainerStateChangePayload(change, client.config.ShouldExcludeIPv6PortBinding.Enabled()) - if pl == nil { + pl, err := client.buildContainerStateChangePayload(change, client.config.ShouldExcludeIPv6PortBinding.Enabled()) + if err != nil { + seelog.Errorf("Could not build container state change payload: [%s]: %v", change.String(), err) + return err + } else if pl == nil { return nil } - _, err := client.submitStateChangeClient.SubmitContainerStateChange(&ecs.SubmitContainerStateChangeInput{ + + _, err = client.submitStateChangeClient.SubmitContainerStateChange(&ecs.SubmitContainerStateChangeInput{ Cluster: aws.String(client.config.Cluster), ContainerName: aws.String(change.ContainerName), ExitCode: pl.ExitCode, @@ -583,6 +654,18 @@ func (client *APIECSClient) DiscoverTelemetryEndpoint(containerInstanceArn strin return aws.StringValue(resp.TelemetryEndpoint), nil } +func (client *APIECSClient) DiscoverServiceConnectEndpoint(containerInstanceArn string) (string, error) { + resp, err := client.discoverPollEndpoint(containerInstanceArn) + if err != nil { + return "", err + } + if resp.ServiceConnectEndpoint == nil { + return "", errors.New("No ServiceConnect endpoint returned; nil") + } + + return aws.StringValue(resp.ServiceConnectEndpoint), nil +} + func (client *APIECSClient) discoverPollEndpoint(containerInstanceArn string) (*ecs.DiscoverPollEndpointOutput, error) { // Try getting an entry from the cache cachedEndpoint, expired, found := client.pollEndpointCache.Get(containerInstanceArn) @@ -590,9 +673,10 @@ func (client *APIECSClient) discoverPollEndpoint(containerInstanceArn string) (* // Cache hit and not expired. Return the output. if output, ok := cachedEndpoint.(*ecs.DiscoverPollEndpointOutput); ok { logger.Info("Using cached DiscoverPollEndpoint", logger.Fields{ - "endpoint": aws.StringValue(output.Endpoint), - "telemetryEndpoint": aws.StringValue(output.TelemetryEndpoint), - "containerInstanceARN": containerInstanceArn, + "endpoint": aws.StringValue(output.Endpoint), + "telemetryEndpoint": aws.StringValue(output.TelemetryEndpoint), + "serviceConnectEndpoint": aws.StringValue(output.ServiceConnectEndpoint), + "containerInstanceARN": containerInstanceArn, }) return output, nil } @@ -610,9 +694,10 @@ func (client *APIECSClient) discoverPollEndpoint(containerInstanceArn string) (* if expired { if output, ok := cachedEndpoint.(*ecs.DiscoverPollEndpointOutput); ok { logger.Info("Error calling DiscoverPollEndpoint. Using cached-but-expired endpoint as a fallback.", logger.Fields{ - "endpoint": aws.StringValue(output.Endpoint), - "telemetryEndpoint": aws.StringValue(output.TelemetryEndpoint), - "containerInstanceARN": containerInstanceArn, + "endpoint": aws.StringValue(output.Endpoint), + "telemetryEndpoint": aws.StringValue(output.TelemetryEndpoint), + "serviceConnectEndpoint": aws.StringValue(output.ServiceConnectEndpoint), + "containerInstanceARN": containerInstanceArn, }) return output, nil } diff --git a/agent/api/ecsclient/client_test.go b/agent/api/ecsclient/client_test.go index 21fedd0ba82..11f7ada479a 100644 --- a/agent/api/ecsclient/client_test.go +++ b/agent/api/ecsclient/client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -49,6 +50,7 @@ const ( iid = "instanceIdentityDocument" iidSignature = "signature" registrationToken = "clientToken" + testNetworkName = "bridge" ) var ( @@ -195,6 +197,12 @@ func TestSubmitContainerStateChange(t *testing.T) { HostPort: int64ptr(intptr(4)), Protocol: strptr("udp"), }, + { + BindIP: strptr("5.6.7.8"), + ContainerPortRange: strptr("11-12"), + HostPortRange: strptr("11-12"), + Protocol: strptr("udp"), + }, }, }, }) @@ -203,6 +211,18 @@ func TestSubmitContainerStateChange(t *testing.T) { ContainerName: "cont", RuntimeID: "runtime id", Status: apicontainerstatus.ContainerRunning, + Container: &apicontainer.Container{ + ContainerArn: "arn", + NetworkModeUnsafe: testNetworkName, + ContainerHasPortRange: true, + ContainerPortSet: map[int]struct{}{ + 1: {}, + 3: {}, + }, + ContainerPortRangeMap: map[string]string{ + "11-12": "11-12", + }, + }, PortBindings: []apicontainer.PortBinding{ { BindIP: "1.2.3.4", @@ -215,6 +235,18 @@ func TestSubmitContainerStateChange(t *testing.T) { HostPort: 4, Protocol: apicontainer.TransportProtocolUDP, }, + { + BindIP: "5.6.7.8", + ContainerPort: 11, + HostPort: 11, + Protocol: apicontainer.TransportProtocolUDP, + }, + { + BindIP: "5.6.7.8", + ContainerPort: 12, + HostPort: 12, + Protocol: apicontainer.TransportProtocolUDP, + }, }, }) if err != nil { @@ -231,21 +263,14 @@ func TestSubmitContainerStateChangeFull(t *testing.T) { mockSubmitStateClient.EXPECT().SubmitContainerStateChange(&containerSubmitInputMatcher{ ecs.SubmitContainerStateChangeInput{ - Cluster: strptr(configuredCluster), - Task: strptr("arn"), - ContainerName: strptr("cont"), - RuntimeId: strptr("runtime id"), - Status: strptr("STOPPED"), - ExitCode: int64ptr(&exitCode), - Reason: strptr(reason), - NetworkBindings: []*ecs.NetworkBinding{ - { - BindIP: strptr(""), - ContainerPort: int64ptr(intptr(0)), - HostPort: int64ptr(intptr(0)), - Protocol: strptr("tcp"), - }, - }, + Cluster: strptr(configuredCluster), + Task: strptr("arn"), + ContainerName: strptr("cont"), + RuntimeId: strptr("runtime id"), + Status: strptr("STOPPED"), + ExitCode: int64ptr(&exitCode), + Reason: strptr(reason), + NetworkBindings: []*ecs.NetworkBinding{}, }, }) err := client.SubmitContainerStateChange(api.ContainerStateChange{ @@ -255,6 +280,9 @@ func TestSubmitContainerStateChangeFull(t *testing.T) { Status: apicontainerstatus.ContainerStopped, ExitCode: &exitCode, Reason: reason, + Container: &apicontainer.Container{ + NetworkModeUnsafe: testNetworkName, + }, PortBindings: []apicontainer.PortBinding{ {}, }, @@ -269,7 +297,7 @@ func TestSubmitContainerStateChangeReason(t *testing.T) { defer mockCtrl.Finish() client, _, mockSubmitStateClient := NewMockClient(mockCtrl, ec2.NewBlackholeEC2MetadataClient(), nil) exitCode := 20 - reason := strings.Repeat("a", ecsMaxReasonLength) + reason := strings.Repeat("a", ecsMaxContainerReasonLength) mockSubmitStateClient.EXPECT().SubmitContainerStateChange(&containerSubmitInputMatcher{ ecs.SubmitContainerStateChangeInput{ @@ -285,9 +313,12 @@ func TestSubmitContainerStateChangeReason(t *testing.T) { err := client.SubmitContainerStateChange(api.ContainerStateChange{ TaskArn: "arn", ContainerName: "cont", - Status: apicontainerstatus.ContainerStopped, - ExitCode: &exitCode, - Reason: reason, + Container: &apicontainer.Container{ + NetworkModeUnsafe: testNetworkName, + }, + Status: apicontainerstatus.ContainerStopped, + ExitCode: &exitCode, + Reason: reason, }) if err != nil { t.Fatal(err) @@ -299,8 +330,8 @@ func TestSubmitContainerStateChangeLongReason(t *testing.T) { defer mockCtrl.Finish() client, _, mockSubmitStateClient := NewMockClient(mockCtrl, ec2.NewBlackholeEC2MetadataClient(), nil) exitCode := 20 - trimmedReason := strings.Repeat("a", ecsMaxReasonLength) - reason := strings.Repeat("a", ecsMaxReasonLength+1) + trimmedReason := strings.Repeat("a", ecsMaxContainerReasonLength) + reason := strings.Repeat("a", ecsMaxContainerReasonLength+1) mockSubmitStateClient.EXPECT().SubmitContainerStateChange(&containerSubmitInputMatcher{ ecs.SubmitContainerStateChangeInput{ @@ -316,9 +347,12 @@ func TestSubmitContainerStateChangeLongReason(t *testing.T) { err := client.SubmitContainerStateChange(api.ContainerStateChange{ TaskArn: "arn", ContainerName: "cont", - Status: apicontainerstatus.ContainerStopped, - ExitCode: &exitCode, - Reason: reason, + Container: &apicontainer.Container{ + NetworkModeUnsafe: testNetworkName, + }, + Status: apicontainerstatus.ContainerStopped, + ExitCode: &exitCode, + Reason: reason, }) if err != nil { t.Errorf("Unable to submit container state change: %v", err) @@ -769,6 +803,44 @@ func TestDiscoverNilTelemetryEndpoint(t *testing.T) { } } +func TestDiscoverServiceConnectEndpoint(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + client, mc, _ := NewMockClient(mockCtrl, ec2.NewBlackholeEC2MetadataClient(), nil) + expectedEndpoint := "http://127.0.0.1" + mc.EXPECT().DiscoverPollEndpoint(gomock.Any()).Return(&ecs.DiscoverPollEndpointOutput{ServiceConnectEndpoint: &expectedEndpoint}, nil) + endpoint, err := client.DiscoverServiceConnectEndpoint("containerInstance") + if err != nil { + t.Error("Error getting service connect endpoint: ", err) + } + if expectedEndpoint != endpoint { + t.Errorf("Expected telemetry endpoint(%s) != endpoint(%s)", expectedEndpoint, endpoint) + } +} + +func TestDiscoverServiceConnectEndpointError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + client, mc, _ := NewMockClient(mockCtrl, ec2.NewBlackholeEC2MetadataClient(), nil) + mc.EXPECT().DiscoverPollEndpoint(gomock.Any()).Return(nil, fmt.Errorf("Error getting endpoint")) + _, err := client.DiscoverServiceConnectEndpoint("containerInstance") + if err == nil { + t.Error("Expected error getting service connect endpoint, didn't get any") + } +} + +func TestDiscoverNilServiceConnectEndpoint(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + client, mc, _ := NewMockClient(mockCtrl, ec2.NewBlackholeEC2MetadataClient(), nil) + pollEndpoint := "http://127.0.0.1" + mc.EXPECT().DiscoverPollEndpoint(gomock.Any()).Return(&ecs.DiscoverPollEndpointOutput{Endpoint: &pollEndpoint}, nil) + _, err := client.DiscoverServiceConnectEndpoint("containerInstance") + if err == nil { + t.Error("Expected error getting service connect endpoint with old response") + } +} + func TestUpdateContainerInstancesState(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -1075,7 +1147,10 @@ func TestSubmitContainerStateChangeWhileTaskInPending(t *testing.T) { TaskArn: "arn", ContainerName: "container", RuntimeID: "runtimeid", - Status: apicontainerstatus.ContainerRunning, + Container: &apicontainer.Container{ + NetworkModeUnsafe: testNetworkName, + }, + Status: apicontainerstatus.ContainerRunning, }, }, } @@ -1113,3 +1188,136 @@ func extractTagsMapFromRegisterContainerInstanceInput(req *ecs.RegisterContainer } return tagsMap } + +func getTestContainerStateChange() api.ContainerStateChange { + testContainer := &apicontainer.Container{ + Name: "cont", + NetworkModeUnsafe: testNetworkName, + Ports: []apicontainer.PortBinding{ + { + ContainerPort: 10, + HostPort: 10, + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 12, + HostPort: 12, + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPort: 15, + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPortRange: "21-22", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPortRange: "96-97", + Protocol: apicontainer.TransportProtocolTCP, + }, + }, + ContainerHasPortRange: true, + ContainerPortSet: map[int]struct{}{ + 10: {}, + 12: {}, + 15: {}, + }, + ContainerPortRangeMap: map[string]string{ + "21-22": "60001-60002", + "96-97": "47001-47002", + }, + } + + testContainerStateChange := api.ContainerStateChange{ + TaskArn: "arn", + ContainerName: "cont", + Status: apicontainerstatus.ContainerRunning, + Container: testContainer, + PortBindings: []apicontainer.PortBinding{ + { + ContainerPort: 10, + HostPort: 10, + BindIP: "0.0.0.0", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 12, + HostPort: 12, + BindIP: "1.2.3.4", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPort: 15, + HostPort: 20, + BindIP: "5.6.7.8", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 21, + HostPort: 60001, + BindIP: "::", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPort: 22, + HostPort: 60002, + BindIP: "::", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPort: 96, + HostPort: 47001, + BindIP: "0.0.0.0", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 97, + HostPort: 47002, + BindIP: "0.0.0.0", + Protocol: apicontainer.TransportProtocolTCP, + }, + }, + } + + return testContainerStateChange +} + +func TestGetNetworkBindings(t *testing.T) { + testContainerStateChange := getTestContainerStateChange() + expectedNetworkBindings := []*ecs.NetworkBinding{ + { + BindIP: strptr("0.0.0.0"), + ContainerPort: int64ptr(intptr(10)), + HostPort: int64ptr(intptr(10)), + Protocol: strptr("tcp"), + }, + { + BindIP: strptr("1.2.3.4"), + ContainerPort: int64ptr(intptr(12)), + HostPort: int64ptr(intptr(12)), + Protocol: strptr("udp"), + }, + { + BindIP: strptr("5.6.7.8"), + ContainerPort: int64ptr(intptr(15)), + HostPort: int64ptr(intptr(20)), + Protocol: strptr("tcp"), + }, + { + BindIP: strptr("::"), + ContainerPortRange: strptr("21-22"), + HostPortRange: strptr("60001-60002"), + Protocol: strptr("udp"), + }, + { + BindIP: strptr("0.0.0.0"), + ContainerPortRange: strptr("96-97"), + HostPortRange: strptr("47001-47002"), + Protocol: strptr("tcp"), + }, + } + + networkBindings := getNetworkBindings(testContainerStateChange, false) + assert.ElementsMatch(t, expectedNetworkBindings, networkBindings) +} diff --git a/agent/api/ecsclient/retry_handler_test.go b/agent/api/ecsclient/retry_handler_test.go index 24f523a0065..2194355ffb9 100644 --- a/agent/api/ecsclient/retry_handler_test.go +++ b/agent/api/ecsclient/retry_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/ecsclient/utils_amd64_test.go b/agent/api/ecsclient/utils_amd64_test.go index 7ecd0e2b17c..5bb95300c54 100644 --- a/agent/api/ecsclient/utils_amd64_test.go +++ b/agent/api/ecsclient/utils_amd64_test.go @@ -1,4 +1,5 @@ //go:build amd64 && unit +// +build amd64,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/ecsclient/utils_arm64_test.go b/agent/api/ecsclient/utils_arm64_test.go index 37d680ad1b0..1a46f13425b 100644 --- a/agent/api/ecsclient/utils_arm64_test.go +++ b/agent/api/ecsclient/utils_arm64_test.go @@ -1,4 +1,5 @@ //go:build arm64 && unit +// +build arm64,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/eni/eni.go b/agent/api/eni/eni.go index d682d3a2ba4..24933e9386a 100644 --- a/agent/api/eni/eni.go +++ b/agent/api/eni/eni.go @@ -214,6 +214,12 @@ func (eni *ENI) GetLinkName() string { break } } + // If the ENI is not matched by MAC address above, we will fail to + // assign the LinkName. Log that here since CNI will fail with the empty + // name. + if eni.LinkName == "" { + seelog.Errorf("Failed to find LinkName for MAC %s", eni.MacAddress) + } } return eni.LinkName diff --git a/agent/api/eni/eni_test.go b/agent/api/eni/eni_test.go index 11b4a3fe52a..2ed2f0b3b15 100644 --- a/agent/api/eni/eni_test.go +++ b/agent/api/eni/eni_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/eni/eniattachment_test.go b/agent/api/eni/eniattachment_test.go index 4c188fd56b5..6f46caecabd 100644 --- a/agent/api/eni/eniattachment_test.go +++ b/agent/api/eni/eniattachment_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/eni/enistatus_test.go b/agent/api/eni/enistatus_test.go index c76acc3b4b1..1f42345ae06 100644 --- a/agent/api/eni/enistatus_test.go +++ b/agent/api/eni/enistatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/generate_mocks.go b/agent/api/generate_mocks.go index c19794a9d82..520dac1f93a 100644 --- a/agent/api/generate_mocks.go +++ b/agent/api/generate_mocks.go @@ -13,4 +13,4 @@ package api -//go:generate mockgen -destination=mocks/api_mocks.go -copyright_file=../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/api ECSSDK,ECSSubmitStateSDK,ECSClient +//go:generate mockgen -destination=mocks/api_mocks.go -copyright_file=../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/api ECSSDK,ECSSubmitStateSDK,ECSClient,AppnetClient,ECSTaskProtectionSDK diff --git a/agent/api/interface.go b/agent/api/interface.go index b053b282a4a..ea1cd484c79 100644 --- a/agent/api/interface.go +++ b/agent/api/interface.go @@ -13,7 +13,13 @@ package api -import "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" +import ( + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + prometheus "github.com/prometheus/client_model/go" +) // ECSClient is an interface over the ECSSDK interface which abstracts away some // details around constructing the request and reading the response down to the @@ -44,6 +50,9 @@ type ECSClient interface { // DiscoverTelemetryEndpoint takes a ContainerInstanceARN and returns the // endpoint at which this Agent should contact Telemetry Service DiscoverTelemetryEndpoint(containerInstanceArn string) (string, error) + // DiscoverServiceConnectEndpoint takes a ContainerInstanceARN and returns the + // endpoint at which this Agent should contact ServiceConnect + DiscoverServiceConnectEndpoint(containerInstanceArn string) (string, error) // GetResourceTags retrieves the Tags associated with a certain resource GetResourceTags(resourceArn string) ([]*ecs.Tag, error) // UpdateContainerInstancesState updates the given container Instance ID with @@ -69,3 +78,21 @@ type ECSSubmitStateSDK interface { SubmitTaskStateChange(*ecs.SubmitTaskStateChangeInput) (*ecs.SubmitTaskStateChangeOutput, error) SubmitAttachmentStateChanges(*ecs.SubmitAttachmentStateChangesInput) (*ecs.SubmitAttachmentStateChangesOutput, error) } + +// AppnetClient is an interface with customized Appnet client that +// implements the GetStats and DrainInboundConnections +type AppnetClient interface { + GetStats(config serviceconnect.RuntimeConfig) (map[string]*prometheus.MetricFamily, error) + DrainInboundConnections(config serviceconnect.RuntimeConfig) error +} + +// ECSTaskProtectionSDK is an interface with customized ecs client that +// implements the UpdateTaskProtection and GetTaskProtection +type ECSTaskProtectionSDK interface { + UpdateTaskProtection(input *ecs.UpdateTaskProtectionInput) (*ecs.UpdateTaskProtectionOutput, error) + UpdateTaskProtectionWithContext(ctx aws.Context, input *ecs.UpdateTaskProtectionInput, + opts ...request.Option) (*ecs.UpdateTaskProtectionOutput, error) + GetTaskProtection(input *ecs.GetTaskProtectionInput) (*ecs.GetTaskProtectionOutput, error) + GetTaskProtectionWithContext(ctx aws.Context, input *ecs.GetTaskProtectionInput, + opts ...request.Option) (*ecs.GetTaskProtectionOutput, error) +} diff --git a/agent/api/mocks/api_mocks.go b/agent/api/mocks/api_mocks.go index cc1eb7c1a00..2800b832fab 100644 --- a/agent/api/mocks/api_mocks.go +++ b/agent/api/mocks/api_mocks.go @@ -13,17 +13,21 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/aws/amazon-ecs-agent/agent/api (interfaces: ECSSDK,ECSSubmitStateSDK,ECSClient) +// Source: github.com/aws/amazon-ecs-agent/agent/api (interfaces: ECSSDK,ECSSubmitStateSDK,ECSClient,AppnetClient,ECSTaskProtectionSDK) // Package mock_api is a generated GoMock package. package mock_api import ( + context "context" reflect "reflect" api "github.com/aws/amazon-ecs-agent/agent/api" + serviceconnect "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" ecs "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" + request "github.com/aws/aws-sdk-go/aws/request" gomock "github.com/golang/mock/gomock" + go0 "github.com/prometheus/client_model/go" ) // MockECSSDK is a mock of ECSSDK interface @@ -230,6 +234,21 @@ func (mr *MockECSClientMockRecorder) DiscoverPollEndpoint(arg0 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverPollEndpoint", reflect.TypeOf((*MockECSClient)(nil).DiscoverPollEndpoint), arg0) } +// DiscoverServiceConnectEndpoint mocks base method +func (m *MockECSClient) DiscoverServiceConnectEndpoint(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DiscoverServiceConnectEndpoint", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DiscoverServiceConnectEndpoint indicates an expected call of DiscoverServiceConnectEndpoint +func (mr *MockECSClientMockRecorder) DiscoverServiceConnectEndpoint(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverServiceConnectEndpoint", reflect.TypeOf((*MockECSClient)(nil).DiscoverServiceConnectEndpoint), arg0) +} + // DiscoverTelemetryEndpoint mocks base method func (m *MockECSClient) DiscoverTelemetryEndpoint(arg0 string) (string, error) { m.ctrl.T.Helper() @@ -331,3 +350,148 @@ func (mr *MockECSClientMockRecorder) UpdateContainerInstancesState(arg0, arg1 in mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContainerInstancesState", reflect.TypeOf((*MockECSClient)(nil).UpdateContainerInstancesState), arg0, arg1) } + +// MockAppnetClient is a mock of AppnetClient interface +type MockAppnetClient struct { + ctrl *gomock.Controller + recorder *MockAppnetClientMockRecorder +} + +// MockAppnetClientMockRecorder is the mock recorder for MockAppnetClient +type MockAppnetClientMockRecorder struct { + mock *MockAppnetClient +} + +// NewMockAppnetClient creates a new mock instance +func NewMockAppnetClient(ctrl *gomock.Controller) *MockAppnetClient { + mock := &MockAppnetClient{ctrl: ctrl} + mock.recorder = &MockAppnetClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockAppnetClient) EXPECT() *MockAppnetClientMockRecorder { + return m.recorder +} + +// DrainInboundConnections mocks base method +func (m *MockAppnetClient) DrainInboundConnections(arg0 serviceconnect.RuntimeConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DrainInboundConnections", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DrainInboundConnections indicates an expected call of DrainInboundConnections +func (mr *MockAppnetClientMockRecorder) DrainInboundConnections(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrainInboundConnections", reflect.TypeOf((*MockAppnetClient)(nil).DrainInboundConnections), arg0) +} + +// GetStats mocks base method +func (m *MockAppnetClient) GetStats(arg0 serviceconnect.RuntimeConfig) (map[string]*go0.MetricFamily, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStats", arg0) + ret0, _ := ret[0].(map[string]*go0.MetricFamily) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStats indicates an expected call of GetStats +func (mr *MockAppnetClientMockRecorder) GetStats(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStats", reflect.TypeOf((*MockAppnetClient)(nil).GetStats), arg0) +} + +// MockECSTaskProtectionSDK is a mock of ECSTaskProtectionSDK interface +type MockECSTaskProtectionSDK struct { + ctrl *gomock.Controller + recorder *MockECSTaskProtectionSDKMockRecorder +} + +// MockECSTaskProtectionSDKMockRecorder is the mock recorder for MockECSTaskProtectionSDK +type MockECSTaskProtectionSDKMockRecorder struct { + mock *MockECSTaskProtectionSDK +} + +// NewMockECSTaskProtectionSDK creates a new mock instance +func NewMockECSTaskProtectionSDK(ctrl *gomock.Controller) *MockECSTaskProtectionSDK { + mock := &MockECSTaskProtectionSDK{ctrl: ctrl} + mock.recorder = &MockECSTaskProtectionSDKMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockECSTaskProtectionSDK) EXPECT() *MockECSTaskProtectionSDKMockRecorder { + return m.recorder +} + +// GetTaskProtection mocks base method +func (m *MockECSTaskProtectionSDK) GetTaskProtection(arg0 *ecs.GetTaskProtectionInput) (*ecs.GetTaskProtectionOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTaskProtection", arg0) + ret0, _ := ret[0].(*ecs.GetTaskProtectionOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTaskProtection indicates an expected call of GetTaskProtection +func (mr *MockECSTaskProtectionSDKMockRecorder) GetTaskProtection(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskProtection", reflect.TypeOf((*MockECSTaskProtectionSDK)(nil).GetTaskProtection), arg0) +} + +// GetTaskProtectionWithContext mocks base method +func (m *MockECSTaskProtectionSDK) GetTaskProtectionWithContext(arg0 context.Context, arg1 *ecs.GetTaskProtectionInput, arg2 ...request.Option) (*ecs.GetTaskProtectionOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetTaskProtectionWithContext", varargs...) + ret0, _ := ret[0].(*ecs.GetTaskProtectionOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTaskProtectionWithContext indicates an expected call of GetTaskProtectionWithContext +func (mr *MockECSTaskProtectionSDKMockRecorder) GetTaskProtectionWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskProtectionWithContext", reflect.TypeOf((*MockECSTaskProtectionSDK)(nil).GetTaskProtectionWithContext), varargs...) +} + +// UpdateTaskProtection mocks base method +func (m *MockECSTaskProtectionSDK) UpdateTaskProtection(arg0 *ecs.UpdateTaskProtectionInput) (*ecs.UpdateTaskProtectionOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTaskProtection", arg0) + ret0, _ := ret[0].(*ecs.UpdateTaskProtectionOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateTaskProtection indicates an expected call of UpdateTaskProtection +func (mr *MockECSTaskProtectionSDKMockRecorder) UpdateTaskProtection(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskProtection", reflect.TypeOf((*MockECSTaskProtectionSDK)(nil).UpdateTaskProtection), arg0) +} + +// UpdateTaskProtectionWithContext mocks base method +func (m *MockECSTaskProtectionSDK) UpdateTaskProtectionWithContext(arg0 context.Context, arg1 *ecs.UpdateTaskProtectionInput, arg2 ...request.Option) (*ecs.UpdateTaskProtectionOutput, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpdateTaskProtectionWithContext", varargs...) + ret0, _ := ret[0].(*ecs.UpdateTaskProtectionOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateTaskProtectionWithContext indicates an expected call of UpdateTaskProtectionWithContext +func (mr *MockECSTaskProtectionSDKMockRecorder) UpdateTaskProtectionWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskProtectionWithContext", reflect.TypeOf((*MockECSTaskProtectionSDK)(nil).UpdateTaskProtectionWithContext), varargs...) +} diff --git a/agent/api/serviceconnect/service_connect.go b/agent/api/serviceconnect/service_connect.go new file mode 100644 index 00000000000..f32703d7f27 --- /dev/null +++ b/agent/api/serviceconnect/service_connect.go @@ -0,0 +1,85 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +const AppNetUID = 20000 // arbitrarily selected + +// Config represents the Service Connect configuration for a task. +type Config struct { + ContainerName string `json:"containerName"` + IngressConfig []IngressConfigEntry `json:"ingressConfig,omitempty"` + EgressConfig *EgressConfig `json:"egressConfig,omitempty"` + DNSConfig []DNSConfigEntry `json:"dnsConfig,omitempty"` + + // Admin configuration for operating with AppNet Agent + RuntimeConfig RuntimeConfig `json:"runtimeConfig"` + // NetworkConfig contains additional network information for setting up task network namespace + NetworkConfig NetworkConfig `json:"networkConfig"` +} + +// RuntimeConfig contains the runtime information for administering AppNet Agent +type RuntimeConfig struct { + // Host path for the administration socket + AdminSocketPath string `json:"adminSocketPath"` + // HTTP Path + Params to get statistical information + StatsRequest string `json:"statsRequest"` + // HTTP Path + Params to drain ServiceConnect connections + DrainRequest string `json:"drainRequest"` +} + +// IngressConfigEntry is the ingress configuration for a given SC service. +type IngressConfigEntry struct { + // ListenerName is the name of the listener for an SC service. + ListenerName string `json:"listenerName"` + // ListenerPort is the port where Envoy listens for ingress traffic for a given SC service. + ListenerPort uint16 `json:"listenerPort"` + // InterceptPort is only relevant for awsvpc mode. If present, SC CNI Plugin will configure netfilter rules to redirect + // traffic destined to this port to ListenerPort. + InterceptPort *uint16 `json:"interceptPort,omitempty"` + // HostPort is only relevant for bridge network mode non-default case, where SC ingress host port is predefined in + // SC Service creation/modification time. + HostPort *uint16 `json:"hostPort,omitempty"` +} + +// EgressConfig is the egress configuration for a given SC service. +type EgressConfig struct { + // ListenerName is the name of the listener for SC service with name ServiceName. + ListenerName string `json:"listenerName"` + // EgressPort represent the port number Envoy will bind to. This port is selected at random by ECS Agent during + // task startup. Port will be in the ephemeral range. + ListenerPort uint16 `json:"listenerPort,omitempty"` + // VIP is the representation of an SC VIP-CIDR + VIP VIP `json:"vip"` +} + +// VIP is the representation of an SC VIP-CIDR +// e.g. 169.254.0.0/16 +type VIP struct { + IPV4CIDR string `json:"ipv4Cidr,omitempty"` + IPV6CIDR string `json:"ipv6Cidr,omitempty"` +} + +// DNSConfigEntry represents a mapping between a VIP in the SC VIP-CIDR and an upstream SC service. +// e.g. DummySCService.my.corp -> 169.254.1.1 +type DNSConfigEntry struct { + HostName string `json:"hostName"` + Address string `json:"address"` +} + +// NetworkConfig contains additional network information for setting up task network namespace. +// This includes SC pause container IP address - used for bridge-mode CNI configuration +type NetworkConfig struct { + SCPauseIPv4Addr string `json:"scPauseIPv4Addr,omitempty"` + SCPauseIPv6Addr string `json:"scPauseIPv6Addr,omitempty"` +} diff --git a/agent/api/serviceconnect/service_connect_attachment_parser.go b/agent/api/serviceconnect/service_connect_attachment_parser.go new file mode 100644 index 00000000000..31c0b96f5e7 --- /dev/null +++ b/agent/api/serviceconnect/service_connect_attachment_parser.go @@ -0,0 +1,78 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "encoding/json" + "fmt" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/aws-sdk-go/aws" +) + +const ( + // serviceConnectConfigKey specifies the key maps to the service connect config in attachment properties + serviceConnectConfigKey = "ServiceConnectConfig" + // serviceConnectContainerNameKey specifies the key maps to the service connect container name in attachment properties + serviceConnectContainerNameKey = "ContainerName" + keyValidationMsgFormat = `missing service connect config required key(s) in the attachment: found service connect config key: %t, found service connect container name key: %t` +) + +func GetServiceConnectConfigKey() string { + return serviceConnectConfigKey +} + +func GetServiceConnectContainerNameKey() string { + return serviceConnectContainerNameKey +} + +// ParseServiceConnectAttachment parses the service connect container name and service connect config value +// from the given attachment. +func ParseServiceConnectAttachment(scAttachment *ecsacs.Attachment) (*Config, error) { + scConfigValue := &Config{} + containerName := "" + foundSCConfigKey := false + foundSCContainerNameKey := false + + for _, property := range scAttachment.AttachmentProperties { + switch aws.StringValue(property.Name) { + case serviceConnectConfigKey: + foundSCConfigKey = true + // extract service connect config value from the attachment property, + // and translate the attachment property value to Config + data := aws.StringValue(property.Value) + if err := json.Unmarshal([]byte(data), scConfigValue); err != nil { + return nil, fmt.Errorf("failed to unmarshal service connect attachment property value: %w", err) + } + case serviceConnectContainerNameKey: + foundSCContainerNameKey = true + // extract service connect container name from the attachment property + containerName = aws.StringValue(property.Value) + default: + logger.Warn("Received an unrecognized attachment property", logger.Fields{ + "attachmentProperty": property.String(), + }) + } + } + + // returns error if service connect config or container name key does not exist + if !foundSCConfigKey || !foundSCContainerNameKey { + return nil, fmt.Errorf(keyValidationMsgFormat, foundSCConfigKey, foundSCContainerNameKey) + } + + scConfigValue.ContainerName = containerName + + return scConfigValue, nil +} diff --git a/agent/api/serviceconnect/service_connect_attachment_parser_test.go b/agent/api/serviceconnect/service_connect_attachment_parser_test.go new file mode 100644 index 00000000000..825ae412a9a --- /dev/null +++ b/agent/api/serviceconnect/service_connect_attachment_parser_test.go @@ -0,0 +1,356 @@ +//go:build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "fmt" + "strings" + "testing" + + "strconv" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" +) + +const ( + testAwsVpcPortOverride = "8080" + testAwsVpcPortDefault = "9090" + testBridgePortOverride = "8080" + testBridgePortDefault = "15000" + testBridgeHostPort = "8080" + testServiceConnectContainerName = "ecs-service-connect" + testServiceConnectAttachmentType = "ServiceConnect" + testHostName = "testHostName" + testAddress = "testAddress" + testOutboundListenerName = "testOutboundListener" + testInboundListenerName = "testInboundListener" + testIPv4Address = "172.31.21.40" + testIPv6Address = "abcd:dcba:1234:4321::" + testIPv4Cidr = "127.255.0.0/16" + testIPv6Cidr = "2002::1234:abcd:ffff:c0a8:101/64" + testEgressConfigFormat = `\"egressConfig\":{\"listenerName\":\"%s\",\"vip\":{\"ipv4Cidr\":\"%s\",\"ipv6Cidr\":\"%s\"}}` + testDnsConfigFormat = `\"dnsConfig\":[{\"hostname\":\"%s\",\"address\":\"%s\"}]` + testIngressConfigAwsVpcDefaultFormat = `\"ingressConfig\":[{\"interceptPort\":%s,\"listenerName\":\"%s\"}]` + testIngressConfigBridgeOverrideFormat = `\"ingressConfig\":[{\"listenerPort\":%s,\"hostPort\":%s}]` + testIngressConfigListenerPortOnlyFormat = `\"ingressConfig\":[{\"listenerPort\":%s}]` +) + +var ( + testAwsVpcDefaultInterceptPort = aws.Uint16(9090) + testListenerPort = uint16(8080) + testBridgeOverrideHostPort = aws.Uint16(8080) + testBridgeDefaultListenerPort = uint16(15000) + testAwsVpcDefaultSCConfig = "" + testAwsVpcDefaultIPv6EnabledSCConfig = "" + testAwsVpcOverrideSCConfig = "" + testAwsVpcOverrideIPv6EnabledSCConfig = "" + testBridgeDefaultSCConfig = "" + testBridgeDefaultEmptyIngressSCConfig = "" + testBridgeDefaultEmptyEgressSCConfig = "" +) + +func initServiceConnectConfValue() { + testAwsVpcDefaultSCConfig = constructTestServiceConnectConfig(AWSVPCNetworkMode, false, false, false, false) + testAwsVpcDefaultIPv6EnabledSCConfig = constructTestServiceConnectConfig(AWSVPCNetworkMode, false, false, false, true) + testAwsVpcOverrideSCConfig = constructTestServiceConnectConfig(AWSVPCNetworkMode, true, false, false, false) + testAwsVpcOverrideIPv6EnabledSCConfig = constructTestServiceConnectConfig(AWSVPCNetworkMode, true, false, false, true) + testBridgeDefaultSCConfig = constructTestServiceConnectConfig(BridgeNetworkMode, false, false, false, false) + testBridgeDefaultEmptyIngressSCConfig = constructTestServiceConnectConfig(BridgeNetworkMode, false, true, false, false) + testBridgeDefaultEmptyEgressSCConfig = constructTestServiceConnectConfig(BridgeNetworkMode, false, false, true, false) +} + +func strptr(s string) *string { return &s } + +// constructTestServiceConnectConfig returns service connect config value as string based on the passed values. +func constructTestServiceConnectConfig(networkMode string, override, emptyIngress, emptyEgress, ipv6Enabled bool) string { + ingressConfig := "" + switch networkMode { + case AWSVPCNetworkMode: + if override { + // awsvpc override case has listener port(s) in the ingress config + ingressConfig = fmt.Sprintf(testIngressConfigListenerPortOnlyFormat, testAwsVpcPortOverride) + } else { + // awsvpc default case has intercept port(s) and listener name(s) in the ingress config + ingressConfig = fmt.Sprintf(testIngressConfigAwsVpcDefaultFormat, testAwsVpcPortDefault, testInboundListenerName) + } + case BridgeNetworkMode: + if override { + // bridge override case has listener port(s) and host port(s) in the ingress config + ingressConfig = fmt.Sprintf(testIngressConfigBridgeOverrideFormat, testBridgePortOverride, testBridgeHostPort) + } else { + // bridge default case has listener port(s) in the ingress config + ingressConfig = fmt.Sprintf(testIngressConfigListenerPortOnlyFormat, testBridgePortDefault) + } + } + + testEgressConfig := fmt.Sprintf(testEgressConfigFormat, testOutboundListenerName, testIPv4Cidr, "") + testDnsConfig := fmt.Sprintf(testDnsConfigFormat, testHostName, testIPv4Address) + if ipv6Enabled { + testEgressConfig = fmt.Sprintf(testEgressConfigFormat, testOutboundListenerName, "", testIPv6Cidr) + testDnsConfig = fmt.Sprintf(testDnsConfigFormat, testHostName, testIPv6Address) + } + + testServiceConnectConfig := strings.Join([]string{`"{`, + testEgressConfig + `,`, + testDnsConfig + `,`, + ingressConfig, + `}"`, + }, "") + + if emptyIngress { + testServiceConnectConfig = strings.Join([]string{`"{`, + testEgressConfig + `,`, + testDnsConfig, + `}"`, + }, "") + } + + if emptyEgress { + testServiceConnectConfig = strings.Join([]string{`"{`, + ingressConfig, + `}"`, + }, "") + } + + unquotedSCConfig, _ := strconv.Unquote(testServiceConnectConfig) + return unquotedSCConfig +} + +// getTestACSAttachmentProperty returns *ecsacs.AttachmentProperty with passed parameters. +func getTestACSAttachmentProperty(propertyName, propertyValue string) *ecsacs.AttachmentProperty { + return &ecsacs.AttachmentProperty{ + Name: strptr(propertyName), + Value: strptr(propertyValue), + } +} + +// getTestACSAttachments returns *ecsacs.Task.getTestACSAttachments. +func getTestACSAttachments(attachmentProperties []*ecsacs.AttachmentProperty) *ecsacs.Attachment { + return &ecsacs.Attachment{ + AttachmentArn: strptr("attachmentArn"), + AttachmentProperties: attachmentProperties, + AttachmentType: strptr(testServiceConnectAttachmentType), + } +} + +// getExpectedTestServiceConnectConfig returns *Config based on given parameters. +func getExpectedTestServiceConnectConfig(scContainerName string, + scIngressConfig []IngressConfigEntry, + scEgressConfig *EgressConfig, + scDNSConfig []DNSConfigEntry) *Config { + return &Config{ + ContainerName: scContainerName, + IngressConfig: scIngressConfig, + EgressConfig: scEgressConfig, + DNSConfig: scDNSConfig, + } +} + +func TestParseServiceConnectAttachment(t *testing.T) { + initServiceConnectConfValue() + testSCContainerNameAttachmentProperty := getTestACSAttachmentProperty(serviceConnectContainerNameKey, testServiceConnectContainerName) + tt := []struct { + testName string + testSCAttachmentProperty *ecsacs.AttachmentProperty + expectedIngressConfig []IngressConfigEntry + expectedEgressConfig *EgressConfig + expectedDnsConfig []DNSConfigEntry + }{ + { + testName: "AWSVPC default case", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testAwsVpcDefaultSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + InterceptPort: testAwsVpcDefaultInterceptPort, + ListenerName: testInboundListenerName, + }, + }, + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: "", + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv4Address, + }, + }, + }, + { + testName: "AWSVPC default case with IPv6 enabled", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testAwsVpcDefaultIPv6EnabledSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + InterceptPort: testAwsVpcDefaultInterceptPort, + ListenerName: testInboundListenerName, + }, + }, + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: "", + IPV6CIDR: testIPv6Cidr, + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv6Address, + }, + }, + }, + { + testName: "AWSVPC override case", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testAwsVpcOverrideSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + ListenerPort: testListenerPort, + }, + }, + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: "", + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv4Address, + }, + }, + }, + { + testName: "AWSVPC override case with IPv6 enabled", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testAwsVpcOverrideIPv6EnabledSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + ListenerPort: testListenerPort, + }, + }, + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: "", + IPV6CIDR: testIPv6Cidr, + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv6Address, + }, + }, + }, + { + testName: "Bridge default case", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testBridgeDefaultSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + ListenerPort: testBridgeDefaultListenerPort, + }, + }, + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: "", + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv4Address, + }, + }, + }, + { + testName: "Bridge default case with no ingress config", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testBridgeDefaultEmptyIngressSCConfig), + expectedEgressConfig: &EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: "", + }, + }, + expectedDnsConfig: []DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv4Address, + }, + }, + }, + { + testName: "Bridge default case with no egress config and dns config", + testSCAttachmentProperty: getTestACSAttachmentProperty(serviceConnectConfigKey, testBridgeDefaultEmptyEgressSCConfig), + expectedIngressConfig: []IngressConfigEntry{ + { + ListenerPort: testBridgeDefaultListenerPort, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + expectedTestServiceConnectConfig := getExpectedTestServiceConnectConfig(testServiceConnectContainerName, + tc.expectedIngressConfig, + tc.expectedEgressConfig, + tc.expectedDnsConfig) + testAttachmentProperties := []*ecsacs.AttachmentProperty{testSCContainerNameAttachmentProperty} + testAttachmentProperties = append(testAttachmentProperties, tc.testSCAttachmentProperty) + testSCAttachment := getTestACSAttachments(testAttachmentProperties) + parsedServiceConnectConfig, err := ParseServiceConnectAttachment(testSCAttachment) + assert.NoError(t, err) + assert.Equal(t, expectedTestServiceConnectConfig, parsedServiceConnectConfig) + }) + } +} + +func TestParseServiceConnectAttachmentWithError(t *testing.T) { + initServiceConnectConfValue() + testSCAttachmentProperty := getTestACSAttachmentProperty(serviceConnectConfigKey, testAwsVpcDefaultSCConfig) + tt := []struct { + testName string + testSCContainerName string + testAttachmentPropertyValue string + }{ + { + testName: "AWSVPC default case with the invalid attachment property value", + testSCContainerName: testServiceConnectContainerName, + testAttachmentPropertyValue: "////hellooooooo////worlddddddd", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + testSCAttachmentProperty = getTestACSAttachmentProperty(serviceConnectConfigKey, tc.testAttachmentPropertyValue) + testSCContainerNameAttachmentProperty := getTestACSAttachmentProperty(serviceConnectContainerNameKey, tc.testSCContainerName) + testAttachmentProperties := []*ecsacs.AttachmentProperty{testSCAttachmentProperty} + testAttachmentProperties = append(testAttachmentProperties, testSCContainerNameAttachmentProperty) + testSCAttachment := getTestACSAttachments(testAttachmentProperties) + _, err := ParseServiceConnectAttachment(testSCAttachment) + assert.Error(t, err) + }) + } +} diff --git a/agent/api/serviceconnect/service_connect_validator.go b/agent/api/serviceconnect/service_connect_validator.go new file mode 100644 index 00000000000..c8e7425f8b0 --- /dev/null +++ b/agent/api/serviceconnect/service_connect_validator.go @@ -0,0 +1,315 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "fmt" + "net" + "strings" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/aws-sdk-go/aws" +) + +const ( + BridgeNetworkMode = "bridge" + AWSVPCNetworkMode = "awsvpc" + missingContainerInTaskFormat = `service connect container name=%s does not exist in the task` + duplicateContainerInTaskFormat = `found %d duplicate service connect container name=%s in the task` + invalidCidrFormat = `CIDR=%s is not a valid %s CIDR` + portCollisionFormat = `%s port collision detected in the ingress config with %s port=%d, and listener name=%s` + invalidDnsAddressFormat = `hostname=%s, address=%s in the DNS config is not valid: %w` + noScSupportNetworkModeFormat = `service connect does not support for %s newtork mode` + missingListenerInIngressFormat = `missing listener name in the ingress config with intercept port=%d` + invalidPortRangeFormat = `port=%d is not a valid port. A valid port ranges from 1 through 65535` + invalidIpAddressFormat = `address=%s is not a valid IP address` + invalidIngressPortFormat = `%s port=%d in the ingress config is not valid: %w` + warningIngressPortFormat = `Service connect config: %s port should not exist in the ingress config for %s network mode` + missingDnsEntryFormat = `missing %s in the DNS config with hostname=%s, and address=%s` +) + +// validateContainerName validates the service connect container name exists in the task and no duplication. +func validateContainerName(scContainerName string, taskContainers []*ecsacs.Container) error { + // service connect container name is required + if scContainerName == "" { + return fmt.Errorf("missing service connect container name") + } + + // validate the specified service connect container name exists in the task + numOfFoundScContainer := 0 + for _, container := range taskContainers { + if aws.StringValue(container.Name) == scContainerName { + numOfFoundScContainer += 1 + } + } + + if numOfFoundScContainer == 0 { + return fmt.Errorf(missingContainerInTaskFormat, scContainerName) + } else if numOfFoundScContainer > 1 { + return fmt.Errorf(duplicateContainerInTaskFormat, numOfFoundScContainer, scContainerName) + } + + return nil +} + +// validateEgressConfig validates the listener name, IPv4 CIDR format and IPv6 CIDR format in the service connect egress config. +func validateEgressConfig(scEgressConfig *EgressConfig, ipv6Enabled bool) error { + // egress config can be empty for the first service since there are no other tasks that it can talk to + if scEgressConfig == nil { + return nil + } + + // ListenerName is required if the egress config exists + if scEgressConfig.ListenerName == "" { + return fmt.Errorf("missing listener name in the egress config") + } + + // validate IPV4CIDR if it exists + if scEgressConfig.VIP.IPV4CIDR != "" { + trimmedIpv4Cidr := strings.TrimSpace(scEgressConfig.VIP.IPV4CIDR) + if err := validateCIDR(trimmedIpv4Cidr, "IPv4"); err != nil { + return err + } + } + + // validate IPV6CIDR if it exists + if scEgressConfig.VIP.IPV6CIDR != "" { + trimmedIpv6Cidr := strings.TrimSpace(scEgressConfig.VIP.IPV6CIDR) + if err := validateCIDR(trimmedIpv6Cidr, "IPv6"); err != nil { + return err + } + } + + return nil +} + +// validateCIDR validates the passed CIDR is a valid IPv4/IPv6 CIDR based on the protocol. +func validateCIDR(cidr, protocol string) error { + ip, _, err := net.ParseCIDR(cidr) + if err == nil { + if valid := getProtocol(ip, protocol); valid { + return nil + } + } + + return fmt.Errorf(invalidCidrFormat, cidr, protocol) +} + +// getProtocol returns validity of the given IP based on the target protocol. +func getProtocol(ip net.IP, protocol string) bool { + switch protocol { + case "IPv4": + if ip.To4() != nil { + return true + } + case "IPv6": + if ip.To16() != nil { + return true + } + default: + return false + } + return false +} + +// validateDnsConfig validates hostnames and addresses in the service connnect DNS config. +func validateDnsConfig(scDnsConfligList []DNSConfigEntry) error { + for _, dnsEntry := range scDnsConfligList { + // HostName is required + if dnsEntry.HostName == "" { + return fmt.Errorf(missingDnsEntryFormat, "hostname", dnsEntry.HostName, dnsEntry.Address) + } + + // Address is required + if dnsEntry.Address == "" { + return fmt.Errorf(missingDnsEntryFormat, "address", dnsEntry.HostName, dnsEntry.Address) + } + + // validate the address is a valid IPv4/IPv6 address + if err := validateAddress(dnsEntry.Address); err != nil { + return fmt.Errorf(invalidDnsAddressFormat, dnsEntry.HostName, dnsEntry.Address, err) + } + } + + return nil +} + +// validateAddress validates the passed address is a valid IPv4/IPv6 address. +func validateAddress(address string) error { + if ip := net.ParseIP(address); ip == nil { + return fmt.Errorf(invalidIpAddressFormat, address) + } + return nil +} + +// validateIngressConfig validates the service connect ingress config based on given network mode. +func validateIngressConfig(scIngressConfigList []IngressConfigEntry, taskNetworkMode string) error { + // ingress config can be empty since an ECS service can only act as a client + if len(scIngressConfigList) == 0 { + return nil + } + + switch taskNetworkMode { + case BridgeNetworkMode, AWSVPCNetworkMode: + if err := validateIngressConfigEntry(scIngressConfigList, taskNetworkMode); err != nil { + return err + } + default: + return fmt.Errorf(noScSupportNetworkModeFormat, taskNetworkMode) + } + + return nil +} + +// validateIngressConfigEntry validates the service connect ingress config entry based on given network mode. +func validateIngressConfigEntry(scIngressConfigList []IngressConfigEntry, networkMode string) error { + interceptAndListenerPortsMap := map[uint16]bool{} + hostPortsMap := map[uint16]bool{} + listenerPortValue := uint16(0) + interceptPortValue := uint16(0) + hostPortValue := uint16(0) + + for _, entry := range scIngressConfigList { + // show a warning message if + // 1) a host port exists in the ingress config for awsvpc mode + // 2) an intercept port exists in the ingress config for bridge mode + if (entry.HostPort != nil && networkMode == AWSVPCNetworkMode) || + (entry.InterceptPort != nil && networkMode == BridgeNetworkMode) { + invalidPort := "a host" + if networkMode == BridgeNetworkMode { + invalidPort = "an intercept" + } + warningMsg := fmt.Sprintf(warningIngressPortFormat, invalidPort, networkMode) + logger.Warn(warningMsg, logger.Fields{ + "listenerName": entry.ListenerName, + "listenerPort": entry.ListenerPort, + "hostPort": aws.Uint16Value(entry.HostPort), + "interceptPort": aws.Uint16Value(entry.InterceptPort), + }) + } + + // verify the intercept port for awsvpc mode + if entry.InterceptPort != nil && networkMode == AWSVPCNetworkMode { + interceptPortValue = aws.Uint16Value(entry.InterceptPort) + if err := validateInterceptPort(interceptPortValue, entry.ListenerName, interceptAndListenerPortsMap); err != nil { + return err + } + // save the listener port value + interceptAndListenerPortsMap[interceptPortValue] = true + } + + // verify the listener port + if entry.ListenerPort > uint16(0) { + listenerPortValue = entry.ListenerPort + if err := validateListenerPort(listenerPortValue, entry.ListenerName, interceptAndListenerPortsMap); err != nil { + return err + } + // save the listener port value + interceptAndListenerPortsMap[listenerPortValue] = true + } + + // verify the host port for bridge mode + if entry.HostPort != nil && networkMode == BridgeNetworkMode { + hostPortValue = aws.Uint16Value(entry.HostPort) + if err := validateHostPort(hostPortValue, entry.ListenerName, hostPortsMap); err != nil { + return err + } + // save the host port value + hostPortsMap[hostPortValue] = true + } + } + + return nil +} + +// validateInterceptPort validates the intercept port is in the valid port range and does not have port collision. +func validateInterceptPort(interceptPortValue uint16, listenerName string, interceptAndListenerPortsMap map[uint16]bool) error { + if err := validatePort(interceptPortValue); err != nil { + return fmt.Errorf(invalidIngressPortFormat, "intercept", interceptPortValue, err) + } + + if listenerName == "" { + return fmt.Errorf(missingListenerInIngressFormat, interceptPortValue) + } + + if present := interceptAndListenerPortsMap[interceptPortValue]; present { + return fmt.Errorf(portCollisionFormat, "intercept", "intercept", interceptPortValue, listenerName) + } + + return nil +} + +// validateListenerPort validates the listener port is in the valid port range and does not have port collision. +func validateListenerPort(listenerPortValue uint16, listenerName string, interceptAndListenerPortsMap map[uint16]bool) error { + if err := validatePort(listenerPortValue); err != nil { + return fmt.Errorf(invalidIngressPortFormat, "listener", listenerPortValue, err) + } + + if present := interceptAndListenerPortsMap[listenerPortValue]; present { + return fmt.Errorf(portCollisionFormat, "listener", "listener", listenerPortValue, listenerName) + } + + return nil +} + +// validateHostPort validates the host port is in the valid port range and does not have port collision. +func validateHostPort(hostPortValue uint16, listenerName string, hostPortsMap map[uint16]bool) error { + if err := validatePort(hostPortValue); err != nil { + return fmt.Errorf(invalidIngressPortFormat, "host", hostPortValue, err) + } + + if present := hostPortsMap[hostPortValue]; present { + return fmt.Errorf(portCollisionFormat, "host", "host", hostPortValue, listenerName) + } + + return nil +} + +// validatePort validates port is in valid range. +func validatePort(port uint16) error { + // valid port range is 1~65535 + if port >= uint16(1) && port <= uint16(65535) { + return nil + } + + return fmt.Errorf(invalidPortRangeFormat, port) +} + +// ValidateServiceConnectConfig validates service connect container name, +// fields in egress config, dns config and ingress config when +// 1) fields consumed and proceeded by ECS Agent +// 2) fields with a global standard, e.g. CIDR format +func ValidateServiceConnectConfig(scConfig *Config, + taskContainers []*ecsacs.Container, + taskNetworkMode string, + ipv6Enabled bool) error { + if err := validateContainerName(scConfig.ContainerName, taskContainers); err != nil { + return err + } + + if err := validateEgressConfig(scConfig.EgressConfig, ipv6Enabled); err != nil { + return err + } + + if err := validateDnsConfig(scConfig.DNSConfig); err != nil { + return err + } + + if err := validateIngressConfig(scConfig.IngressConfig, taskNetworkMode); err != nil { + return err + } + + return nil +} diff --git a/agent/api/serviceconnect/service_connect_validator_test.go b/agent/api/serviceconnect/service_connect_validator_test.go new file mode 100644 index 00000000000..ba4e2b7fc82 --- /dev/null +++ b/agent/api/serviceconnect/service_connect_validator_test.go @@ -0,0 +1,433 @@ +//go:build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "testing" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" +) + +var ( + testTaskContainers = []*ecsacs.Container{ + { + Name: aws.String(testServiceConnectContainerName), + }, + { + Name: aws.String("testTaskFirstContainerName"), + }, + { + Name: aws.String("testTaskSecondContainerName"), + }, + } +) + +// getTestServiceConnectConfig returns *Config based on given parameters. +func getTestServiceConnectConfig( + scContainerName string, + scEgressConfig *EgressConfig, + scDnsConfig []DNSConfigEntry, + scIngressConfig []IngressConfigEntry) *Config { + return &Config{ + ContainerName: scContainerName, + IngressConfig: scIngressConfig, + EgressConfig: scEgressConfig, + DNSConfig: scDnsConfig, + } +} + +// getTestEgressConfig returns *EgressConfig based on given parameters. +func getTestEgressConfig(scListenerName, scIPv4Cidr, scIPv6Cidr string) *EgressConfig { + return &EgressConfig{ + ListenerName: scListenerName, + VIP: VIP{ + IPV4CIDR: scIPv4Cidr, + IPV6CIDR: scIPv6Cidr, + }, + } +} + +// getTestDnsConfig returns DNSConfigEntry based on given parameters. +func getTestDnsConfigEntry(scHostname, scAddress string) DNSConfigEntry { + return DNSConfigEntry{ + HostName: testHostName, + Address: scAddress, + } +} + +// getTestIngressConfigEntry returns IngressConfigEntry based on given parameters. +func getTestIngressConfigEntry(networkMode, scListenerName string, + override bool, + scListenerPort uint16, + scInterceptPort, scHostPort *uint16) IngressConfigEntry { + var entry IngressConfigEntry + switch networkMode { + case AWSVPCNetworkMode: + if override { + // awsvpc override case has listener port(s) in the ingress config + entry = IngressConfigEntry{ + ListenerPort: scListenerPort, + } + } else { + // awsvpc default case has intercept port(s) and listener name(s) in the ingress config + entry = IngressConfigEntry{ + ListenerName: scListenerName, + InterceptPort: scInterceptPort, + } + } + case BridgeNetworkMode: + if override { + // bridge override case has listener port(s) and host port(s) in the ingress config + entry = IngressConfigEntry{ + ListenerPort: scListenerPort, + HostPort: scHostPort, + } + } else { + // bridge default case has listener port(s) in the ingress config + entry = IngressConfigEntry{ + ListenerPort: scListenerPort, + } + } + } + return entry +} +func TestValidateServiceConnectConfig(t *testing.T) { + tt := []struct { + testName string + testNetworkMode string + testIsIPv6Enabled bool + testEgressConfig *EgressConfig + testDnsConfigEntry DNSConfigEntry + testIngressConfigEntry IngressConfigEntry + }{ + { + testName: "AWSVPC default case", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC default case with IPv6 enabled", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: true, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, testIPv6Cidr), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv6Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC override case", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "AWSVPC override case with IPv6 enabled", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: true, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, testIPv6Cidr), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv6Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge default case", + testNetworkMode: BridgeNetworkMode, + testIsIPv6Enabled: false, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", false, testBridgeDefaultListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge default case with IPv6 enabled", + testNetworkMode: BridgeNetworkMode, + testIsIPv6Enabled: true, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, testIPv6Cidr), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv6Address), + testIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", false, testBridgeDefaultListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge override case", + testNetworkMode: BridgeNetworkMode, + testIsIPv6Enabled: false, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + }, + { + testName: "Bridge override case with IPv6 enabled", + testNetworkMode: BridgeNetworkMode, + testIsIPv6Enabled: true, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, testIPv6Cidr), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv6Address), + testIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + dnsConfig := []DNSConfigEntry{} + dnsConfig = append(dnsConfig, tc.testDnsConfigEntry) + ingressConfig := []IngressConfigEntry{} + ingressConfig = append(ingressConfig, tc.testIngressConfigEntry) + testServiceConnectConfig := getTestServiceConnectConfig( + testServiceConnectContainerName, + tc.testEgressConfig, + dnsConfig, + ingressConfig, + ) + err := ValidateServiceConnectConfig(testServiceConnectConfig, testTaskContainers, tc.testNetworkMode, tc.testIsIPv6Enabled) + assert.NoError(t, err) + }) + } +} + +func TestValidateServiceConnectConfigWithWarning(t *testing.T) { + tt := []struct { + testName string + testNetworkMode string + testEgressConfig *EgressConfig + testDnsConfigEntry DNSConfigEntry + testFirstIngressConfigEntry IngressConfigEntry + testSecondIngressConfigEntry IngressConfigEntry + }{ + { + testName: "AWSVPC default case with both intercept port and host port in the ingress config", + testNetworkMode: AWSVPCNetworkMode, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testFirstIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + testSecondIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + }, + { + testName: "Bridge override case with both host port and intercept port in the ingress config", + testNetworkMode: BridgeNetworkMode, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testFirstIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + testSecondIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + dnsConfig := []DNSConfigEntry{} + dnsConfig = append(dnsConfig, tc.testDnsConfigEntry) + ingressConfig := []IngressConfigEntry{} + ingressConfig = append(ingressConfig, tc.testFirstIngressConfigEntry) + ingressConfig = append(ingressConfig, tc.testSecondIngressConfigEntry) + testServiceConnectConfig := getTestServiceConnectConfig( + testServiceConnectContainerName, + tc.testEgressConfig, + dnsConfig, + ingressConfig, + ) + err := ValidateServiceConnectConfig(testServiceConnectConfig, testTaskContainers, tc.testNetworkMode, false) + assert.NoError(t, err) + }) + } +} + +func TestValidateServiceConnectConfigWithEmptyConfig(t *testing.T) { + var testEgressConfig *EgressConfig + var testDnsConfig []DNSConfigEntry + var testIngressConfig []IngressConfigEntry + tt := []struct { + testName string + testEgressConfigIsEmpty bool + testIngressConfigIsEmpty bool + }{ + { + testName: "AWSVPC default case with the empty egress config and dns config", + testEgressConfigIsEmpty: true, + testIngressConfigIsEmpty: false, + }, + { + testName: "AWSVPC default case with the empty ingress config", + testEgressConfigIsEmpty: false, + testIngressConfigIsEmpty: true, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + if tc.testEgressConfigIsEmpty { + testEgressConfig = nil + testDnsConfig = []DNSConfigEntry{} + } else { + testEgressConfig = getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, "") + testDnsConfig = append(testDnsConfig, getTestDnsConfigEntry(testHostName, testIPv4Address)) + } + + if tc.testIngressConfigIsEmpty { + testIngressConfig = []IngressConfigEntry{} + } else { + testIngressConfig = append(testIngressConfig, getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0))) + } + + testServiceConnectConfig := getTestServiceConnectConfig( + testServiceConnectContainerName, + testEgressConfig, + testDnsConfig, + testIngressConfig, + ) + err := ValidateServiceConnectConfig(testServiceConnectConfig, testTaskContainers, AWSVPCNetworkMode, false) + assert.NoError(t, err) + }) + } +} + +func TestValidateServiceConnectConfigWithError(t *testing.T) { + tt := []struct { + testName string + testNetworkMode string + testIsIPv6Enabled bool + testContainerName string + testEgressConfig *EgressConfig + testDnsConfigEntry DNSConfigEntry + testIngressConfigEntry IngressConfigEntry + }{ + { + testName: "AWSVPC default case with no service connect container name", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testContainerName: "", + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC default case with the service connect container name not exists in the task", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testContainerName: "helloworld", + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC default case with no listener name in the egress config", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testContainerName: testServiceConnectContainerName, + testEgressConfig: getTestEgressConfig("", testIPv4Cidr, ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC override case with the invalid IPv4 CIDR in the egress config", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: false, + testContainerName: testServiceConnectContainerName, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, "999999999", ""), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv4Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "AWSVPC override case with the invalid IPv6 CIDR in the egress config when IPv6 is enabled", + testNetworkMode: AWSVPCNetworkMode, + testIsIPv6Enabled: true, + testContainerName: testServiceConnectContainerName, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, "999999999"), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, testIPv6Address), + testIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge override case with no the invalid address in dns config when IPv6 is enabled", + testNetworkMode: BridgeNetworkMode, + testIsIPv6Enabled: true, + testContainerName: testServiceConnectContainerName, + testEgressConfig: getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, testIPv6Cidr), + testDnsConfigEntry: getTestDnsConfigEntry(testHostName, "999999999"), + testIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + dnsConfig := []DNSConfigEntry{} + dnsConfig = append(dnsConfig, tc.testDnsConfigEntry) + ingressConfig := []IngressConfigEntry{} + ingressConfig = append(ingressConfig, tc.testIngressConfigEntry) + + testServiceConnectConfig := getTestServiceConnectConfig( + tc.testContainerName, + tc.testEgressConfig, + dnsConfig, + ingressConfig, + ) + err := ValidateServiceConnectConfig(testServiceConnectConfig, testTaskContainers, tc.testNetworkMode, tc.testIsIPv6Enabled) + assert.Error(t, err) + }) + } +} + +func TestValidateServiceConnectConfigWithPortCollision(t *testing.T) { + testEgressConfig := getTestEgressConfig(testOutboundListenerName, testIPv4Cidr, "") + testDnsConfigEntry := getTestDnsConfigEntry(testHostName, testIPv4Address) + tt := []struct { + testName string + testNetworkMode string + testFirstIngressConfigEntry IngressConfigEntry + testSecondIngressConfigEntry IngressConfigEntry + }{ + { + testName: "AWSVPC default case with the intercept port collision in the ingress config", + testNetworkMode: AWSVPCNetworkMode, + testFirstIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + testSecondIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, testInboundListenerName, false, uint16(0), testAwsVpcDefaultInterceptPort, aws.Uint16(0)), + }, + { + testName: "AWSVPC override case with the listener port collision in the ingress config", + testNetworkMode: AWSVPCNetworkMode, + testFirstIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + testSecondIngressConfigEntry: getTestIngressConfigEntry(AWSVPCNetworkMode, "", true, testListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge default case with the listener port collision in the ingress config", + testNetworkMode: BridgeNetworkMode, + testFirstIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", false, testBridgeDefaultListenerPort, aws.Uint16(0), aws.Uint16(0)), + testSecondIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", false, testBridgeDefaultListenerPort, aws.Uint16(0), aws.Uint16(0)), + }, + { + testName: "Bridge override case with the host port collision in the ingress config", + testNetworkMode: BridgeNetworkMode, + testFirstIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + testSecondIngressConfigEntry: getTestIngressConfigEntry(BridgeNetworkMode, "", true, testListenerPort, aws.Uint16(0), testBridgeOverrideHostPort), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + dnsConfig := []DNSConfigEntry{testDnsConfigEntry} + ingressConfig := []IngressConfigEntry{} + ingressConfig = append(ingressConfig, tc.testFirstIngressConfigEntry) + ingressConfig = append(ingressConfig, tc.testSecondIngressConfigEntry) + testServiceConnectConfig := getTestServiceConnectConfig( + testServiceConnectContainerName, + testEgressConfig, + dnsConfig, + ingressConfig, + ) + err := ValidateServiceConnectConfig(testServiceConnectConfig, testTaskContainers, tc.testNetworkMode, false) + assert.Error(t, err) + }) + } +} diff --git a/agent/api/statechange.go b/agent/api/statechange.go index cee70577b63..73915f12b54 100644 --- a/agent/api/statechange.go +++ b/agent/api/statechange.go @@ -105,6 +105,9 @@ type AttachmentStateChange struct { // returns error if the state change doesn't need to be sent to the ECS backend. func NewTaskStateChangeEvent(task *apitask.Task, reason string) (TaskStateChange, error) { var event TaskStateChange + if task.IsInternal { + return event, errors.Errorf("skip creating task stage change event for internal task %v", task.Arn) + } taskKnownStatus := task.GetKnownStatus() if !taskKnownStatus.BackendRecognized() { return event, errors.Errorf( @@ -161,6 +164,14 @@ func newUncheckedContainerStateChangeEvent(task *apitask.Task, cont *apicontaine "create container state change event api: internal container: %s", cont.Name) } + portBindings := cont.GetKnownPortBindings() + if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() { + pauseCont, err := task.GetBridgeModePauseContainerForTaskContainer(cont) + if err != nil { + return event, fmt.Errorf("error resolving pause container for bridge mode SC container: %s", cont.Name) + } + portBindings = pauseCont.GetKnownPortBindings() + } contKnownStatus := cont.GetKnownStatus() event = ContainerStateChange{ TaskArn: task.Arn, @@ -168,7 +179,7 @@ func newUncheckedContainerStateChangeEvent(task *apitask.Task, cont *apicontaine RuntimeID: cont.GetRuntimeID(), Status: contKnownStatus.BackendStatus(cont.GetSteadyStateStatus()), ExitCode: cont.GetKnownExitCode(), - PortBindings: cont.GetKnownPortBindings(), + PortBindings: portBindings, ImageDigest: cont.GetImageDigest(), Reason: reason, Container: cont, diff --git a/agent/api/statechange_test.go b/agent/api/statechange_test.go index 72ed57f027e..0240f6dfa7d 100644 --- a/agent/api/statechange_test.go +++ b/agent/api/statechange_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -22,6 +23,7 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" execcmd "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" @@ -195,6 +197,100 @@ func TestNewUncheckedContainerStateChangeEvent(t *testing.T) { } } +func TestNewUncheckedContainerStateChangeEvent_SCBridge(t *testing.T) { + testContainerName := "c1" + tests := []struct { + name string + addPauseContainer bool + pauseContainerName string + pauseContainerPortBindings []apicontainer.PortBinding + err error + }{ + { + name: "should fail to resolve pause container - pause container name doesn't match", + addPauseContainer: true, + pauseContainerName: "invalid-pause-container-name", + pauseContainerPortBindings: []apicontainer.PortBinding{{}}, + err: fmt.Errorf("error resolving pause container for bridge mode SC container: %s", testContainerName), + }, + { + name: "should use pause container port mapping", + addPauseContainer: true, + pauseContainerName: fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, testContainerName), + pauseContainerPortBindings: []apicontainer.PortBinding{{ + ContainerPort: 1, + HostPort: 2, + BindIP: "1.2.3.4", + Protocol: 3, + }}, + err: nil, + }, + { + name: "should fail to resolve pause container - no pause container available", + addPauseContainer: false, + err: fmt.Errorf("error resolving pause container for bridge mode SC container: %s", testContainerName), + }, + } + steadyStateStatus := apicontainerstatus.ContainerRunning + exitCode := 1 + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + task := &apitask.Task{ + Arn: "arn:123", + NetworkMode: apitask.BridgeNetworkMode, + ServiceConnectConfig: &serviceconnect.Config{ + ContainerName: "service-connect", + }, + Containers: []*apicontainer.Container{ + { + Name: testContainerName, + RuntimeID: "222", + KnownStatusUnsafe: apicontainerstatus.ContainerRunning, + SentStatusUnsafe: apicontainerstatus.ContainerStatusNone, + Type: apicontainer.ContainerNormal, + SteadyStateStatusUnsafe: &steadyStateStatus, + KnownExitCodeUnsafe: &exitCode, + KnownPortBindingsUnsafe: []apicontainer.PortBinding{{ + ContainerPort: 8080, // we get this from task definition + }}, + ImageDigest: "image", + }, + { + Name: "service-connect", + }, + }} + if tc.addPauseContainer { + task.Containers = append(task.Containers, &apicontainer.Container{ + Name: tc.pauseContainerName, + Type: apicontainer.ContainerCNIPause, + KnownPortBindingsUnsafe: tc.pauseContainerPortBindings, + }) + } + + expectedEvent := ContainerStateChange{ + TaskArn: "arn:123", + ContainerName: testContainerName, + RuntimeID: "222", + Status: apicontainerstatus.ContainerRunning, + ExitCode: &exitCode, + PortBindings: tc.pauseContainerPortBindings, + ImageDigest: "image", + Reason: "reason", + Container: task.Containers[0], + } + + event, err := newUncheckedContainerStateChangeEvent(task, task.Containers[0], "reason") + if tc.err == nil { + assert.NoError(t, err) + assert.Equal(t, expectedEvent, event) + } else { + assert.Error(t, err) + assert.Equal(t, tc.err, err) + } + }) + } +} + func TestNewManagedAgentChangeEvent(t *testing.T) { tests := []struct { name string diff --git a/agent/api/task/association_test.go b/agent/api/task/association_test.go index a3056eae414..0544af0aab6 100644 --- a/agent/api/task/association_test.go +++ b/agent/api/task/association_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/task/status/statusmapping_test.go b/agent/api/task/status/statusmapping_test.go index a4f345f34bc..0d5c757bc92 100644 --- a/agent/api/task/status/statusmapping_test.go +++ b/agent/api/task/status/statusmapping_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/task/status/taskstatus_test.go b/agent/api/task/status/taskstatus_test.go index 270223bee9c..4a9938581d6 100644 --- a/agent/api/task/status/taskstatus_test.go +++ b/agent/api/task/status/taskstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/task/task.go b/agent/api/task/task.go index 93958671e53..d2f95d3b192 100644 --- a/agent/api/task/task.go +++ b/agent/api/task/task.go @@ -24,27 +24,24 @@ import ( "sync" "time" - "github.com/aws/amazon-ecs-agent/agent/logger" - "github.com/aws/amazon-ecs-agent/agent/logger/field" - "github.com/aws/amazon-ecs-agent/agent/utils/ttime" - "github.com/aws/aws-sdk-go/aws" - "github.com/docker/docker/api/types" - "github.com/docker/go-connections/nat" - "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" apiappmesh "github.com/aws/amazon-ecs-agent/agent/api/appmesh" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/taskresource/asmauth" "github.com/aws/amazon-ecs-agent/agent/taskresource/asmsecret" + "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" "github.com/aws/amazon-ecs-agent/agent/taskresource/envFiles" "github.com/aws/amazon-ecs-agent/agent/taskresource/firelens" "github.com/aws/amazon-ecs-agent/agent/taskresource/ssmsecret" @@ -52,14 +49,21 @@ import ( resourcetype "github.com/aws/amazon-ecs-agent/agent/taskresource/types" taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/amazon-ecs-agent/agent/utils/ttime" + + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil" + "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" "github.com/pkg/errors" ) const ( // NetworkPauseContainerName is the internal name for the pause container NetworkPauseContainerName = "~internal~ecs~pause" + // ServiceConnectPauseContainerNameFormat is the naming format for SC pause containers + ServiceConnectPauseContainerNameFormat = "~internal~ecs~pause-%s" // NamespacePauseContainerName is the internal name for the IPC resource namespace and/or // PID namespace sharing pause container @@ -78,8 +82,9 @@ const ( // neuronRuntime is the name of the neuron docker runtime. neuronRuntime = "neuron" - ContainerOrderingCreateCondition = "CREATE" - ContainerOrderingStartCondition = "START" + ContainerOrderingCreateCondition = "CREATE" + ContainerOrderingStartCondition = "START" + ContainerOrderingHealthyCondition = "HEALTHY" // networkModeNone specifies the string used to define the `none` docker networking mode networkModeNone = "none" @@ -140,10 +145,18 @@ const ( // specifies awsvpc type mode for a task AWSVPCNetworkMode = "awsvpc" + // specifies host type mode for a task + HostNetworkMode = "host" + // disableIPv6SysctlKey specifies the setting that controls whether ipv6 is disabled. disableIPv6SysctlKey = "net.ipv6.conf.all.disable_ipv6" // sysctlValueOff specifies the value to use to turn off a sysctl setting. sysctlValueOff = "0" + + serviceConnectListenerPortMappingEnvVar = "APPNET_LISTENER_PORT_MAPPING" + serviceConnectContainerMappingEnvVar = "APPNET_CONTAINER_IP_MAPPING" + // ServiceConnectAttachmentType specifies attachment type for service connect + serviceConnectAttachmentType = "serviceconnectdetail" ) // TaskOverrides are the overrides applied to a task @@ -161,6 +174,9 @@ type Task struct { Family string // Version is the version of the task definition Version string + // ServiceName is the name of the service to which the task belongs. + // It is empty if the task does not belong to any service. + ServiceName string // Containers are the containers for the task Containers []*apicontainer.Container // Associations are the available associations for the task. @@ -273,6 +289,14 @@ type Task struct { // setIdOnce is used to set the value of this task's id only the first time GetID is invoked setIdOnce sync.Once + + ServiceConnectConfig *serviceconnect.Config `json:"ServiceConnectConfig,omitempty"` + + ServiceConnectConnectionDrainingUnsafe bool `json:"ServiceConnectConnectionDraining,omitempty"` + + NetworkMode string `json:"NetworkMode,omitempty"` + + IsInternal bool `json:"IsInternal,omitempty"` } // TaskFromACS translates ecsacs.Task to apitask.Task by first marshaling the received @@ -302,6 +326,14 @@ func TaskFromACS(acsTask *ecsacs.Task, envelope *ecsacs.PayloadMessage) (*Task, //initialize resources map for task task.ResourcesMapUnsafe = make(map[string][]taskresource.TaskResource) + + task.initNetworkMode(acsTask.NetworkMode) + + // extract and validate attachments + if err := handleTaskAttachments(acsTask, task); err != nil { + return nil, err + } + return task, nil } @@ -327,29 +359,29 @@ func (task *Task) initializeVolumes(cfg *config.Config, dockerClient dockerapi.D func (task *Task) PostUnmarshalTask(cfg *config.Config, credentialsManager credentials.Manager, resourceFields *taskresource.ResourceFields, dockerClient dockerapi.DockerClient, ctx context.Context, options ...Option) error { + + task.adjustForPlatform(cfg) + // TODO, add rudimentary plugin support and call any plugins that want to // hook into this - task.adjustForPlatform(cfg) - if task.MemoryCPULimitsEnabled { - if err := task.initializeCgroupResourceSpec(cfg.CgroupPath, cfg.CgroupCPUPeriod, resourceFields); err != nil { - logger.Error("Could not initialize resource", logger.Fields{ - field.TaskID: task.GetID(), - field.Error: err, - }) - return apierrors.NewResourceInitError(task.Arn, err) - } + if err := task.initializeCgroupResourceSpec(cfg.CgroupPath, cfg.CgroupCPUPeriod, resourceFields); err != nil { + logger.Error("Could not initialize resource", logger.Fields{ + field.TaskID: task.GetID(), + field.Error: err, + }) + return apierrors.NewResourceInitError(task.Arn, err) } - if err := task.initializeContainerOrderingForVolumes(); err != nil { - logger.Error("Could not initialize volumes dependency for container", logger.Fields{ + if err := task.initServiceConnectResources(); err != nil { + logger.Error("Could not initialize Service Connect resources", logger.Fields{ field.TaskID: task.GetID(), field.Error: err, }) return apierrors.NewResourceInitError(task.Arn, err) } - if err := task.initializeContainerOrderingForLinks(); err != nil { - logger.Error("Could not initialize links dependency for container", logger.Fields{ + if err := task.initializeContainerOrdering(); err != nil { + logger.Error("Could not initialize dependency for container", logger.Fields{ field.TaskID: task.GetID(), field.Error: err, }) @@ -376,6 +408,7 @@ func (task *Task) PostUnmarshalTask(cfg *config.Config, task.initializeContainersV3MetadataEndpoint(utils.NewDynamicUUIDProvider()) task.initializeContainersV4MetadataEndpoint(utils.NewDynamicUUIDProvider()) + task.initializeContainersV1AgentAPIEndpoint(utils.NewDynamicUUIDProvider()) if err := task.addNetworkResourceProvisioningDependency(cfg); err != nil { logger.Error("Could not provision network resource", logger.Fields{ field.TaskID: task.GetID(), @@ -432,6 +465,149 @@ func (task *Task) PostUnmarshalTask(cfg *config.Config, return nil } +// initializeCredentialSpecResource builds the resource dependency map for the credentialspec resource +func (task *Task) initializeCredentialSpecResource(config *config.Config, credentialsManager credentials.Manager, + resourceFields *taskresource.ResourceFields) error { + credspecContainerMapping := task.getAllCredentialSpecRequirements() + credentialspecResource, err := credentialspec.NewCredentialSpecResource(task.Arn, config.AWSRegion, task.ExecutionCredentialsID, + credentialsManager, resourceFields.SSMClientCreator, resourceFields.S3ClientCreator, credspecContainerMapping) + if err != nil { + return err + } + + task.AddResource(credentialspec.ResourceName, credentialspecResource) + + // for every container that needs credential spec vending, it needs to wait for all credential spec resources + for _, container := range task.Containers { + if container.RequiresCredentialSpec() { + container.BuildResourceDependency(credentialspecResource.GetName(), + resourcestatus.ResourceStatus(credentialspec.CredentialSpecCreated), + apicontainerstatus.ContainerCreated) + } + } + + return nil +} + +// initNetworkMode initializes/infers the network mode for the task and assigns the result to this task's NetworkMode field. +// ACS is streaming down this value with task payload. In case of docker bridge mode task, this value might be left empty +// as it's the default task network mode. +func (task *Task) initNetworkMode(acsTaskNetworkMode *string) { + switch aws.StringValue(acsTaskNetworkMode) { + case AWSVPCNetworkMode: + task.NetworkMode = AWSVPCNetworkMode + case HostNetworkMode: + task.NetworkMode = HostNetworkMode + case BridgeNetworkMode, "": + task.NetworkMode = BridgeNetworkMode + case networkModeNone: + task.NetworkMode = networkModeNone + default: + logger.Warn("Unmapped task network mode", logger.Fields{ + field.TaskID: task.GetID(), + field.NetworkMode: aws.StringValue(acsTaskNetworkMode), + }) + } + logger.Info("Task network mode initialized", logger.Fields{ + field.TaskID: task.GetID(), + field.NetworkMode: task.NetworkMode, + }) +} + +func (task *Task) initServiceConnectResources() error { + // TODO [SC]: ServiceConnectConfig will come from ACS. Adding this here for dev/testing purposes only Remove when + // ACS model is integrated + if task.ServiceConnectConfig == nil { + task.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: "service-connect", + } + } + if task.IsServiceConnectEnabled() { + // TODO [SC]: initDummyServiceConnectConfig is for dev testing only, remove it when final SC model from ACS is in place + task.initDummyServiceConnectConfig() + if err := task.initServiceConnectEphemeralPorts(); err != nil { + return err + } + } + return nil +} + +// TODO [SC]: This is for dev testing only, remove it when final SC model from ACS is in place +func (task *Task) initDummyServiceConnectConfig() { + scContainer := task.GetServiceConnectContainer() + if _, ok := scContainer.Environment["SC_CONFIG"]; !ok { + // no SC_CONFIG :( + return + } + if err := json.Unmarshal([]byte(scContainer.Environment["SC_CONFIG"]), task.ServiceConnectConfig); err != nil { + logger.Error("Error parsing SC_CONFIG", logger.Fields{ + field.Error: err, + }) + return + } +} + +func (task *Task) initServiceConnectEphemeralPorts() error { + var utilizedPorts []uint16 + // First determine how many ephemeral ports we need + var numEphemeralPortsNeeded int + for _, ic := range task.ServiceConnectConfig.IngressConfig { + if ic.ListenerPort == 0 { // This means listener port was not sent to us by ACS, signaling the port needs to be ephemeral + numEphemeralPortsNeeded++ + } else { + utilizedPorts = append(utilizedPorts, ic.ListenerPort) + } + } + + // Presently, SC egress port is always ephemeral, but adding this for future-proofing + if task.ServiceConnectConfig.EgressConfig != nil { + if task.ServiceConnectConfig.EgressConfig.ListenerPort == 0 { + numEphemeralPortsNeeded++ + } else { + utilizedPorts = append(utilizedPorts, task.ServiceConnectConfig.EgressConfig.ListenerPort) + } + } + + // Get all exposed ports in the task so that the ephemeral port generator doesn't take those into account in order + // to avoid port conflicts. + for _, c := range task.Containers { + for _, p := range c.Ports { + utilizedPorts = append(utilizedPorts, p.ContainerPort) + } + } + + ephemeralPorts, err := utils.GenerateEphemeralPortNumbers(numEphemeralPortsNeeded, utilizedPorts) + if err != nil { + return fmt.Errorf("error initializing ports for Service Connect: %w", err) + } + + // Assign ephemeral ports + portMapping := make(map[string]uint16) + var curEphemeralIndex int + for i, ic := range task.ServiceConnectConfig.IngressConfig { + if ic.ListenerPort == 0 { + portMapping[ic.ListenerName] = ephemeralPorts[curEphemeralIndex] + task.ServiceConnectConfig.IngressConfig[i].ListenerPort = ephemeralPorts[curEphemeralIndex] + curEphemeralIndex++ + } + } + + if task.ServiceConnectConfig.EgressConfig != nil && task.ServiceConnectConfig.EgressConfig.ListenerPort == 0 { + portMapping[task.ServiceConnectConfig.EgressConfig.ListenerName] = ephemeralPorts[curEphemeralIndex] + task.ServiceConnectConfig.EgressConfig.ListenerPort = ephemeralPorts[curEphemeralIndex] + } + + // Add the APPNET_LISTENER_PORT_MAPPING env var for listeners that require it + envVars := make(map[string]string) + portMappingJson, err := json.Marshal(portMapping) + if err != nil { + return fmt.Errorf("error injecting required env vars to Service Connect container: %w", err) + } + envVars[serviceConnectListenerPortMappingEnvVar] = string(portMappingJson) + task.GetServiceConnectContainer().MergeEnvironmentVariables(envVars) + return nil +} + // populateTaskARN populates the arn of the task to the containers. func (task *Task) populateTaskARN() { for _, c := range task.Containers { @@ -809,12 +985,8 @@ func (task *Task) initializeCredentialsEndpoint(credentialsManager credentials.M // initializeContainersV3MetadataEndpoint generates an v3 endpoint id for each container, constructs the // v3 metadata endpoint, and injects it as an environment variable func (task *Task) initializeContainersV3MetadataEndpoint(uuidProvider utils.UUIDProvider) { + task.initializeV3EndpointIDForAllContainers(uuidProvider) for _, container := range task.Containers { - v3EndpointID := container.GetV3EndpointID() - if v3EndpointID == "" { // if container's v3 endpoint has not been set - container.SetV3EndpointID(uuidProvider.New()) - } - container.InjectV3MetadataEndpoint() } } @@ -823,13 +995,30 @@ func (task *Task) initializeContainersV3MetadataEndpoint(uuidProvider utils.UUID // (they are the same) for each container, constructs the v4 metadata endpoint, // and injects it as an environment variable func (task *Task) initializeContainersV4MetadataEndpoint(uuidProvider utils.UUIDProvider) { + task.initializeV3EndpointIDForAllContainers(uuidProvider) + for _, container := range task.Containers { + container.InjectV4MetadataEndpoint() + } +} + +// For each container of the task, initializeContainersV1AgentAPIEndpoint initializes +// its V3EndpointID (if not already initialized), and injects V1 Agent API Endpoint +// into the container. +func (task *Task) initializeContainersV1AgentAPIEndpoint(uuidProvider utils.UUIDProvider) { + task.initializeV3EndpointIDForAllContainers(uuidProvider) + for _, container := range task.Containers { + container.InjectV1AgentAPIEndpoint() + } +} + +// Initializes V3EndpointID for all containers of the task if not already initialized. +// The ID is generated using the passed in UUIDProvider. +func (task *Task) initializeV3EndpointIDForAllContainers(uuidProvider utils.UUIDProvider) { for _, container := range task.Containers { v3EndpointID := container.GetV3EndpointID() if v3EndpointID == "" { // if container's v3 endpoint has not been set container.SetV3EndpointID(uuidProvider.New()) } - - container.InjectV4MetadataEndpoint() } } @@ -1204,13 +1393,29 @@ func (task *Task) AddFirelensContainerBindMounts(firelensConfig *apicontainer.Fi // IsNetworkModeAWSVPC checks if the task is configured to use the AWSVPC task networking feature. func (task *Task) IsNetworkModeAWSVPC() bool { - return len(task.ENIs) > 0 + return task.NetworkMode == AWSVPCNetworkMode +} + +// IsNetworkModeBridge checks if the task is configured to use the bridge network mode. +func (task *Task) IsNetworkModeBridge() bool { + return task.NetworkMode == BridgeNetworkMode +} + +// IsNetworkModeHost checks if the task is configured to use the host network mode. +func (task *Task) IsNetworkModeHost() bool { + return task.NetworkMode == HostNetworkMode } func (task *Task) addNetworkResourceProvisioningDependency(cfg *config.Config) error { - if !task.IsNetworkModeAWSVPC() { - return nil + if task.IsNetworkModeAWSVPC() { + return task.addNetworkResourceProvisioningDependencyAwsvpc(cfg) + } else if task.IsNetworkModeBridge() && task.IsServiceConnectEnabled() { + return task.addNetworkResourceProvisioningDependencyServiceConnectBridge(cfg) } + return nil +} + +func (task *Task) addNetworkResourceProvisioningDependencyAwsvpc(cfg *config.Config) error { pauseContainer := apicontainer.NewContainerWithSteadyState(apicontainerstatus.ContainerResourcesProvisioned) pauseContainer.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) pauseContainer.Name = NetworkPauseContainerName @@ -1279,6 +1484,79 @@ func (task *Task) addNetworkResourceProvisioningDependency(cfg *config.Config) e return nil } +// addNetworkResourceProvisioningDependencyServiceConnectBridge creates one pause container per task container +// including SC container, and add a dependency for SC container to wait for all pause container RESOURCES_PROVISIONED. +// +// SC pause container will use CNI plugin for configuring tproxy, while other pause container(s) will configure ip route +// to send SC traffic to SC container +func (task *Task) addNetworkResourceProvisioningDependencyServiceConnectBridge(cfg *config.Config) error { + scContainer := task.GetServiceConnectContainer() + var scPauseContainer *apicontainer.Container + for _, container := range task.Containers { + if container.IsInternal() { + continue + } + pauseContainer := apicontainer.NewContainerWithSteadyState(apicontainerstatus.ContainerResourcesProvisioned) + pauseContainer.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + // The pause container name is used internally by task engine but still needs to be unique for every task, + // hence we are appending the corresponding application container name (which must already be unique within the task) + pauseContainer.Name = fmt.Sprintf(ServiceConnectPauseContainerNameFormat, container.Name) + pauseContainer.Image = fmt.Sprintf("%s:%s", cfg.PauseContainerImageName, cfg.PauseContainerTag) + pauseContainer.Essential = true + pauseContainer.Type = apicontainer.ContainerCNIPause + + task.Containers = append(task.Containers, pauseContainer) + // SC container CREATED will depend on ALL pause containers RESOURCES_PROVISIONED + scContainer.BuildContainerDependency(pauseContainer.Name, apicontainerstatus.ContainerResourcesProvisioned, apicontainerstatus.ContainerCreated) + pauseContainer.BuildContainerDependency(scContainer.Name, apicontainerstatus.ContainerStopped, apicontainerstatus.ContainerStopped) + if container == scContainer { + scPauseContainer = pauseContainer + } + } + + // All other task pause container RESOURCES_PROVISIONED depends on SC pause container RUNNING because task pause container + // CNI plugin invocation needs the IP of SC pause container (to send SC traffic to) + for _, container := range task.Containers { + if container.Type != apicontainer.ContainerCNIPause || container == scPauseContainer { + continue + } + container.BuildContainerDependency(scPauseContainer.Name, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerResourcesProvisioned) + } + return nil +} + +// GetBridgeModePauseContainerForTaskContainer retrieves the associated pause container for a task container (SC container +// or customer-defined containers) in a bridge-mode SC-enabled task. +// For a container with name "abc", the pause container will always be named "~internal~ecs~pause-abc" +func (task *Task) GetBridgeModePauseContainerForTaskContainer(container *apicontainer.Container) (*apicontainer.Container, error) { + // "~internal~ecs~pause-$TASK_CONTAINER_NAME" + pauseContainerName := fmt.Sprintf(ServiceConnectPauseContainerNameFormat, container.Name) + pauseContainer, ok := task.ContainerByName(pauseContainerName) + if !ok { + return nil, fmt.Errorf("could not find pause container %s for task container %s", pauseContainerName, container.Name) + } + return pauseContainer, nil +} + +// getBridgeModeTaskContainerForPauseContainer retrieves the associated task container for a pause container in a bridge-mode SC-enabled task. +// For a container with name "abc", the pause container will always be named "~internal~ecs~pause-abc" +func (task *Task) getBridgeModeTaskContainerForPauseContainer(container *apicontainer.Container) (*apicontainer.Container, error) { + if container.Type != apicontainer.ContainerCNIPause { + return nil, fmt.Errorf("container %s is not a CNI pause container", container.Name) + } + // limit the result to 2 substrings as $TASK_CONTAINER_NAME may also container '-' + stringSlice := strings.SplitN(container.Name, "-", 2) + if len(stringSlice) < 2 { + return nil, fmt.Errorf("SC bridge mode pause container %s does not conform to %s-$TASK_CONTAINER_NAME format", container.Name, NetworkPauseContainerName) + } + taskContainerName := stringSlice[1] + taskContainer, ok := task.ContainerByName(taskContainerName) + if !ok { + return nil, fmt.Errorf("could not find task container %s for pause container %s", taskContainerName, container.Name) + } + return taskContainer, nil +} + func (task *Task) addNamespaceSharingProvisioningDependency(cfg *config.Config) { // Pause container does not need to be created if no namespace sharing will be done at task level if task.getIPCMode() != ipcModeTask && task.getPIDMode() != pidModeTask { @@ -1443,14 +1721,26 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc entryPoint = *container.EntryPoint } + var exposedPorts nat.PortSet + var err error + if exposedPorts, err = task.dockerExposedPorts(container); err != nil { + return nil, &apierrors.DockerClientConfigError{Msg: "error resolving docker exposed ports for container: " + err.Error()} + } + containerConfig := &dockercontainer.Config{ Image: container.Image, Cmd: container.Command, Entrypoint: entryPoint, - ExposedPorts: task.dockerExposedPorts(container), + ExposedPorts: exposedPorts, Env: dockerEnv, } + // TODO [SC] - Move this as well as 'dockerExposedPorts' SC-specific logic into a separate file + if (task.IsServiceConnectEnabled() && container == task.GetServiceConnectContainer()) || + container.Type == apicontainer.ContainerServiceConnectRelay { + containerConfig.User = strconv.Itoa(serviceconnect.AppNetUID) + } + if container.DockerConfig.Config != nil { if err := json.Unmarshal([]byte(aws.StringValue(container.DockerConfig.Config)), &containerConfig); err != nil { return nil, &apierrors.DockerClientConfigError{Msg: "Unable decode given docker config: " + err.Error()} @@ -1465,7 +1755,7 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc containerConfig.Labels = make(map[string]string) } - if container.Type == apicontainer.ContainerCNIPause { + if container.Type == apicontainer.ContainerCNIPause && task.IsNetworkModeAWSVPC() { // apply hostname to pause container's docker config return task.applyENIHostname(containerConfig), nil } @@ -1473,14 +1763,66 @@ func (task *Task) dockerConfig(container *apicontainer.Container, apiVersion doc return containerConfig, nil } -func (task *Task) dockerExposedPorts(container *apicontainer.Container) nat.PortSet { - dockerExposedPorts := make(map[nat.Port]struct{}) - - for _, portBinding := range container.Ports { - dockerPort := nat.Port(strconv.Itoa(int(portBinding.ContainerPort)) + "/" + portBinding.Protocol.String()) - dockerExposedPorts[dockerPort] = struct{}{} +// dockerExposedPorts returns the container ports that need to be exposed for a container +// 1. For bridge-mode ServiceConnect-enabled tasks: +// 1a. Pause containers need to expose the port(s) for their associated task container. In particular, SC pause container +// +// needs to expose all listener ports for SC container +// +// 1b. Other containers (customer-defined task containers as well as SC container) will not expose any ports as they are +// +// already exposed through pause container +// 2. For all other tasks, we expose the application container ports. +func (task *Task) dockerExposedPorts(container *apicontainer.Container) (dockerExposedPorts nat.PortSet, err error) { + containerToCheck := container + scContainer := task.GetServiceConnectContainer() + dockerExposedPorts = make(map[nat.Port]struct{}) + + if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() { + if container.Type == apicontainer.ContainerCNIPause { + // find the task container associated with this particular pause container, and let pause container + // expose the application container port + containerToCheck, err = task.getBridgeModeTaskContainerForPauseContainer(container) + if err != nil { + return nil, err + } + // if the associated task container is SC container, expose all its ingress and egress listener ports if present + if containerToCheck == scContainer { + for _, ic := range task.ServiceConnectConfig.IngressConfig { + dockerPort := nat.Port(strconv.Itoa(int(ic.ListenerPort))) + "/tcp" + dockerExposedPorts[dockerPort] = struct{}{} + } + ec := task.ServiceConnectConfig.EgressConfig + if ec != nil { // it's possible that task does not have an egress listener + dockerPort := nat.Port(strconv.Itoa(int(ec.ListenerPort))) + "/tcp" + dockerExposedPorts[dockerPort] = struct{}{} + } + return dockerExposedPorts, nil + } + } else { + // This is a task container which is launched with "--network container:$pause_container_id" + // In such case we don't expose any ports (docker won't allow anyway) because they are exposed by their + // pause container instead. + return dockerExposedPorts, nil + } + } + + for _, portBinding := range containerToCheck.Ports { + protocol := portBinding.Protocol.String() + // per port binding config, either one of ContainerPort or ContainerPortRange is set + if portBinding.ContainerPort != 0 { + dockerPort := nat.Port(strconv.Itoa(int(portBinding.ContainerPort)) + "/" + protocol) + dockerExposedPorts[dockerPort] = struct{}{} + } else if portBinding.ContainerPortRange != "" { + // we supply containerPortRange here in case we did not assign a host port range and ask docker to do so + dockerPortRange, err := nat.NewPort(protocol, portBinding.ContainerPortRange) + if err != nil { + return nil, err + } + dockerExposedPorts[dockerPortRange] = struct{}{} + } } - return dockerExposedPorts + return dockerExposedPorts, nil } // DockerHostConfig construct the configuration recognized by docker @@ -1518,8 +1860,10 @@ func (task *Task) dockerHostConfig(container *apicontainer.Container, dockerCont if err != nil { return nil, &apierrors.HostConfigError{Msg: err.Error()} } - - dockerPortMap := task.dockerPortMap(container) + dockerPortMap, err := task.dockerPortMap(container, cfg.DynamicHostPortRange) + if err != nil { + return nil, &apierrors.HostConfigError{Msg: fmt.Sprintf("error retrieving docker port map: %+v", err.Error())} + } volumesFrom, err := task.dockerVolumesFrom(container, dockerContainerMap) if err != nil { @@ -1562,7 +1906,7 @@ func (task *Task) dockerHostConfig(container *apicontainer.Container, dockerCont if ok { hostConfig.NetworkMode = dockercontainer.NetworkMode(networkMode) // Override 'awsvpc' parameters if needed - if container.Type == apicontainer.ContainerCNIPause { + if container.Type == apicontainer.ContainerCNIPause && task.IsNetworkModeAWSVPC() { // apply ExtraHosts to HostConfig for pause container if hosts := task.generateENIExtraHosts(); hosts != nil { hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, hosts...) @@ -1579,15 +1923,8 @@ func (task *Task) dockerHostConfig(container *apicontainer.Container, dockerCont } } - ok, pidMode := task.shouldOverridePIDMode(container, dockerContainerMap) - if ok { - hostConfig.PidMode = dockercontainer.PidMode(pidMode) - } - - ok, ipcMode := task.shouldOverrideIPCMode(container, dockerContainerMap) - if ok { - hostConfig.IpcMode = dockercontainer.IpcMode(ipcMode) - } + task.pidModeOverride(container, dockerContainerMap, hostConfig) + task.ipcModeOverride(container, dockerContainerMap, hostConfig) return hostConfig, nil } @@ -1655,10 +1992,22 @@ func (task *Task) shouldOverrideNetworkMode(container *apicontainer.Container, d // TODO. We can do an early return here by determining which kind of task it is // Example: Does this task have ENIs in its payload, what is its networking mode etc if container.IsInternal() { - // If it's an internal container, set the network mode to none. - // Currently, internal containers are either for creating empty host - // volumes or for creating the 'pause' container. Both of these - // only need the network mode to be set to "none" + // If it's a CNI pause container, set the network mode to none for awsvpc, set to bridge if task is using + // bridge mode and this is an SC-enabled task. + // If it's a ServiceConnect relay container, the container is internally managed, and should keep its "host" + // network mode by design + // Other internal containers are either for creating empty host volumes or for creating the 'pause' container. + // Both of these only need the network mode to be set to "none" + if container.Type == apicontainer.ContainerCNIPause { + if task.IsNetworkModeAWSVPC() { + return true, networkModeNone + } else if task.IsNetworkModeBridge() && task.IsServiceConnectEnabled() { + return true, BridgeNetworkMode + } + } + if container.Type == apicontainer.ContainerServiceConnectRelay { + return false, "" + } return true, networkModeNone } @@ -1667,10 +2016,15 @@ func (task *Task) shouldOverrideNetworkMode(container *apicontainer.Container, d // when using non docker daemon supported network modes, its existence // indicates the need to configure the network mode outside of supported // network drivers - if !task.IsNetworkModeAWSVPC() { - return false, "" + if task.IsNetworkModeAWSVPC() { + return task.shouldOverrideNetworkModeAwsvpc(container, dockerContainerMap) + } else if task.IsNetworkModeBridge() && task.IsServiceConnectEnabled() { + return task.shouldOverrideNetworkModeServiceConnectBridge(container, dockerContainerMap) } + return false, "" +} +func (task *Task) shouldOverrideNetworkModeAwsvpc(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer) (bool, string) { pauseContName := "" for _, cont := range task.Containers { if cont.Type == apicontainer.ContainerCNIPause { @@ -1696,6 +2050,34 @@ func (task *Task) shouldOverrideNetworkMode(container *apicontainer.Container, d return true, dockerMappingContainerPrefix + pauseContainer.DockerID } +// shouldOverrideNetworkModeServiceConnectBridge checks if a bridge-mode SC task container needs network mode override +// For non-internal containers in an SC bridge-mode task, each gets a pause container, and should be launched +// with container network mode use pause container netns (the "docker run" equivalent option is +// "--network container:$pause_container_id") +func (task *Task) shouldOverrideNetworkModeServiceConnectBridge(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer) (bool, string) { + pauseContainer, err := task.GetBridgeModePauseContainerForTaskContainer(container) + if err != nil { + // This should never be the case and implies a code-bug. + logger.Critical("Pause container required per task container for Service Connect task bridge mode, but "+ + "not found for task container", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + }) + return false, "" + } + dockerPauseContainer, ok := dockerContainerMap[pauseContainer.Name] + if !ok || dockerPauseContainer == nil { + // This should never be the case and implies a code-bug. + logger.Critical("Pause container required per task container for Service Connect task bridge mode, but "+ + "not found in docker container map for task container", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + }) + return false, "" + } + return true, dockerMappingContainerPrefix + dockerPauseContainer.DockerID +} + // overrideDNS overrides a container's host config if the following conditions are // true: // 1. Task has an ENI associated with it @@ -1754,6 +2136,14 @@ func (task *Task) generateENIExtraHosts() []string { return extraHosts } +func (task *Task) shouldEnableIPv4() bool { + eni := task.GetPrimaryENI() + if eni == nil { + return false + } + return len(eni.GetIPV4Addresses()) > 0 +} + func (task *Task) shouldEnableIPv6() bool { eni := task.GetPrimaryENI() if eni == nil { @@ -1762,20 +2152,23 @@ func (task *Task) shouldEnableIPv6() bool { return len(eni.GetIPV6Addresses()) > 0 } -// shouldOverridePIDMode returns true if the PIDMode of the container needs -// to be overridden. It also returns the override string in this case. It returns -// false otherwise -func (task *Task) shouldOverridePIDMode(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer) (bool, string) { +func setPIDMode(hostConfig *dockercontainer.HostConfig, pidMode string) { + hostConfig.PidMode = dockercontainer.PidMode(pidMode) +} + +// pidModeOverride sets the PIDMode of the container if needed +func (task *Task) pidModeOverride(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer, hostConfig *dockercontainer.HostConfig) { // If the container is an internal container (ContainerEmptyHostVolume, // ContainerCNIPause, or ContainerNamespacePause), then PID namespace for // the container itself should be private (default Docker option) if container.IsInternal() { - return false, "" + return } switch task.getPIDMode() { case pidModeHost: - return true, pidModeHost + setPIDMode(hostConfig, pidModeHost) + return case pidModeTask: pauseCont, ok := task.ContainerByName(NamespacePauseContainerName) @@ -1784,7 +2177,7 @@ func (task *Task) shouldOverridePIDMode(container *apicontainer.Container, docke field.TaskID: task.GetID(), }) task.SetDesiredStatus(apitaskstatus.TaskStopped) - return false, "" + return } pauseDockerID, ok := dockerContainerMap[pauseCont.Name] if !ok || pauseDockerID == nil { @@ -1793,20 +2186,23 @@ func (task *Task) shouldOverridePIDMode(container *apicontainer.Container, docke field.TaskID: task.GetID(), }) task.SetDesiredStatus(apitaskstatus.TaskStopped) - return false, "" + return } - return true, dockerMappingContainerPrefix + pauseDockerID.DockerID + setPIDMode(hostConfig, dockerMappingContainerPrefix+pauseDockerID.DockerID) + return // If PIDMode is not Host or Task, then no need to override default: - return false, "" + break } } -// shouldOverrideIPCMode returns true if the IPCMode of the container needs -// to be overridden. It also returns the override string in this case. It returns -// false otherwise -func (task *Task) shouldOverrideIPCMode(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer) (bool, string) { +func setIPCMode(hostConfig *dockercontainer.HostConfig, mode string) { + hostConfig.IpcMode = dockercontainer.IpcMode(mode) +} + +// ipcModeOverride will override the IPCMode of the container if needed +func (task *Task) ipcModeOverride(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer, hostConfig *dockercontainer.HostConfig) { // All internal containers do not need the same IPCMode. The NamespaceContainerPause // needs to be "shareable" if ipcMode is "task". All other internal containers should // defer to the Docker daemon default option (either shareable or private depending on @@ -1815,24 +2211,23 @@ func (task *Task) shouldOverrideIPCMode(container *apicontainer.Container, docke if container.Type == apicontainer.ContainerNamespacePause { // Setting NamespaceContainerPause to be sharable with other containers if task.getIPCMode() == ipcModeTask { - return true, ipcModeSharable + setIPCMode(hostConfig, ipcModeSharable) + return } } // Defaulting to Docker daemon default option - return false, "" + return } switch task.getIPCMode() { - // No IPCMode provided in Task Definition, no need to override - case "": - return false, "" - - // IPCMode is none - container will have own private namespace with /dev/shm not mounted + // IPCMode is none - container will have own private namespace with /dev/shm not mounted case ipcModeNone: - return true, ipcModeNone + setIPCMode(hostConfig, ipcModeNone) + return case ipcModeHost: - return true, ipcModeHost + setIPCMode(hostConfig, ipcModeHost) + return case ipcModeTask: pauseCont, ok := task.ContainerByName(NamespacePauseContainerName) @@ -1841,7 +2236,7 @@ func (task *Task) shouldOverrideIPCMode(container *apicontainer.Container, docke field.TaskID: task.GetID(), }) task.SetDesiredStatus(apitaskstatus.TaskStopped) - return false, "" + break } pauseDockerID, ok := dockerContainerMap[pauseCont.Name] if !ok || pauseDockerID == nil { @@ -1850,31 +2245,44 @@ func (task *Task) shouldOverrideIPCMode(container *apicontainer.Container, docke field.TaskID: task.GetID(), }) task.SetDesiredStatus(apitaskstatus.TaskStopped) - return false, "" + break } - return true, dockerMappingContainerPrefix + pauseDockerID.DockerID + setIPCMode(hostConfig, dockerMappingContainerPrefix+pauseDockerID.DockerID) + return default: - return false, "" + break } } -func (task *Task) initializeContainerOrderingForVolumes() error { +func (task *Task) initializeContainerOrdering() error { + // Handle ordering for Service Connect + if task.IsServiceConnectEnabled() { + scContainer := task.GetServiceConnectContainer() + + for _, container := range task.Containers { + if container.IsInternal() || container == scContainer { + continue + } + container.AddContainerDependency(scContainer.Name, ContainerOrderingHealthyCondition) + scContainer.BuildContainerDependency(container.Name, apicontainerstatus.ContainerStopped, apicontainerstatus.ContainerStopped) + } + } + + // Handle ordering for Volumes for _, container := range task.Containers { if len(container.VolumesFrom) > 0 { for _, volume := range container.VolumesFrom { if _, ok := task.ContainerByName(volume.SourceContainer); !ok { - return fmt.Errorf("could not find container with name %s", volume.SourceContainer) + return fmt.Errorf("could not find volume source container with name %s", volume.SourceContainer) } dependOn := apicontainer.DependsOn{ContainerName: volume.SourceContainer, Condition: ContainerOrderingCreateCondition} container.SetDependsOn(append(container.GetDependsOn(), dependOn)) } } } - return nil -} -func (task *Task) initializeContainerOrderingForLinks() error { + // Handle ordering for Links for _, container := range task.Containers { if len(container.Links) > 0 { for _, link := range container.Links { @@ -1884,7 +2292,7 @@ func (task *Task) initializeContainerOrderingForLinks() error { } linkName := linkParts[0] if _, ok := task.ContainerByName(linkName); !ok { - return fmt.Errorf("could not find container with name %s", linkName) + return fmt.Errorf("could not find container for link %s", link) } dependOn := apicontainer.DependsOn{ContainerName: linkName, Condition: ContainerOrderingStartCondition} container.SetDependsOn(append(container.GetDependsOn(), dependOn)) @@ -1924,19 +2332,116 @@ func (task *Task) dockerLinks(container *apicontainer.Container, dockerContainer return dockerLinkArr, nil } -func (task *Task) dockerPortMap(container *apicontainer.Container) nat.PortMap { - dockerPortMap := nat.PortMap{} +var getHostPortRange = utils.GetHostPortRange - for _, portBinding := range container.Ports { - dockerPort := nat.Port(strconv.Itoa(int(portBinding.ContainerPort)) + "/" + portBinding.Protocol.String()) - currentMappings, existing := dockerPortMap[dockerPort] - if existing { - dockerPortMap[dockerPort] = append(currentMappings, nat.PortBinding{HostPort: strconv.Itoa(int(portBinding.HostPort))}) +func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPortRange string) (nat.PortMap, error) { + dockerPortMap := nat.PortMap{} + scContainer := task.GetServiceConnectContainer() + containerToCheck := container + containerPortSet := make(map[int]struct{}) + containerPortRangeMap := make(map[string]string) + if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() { + if container.Type == apicontainer.ContainerCNIPause { + // we will create bindings for task containers (including both customer containers and SC Appnet container) + // and let them be published by the associated pause container. + // Note - for SC bridge mode we do not allow customer to specify a host port for their containers. Additionally, + // When an ephemeral host port is assigned, Appnet will NOT proxy traffic to that port + taskContainer, err := task.getBridgeModeTaskContainerForPauseContainer(container) + if err != nil { + return nil, err + } + if taskContainer == scContainer { + // create bindings for all ingress listener ports + // no need to create binding for egress listener port as it won't be access from host level or from outside + for _, ic := range task.ServiceConnectConfig.IngressConfig { + listenerPortInt := int(ic.ListenerPort) + dockerPort := nat.Port(strconv.Itoa(listenerPortInt)) + "/tcp" + hostPort := 0 // default bridge-mode SC experience - host port will be an ephemeral port assigned by docker + if ic.HostPort != nil { // non-default bridge-mode SC experience - host port specified by customer + hostPort = int(*ic.HostPort) + } + dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: strconv.Itoa(hostPort)}) + // append non-range, singular container port to the containerPortSet + containerPortSet[listenerPortInt] = struct{}{} + // set taskContainer.ContainerPortSet to be used during network binding creation + taskContainer.SetContainerPortSet(containerPortSet) + } + return dockerPortMap, nil + } + containerToCheck = taskContainer } else { - dockerPortMap[dockerPort] = []nat.PortBinding{{HostPort: strconv.Itoa(int(portBinding.HostPort))}} + // If container is neither SC container nor pause container, it's a regular task container. Its port bindings(s) + // are published by the associated pause container, and we leave the map empty here (docker would actually complain + // otherwise). + return dockerPortMap, nil + } + } + + for _, portBinding := range containerToCheck.Ports { + // for each port binding config, either one of containerPort or containerPortRange is set + if portBinding.ContainerPort != 0 { + containerPort := int(portBinding.ContainerPort) + + dockerPort := nat.Port(strconv.Itoa(containerPort) + "/" + portBinding.Protocol.String()) + dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: strconv.Itoa(int(portBinding.HostPort))}) + + // append non-range, singular container port to the containerPortSet + containerPortSet[containerPort] = struct{}{} + } else if portBinding.ContainerPortRange != "" { + containerToCheck.SetContainerHasPortRange(true) + + containerPortRange := portBinding.ContainerPortRange + // nat.ParsePortRangeToInt validates a port range; if valid, it returns start and end ports as integers + startContainerPort, endContainerPort, err := nat.ParsePortRangeToInt(containerPortRange) + if err != nil { + return nil, err + } + + numberOfPorts := endContainerPort - startContainerPort + 1 + protocol := portBinding.Protocol.String() + // we will try to get a contiguous set of host ports from the ephemeral host port range. + // this is to ensure that docker maps host ports in a contiguous manner, and + // we are guaranteed to have the entire hostPortRange in a single network binding while sending this info to ECS. + hostPortRange, err := getHostPortRange(numberOfPorts, protocol, dynamicHostPortRange) + if err != nil { + // in the odd case where we're unable to find a contiguous set of host ports, we fall back to docker dynamic port + // assignment for the requested ContainerPortRange. + logger.Error("Unable to find contiguous host ports for container, falling back to "+ + "docker dynamic port assignment", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + "containerPortRange": containerPortRange, + field.Error: err, + }) + + // append individual container port from the containerPortRange into the containerPortSet. + // this will ensure that we populate network bindings for ports that docker dynamically assigned. + for port := startContainerPort; port <= endContainerPort; port++ { + containerPortSet[port] = struct{}{} + } + } else { + // append ranges to the dockerPortMap + // nat.ParsePortSpec returns a list of port mappings in a format that docker likes + mappings, err := nat.ParsePortSpec(hostPortRange + ":" + containerPortRange + "/" + protocol) + if err != nil { + return nil, err + } + + for _, mapping := range mappings { + dockerPortMap[mapping.Port] = append(dockerPortMap[mapping.Port], mapping.Binding) + } + + // append containerPortRange and associated hostPortRange to the containerPortRangeMap + // this will ensure that we consolidate range into 1 network binding while sending it to ECS + containerPortRangeMap[containerPortRange] = hostPortRange + } } } - return dockerPortMap + + // set Container.ContainerPortSet and Container.ContainerPortRangeMap to be used during network binding creation + containerToCheck.SetContainerPortSet(containerPortSet) + containerToCheck.SetContainerPortRangeMap(containerPortRangeMap) + return dockerPortMap, nil } func (task *Task) dockerVolumesFrom(container *apicontainer.Container, dockerContainerMap map[string]*apicontainer.DockerContainer) ([]string, error) { @@ -2014,8 +2519,9 @@ func (task *Task) UpdateDesiredStatus() { // Invariant: task desired status must be stopped if any essential container is stopped func (task *Task) updateTaskDesiredStatusUnsafe() { logger.Debug("Updating task's desired status", logger.Fields{ - field.TaskID: task.GetID(), - field.KnownStatus: task.KnownStatusUnsafe.String(), + field.TaskID: task.GetID(), + field.KnownStatus: task.KnownStatusUnsafe.String(), + field.DesiredStatus: task.DesiredStatusUnsafe.String(), }) // A task's desired status is stopped if any essential container is stopped @@ -2212,6 +2718,7 @@ func (task *Task) GetPrimaryENI() *apieni.ENI { if len(task.ENIs) == 0 { return nil } + return task.ENIs[0] } @@ -2381,6 +2888,38 @@ func (task *Task) AddResource(resourceType string, resource taskresource.TaskRes task.ResourcesMapUnsafe[resourceType] = append(task.ResourcesMapUnsafe[resourceType], resource) } +// requiresCredentialSpecResource returns true if at least one container in the task +// needs a valid credentialspec resource +func (task *Task) requiresCredentialSpecResource() bool { + for _, container := range task.Containers { + if container.RequiresCredentialSpec() { + return true + } + } + return false +} + +// GetCredentialSpecResource retrieves credentialspec resource from resource map +func (task *Task) GetCredentialSpecResource() ([]taskresource.TaskResource, bool) { + task.lock.RLock() + defer task.lock.RUnlock() + + res, ok := task.ResourcesMapUnsafe[credentialspec.ResourceName] + return res, ok +} + +// getAllCredentialSpecRequirements is used to build all the credential spec requirements for the task +func (task *Task) getAllCredentialSpecRequirements() map[string]string { + reqsContainerMap := make(map[string]string) + for _, container := range task.Containers { + credentialSpec, err := container.GetCredentialSpec() + if err == nil && credentialSpec != "" { + reqsContainerMap[credentialSpec] = container.Name + } + } + return reqsContainerMap +} + // SetTerminalReason sets the terminalReason string and this can only be set // once per the task's lifecycle. This field does not accept updates. func (task *Task) SetTerminalReason(reason string) { @@ -2779,3 +3318,96 @@ func (task *Task) UpdateTaskENIsLinkName() { eni.GetLinkName() } } + +func (task *Task) GetServiceConnectContainer() *apicontainer.Container { + if task.ServiceConnectConfig == nil { + return nil + } + c, _ := task.ContainerByName(task.ServiceConnectConfig.ContainerName) + return c +} + +// IsContainerServiceConnectPause checks whether a given container name is the name of the task service connect pause +// container. We construct the name of SC pause container by taking SC container name from SC config, and using the +// pause container naming pattern. +func (task *Task) IsContainerServiceConnectPause(containerName string) bool { + scContainer := task.GetServiceConnectContainer() + if scContainer == nil { + return false + } + scPauseName := fmt.Sprintf(ServiceConnectPauseContainerNameFormat, scContainer.Name) + return containerName == scPauseName +} + +// IsServiceConnectEnabled returns true if Service Connect is enabled for this task. +func (task *Task) IsServiceConnectEnabled() bool { + return task.GetServiceConnectContainer() != nil +} + +// PopulateServiceConnectContainerMappingEnvVar populates APPNET_CONTAINER_IP_MAPPING env var for AppNet Agent container +// aka SC container +func (task *Task) PopulateServiceConnectContainerMappingEnvVar() error { + envVars := make(map[string]string) + containerMapping := make(map[string]string) + for _, c := range task.Containers { + if c.Type != apicontainer.ContainerCNIPause { + continue + } + taskContainer, err := task.getBridgeModeTaskContainerForPauseContainer(c) + if err != nil { + return fmt.Errorf("error retrieving task container for pause container %s: %+v", c.Name, err) + } + containerMapping[taskContainer.Name] = c.GetNetworkSettings().IPAddress + } + containerMappingJson, err := json.Marshal(containerMapping) + if err != nil { + return fmt.Errorf("error injecting required env vars APPNET_CONTAINER_MAPPING to Service Connect container: %w", err) + } + envVars[serviceConnectContainerMappingEnvVar] = string(containerMappingJson) + task.GetServiceConnectContainer().MergeEnvironmentVariables(envVars) + return nil +} + +func (task *Task) PopulateServiceConnectRuntimeConfig(serviceConnectConfig serviceconnect.RuntimeConfig) { + task.lock.Lock() + defer task.lock.Unlock() + + task.ServiceConnectConfig.RuntimeConfig = serviceConnectConfig +} + +// PopulateServiceConnectPauseIPConfig is called once we've started SC pause container and retrieved its container IPs. +func (task *Task) PopulateServiceConnectNetworkConfig(ipv4Addr string, ipv6Addr string) { + task.lock.Lock() + defer task.lock.Unlock() + + task.ServiceConnectConfig.NetworkConfig = serviceconnect.NetworkConfig{ + SCPauseIPv4Addr: ipv4Addr, + SCPauseIPv6Addr: ipv6Addr, + } +} + +func (task *Task) GetServiceConnectRuntimeConfig() serviceconnect.RuntimeConfig { + task.lock.RLock() + defer task.lock.RUnlock() + + return task.ServiceConnectConfig.RuntimeConfig +} + +func (task *Task) GetServiceConnectNetworkConfig() serviceconnect.NetworkConfig { + task.lock.RLock() + defer task.lock.RUnlock() + + return task.ServiceConnectConfig.NetworkConfig +} + +func (task *Task) SetServiceConnectConnectionDraining(draining bool) { + task.lock.Lock() + defer task.lock.Unlock() + task.ServiceConnectConnectionDrainingUnsafe = draining +} + +func (task *Task) IsServiceConnectConnectionDraining() bool { + task.lock.RLock() + defer task.lock.RUnlock() + return task.ServiceConnectConnectionDrainingUnsafe +} diff --git a/agent/api/task/task_attachment_handler.go b/agent/api/task/task_attachment_handler.go new file mode 100644 index 00000000000..7d9e6a58618 --- /dev/null +++ b/agent/api/task/task_attachment_handler.go @@ -0,0 +1,109 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package task + +import ( + "fmt" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/aws-sdk-go/aws" +) + +// AttachmentHandler defines an interface to handel attachment received from ACS. +type AttachmentHandler interface { + parseAttachment(acsAttachment *ecsacs.Attachment) error + validateAttachment(acsTask *ecsacs.Task, task *Task) error +} + +// ServiceConnectAttachmentHandler defines a service connect type attachment handler. +type ServiceConnectAttachmentHandler struct { + scConfig *serviceconnect.Config +} + +// NewAttachmentHandlers returns all type of handlers to handle different types of attachment. +func NewAttachmentHandlers() map[string]AttachmentHandler { + attachmentHandlers := make(map[string]AttachmentHandler) + attachmentHandlers[serviceConnectAttachmentType] = &ServiceConnectAttachmentHandler{} + return attachmentHandlers +} + +// getHandlerByType returns the attachment handler based on the given type, and returns error if no matching hander can be found. +func getHandlerByType(handlerType string, handlers map[string]AttachmentHandler) (AttachmentHandler, error) { + if handler, ok := handlers[handlerType]; ok { + return handler, nil + } + return nil, fmt.Errorf("error to find an attachment handler for %s attachment type", handlerType) +} + +// attachment parser of service connect attachment handler. +func (scAttachment *ServiceConnectAttachmentHandler) parseAttachment(acsAttachment *ecsacs.Attachment) error { + config, err := serviceconnect.ParseServiceConnectAttachment(acsAttachment) + scAttachment.scConfig = config + return err +} + +// attachment validator of service connect attachment handler. +func (scAttachment *ServiceConnectAttachmentHandler) validateAttachment(acsTask *ecsacs.Task, task *Task) error { + config := scAttachment.scConfig + taskContainers := acsTask.Containers + ipv6Enabled := false + networkMode := task.NetworkMode + if acsTask.ElasticNetworkInterfaces != nil { + for _, eni := range acsTask.ElasticNetworkInterfaces { + if len(eni.Ipv6Addresses) != 0 { + ipv6Enabled = true + break + } + } + } + return serviceconnect.ValidateServiceConnectConfig(config, taskContainers, networkMode, ipv6Enabled) +} + +// handleTaskAttachments parses and validates attachments based on attachment type. +func handleTaskAttachments(acsTask *ecsacs.Task, task *Task) error { + if acsTask.Attachments != nil { + var serviceConnectAttachment *ecsacs.Attachment + for _, attachment := range acsTask.Attachments { + switch aws.StringValue(attachment.AttachmentType) { + case serviceConnectAttachmentType: + serviceConnectAttachment = attachment + default: + logger.Debug("Received an attachment type", logger.Fields{ + "attachmentType": attachment.AttachmentType, + }) + } + } + + handlers := NewAttachmentHandlers() + if serviceConnectAttachment != nil { + scHandler, err := getHandlerByType(serviceConnectAttachmentType, handlers) + if err != nil { + return err + } + + if err := scHandler.(*ServiceConnectAttachmentHandler).parseAttachment(serviceConnectAttachment); err != nil { + return fmt.Errorf("error parsing service connect config value from the service connect attachment: %w", err) + } + + // validate the service connect config parsed from the service connect attachment + if err := scHandler.(*ServiceConnectAttachmentHandler).validateAttachment(acsTask, task); err != nil { + return fmt.Errorf("service connect config validation failed: %w", err) + } + task.ServiceConnectConfig = scHandler.(*ServiceConnectAttachmentHandler).scConfig + } + } + return nil +} diff --git a/agent/api/task/task_attachment_handler_test.go b/agent/api/task/task_attachment_handler_test.go new file mode 100644 index 00000000000..00cbf1d6dfd --- /dev/null +++ b/agent/api/task/task_attachment_handler_test.go @@ -0,0 +1,203 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package task + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" +) + +var ( + testSCContainerName = "ecs-service-connect" + testInboundListener = "testInboundListener" + testOutboundListener = "testOutboundListenerName" + testHost = "testHostName" + testIngressPort = "9090" + testIPv4 = "172.31.21.40" + testIPv4CIDR = "127.255.0.0/16" + testIPv6 = "abcd:dcba:1234:4321::" + testIPv6CIDR = "2002::1234:abcd:ffff:c0a8:101/64" + testIpv4ElasticNetworkInterface = &ecsacs.ElasticNetworkInterface{ + Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(testIPv4), + }, + }, + } + testIpv6ElasticNetworkInterface = &ecsacs.ElasticNetworkInterface{ + Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ + { + Address: aws.String(testIPv6), + }, + }, + } +) + +func stringToPointer(s string) *string { return &s } + +func getTestcontainerFromACS(containerName, networkMode string) *ecsacs.Container { + return &ecsacs.Container{ + Name: aws.String(containerName), + DockerConfig: &ecsacs.DockerConfig{ + HostConfig: aws.String(fmt.Sprintf( + `{"NetworkMode":"%s"}`, networkMode)), + }, + } +} + +func constructTestServiceConnectConfig( + ingressPort, + ingressListenerName, + egressListenerName, + egressIPv4Cidr, + egressIPv6Cidr, + dnsHostName, + dnsAddress string) string { + testIngressConfig := fmt.Sprintf(`\"ingressConfig\":[{\"interceptPort\":%s,\"listenerName\":\"%s\"}]`, ingressPort, ingressListenerName) + testEgressConfig := fmt.Sprintf(`\"egressConfig\":{\"listenerName\":\"%s\",\"vip\":{\"ipv4Cidr\":\"%s\",\"ipv6Cidr\":\"%s\"}}`, egressListenerName, egressIPv4Cidr, egressIPv6Cidr) + testDnsConfig := fmt.Sprintf(`\"dnsConfig\":[{\"hostname\":\"%s\",\"address\":\"%s\"}]`, dnsHostName, dnsAddress) + testServiceConnectConfig := strings.Join([]string{`"{`, + testEgressConfig + `,`, + testDnsConfig + `,`, + testIngressConfig, + `}"`, + }, "") + unquotedSCConfig, _ := strconv.Unquote(testServiceConnectConfig) + return unquotedSCConfig +} + +func TestNewAttachmentHandlers(t *testing.T) { + handlers := NewAttachmentHandlers() + scHandler, err := getHandlerByType(serviceConnectAttachmentType, handlers) + assert.Nil(t, err, "Should not return error") + assert.NotNil(t, scHandler, "Should find service connect attachment type handler") +} + +func TestHandleTaskAttachmentsWithServiceConnectAttachment(t *testing.T) { + tt := []struct { + testName string + testServiceConnectConfig string + shouldReturnError bool + }{ + { + testName: "AWSVPC IPv6 enabled without error", + testServiceConnectConfig: constructTestServiceConnectConfig( + testIngressPort, + testInboundListener, + testOutboundListener, + testIPv4CIDR, + testIPv6CIDR, + testHost, + testIPv6, + ), + shouldReturnError: false, + }, + { + testName: "AWSVPC IPv6 enabled with error", + testServiceConnectConfig: constructTestServiceConnectConfig( + testIngressPort, + "", + testOutboundListener, + "", + testIPv6CIDR, + testHost, + testIPv6, + ), + shouldReturnError: true, + }, + } + + testExpectedSCConfig := &serviceconnect.Config{ + ContainerName: testSCContainerName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + InterceptPort: aws.Uint16(9090), + ListenerName: testInboundListener, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: testOutboundListener, + VIP: serviceconnect.VIP{ + IPV4CIDR: testIPv4CIDR, + IPV6CIDR: testIPv6CIDR, + }, + }, + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: testHost, + Address: testIPv6, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + testAcsTask := &ecsacs.Task{ + ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{testIpv6ElasticNetworkInterface}, + Containers: []*ecsacs.Container{ + getTestcontainerFromACS(testSCContainerName, AWSVPCNetworkMode), + }, + Attachments: []*ecsacs.Attachment{ + { + AttachmentArn: stringToPointer("attachmentArn"), + AttachmentProperties: []*ecsacs.AttachmentProperty{ + { + Name: stringToPointer(serviceconnect.GetServiceConnectConfigKey()), + Value: stringToPointer(tc.testServiceConnectConfig), + }, + { + Name: stringToPointer(serviceconnect.GetServiceConnectContainerNameKey()), + Value: stringToPointer(testSCContainerName), + }, + }, + AttachmentType: stringToPointer(serviceConnectAttachmentType), + }, + }, + NetworkMode: stringToPointer(AWSVPCNetworkMode), + } + testTask := &Task{} + testTask.NetworkMode = AWSVPCNetworkMode + err := handleTaskAttachments(testAcsTask, testTask) + if tc.shouldReturnError { + assert.NotNil(t, err, "Should return error") + } else { + assert.Nil(t, err, "Should not return error") + assert.NotNil(t, testTask.ServiceConnectConfig, "Should get valid service connect config from attachments") + assert.Equal(t, testExpectedSCConfig, testTask.ServiceConnectConfig) + } + }) + } +} + +func TestHandleTaskAttachmentsWithoutAttachment(t *testing.T) { + testAcsTask := &ecsacs.Task{ + ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{testIpv4ElasticNetworkInterface}, + Containers: []*ecsacs.Container{ + getTestcontainerFromACS("C1", BridgeNetworkMode), + }, + NetworkMode: stringToPointer(BridgeNetworkMode), + } + testTask := &Task{} + err := handleTaskAttachments(testAcsTask, testTask) + assert.Nil(t, err, "Should not return error") + assert.Nil(t, testTask.ServiceConnectConfig, "Should not return service connect config from attachments") +} diff --git a/agent/api/task/task_linux.go b/agent/api/task/task_linux.go index 605cb7ab214..09ae73aef8e 100644 --- a/agent/api/task/task_linux.go +++ b/agent/api/task/task_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -20,6 +21,9 @@ import ( "path/filepath" "time" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/utils" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" @@ -39,8 +43,6 @@ import ( ) const ( - // With a 100ms CPU period, we can express 0.01 vCPU to 10 vCPUs - maxTaskVCPULimit = 10 // Reference: http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html minimumCPUShare = 2 @@ -58,6 +60,17 @@ func (task *Task) adjustForPlatform(cfg *config.Config) { } func (task *Task) initializeCgroupResourceSpec(cgroupPath string, cGroupCPUPeriod time.Duration, resourceFields *taskresource.ResourceFields) error { + if !task.MemoryCPULimitsEnabled { + if task.CPU > 0 || task.Memory > 0 { + // Client-side validation/warning if a task with task-level CPU/memory limits specified somehow lands on an instance + // where agent does not support it. These limits will be ignored. + logger.Warn("Ignoring task-level CPU/memory limits since agent does not support the TaskCPUMemLimits capability", logger.Fields{ + field.TaskID: task.GetID(), + }) + } + return nil + } + cgroupRoot, err := task.BuildCgroupRoot() if err != nil { return errors.Wrapf(err, "cgroup resource: unable to determine cgroup root for task") @@ -78,13 +91,35 @@ func (task *Task) initializeCgroupResourceSpec(cgroupPath string, cGroupCPUPerio } // BuildCgroupRoot helps build the task cgroup prefix -// Example: /ecs/task-id +// Example v1: /ecs/task-id +// Example v2: ecstasks-$TASKID.slice func (task *Task) BuildCgroupRoot() (string, error) { taskID, err := utils.TaskIdFromArn(task.Arn) if err != nil { return "", err } - return filepath.Join(config.DefaultTaskCgroupPrefix, taskID), nil + + if config.CgroupV2 { + return buildCgroupV2Root(taskID), nil + } + return buildCgroupV1Root(taskID), nil +} + +func buildCgroupV1Root(taskID string) string { + return filepath.Join(config.DefaultTaskCgroupV1Prefix, taskID) +} + +// buildCgroupV2Root creates a root cgroup using the systemd driver's special "-" +// character. The "-" specifies a parent slice, so tasks and their containers end up +// looking like this in the cgroup directory: +// +// /sys/fs/cgroup/ecstasks.slice/ +// ├── ecstasks-XXXXf406f70c4c678073ae96944fXXXX.slice +// │ └── docker-XXXX7c6dc81f2e9a8bf1c566dc769733ccba594b3007dd289a0f50ad7923XXXX.scope +// └── ecstasks-XXXX30467358463ab6bbba4e73afXXXX.slice +// └── docker-XXXX7ef4e942552437c96051356859c1df169f16e1cf9a9fc96fd30614e6XXXX.scope +func buildCgroupV2Root(taskID string) string { + return fmt.Sprintf("%s-%s.slice", config.DefaultTaskCgroupV2Prefix, taskID) } // BuildLinuxResourceSpec returns a linuxResources object for the task cgroup @@ -120,21 +155,9 @@ func (task *Task) BuildLinuxResourceSpec(cGroupCPUPeriod time.Duration) (specs.L // buildExplicitLinuxCPUSpec builds CPU spec when task CPU limits are // explicitly requested func (task *Task) buildExplicitLinuxCPUSpec(cGroupCPUPeriod time.Duration) (specs.LinuxCPU, error) { - if task.CPU > maxTaskVCPULimit { - return specs.LinuxCPU{}, - errors.Errorf("task CPU spec builder: unsupported CPU limits, requested=%f, max-supported=%d", - task.CPU, maxTaskVCPULimit) - } taskCPUPeriod := uint64(cGroupCPUPeriod / time.Microsecond) taskCPUQuota := int64(task.CPU * float64(taskCPUPeriod)) - // TODO: DefaultCPUPeriod only permits 10VCPUs. - // Adaptive calculation of CPUPeriod required for further support - // (samuelkarp) The largest available EC2 instance in terms of CPU count is a x1.32xlarge, - // with 128 vCPUs. If we assume a fixed evaluation period of 100ms (100000us), - // we'd need a quota of 12800000us, which is longer than the maximum of 1000000. - // For 128 vCPUs, we'd probably need something like a 1ms (1000us - the minimum) - // evaluation period, an 128000us quota in order to stay within the min/max limits. return specs.LinuxCPU{ Quota: &taskCPUQuota, Period: &taskCPUPeriod, @@ -148,18 +171,13 @@ func (task *Task) buildImplicitLinuxCPUSpec() specs.LinuxCPU { // aggregate container CPU shares when present var taskCPUShares uint64 for _, container := range task.Containers { - if container.CPU > 0 { + if container.CPU < minimumCPUShare { + taskCPUShares += minimumCPUShare + } else { taskCPUShares += uint64(container.CPU) } } - // If there are are no CPU limits at task or container level, - // default task CPU shares - if taskCPUShares == 0 { - // Set default CPU shares - taskCPUShares = minimumCPUShare - } - return specs.LinuxCPU{ Shares: &taskCPUShares, } @@ -221,23 +239,6 @@ func (task *Task) dockerCPUShares(containerCPU uint) int64 { return int64(containerCPU) } -// requiresCredentialSpecResource returns true if at least one container in the task -// needs a valid credentialspec resource -func (task *Task) requiresCredentialSpecResource() bool { - return false -} - -// initializeCredentialSpecResource builds the resource dependency map for the credentialspec resource -func (task *Task) initializeCredentialSpecResource(config *config.Config, credentialsManager credentials.Manager, - resourceFields *taskresource.ResourceFields) error { - return errors.New("task credentialspec is only supported on windows") -} - -// GetCredentialSpecResource retrieves credentialspec resource from resource map -func (task *Task) GetCredentialSpecResource() ([]taskresource.TaskResource, bool) { - return []taskresource.TaskResource{}, false -} - func enableIPv6SysctlSetting(hostConfig *dockercontainer.HostConfig) { if hostConfig.Sysctls == nil { hostConfig.Sysctls = make(map[string]string) @@ -257,9 +258,9 @@ func (task *Task) initializeFSxWindowsFileServerResource(cfg *config.Config, cre return errors.New("task with FSx for Windows File Server volumes is only supported on Windows container instance") } -// BuildCNIConfig builds a list of CNI network configurations for the task. +// BuildCNIConfigAwsvpc builds a list of CNI network configurations for the task. // If includeIPAMConfig is set to true, the list also includes the bridge IPAM configuration. -func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { +func (task *Task) BuildCNIConfigAwsvpc(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { if !task.IsNetworkModeAWSVPC() { return nil, errors.New("task config: task network mode is not AWSVPC") } @@ -318,7 +319,56 @@ func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Confi }) } + // Build a CNI network configuration for ServiceConnect-enabled task in AWSVPC mode + if task.IsServiceConnectEnabled() { + ifName, netconf, err = ecscni.NewServiceConnectNetworkConfig( + task.ServiceConnectConfig, + ecscni.NAT, + false, + task.shouldEnableIPv4(), + task.shouldEnableIPv6(), + cniConfig) + if err != nil { + return nil, err + } + cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{ + IfName: ifName, + CNINetworkConfig: netconf, + }) + } + cniConfig.ContainerNetNS = fmt.Sprintf(ecscni.NetnsFormat, cniConfig.ContainerPID) return cniConfig, nil } + +// BuildCNIConfigBridgeMode builds a list of CNI network configurations for a task in docker bridge mode. +// Currently the only plugin in available is for Service Connect +func (task *Task) BuildCNIConfigBridgeMode(cniConfig *ecscni.Config, containerName string) (*ecscni.Config, error) { + if !task.IsNetworkModeBridge() || !task.IsServiceConnectEnabled() { + return nil, errors.New("only bridge-mode Service-Connect-enabled task should invoke BuildCNIConfigBridgeMode") + } + + var netconf *libcni.NetworkConfig + var ifName string + var err error + + scNetworkConfig := task.GetServiceConnectNetworkConfig() + ifName, netconf, err = ecscni.NewServiceConnectNetworkConfig( + task.ServiceConnectConfig, + ecscni.TPROXY, + !task.IsContainerServiceConnectPause(containerName), + scNetworkConfig.SCPauseIPv4Addr != "", + scNetworkConfig.SCPauseIPv6Addr != "", + cniConfig) + if err != nil { + return nil, err + } + cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{ + IfName: ifName, + CNINetworkConfig: netconf, + }) + + cniConfig.ContainerNetNS = fmt.Sprintf(ecscni.NetnsFormat, cniConfig.ContainerPID) + return cniConfig, nil +} diff --git a/agent/api/task/task_linux_test.go b/agent/api/task/task_linux_test.go index a241ee5d42f..a1ad635e75e 100644 --- a/agent/api/task/task_linux_test.go +++ b/agent/api/task/task_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -21,6 +22,8 @@ import ( "testing" "time" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/api/appmesh" apiappmesh "github.com/aws/amazon-ecs-agent/agent/api/appmesh" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" @@ -48,7 +51,8 @@ const ( validTaskArn = "arn:aws:ecs:region:account-id:task/task-id" invalidTaskArn = "invalid:task::arn" - expectedCgroupRoot = "/ecs/task-id" + expectedCgroupV1Root = "/ecs/task-id" + expectedCgroupV2Root = "ecstasks-task-id.slice" taskVCPULimit = 2.0 taskMemoryLimit = 512 @@ -66,8 +70,25 @@ const ( testExecutionCredentialsID = "testExecutionCredentialsID" defaultCPUPeriod = 100 * time.Millisecond // 100ms + + scContainerName = "service-connect" + scEgressListenerPort = 12345 + scInterceptPort = 8080 + scListenerPort = 15000 + scPauseIPv4 = "172.0.0.2" +) + +var ( + scPauseContainerName = fmt.Sprintf(ServiceConnectPauseContainerNameFormat, scContainerName) ) +func getExpectedCgroupRoot() string { + if config.CgroupV2 { + return expectedCgroupV2Root + } + return expectedCgroupV1Root +} + func TestAddNetworkResourceProvisioningDependencyNop(t *testing.T) { testTask := &Task{ Containers: []*apicontainer.Container{ @@ -89,6 +110,7 @@ func TestAddNetworkResourceProvisioningDependencyWithENI(t *testing.T) { TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), }, }, + NetworkMode: AWSVPCNetworkMode, } cfg := &config.Config{ PauseContainerImageName: "pause-container-image-name", @@ -117,7 +139,8 @@ func TestAddNetworkResourceProvisioningDependencyWithAppMesh(t *testing.T) { AppMesh: &apiappmesh.AppMesh{ ContainerName: proxyName, }, - ENIs: []*apieni.ENI{{}}, + ENIs: []*apieni.ENI{{}}, + NetworkMode: AWSVPCNetworkMode, Containers: []*apicontainer.Container{ { Name: "c1", @@ -163,7 +186,8 @@ func TestAddNetworkResourceProvisioningDependencyWithAppMeshDefaultImage(t *test AppMesh: &apiappmesh.AppMesh{ ContainerName: proxyName, }, - ENIs: []*apieni.ENI{{}}, + ENIs: []*apieni.ENI{{}}, + NetworkMode: AWSVPCNetworkMode, Containers: []*apicontainer.Container{ { Name: "c1", @@ -199,7 +223,8 @@ func TestAddNetworkResourceProvisioningDependencyWithAppMeshError(t *testing.T) AppMesh: &apiappmesh.AppMesh{ ContainerName: proxyName, }, - ENIs: []*apieni.ENI{{}}, + ENIs: []*apieni.ENI{{}}, + NetworkMode: AWSVPCNetworkMode, Containers: []*apicontainer.Container{ { Name: "c1", @@ -227,6 +252,7 @@ func TestBuildCgroupRootHappyPath(t *testing.T) { } cgroupRoot, err := task.BuildCgroupRoot() + expectedCgroupRoot := getExpectedCgroupRoot() assert.NoError(t, err) assert.Equal(t, expectedCgroupRoot, cgroupRoot) @@ -244,6 +270,16 @@ func TestBuildCgroupRootErrorPath(t *testing.T) { assert.Empty(t, cgroupRoot) } +func TestBuildCgroupV1Root(t *testing.T) { + cgroupRoot := buildCgroupV1Root("111mytaskid") + assert.Equal(t, "/ecs/111mytaskid", cgroupRoot) +} + +func TestBuildCgroupV2Root(t *testing.T) { + cgroupRoot := buildCgroupV2Root("111mytaskid") + assert.Equal(t, "ecstasks-111mytaskid.slice", cgroupRoot) +} + // TestBuildLinuxResourceSpecCPUMem validates the linux resource spec builder func TestBuildLinuxResourceSpecCPUMem(t *testing.T) { taskMemoryLimit := int64(taskMemoryLimit) @@ -295,6 +331,29 @@ func TestBuildLinuxResourceSpecCPU(t *testing.T) { assert.EqualValues(t, expectedLinuxResourceSpec, linuxResourceSpec) } +// TestBuildLinuxResourceSpecIncreasedTaskCPULimit validates the linux resource spec builder +// with increased task CPU limit (>10 vCPUs). +func TestBuildLinuxResourceSpecIncreasedTaskCPULimit(t *testing.T) { + const increasedTaskVCPULimit float64 = 15 + task := &Task{ + Arn: validTaskArn, + CPU: increasedTaskVCPULimit, + } + + linuxResourceSpec, err := task.BuildLinuxResourceSpec(defaultCPUPeriod) + + expectedTaskCPUPeriod := uint64(defaultCPUPeriod / time.Microsecond) + expectedTaskCPUQuota := int64(increasedTaskVCPULimit * float64(expectedTaskCPUPeriod)) + expectedLinuxResourceSpec := specs.LinuxResources{ + CPU: &specs.LinuxCPU{ + Quota: &expectedTaskCPUQuota, + Period: &expectedTaskCPUPeriod, + }, + } + assert.NoError(t, err) + assert.EqualValues(t, expectedLinuxResourceSpec, linuxResourceSpec) +} + // TestBuildLinuxResourceSpecWithoutTaskCPULimits validates behavior of CPU Shares func TestBuildLinuxResourceSpecWithoutTaskCPULimits(t *testing.T) { task := &Task{ @@ -342,6 +401,31 @@ func TestBuildLinuxResourceSpecWithoutTaskCPUWithContainerCPULimits(t *testing.T assert.EqualValues(t, expectedLinuxResourceSpec, linuxResourceSpec) } +// TestBuildLinuxResourceSpecWithoutTaskCPUWithLessThanMinimumContainerCPULimits validates behavior of CPU Shares +// when container CPU share is 1 (less than the current minimumCPUShare which is 2) +func TestBuildLinuxResourceSpecWithoutTaskCPUWithLessThanMinimumContainerCPULimits(t *testing.T) { + task := &Task{ + Arn: validTaskArn, + Containers: []*apicontainer.Container{ + { + Name: "C1", + CPU: uint(1), + }, + }, + } + expectedCPUShares := uint64(2) + expectedLinuxResourceSpec := specs.LinuxResources{ + CPU: &specs.LinuxCPU{ + Shares: &expectedCPUShares, + }, + } + + linuxResourceSpec, err := task.BuildLinuxResourceSpec(defaultCPUPeriod) + + assert.NoError(t, err) + assert.EqualValues(t, expectedLinuxResourceSpec, linuxResourceSpec) +} + // TestBuildLinuxResourceSpecInvalidMem validates the linux resource spec builder func TestBuildLinuxResourceSpecInvalidMem(t *testing.T) { taskMemoryLimit := int64(taskMemoryLimit) @@ -375,6 +459,7 @@ func TestOverrideCgroupParentHappyPath(t *testing.T) { } hostConfig := &dockercontainer.HostConfig{} + expectedCgroupRoot := getExpectedCgroupRoot() assert.NoError(t, task.overrideCgroupParent(hostConfig)) assert.NotEmpty(t, hostConfig) @@ -407,6 +492,7 @@ func TestPlatformHostConfigOverride(t *testing.T) { } hostConfig := &dockercontainer.HostConfig{} + expectedCgroupRoot := getExpectedCgroupRoot() assert.NoError(t, task.platformHostConfigOverride(hostConfig)) assert.NotEmpty(t, hostConfig) @@ -1212,6 +1298,7 @@ func TestBuildCNIConfigRegularENIWithAppMesh(t *testing.T) { for _, blockIMDS := range []bool{true, false} { t.Run(fmt.Sprintf("When BlockInstanceMetadata is %t", blockIMDS), func(t *testing.T) { testTask := &Task{} + testTask.NetworkMode = AWSVPCNetworkMode testTask.AddTaskENI(getTestENI()) testTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, @@ -1224,7 +1311,7 @@ func TestBuildCNIConfigRegularENIWithAppMesh(t *testing.T) { egressIgnoredIP, }, }) - cniConfig, err := testTask.BuildCNIConfig(true, &ecscni.Config{ + cniConfig, err := testTask.BuildCNIConfigAwsvpc(true, &ecscni.Config{ BlockInstanceMetadata: blockIMDS, }) assert.NoError(t, err) @@ -1257,10 +1344,60 @@ func TestBuildCNIConfigRegularENIWithAppMesh(t *testing.T) { } } +func TestBuildCNIConfigRegularENIWithServiceConnect(t *testing.T) { + for _, blockIMDS := range []bool{true, false} { + t.Run(fmt.Sprintf("When BlockInstanceMetadata is %t", blockIMDS), func(t *testing.T) { + testTask := &Task{} + testTask.AddTaskENI(getTestENI()) + testTask.NetworkMode = AWSVPCNetworkMode + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: scContainerName, + IngressConfig: []serviceconnect.IngressConfigEntry{{ListenerPort: scListenerPort}}, + EgressConfig: &serviceconnect.EgressConfig{ListenerPort: scEgressListenerPort}, + } + testTask.Containers = []*apicontainer.Container{{Name: scContainerName}} + + cniConfig, err := testTask.BuildCNIConfigAwsvpc(true, &ecscni.Config{ + BlockInstanceMetadata: blockIMDS, + }) + assert.NoError(t, err) + // We expect 3 NetworkConfig objects in the cni Config wrapper object: + // ENI, Bridge and ServiceConnect + require.Len(t, cniConfig.NetworkConfigs, 3) + // The first one should be for the ENI. + var eniConfig ecscni.ENIConfig + err = json.Unmarshal(cniConfig.NetworkConfigs[0].CNINetworkConfig.Bytes, &eniConfig) + require.NoError(t, err) + assert.Equal(t, mac, eniConfig.MACAddress, eniConfig) + assert.Equal(t, []string{ipv4 + ipv4Block, ipv6 + ipv6Block}, eniConfig.IPAddresses) + assert.Equal(t, []string{ipv4Gateway}, eniConfig.GatewayIPAddresses) + assert.Equal(t, blockIMDS, eniConfig.BlockInstanceMetadata) + // The second one should be for the Bridge. + var bridgeConfig ecscni.BridgeConfig + err = json.Unmarshal(cniConfig.NetworkConfigs[1].CNINetworkConfig.Bytes, &bridgeConfig) + require.NoError(t, err) + assert.Equal(t, "ecs-bridge", bridgeConfig.BridgeName) + // The third one should be for ServiceConnect. + var scConfig ecscni.ServiceConnectConfig + err = json.Unmarshal(cniConfig.NetworkConfigs[2].CNINetworkConfig.Bytes, &scConfig) + require.NoError(t, err) + assert.Equal(t, 1, len(scConfig.IngressConfig)) + assert.Equal(t, uint16(scListenerPort), scConfig.IngressConfig[0].ListenerPort) + assert.NotNil(t, scConfig.EgressConfig) + assert.Equal(t, string(ecscni.NAT), scConfig.EgressConfig.RedirectMode) + assert.Equal(t, uint16(scEgressListenerPort), scConfig.EgressConfig.ListenerPort) + assert.Nil(t, scConfig.EgressConfig.RedirectIP) // AWSVPC mode task should not include RedirectIP + assert.True(t, scConfig.EnableIPv4) + assert.True(t, scConfig.EnableIPv6) + }) + } +} + func TestBuildCNIConfigTrunkBranchENI(t *testing.T) { for _, blockIMDS := range []bool{true, false} { t.Run(fmt.Sprintf("When BlockInstanceMetadata is %t", blockIMDS), func(t *testing.T) { testTask := &Task{} + testTask.NetworkMode = AWSVPCNetworkMode testTask.AddTaskENI(&apieni.ENI{ ID: "TestBuildCNIConfigTrunkBranchENI", MacAddress: mac, @@ -1283,7 +1420,7 @@ func TestBuildCNIConfigTrunkBranchENI(t *testing.T) { }, }) - cniConfig, err := testTask.BuildCNIConfig(true, &ecscni.Config{ + cniConfig, err := testTask.BuildCNIConfigAwsvpc(true, &ecscni.Config{ BlockInstanceMetadata: blockIMDS, }) assert.NoError(t, err) @@ -1308,3 +1445,48 @@ func TestBuildCNIConfigTrunkBranchENI(t *testing.T) { }) } } + +func TestBuildCNIBridgeModeWithServiceConnect(t *testing.T) { + for _, containerName := range []string{"other-pause", scPauseContainerName} { + t.Run(fmt.Sprintf("When container name is %s", containerName), func(t *testing.T) { + testTask := &Task{} + testTask.NetworkMode = BridgeNetworkMode + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: scContainerName, + IngressConfig: []serviceconnect.IngressConfigEntry{{ListenerPort: scListenerPort}}, + EgressConfig: &serviceconnect.EgressConfig{ListenerPort: scEgressListenerPort}, + NetworkConfig: serviceconnect.NetworkConfig{ + SCPauseIPv4Addr: scPauseIPv4, + SCPauseIPv6Addr: "", + }, + } + testTask.Containers = []*apicontainer.Container{{Name: scContainerName}} + + cniConfig := &ecscni.Config{} + cniConfig, err := testTask.BuildCNIConfigBridgeMode(cniConfig, containerName) + assert.NoError(t, err) + // We expect 1 NetworkConfig objects in the cni Config wrapper object which is ServiceConnect + require.Len(t, cniConfig.NetworkConfigs, 1) + // The first one should be for the ENI. + var scConfig ecscni.ServiceConnectConfig + err = json.Unmarshal(cniConfig.NetworkConfigs[0].CNINetworkConfig.Bytes, &scConfig) + require.NoError(t, err) + assert.Equal(t, 1, len(scConfig.IngressConfig)) + assert.Equal(t, uint16(scListenerPort), scConfig.IngressConfig[0].ListenerPort) + assert.NotNil(t, scConfig.EgressConfig) + assert.Equal(t, string(ecscni.TPROXY), scConfig.EgressConfig.RedirectMode) + if containerName != scPauseContainerName { + // Should only include redirect IPs if container is an application pause container + assert.Equal(t, uint16(0), scConfig.EgressConfig.ListenerPort) + assert.Equal(t, scPauseIPv4, scConfig.EgressConfig.RedirectIP.IPv4) + assert.Equal(t, "", scConfig.EgressConfig.RedirectIP.IPv6) + } else { + // SC pause container should not include redirect IP in CNI config + assert.Equal(t, uint16(scEgressListenerPort), scConfig.EgressConfig.ListenerPort) + assert.Nil(t, scConfig.EgressConfig.RedirectIP) + } + assert.True(t, scConfig.EnableIPv4) + assert.False(t, scConfig.EnableIPv6) + }) + } +} diff --git a/agent/api/task/task_test.go b/agent/api/task/task_test.go index f25a11f2c16..b1e1cecf458 100644 --- a/agent/api/task/task_test.go +++ b/agent/api/task/task_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,12 +18,20 @@ package task import ( "encoding/json" + "errors" "fmt" "reflect" "runtime" + "strconv" + "strings" "testing" "time" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" + + "github.com/docker/go-connections/nat" + "github.com/aws/amazon-ecs-agent/agent/acs/model/ecsacs" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" @@ -37,6 +46,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" mock_dockerapi "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi/mocks" + mock_s3_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" mock_ssm_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/taskresource/asmauth" @@ -57,12 +67,50 @@ import ( "github.com/stretchr/testify/require" ) +const ( + serviceConnectContainerTestName = "service-connect" + testHostName = "testHostName" + testOutboundListenerName = "testOutboundListener" + testIPv4Address = "172.31.21.40" + testIPv6Address = "abcd:dcba:1234:4321::" + testIPv4Cidr = "127.255.0.0/16" + testIPv6Cidr = "2002::1234:abcd:ffff:c0a8:101/64" +) + +var ( + testListenerPort = uint16(8080) + testBridgeDefaultListenerPort = uint16(15000) +) + func TestDockerConfigPortBinding(t *testing.T) { testTask := &Task{ Containers: []*apicontainer.Container{ { - Name: "c1", - Ports: []apicontainer.PortBinding{{10, 10, "", apicontainer.TransportProtocolTCP}, {20, 20, "", apicontainer.TransportProtocolUDP}}, + Name: "c1", + Ports: []apicontainer.PortBinding{ + { + ContainerPort: 10, + HostPort: 10, + BindIP: "", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 20, + HostPort: 20, + BindIP: "", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPortRange: "99-999", + BindIP: "", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPortRange: "121-221", + BindIP: "", + Protocol: apicontainer.TransportProtocolUDP, + }, + }, }, }, } @@ -80,6 +128,15 @@ func TestDockerConfigPortBinding(t *testing.T) { if !ok { t.Fatal("Could not get exposed ports 20/udp") } + _, ok = config.ExposedPorts["99-999/tcp"] + if !ok { + t.Fatal("Could not get exposed ports 99-999/tcp") + } + _, ok = config.ExposedPorts["121-221/udp"] + if !ok { + t.Fatal("Could not get exposed ports 121-221/udp") + } + } func TestDockerHostConfigCPUShareZero(t *testing.T) { @@ -160,28 +217,362 @@ func TestDockerHostConfigCPUShareUnchanged(t *testing.T) { } func TestDockerHostConfigPortBinding(t *testing.T) { + testTask1 := &Task{ + Containers: []*apicontainer.Container{ + { + Name: "c1", + Ports: []apicontainer.PortBinding{ + { + ContainerPort: 10, + HostPort: 10, + BindIP: "", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPort: 20, + HostPort: 20, + BindIP: "", + Protocol: apicontainer.TransportProtocolUDP, + }, + }, + }, + }, + } + + testTask2 := &Task{ + Containers: []*apicontainer.Container{ + { + Name: "c1", + Ports: []apicontainer.PortBinding{ + { + ContainerPortRange: "999-1000", + BindIP: "", + Protocol: apicontainer.TransportProtocolTCP, + }, + { + ContainerPortRange: "1-3", + BindIP: "", + Protocol: apicontainer.TransportProtocolUDP, + }, + }, + }, + }, + } + + testTask3 := &Task{ + Containers: []*apicontainer.Container{ + { + Name: "c1", + Ports: []apicontainer.PortBinding{ + { + ContainerPortRange: "55-57", + BindIP: "", + Protocol: apicontainer.TransportProtocolUDP, + }, + { + ContainerPort: 80, + BindIP: "", + Protocol: apicontainer.TransportProtocolTCP, + }, + }, + }, + }, + } + + testCases := []struct { + testName string + testTask *Task + getHostPortRange func(numberOfPorts int, protocol string, dynamicHostPortRange string) (string, error) + expectedPortBinding nat.PortMap + expectedContainerPortSet map[int]struct{} + expectedContainerPortRangeMap map[string]string + }{ + { + testName: "2 port bindings, each with singular container port - host port", + testTask: testTask1, + expectedPortBinding: nat.PortMap{ + nat.Port("10/tcp"): []nat.PortBinding{{HostPort: "10"}}, + nat.Port("20/udp"): []nat.PortBinding{{HostPort: "20"}}, + }, + expectedContainerPortSet: map[int]struct{}{ + 10: {}, + 20: {}, + }, + expectedContainerPortRangeMap: map[string]string{}, + }, + { + testName: "2 port bindings, each with container port range, 1 found valid host port range, other didn't", + testTask: testTask2, + getHostPortRange: func(numberOfPorts int, protocol string, dynamicHostPortRange string) (string, error) { + if numberOfPorts == 3 { + return "", errors.New("couldn't find host ports") + } + return "99-100", nil + }, + expectedPortBinding: nat.PortMap{ + nat.Port("999/tcp"): []nat.PortBinding{{HostPort: "99"}}, + nat.Port("1000/tcp"): []nat.PortBinding{{HostPort: "100"}}, + }, + expectedContainerPortSet: map[int]struct{}{ + 1: {}, + 2: {}, + 3: {}, + }, + expectedContainerPortRangeMap: map[string]string{ + "999-1000": "99-100", + }, + }, + { + testName: "2 port bindings, one with container port range, other with singular container port", + testTask: testTask3, + getHostPortRange: func(numberOfPorts int, protocol string, dynamicHostPortRange string) (string, error) { + return "155-157", nil + }, + expectedPortBinding: nat.PortMap{ + nat.Port("55/udp"): []nat.PortBinding{{HostPort: "155"}}, + nat.Port("56/udp"): []nat.PortBinding{{HostPort: "156"}}, + nat.Port("57/udp"): []nat.PortBinding{{HostPort: "157"}}, + nat.Port("80/tcp"): []nat.PortBinding{{HostPort: "0"}}, + }, + expectedContainerPortSet: map[int]struct{}{ + 80: {}, + }, + expectedContainerPortRangeMap: map[string]string{ + "55-57": "155-157", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + defer func() { + getHostPortRange = utils.GetHostPortRange + }() + getHostPortRange = tc.getHostPortRange + + config, err := tc.testTask.DockerHostConfig(tc.testTask.Containers[0], dockerMap(tc.testTask), defaultDockerClientAPIVersion, + &config.Config{}) + assert.Nil(t, err) + + if !reflect.DeepEqual(config.PortBindings, tc.expectedPortBinding) { + t.Error("Expected port bindings to be resolved, was: ", config.PortBindings) + } + + if !reflect.DeepEqual(tc.testTask.Containers[0].ContainerPortSet, tc.expectedContainerPortSet) { + t.Error("Expected container port set to be resolved, was: ", tc.testTask.Containers[0].GetContainerPortSet()) + } + + if !reflect.DeepEqual(tc.testTask.Containers[0].ContainerPortRangeMap, tc.expectedContainerPortRangeMap) { + t.Error("Expected container port range map to be resolved, was: ", tc.testTask.Containers[0].GetContainerPortRangeMap()) + } + }) + } +} + +var ( + SCTaskContainerPort1 uint16 = 8080 + SCTaskContainerPort2 uint16 = 9090 + SCIngressListener1ContainerPort uint16 = 15000 + SCIngressListener2ContainerPort uint16 = 16000 + SCIngressListener2HostPort uint16 = 17000 + SCEgressListenerContainerPort uint16 = 12345 + defaultSCProtocol = "/tcp" +) + +func getTestTaskServiceConnectBridgeMode() *Task { testTask := &Task{ + NetworkMode: BridgeNetworkMode, Containers: []*apicontainer.Container{ { - Name: "c1", - Ports: []apicontainer.PortBinding{{10, 10, "", apicontainer.TransportProtocolTCP}, {20, 20, "", apicontainer.TransportProtocolUDP}}, + Name: "C1", + Ports: []apicontainer.PortBinding{ + {ContainerPort: SCTaskContainerPort1, HostPort: 0, BindIP: "", Protocol: apicontainer.TransportProtocolTCP}, + {ContainerPort: SCTaskContainerPort2, HostPort: 0, BindIP: "", Protocol: apicontainer.TransportProtocolTCP}, + }, + NetworkModeUnsafe: "", // should later be overridden to container mode + }, + { + Name: fmt.Sprintf("%s-%s", NetworkPauseContainerName, "C1"), + Type: apicontainer.ContainerCNIPause, + NetworkModeUnsafe: "", // should later be overridden to explicit bridge mode + }, + { + Name: serviceConnectContainerTestName, // port binding is retrieved through listener config and published by pause container + NetworkModeUnsafe: "", // should later be overridden to container mode + }, + { + Name: fmt.Sprintf("%s-%s", NetworkPauseContainerName, serviceConnectContainerTestName), + Type: apicontainer.ContainerCNIPause, + NetworkModeUnsafe: "", // should later be overridden to explicit bridge mode }, }, } - config, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion, + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: serviceConnectContainerTestName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerName: "testListener1", // bridge mode default - ephemeral listener host port + ListenerPort: SCIngressListener1ContainerPort, + }, + { + ListenerName: "testListener2", // bridge mode non-default - user-specified listener host port + ListenerPort: SCIngressListener2ContainerPort, + HostPort: &SCIngressListener2HostPort, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: "testEgressListener", + ListenerPort: SCEgressListenerContainerPort, // Presently this should always get ephemeral port + }, + } + return testTask +} + +func convertSCPort(port uint16) nat.Port { + return nat.Port(strconv.Itoa(int(port)) + defaultSCProtocol) +} + +// TestDockerHostConfigSCBridgeMode verifies port bindings and network mode overrides for each +// container in an SC-enabled bridge mode task. The test task is consisted of the SC container, a regular container, +// and two pause containers associated with each. +func TestDockerHostConfigSCBridgeMode(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + // task container and SC container should both get empty port binding map and "container" network mode + actualConfig, err := testTask.DockerHostConfig(testTask.Containers[0], dockerMap(testTask), defaultDockerClientAPIVersion, &config.Config{}) assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Equal(t, dockercontainer.NetworkMode(fmt.Sprintf("%s-%s", // e.g. "container:dockerid-~internal~ecs~pause-C1" + dockerMappingContainerPrefix+dockerIDPrefix+NetworkPauseContainerName, "C1")), actualConfig.NetworkMode) + assert.Empty(t, actualConfig.PortBindings, "Task container port binding should be empty") - bindings, ok := config.PortBindings["10/tcp"] + actualConfig, err = testTask.DockerHostConfig(testTask.Containers[2], dockerMap(testTask), defaultDockerClientAPIVersion, + &config.Config{}) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Equal(t, dockercontainer.NetworkMode(fmt.Sprintf("%s-%s", // e.g. "container:dockerid-~internal~ecs~pause-C1" + dockerMappingContainerPrefix+dockerIDPrefix+NetworkPauseContainerName, serviceConnectContainerTestName)), actualConfig.NetworkMode) + assert.Empty(t, actualConfig.PortBindings, "SC container port binding should be empty") + + // task pause container should get port binding map of the task container + actualConfig, err = testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask), defaultDockerClientAPIVersion, + &config.Config{}) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Equal(t, dockercontainer.NetworkMode(BridgeNetworkMode), actualConfig.NetworkMode) + bindings, ok := actualConfig.PortBindings[convertSCPort(SCTaskContainerPort1)] assert.True(t, ok, "Could not get port bindings") assert.Equal(t, 1, len(bindings), "Wrong number of bindings") - assert.Equal(t, "10", bindings[0].HostPort, "Wrong hostport") + assert.Equal(t, "0", bindings[0].HostPort, "Wrong hostport") + bindings, ok = actualConfig.PortBindings[convertSCPort(SCTaskContainerPort2)] + assert.True(t, ok, "Could not get port bindings") + assert.Equal(t, 1, len(bindings), "Wrong number of bindings") + assert.Equal(t, "0", bindings[0].HostPort, "Wrong hostport") - bindings, ok = config.PortBindings["20/udp"] + // SC pause container should get port binding map of all ingress listeners + actualConfig, err = testTask.DockerHostConfig(testTask.Containers[3], dockerMap(testTask), defaultDockerClientAPIVersion, + &config.Config{}) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Equal(t, dockercontainer.NetworkMode(BridgeNetworkMode), actualConfig.NetworkMode) + // SC - ingress listener 1 - default experience + bindings, ok = actualConfig.PortBindings[convertSCPort(SCIngressListener1ContainerPort)] assert.True(t, ok, "Could not get port bindings") assert.Equal(t, 1, len(bindings), "Wrong number of bindings") - assert.Equal(t, "20", bindings[0].HostPort, "Wrong hostport") + assert.Equal(t, "0", bindings[0].HostPort, "Wrong hostport") + // SC - ingress listener 2 - non-default host port + bindings, ok = actualConfig.PortBindings[convertSCPort(SCIngressListener2ContainerPort)] + assert.True(t, ok, "Could not get port bindings") + assert.Equal(t, 1, len(bindings), "Wrong number of bindings") + assert.Equal(t, strconv.Itoa(int(SCIngressListener2HostPort)), bindings[0].HostPort, "Wrong hostport") + // SC - egress listener - should not have port binding + bindings, ok = actualConfig.PortBindings[convertSCPort(SCEgressListenerContainerPort)] + assert.False(t, ok, "egress listener has port binding but it shouldn't") +} + +// TestDockerHostConfigSCBridgeMode_getPortBindingFailure verifies that when we can't find the task +// container associated with the pause container, DockerHostConfig should return failure (from getPortBinding) +func TestDockerHostConfigSCBridgeMode_getPortBindingFailure(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.Containers[1].Name = "invalid" // make the pause container name invalid such that we can't resolve task container from it + _, err := testTask.DockerHostConfig(testTask.Containers[1], dockerMap(testTask), defaultDockerClientAPIVersion, + &config.Config{}) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Msg, "error retrieving docker port map")) +} + +// TestDockerContainerConfigSCBridgeMode verifies exposed port and uid configuration for each container +// in an SC-enabled bridge mode task. The test task is consisted of the SC container, a regular container, +// and two pause container associated with each of them. +func TestDockerContainerConfigSCBridgeMode(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + + // Containers[0] aka user-defined task container should NOT expose any ports (it's done through the associated pause container) + // It should NOT get UID override + actualConfig, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Empty(t, actualConfig.ExposedPorts) + assert.Empty(t, actualConfig.User) + + // Containers[2] aka SC container should NOT expose any ports (it's done through the associated pause container) + // It should get UID override + actualConfig, err = testTask.DockerConfig(testTask.Containers[2], defaultDockerClientAPIVersion) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.Empty(t, actualConfig.ExposedPorts) + assert.Equal(t, strconv.Itoa(serviceconnect.AppNetUID), actualConfig.User) + + // Containers[1] aka task pause container should expose all container ports from the associated user-defined task containers + // It should NOT get UID override + actualConfig, err = testTask.DockerConfig(testTask.Containers[1], defaultDockerClientAPIVersion) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.NotNil(t, actualConfig.ExposedPorts) + assert.Equal(t, 2, len(actualConfig.ExposedPorts)) + _, ok := actualConfig.ExposedPorts[convertSCPort(SCTaskContainerPort1)] + assert.True(t, ok) + _, ok = actualConfig.ExposedPorts[convertSCPort(SCTaskContainerPort2)] + assert.True(t, ok) + assert.Empty(t, actualConfig.User) + + // Containers[3] aka SC pause container should expose all container ports from SC ingress and egress listeners + // It should NOT get UID override + actualConfig, err = testTask.DockerConfig(testTask.Containers[3], defaultDockerClientAPIVersion) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.NotNil(t, actualConfig.ExposedPorts) + assert.Equal(t, 3, len(actualConfig.ExposedPorts)) + _, ok = actualConfig.ExposedPorts[convertSCPort(SCIngressListener1ContainerPort)] + assert.True(t, ok) + _, ok = actualConfig.ExposedPorts[convertSCPort(SCIngressListener2ContainerPort)] + assert.True(t, ok) + _, ok = actualConfig.ExposedPorts[convertSCPort(SCEgressListenerContainerPort)] + assert.True(t, ok) + assert.Empty(t, actualConfig.User) +} + +func TestDockerContainerConfigSCBridgeMode_getExposedPortsFailure(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.Containers[1].Name = "invalid" // make the pause container name invalid such that we can't resolve task container from it + _, err := testTask.DockerConfig(testTask.Containers[1], defaultDockerClientAPIVersion) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Msg, "error resolving docker exposed ports")) +} + +func TestDockerContainerConfigSCBridgeMode_emptyEgressConfig(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.ServiceConnectConfig.EgressConfig = nil + actualConfig, err := testTask.DockerConfig(testTask.Containers[3], defaultDockerClientAPIVersion) + assert.Nil(t, err) + assert.NotNil(t, actualConfig) + assert.NotNil(t, actualConfig.ExposedPorts) + assert.Equal(t, 2, len(actualConfig.ExposedPorts)) + _, ok := actualConfig.ExposedPorts[convertSCPort(SCIngressListener1ContainerPort)] + assert.True(t, ok) + _, ok = actualConfig.ExposedPorts[convertSCPort(SCIngressListener2ContainerPort)] + assert.True(t, ok) } func TestDockerHostConfigVolumesFrom(t *testing.T) { @@ -263,6 +654,7 @@ func TestDockerHostConfigPauseContainer(t *testing.T) { ID: "eniID", }, }, + NetworkMode: AWSVPCNetworkMode, Containers: []*apicontainer.Container{ { Name: "c1", @@ -879,6 +1271,36 @@ func TestInitializeContainersV4MetadataEndpoint(t *testing.T) { fmt.Sprintf(apicontainer.MetadataURIFormatV4, "new-uuid")) } +// Tests that task.initializeContainersV1AgentAPIEndpoint method initializes +// V3EndpointID for all containers of the task and injects v1 Agent API Endpoint +// as an environment variable into each container. +func TestInitializeContainersV1AgentAPIEndpoint(t *testing.T) { + // Create a dummy task + task := Task{ + Containers: []*apicontainer.Container{ + { + Name: "c1", + }, + { + Name: "c2", + }, + }, + } + + // Call the method + task.initializeContainersV1AgentAPIEndpoint(utils.NewStaticUUIDProvider("new-uuid")) + + // Assert that v3 endpoint id is set and the endpoint is injected to env of each container + for _, container := range task.Containers { + assert.Equal(t, "new-uuid", container.GetV3EndpointID()) + assert.Equal(t, + map[string]string{ + apicontainer.AgentURIEnvVarName: "http://169.254.170.2/api/new-uuid", + }, + container.Environment) + } +} + func TestPostUnmarshalTaskWithLocalVolumes(t *testing.T) { // Constants used here are defined in task_unix_test.go and task_windows_test.go taskFromACS := ecsacs.Task{ @@ -1177,6 +1599,7 @@ func TestTaskFromACS(t *testing.T) { DesiredStatus: strptr("RUNNING"), Family: strptr("myFamily"), Version: strptr("1"), + ServiceName: strptr("myService"), Containers: []*ecsacs.Container{ { Name: strptr("myName"), @@ -1202,6 +1625,10 @@ func TestTaskFromACS(t *testing.T) { ContainerPort: intptr(900), Protocol: strptr("udp"), }, + { + ContainerPortRange: strptr("99-199"), + Protocol: strptr("tcp"), + }, }, VolumesFrom: []*ecsacs.VolumeFrom{ { @@ -1272,6 +1699,8 @@ func TestTaskFromACS(t *testing.T) { DesiredStatusUnsafe: apitaskstatus.TaskRunning, Family: "myFamily", Version: "1", + ServiceName: "myService", + NetworkMode: BridgeNetworkMode, Containers: []*apicontainer.Container{ { Name: "myName", @@ -1299,6 +1728,10 @@ func TestTaskFromACS(t *testing.T) { ContainerPort: 900, Protocol: apicontainer.TransportProtocolUDP, }, + { + ContainerPortRange: "99-199", + Protocol: apicontainer.TransportProtocolTCP, + }, }, VolumesFrom: []apicontainer.VolumeFrom{ { @@ -1366,6 +1799,7 @@ func TestTaskFromACS(t *testing.T) { Memory: 512, ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), } + expectedTask.GetID() // to set the task setIdOnce (sync.Once) property seqNum := int64(42) task, err := TaskFromACS(&taskFromAcs, &ecsacs.PayloadMessage{SeqNum: &seqNum}) @@ -3195,9 +3629,7 @@ func TestInitializeContainerOrderingWithLinksAndVolumesFrom(t *testing.T) { containerWithBothVolumeAndLink, containerWithNoVolumeOrLink}, } - err := task.initializeContainerOrderingForVolumes() - assert.NoError(t, err) - err = task.initializeContainerOrderingForLinks() + err := task.initializeContainerOrdering() assert.NoError(t, err) containerResultWithVolume := task.Containers[0] @@ -3237,26 +3669,30 @@ func TestInitializeContainerOrderingWithError(t *testing.T) { Links: []string{"myName:link1:link2"}, } - task1 := &Task{ + task1v := &Task{ Arn: "test", ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), - Containers: []*apicontainer.Container{containerWithVolumeError, containerWithLinkError1}, + Containers: []*apicontainer.Container{containerWithVolumeError}, } - task2 := &Task{ + task1l := &Task{ + Arn: "test", + ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + Containers: []*apicontainer.Container{containerWithLinkError1}, + } + + task2l := &Task{ Arn: "test", ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), - Containers: []*apicontainer.Container{containerWithVolumeError, containerWithLinkError2}, + Containers: []*apicontainer.Container{containerWithLinkError2}, } - errVolume1 := task1.initializeContainerOrderingForVolumes() + errVolume1 := task1v.initializeContainerOrdering() assert.Error(t, errVolume1) - errLink1 := task1.initializeContainerOrderingForLinks() + errLink1 := task1l.initializeContainerOrdering() assert.Error(t, errLink1) - errVolume2 := task2.initializeContainerOrderingForVolumes() - assert.Error(t, errVolume2) - errLink2 := task2.initializeContainerOrderingForLinks() + errLink2 := task2l.initializeContainerOrdering() assert.Error(t, errLink2) } @@ -3280,6 +3716,16 @@ func TestTaskFromACSPerContainerTimeouts(t *testing.T) { assert.Equal(t, task.Containers[0].StopTimeout, expectedTimeout) } +// Tests that ACS Task to Task translation does not fail when ServiceName is missing. +// Asserts that Task.ServiceName is empty in such a case. +func TestTaskFromACSServiceNameMissing(t *testing.T) { + taskFromACS := ecsacs.Task{} // No service name + seqNum := int64(42) + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + assert.Nil(t, err, "Should be able to handle acs task") + assert.Equal(t, task.ServiceName, "") +} + func TestGetContainerIndex(t *testing.T) { task := &Task{ Containers: []*apicontainer.Container{ @@ -3476,3 +3922,792 @@ func TestPostUnmarshalTaskWithOptions(t *testing.T) { task.PostUnmarshalTask(&config.Config{}, nil, nil, nil, nil, opt, opt) assert.Equal(t, 2, numCalls) } + +func TestGetServiceConnectContainer(t *testing.T) { + const serviceConnectContainerName = "service-connect" + scContainer := &apicontainer.Container{ + Name: serviceConnectContainerName, + } + tt := []struct { + scConfig *serviceconnect.Config + }{ + { + scConfig: nil, + }, + { + scConfig: &serviceconnect.Config{ + ContainerName: serviceConnectContainerName, + }, + }, + } + for _, tc := range tt { + task := &Task{ + ServiceConnectConfig: tc.scConfig, + Containers: []*apicontainer.Container{ + scContainer, + }, + } + c := task.GetServiceConnectContainer() + if tc.scConfig == nil { + assert.Nil(t, c) + } else { + assert.Equal(t, scContainer, c) + } + } +} + +func TestIsServiceConnectEnabled(t *testing.T) { + const serviceConnectContainerName = "service-connect" + tt := []struct { + scConfig *serviceconnect.Config + scContainer *apicontainer.Container + expectedSCEnabled bool + }{ + { + scConfig: nil, + scContainer: nil, + expectedSCEnabled: false, + }, + { + scConfig: &serviceconnect.Config{ + ContainerName: serviceConnectContainerName, + }, + scContainer: nil, + expectedSCEnabled: false, + }, + { + scConfig: nil, + scContainer: &apicontainer.Container{ + Name: serviceConnectContainerName, + }, + expectedSCEnabled: false, + }, + { + scConfig: &serviceconnect.Config{ + ContainerName: serviceConnectContainerName, + }, + scContainer: &apicontainer.Container{ + Name: serviceConnectContainerName, + }, + expectedSCEnabled: true, + }, + } + + for _, tc := range tt { + task := &Task{ + ServiceConnectConfig: tc.scConfig, + } + if tc.scContainer != nil { + task.Containers = append(task.Containers, tc.scContainer) + } + assert.Equal(t, tc.expectedSCEnabled, task.IsServiceConnectEnabled()) + } +} + +func TestPostUnmarshalTaskWithServiceConnectAWSVPCMode(t *testing.T) { + const ( + utilizedPort1 = 33333 + utilizedPort2 = 44444 + utilizedPort3 = 55555 + ) + utilizedPorts := map[uint16]struct{}{ + utilizedPort1: {}, + utilizedPort2: {}, + utilizedPort3: {}, + } + + taskFromACS := ecsacs.Task{ + Arn: strptr("myArn"), + DesiredStatus: strptr("RUNNING"), + Family: strptr("myFamily"), + Version: strptr("1"), + Containers: []*ecsacs.Container{ + containerFromACS("C1", utilizedPort1, 0, AWSVPCNetworkMode), + containerFromACS("C2", utilizedPort2, 0, AWSVPCNetworkMode), + containerFromACS(serviceConnectContainerTestName, 0, 0, AWSVPCNetworkMode), + }, + } + seqNum := int64(42) + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + testSCConfig := serviceconnect.Config{ + ContainerName: serviceConnectContainerTestName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerName: "testListener1", + ListenerPort: 0, // this one should get ephemeral port after PostUnmarshalTask + }, + { + ListenerName: "testListener2", + ListenerPort: utilizedPort3, // this one should NOT get ephemeral port after PostUnmarshalTask + }, + { + ListenerName: "testListener3", + ListenerPort: 0, // this one should get ephemeral port after PostUnmarshalTask + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: "testEgressListener", + ListenerPort: 0, // Presently this should always get ephemeral port + }, + } + originalSCConfig := cloneSCConfig(testSCConfig) + task.ServiceConnectConfig = &testSCConfig + assert.Nil(t, err, "Should be able to handle acs task") + err = task.PostUnmarshalTask(&config.Config{}, nil, nil, nil, nil) + assert.NoError(t, err) + task.NetworkMode = AWSVPCNetworkMode + + validateServiceConnectContainerOrder(t, task) + validateEphemeralPorts(t, task, originalSCConfig, utilizedPorts) + validateAppnetEnvVars(t, task) + +} + +// TestPostUnmarshalTaskWithServiceConnectBridgeMode verifies pause container creation and container dependency/ordering +// for an SC-enabled bridge mode task. We verify: +// - regular taskContainer.CREATED depends on SCContainer.RESOURCES_PROVISIONED +// - regular taskContainer.CREATED depends on SCContainer.HEALTHY +// - a pause container is created for each regular task container, and has steady state RUNNING +// - SCContainer.PULLED depends on ALL pauseContainer.RUNNING +// - SCContainer.STOPPED depends on ALL taskContainer.STOPPED +// - pauseContainer.STOPPED depends on SCContainer.STOPPED +func TestPostUnmarshalTaskWithServiceConnectBridgeMode(t *testing.T) { + const ( + utilizedPort1 = 33333 + utilizedPort2 = 44444 + listenerPort1 = 15000 + listenerPort2 = 16000 + listenerPort3 = 17000 + ) + utilizedPorts := map[uint16]struct{}{ + utilizedPort1: {}, + utilizedPort2: {}, + listenerPort1: {}, + listenerPort2: {}, + listenerPort3: {}, + } + taskFromACS := ecsacs.Task{ + Arn: strptr("myArn"), + DesiredStatus: strptr("RUNNING"), + Family: strptr("myFamily"), + Version: strptr("1"), + Containers: []*ecsacs.Container{ + containerFromACS("C1", utilizedPort1, 0, BridgeNetworkMode), + containerFromACS("C2", utilizedPort2, 0, BridgeNetworkMode), + containerFromACS(serviceConnectContainerTestName, 0, 0, BridgeNetworkMode), + }, + } + seqNum := int64(42) + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + testSCConfig := serviceconnect.Config{ + ContainerName: serviceConnectContainerTestName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerName: "testListener1", + ListenerPort: listenerPort1, + }, + { + ListenerName: "testListener2", + ListenerPort: listenerPort2, + }, + { + ListenerName: "testListener3", + ListenerPort: listenerPort3, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: "testEgressListener", + ListenerPort: 0, // Presently this should always get ephemeral port + }, + } + originalSCConfig := cloneSCConfig(testSCConfig) + task.ServiceConnectConfig = &testSCConfig + assert.Nil(t, err, "Should be able to handle acs task") + err = task.PostUnmarshalTask(&config.Config{}, nil, nil, nil, nil) + assert.NoError(t, err) + validateServiceConnectContainerOrder(t, task) + validateEphemeralPorts(t, task, originalSCConfig, utilizedPorts) + validateAppnetEnvVars(t, task) + validateServiceConnectBridgeModePauseContainer(t, task) +} + +func containerFromACS(name string, containerPort int64, hostPort int64, networkMode string) *ecsacs.Container { + var portMapping *ecsacs.PortMapping + if containerPort != 0 || hostPort != 0 { + portMapping = &ecsacs.PortMapping{} + if containerPort != 0 { + portMapping.ContainerPort = aws.Int64(containerPort) + } + if hostPort != 0 { + portMapping.HostPort = aws.Int64(hostPort) + } + } + + container := &ecsacs.Container{ + Name: aws.String(name), + DockerConfig: &ecsacs.DockerConfig{ + HostConfig: aws.String(fmt.Sprintf( + `{"NetworkMode":"%s"}`, networkMode)), + }, + } + if portMapping != nil { + container.PortMappings = []*ecsacs.PortMapping{ + portMapping, + } + } + return container +} + +func cloneSCConfig(scConfig serviceconnect.Config) serviceconnect.Config { + clone := scConfig + clone.IngressConfig = nil + for _, ic := range scConfig.IngressConfig { + clone.IngressConfig = append(clone.IngressConfig, ic) + } + return clone +} + +func validateServiceConnectContainerOrder(t *testing.T, task *Task) { + c1, _ := task.ContainerByName("C1") + c2, _ := task.ContainerByName("C2") + scC, _ := task.ContainerByName(serviceConnectContainerTestName) + + // Check that regular containers have a dependency on SC container becoming HEALTHY + assert.NotEmpty(t, c1.DependsOnUnsafe) + assert.Equal(t, serviceConnectContainerTestName, c1.DependsOnUnsafe[0].ContainerName) + assert.Equal(t, ContainerOrderingHealthyCondition, c1.DependsOnUnsafe[0].Condition) + + assert.NotEmpty(t, c2.DependsOnUnsafe) + assert.Equal(t, serviceConnectContainerTestName, c2.DependsOnUnsafe[0].ContainerName) + assert.Equal(t, ContainerOrderingHealthyCondition, c2.DependsOnUnsafe[0].Condition) + + // Check that SC container has a stop dependency on regular containers + assert.Empty(t, scC.DependsOnUnsafe) + assert.NotEmpty(t, scC.TransitionDependenciesMap) + assert.NotEmpty(t, scC.TransitionDependenciesMap[apicontainerstatus.ContainerStopped].ContainerDependencies) + assert.Equal(t, apicontainer.ContainerDependency{ + ContainerName: "C1", + SatisfiedStatus: apicontainerstatus.ContainerStopped, + }, scC.TransitionDependenciesMap[apicontainerstatus.ContainerStopped].ContainerDependencies[0]) + assert.Equal(t, apicontainer.ContainerDependency{ + ContainerName: "C2", + SatisfiedStatus: apicontainerstatus.ContainerStopped, + }, scC.TransitionDependenciesMap[apicontainerstatus.ContainerStopped].ContainerDependencies[1]) +} + +func validateEphemeralPorts(t *testing.T, task *Task, originalSCConfig serviceconnect.Config, utilizedPorts map[uint16]struct{}) { + for i, ic := range originalSCConfig.IngressConfig { + if ic.ListenerPort == 0 { + assignedPort := task.ServiceConnectConfig.IngressConfig[i].ListenerPort + _, ok := utilizedPorts[assignedPort] + assert.Falsef(t, ok, "An already-utilized port [%d] was assigned to ingress listener: %s", assignedPort, ic.ListenerName) + assert.NotZerof(t, assignedPort, + "Ephemeral port was not assigned for ingress listener: %s", ic.ListenerName) + utilizedPorts[assignedPort] = struct{}{} + } else { + assert.Equalf(t, ic.ListenerPort, task.ServiceConnectConfig.IngressConfig[i].ListenerPort, + "Ingress port incorrectly modified for listener: %s", ic.ListenerName) + } + assert.Equalf(t, ic.HostPort, task.ServiceConnectConfig.IngressConfig[i].HostPort, + "Ingress host port incorrectly modified for listener: %s", ic.ListenerName) + assert.Equalf(t, ic.InterceptPort, task.ServiceConnectConfig.IngressConfig[i].InterceptPort, + "Ingress intercept port incorrectly modified for listener: %s", ic.ListenerName) + assert.Equalf(t, ic.ListenerName, task.ServiceConnectConfig.IngressConfig[i].ListenerName, + "Ingress listener name incorrectly modified for listener: %s", ic.ListenerName) + } + if originalSCConfig.EgressConfig != nil && originalSCConfig.EgressConfig.ListenerPort == 0 { + assignedPort := task.ServiceConnectConfig.EgressConfig.ListenerPort + _, ok := utilizedPorts[assignedPort] + assert.Falsef(t, ok, "An already-utilized port [%d] was assigned to egress listener", assignedPort) + assert.NotZero(t, assignedPort, + "Ephemeral port was not assigned for egress listener") + utilizedPorts[assignedPort] = struct{}{} + } else { + assert.Equal(t, originalSCConfig.EgressConfig.ListenerPort, task.ServiceConnectConfig.EgressConfig.ListenerPort, + "Egress port incorrectly modified for egress listener") + } + assert.Equalf(t, originalSCConfig.EgressConfig.ListenerName, task.ServiceConnectConfig.EgressConfig.ListenerName, + "Egress listener name incorrectly modified") + assert.Equalf(t, originalSCConfig.EgressConfig.VIP, task.ServiceConnectConfig.EgressConfig.VIP, + "Egress VIP incorrectly modified") +} + +func validateAppnetEnvVars(t *testing.T, task *Task) { + // Validate the env vars were injected to SC container + for _, c := range task.Containers { + if c.Name != serviceConnectContainerTestName { + continue + } + portMappingStr := c.Environment["APPNET_LISTENER_PORT_MAPPING"] + // TODO [SC]: this map probably needs to change to the real Appnet model when it's ready + portMapping := make(map[string]int) + err := json.Unmarshal([]byte(portMappingStr), &portMapping) + assert.NoError(t, err, "Error parsing APPNET_LISTENER_PORT_MAPPING") + if task.IsNetworkModeAWSVPC() { // ECS Agent only select ephemeral listener ports for default SC AWSVPC tasks + listener1 := task.ServiceConnectConfig.IngressConfig[0] + listener3 := task.ServiceConnectConfig.IngressConfig[2] + assert.Equalf(t, int(listener1.ListenerPort), portMapping[listener1.ListenerName], "Listener-port mapping incorrectly configured for %s", listener1.ListenerName) + assert.Equalf(t, int(listener3.ListenerPort), portMapping[listener3.ListenerName], "Listener-port mapping incorrectly configured for %s", listener3.ListenerName) + } + egressListener := task.ServiceConnectConfig.EgressConfig + assert.Equalf(t, int(egressListener.ListenerPort), portMapping[egressListener.ListenerName], "Listener-port mapping incorrectly configured for %s", egressListener.ListenerName) + } +} + +func validateServiceConnectBridgeModePauseContainer(t *testing.T, task *Task) { + scC, _ := task.ContainerByName(serviceConnectContainerTestName) + scPauseC, ok := task.ContainerByName(fmt.Sprintf("%s-%s", NetworkPauseContainerName, serviceConnectContainerTestName)) + assert.True(t, ok) + p1, ok := task.ContainerByName(fmt.Sprintf("%s-%s", NetworkPauseContainerName, "C1")) + assert.True(t, ok) + p2, ok := task.ContainerByName(fmt.Sprintf("%s-%s", NetworkPauseContainerName, "C2")) + assert.True(t, ok) + pauseContainers := [...]*apicontainer.Container{p1, p2, scPauseC} + + // verify that SCContainer.CREATED depends on ALL PauseContainer.RESOURCES_PROVISIONED, and dthat + // ALL PauseContainer.STOPPED depends on SCContainer.STOPPED + assert.NotEmpty(t, scC.TransitionDependenciesMap) + assert.NotNil(t, scC.TransitionDependenciesMap[apicontainerstatus.ContainerCreated]) + containerDependencies := scC.TransitionDependenciesMap[apicontainerstatus.ContainerCreated].ContainerDependencies + assert.Equal(t, len(pauseContainers), len(containerDependencies)) + for i, pc := range pauseContainers { + assert.Equal(t, pc.Name, containerDependencies[i].ContainerName) + assert.Equal(t, apicontainerstatus.ContainerResourcesProvisioned, containerDependencies[i].SatisfiedStatus) + + assert.NotEmpty(t, pc.TransitionDependenciesMap) + assert.NotNil(t, pc.TransitionDependenciesMap[apicontainerstatus.ContainerStopped]) + assert.NotEmpty(t, pc.TransitionDependenciesMap[apicontainerstatus.ContainerStopped].ContainerDependencies) + assert.Equal(t, apicontainer.ContainerDependency{ + ContainerName: serviceConnectContainerTestName, + SatisfiedStatus: apicontainerstatus.ContainerStopped, + }, pc.TransitionDependenciesMap[apicontainerstatus.ContainerStopped].ContainerDependencies[0]) + } + + // verify that taskPauseContainer.RESOURCES_PROVISIONED depends on SCPauseContainer.RUNNING + assert.NotNil(t, p1.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned]) + assert.NotEmpty(t, p1.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned].ContainerDependencies) + assert.Equal(t, apicontainer.ContainerDependency{ + ContainerName: scPauseC.Name, + SatisfiedStatus: apicontainerstatus.ContainerRunning, + }, p1.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned].ContainerDependencies[0]) + + assert.NotNil(t, p2.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned]) + assert.NotEmpty(t, p2.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned].ContainerDependencies) + assert.Equal(t, apicontainer.ContainerDependency{ + ContainerName: scPauseC.Name, + SatisfiedStatus: apicontainerstatus.ContainerRunning, + }, p2.TransitionDependenciesMap[apicontainerstatus.ContainerResourcesProvisioned].ContainerDependencies[0]) +} + +func TestTaskFromACS_InitNetworkMode(t *testing.T) { + for _, tc := range []struct { + inputNetworkMode string + expectedTaskNetworkMode string + }{ + { + inputNetworkMode: AWSVPCNetworkMode, + expectedTaskNetworkMode: AWSVPCNetworkMode, + }, + { + inputNetworkMode: BridgeNetworkMode, + expectedTaskNetworkMode: BridgeNetworkMode, + }, + { + inputNetworkMode: "", + expectedTaskNetworkMode: BridgeNetworkMode, + }, + { + inputNetworkMode: HostNetworkMode, + expectedTaskNetworkMode: HostNetworkMode, + }, + } { + taskFromACS := ecsacs.Task{ + Arn: strptr("myArn"), + DesiredStatus: strptr("RUNNING"), + Family: strptr("myFamily"), + Version: strptr("1"), + NetworkMode: aws.String(tc.inputNetworkMode), + Containers: []*ecsacs.Container{ + { + Name: aws.String("C1"), + }, + { + Name: aws.String("C2"), + }, + }, + } + seqNum := int64(42) + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + assert.Nil(t, err, "Should be able to handle acs task") + assert.Equal(t, tc.expectedTaskNetworkMode, task.NetworkMode) + switch tc.inputNetworkMode { + case AWSVPCNetworkMode: + assert.True(t, task.IsNetworkModeAWSVPC()) + assert.False(t, task.IsNetworkModeBridge()) + assert.False(t, task.IsNetworkModeHost()) + case BridgeNetworkMode, "": + assert.False(t, task.IsNetworkModeAWSVPC()) + assert.True(t, task.IsNetworkModeBridge()) + assert.False(t, task.IsNetworkModeHost()) + case HostNetworkMode: + assert.False(t, task.IsNetworkModeAWSVPC()) + assert.False(t, task.IsNetworkModeBridge()) + assert.True(t, task.IsNetworkModeHost()) + } + } +} + +func TestGetBridgeModePauseContainerForTaskContainer(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + container, err := testTask.GetBridgeModePauseContainerForTaskContainer(testTask.Containers[0]) + assert.Nil(t, err) + assert.NotNil(t, container) + assert.Equal(t, testTask.Containers[1].Name, container.Name) +} + +func TestGetBridgeModePauseContainerForTaskContainer_NotFound(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.Containers[1].Name = "invalid" + _, err := testTask.GetBridgeModePauseContainerForTaskContainer(testTask.Containers[0]) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "could not find pause container")) +} + +func TestGetBridgeModeTaskContainerForPauseContainer(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + // make the service container name include "-" to make sure we can still resolve service container from pause container name correctly + serviceContainerName := "service-container-name" + testTask.Containers[0].Name = serviceContainerName + testTask.Containers[1].Name = fmt.Sprintf("%s-%s", NetworkPauseContainerName, serviceContainerName) + + container, err := testTask.getBridgeModeTaskContainerForPauseContainer(testTask.Containers[1]) + assert.Nil(t, err) + assert.NotNil(t, container) + assert.Equal(t, testTask.Containers[0].Name, container.Name) +} + +func TestGetBridgeModeTaskContainerForPauseContainer_InvalidPauseContainerName(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.Containers[1].Name = "invalid" + _, err := testTask.getBridgeModeTaskContainerForPauseContainer(testTask.Containers[1]) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "does not conform to ~internal~ecs~pause-$TASK_CONTAINER_NAME format")) +} + +func TestGetBridgeModeTaskContainerForPauseContainer_NotFound(t *testing.T) { + testTask := getTestTaskServiceConnectBridgeMode() + testTask.Containers[0].Name = "anotherTaskContainer" + _, err := testTask.getBridgeModeTaskContainerForPauseContainer(testTask.Containers[1]) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "could not find task container")) +} + +func TestTaskServiceConnectAttachment(t *testing.T) { + seqNum := int64(42) + tt := []struct { + testName string + testElasticNetworkInterface *ecsacs.ElasticNetworkInterface + testNetworkMode string + testSCConfigValue string + testExpectedSCConfig *serviceconnect.Config + }{ + { + testName: "Bridge default case", + testElasticNetworkInterface: &ecsacs.ElasticNetworkInterface{ + Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(ipv4), + }, + }, + }, + testNetworkMode: BridgeNetworkMode, + testSCConfigValue: "{\"egressConfig\":{\"listenerName\":\"testOutboundListener\",\"vip\":{\"ipv4Cidr\":\"127.255.0.0/16\",\"ipv6Cidr\":\"\"}},\"dnsConfig\":[{\"hostname\":\"testHostName\",\"address\":\"172.31.21.40\"}],\"ingressConfig\":[{\"listenerPort\":15000}]}", + testExpectedSCConfig: &serviceconnect.Config{ + ContainerName: serviceConnectContainerTestName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerPort: testBridgeDefaultListenerPort, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: serviceconnect.VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: "", + }, + }, + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv4Address, + }, + }, + }, + }, + { + testName: "AWSVPC override case with IPv6 enabled", + testElasticNetworkInterface: &ecsacs.ElasticNetworkInterface{ + Ipv6Addresses: []*ecsacs.IPv6AddressAssignment{ + { + Address: aws.String("ipv6"), + }, + }, + }, + testNetworkMode: AWSVPCNetworkMode, + testSCConfigValue: "{\"egressConfig\":{\"listenerName\":\"testOutboundListener\",\"vip\":{\"ipv4Cidr\":\"127.255.0.0/16\",\"ipv6Cidr\":\"2002::1234:abcd:ffff:c0a8:101/64\"}},\"dnsConfig\":[{\"hostname\":\"testHostName\",\"address\":\"abcd:dcba:1234:4321::\"}],\"ingressConfig\":[{\"listenerPort\":8080}]}", + testExpectedSCConfig: &serviceconnect.Config{ + ContainerName: serviceConnectContainerTestName, + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerPort: testListenerPort, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: testOutboundListenerName, + VIP: serviceconnect.VIP{ + IPV4CIDR: testIPv4Cidr, + IPV6CIDR: testIPv6Cidr, + }, + }, + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: testHostName, + Address: testIPv6Address, + }, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + taskFromACS := ecsacs.Task{ + Arn: strptr("myArn"), + DesiredStatus: strptr("RUNNING"), + Family: strptr("myFamily"), + Version: strptr("1"), + ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{tc.testElasticNetworkInterface}, + Containers: []*ecsacs.Container{ + containerFromACS("C1", 33333, 0, tc.testNetworkMode), + containerFromACS(serviceConnectContainerTestName, 0, 0, tc.testNetworkMode), + }, + Attachments: []*ecsacs.Attachment{ + { + AttachmentArn: strptr("attachmentArn"), + AttachmentProperties: []*ecsacs.AttachmentProperty{ + { + Name: strptr(serviceconnect.GetServiceConnectConfigKey()), + Value: strptr(tc.testSCConfigValue), + }, + { + Name: strptr(serviceconnect.GetServiceConnectContainerNameKey()), + Value: strptr(serviceConnectContainerTestName), + }, + }, + AttachmentType: strptr(serviceConnectAttachmentType), + }, + }, + NetworkMode: strptr(tc.testNetworkMode), + } + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + assert.Nil(t, err, "Should be able to handle acs task") + assert.Equal(t, tc.testNetworkMode, task.NetworkMode) + assert.Equal(t, tc.testExpectedSCConfig, task.ServiceConnectConfig) + }) + } +} + +func TestTaskWithoutServiceConnectAttachment(t *testing.T) { + seqNum := int64(42) + testElasticNetworkInterface := &ecsacs.ElasticNetworkInterface{ + Ipv4Addresses: []*ecsacs.IPv4AddressAssignment{ + { + Primary: aws.Bool(true), + PrivateAddress: aws.String(ipv4), + }, + }, + } + taskFromACS := ecsacs.Task{ + Arn: strptr("myArn"), + DesiredStatus: strptr("RUNNING"), + Family: strptr("myFamily"), + Version: strptr("1"), + ElasticNetworkInterfaces: []*ecsacs.ElasticNetworkInterface{testElasticNetworkInterface}, + Containers: []*ecsacs.Container{ + containerFromACS("C1", 33333, 0, BridgeNetworkMode), + }, + NetworkMode: strptr(BridgeNetworkMode), + } + + task, err := TaskFromACS(&taskFromACS, &ecsacs.PayloadMessage{SeqNum: &seqNum}) + assert.Nil(t, err, "Should be able to handle acs task") + assert.Equal(t, BridgeNetworkMode, task.NetworkMode) + assert.Nil(t, task.ServiceConnectConfig, "Should be no service connect config") +} + +func TestRequiresCredentialSpecResource(t *testing.T) { + container1 := &apicontainer.Container{} + task1 := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{container1}, + } + + hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" + container2 := &apicontainer.Container{} + container2.DockerConfig.HostConfig = &hostConfig + task2 := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{container2}, + } + + testCases := []struct { + name string + task *Task + expectedOutput bool + }{ + { + name: "missing_credentialspec", + task: task1, + expectedOutput: false, + }, + { + name: "valid_credentialspec", + task: task2, + expectedOutput: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, tc.task.requiresCredentialSpecResource()) + }) + } + +} + +func TestGetAllCredentialSpecRequirements(t *testing.T) { + hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" + container := &apicontainer.Container{Name: "webapp1"} + container.DockerConfig.HostConfig = &hostConfig + + task := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{container}, + } + + credentialSpecContainerMap := task.getAllCredentialSpecRequirements() + + credentialspecFileLocation := "credentialspec:file://gmsa_gmsa-acct.json" + expectedCredentialSpecContainerMap := map[string]string{credentialspecFileLocation: "webapp1"} + + assert.True(t, reflect.DeepEqual(expectedCredentialSpecContainerMap, credentialSpecContainerMap)) +} + +func TestGetAllCredentialSpecRequirementsWithMultipleContainersUsingSameSpec(t *testing.T) { + hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" + c1 := &apicontainer.Container{Name: "webapp1"} + c1.DockerConfig.HostConfig = &hostConfig + + c2 := &apicontainer.Container{Name: "webapp2"} + c2.DockerConfig.HostConfig = &hostConfig + + task := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{c1, c2}, + } + + credentialSpecContainerMap := task.getAllCredentialSpecRequirements() + + credentialspecFileLocation := "credentialspec:file://gmsa_gmsa-acct.json" + expectedCredentialSpecContainerMap := map[string]string{credentialspecFileLocation: "webapp2"} + + assert.Equal(t, len(expectedCredentialSpecContainerMap), len(credentialSpecContainerMap)) + assert.True(t, reflect.DeepEqual(expectedCredentialSpecContainerMap, credentialSpecContainerMap)) +} + +func TestGetAllCredentialSpecRequirementsWithMultipleContainers(t *testing.T) { + hostConfig1 := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct-1.json\"]}" + hostConfig2 := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct-2.json\"]}" + + c1 := &apicontainer.Container{Name: "webapp1"} + c1.DockerConfig.HostConfig = &hostConfig1 + + c2 := &apicontainer.Container{Name: "webapp2"} + c2.DockerConfig.HostConfig = &hostConfig1 + + c3 := &apicontainer.Container{Name: "webapp3"} + c3.DockerConfig.HostConfig = &hostConfig2 + + task := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{c1, c2, c3}, + } + + credentialSpecContainerMap := task.getAllCredentialSpecRequirements() + + credentialspec1 := "credentialspec:file://gmsa_gmsa-acct-1.json" + credentialspec2 := "credentialspec:file://gmsa_gmsa-acct-2.json" + + expectedCredentialSpecContainerMap := map[string]string{credentialspec1: "webapp2", credentialspec2: "webapp3"} + + assert.True(t, reflect.DeepEqual(expectedCredentialSpecContainerMap, credentialSpecContainerMap)) +} + +func TestGetCredentialSpecResource(t *testing.T) { + credentialspecResource := &credentialspec.CredentialSpecResource{} + task := &Task{ + ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + } + task.AddResource(credentialspec.ResourceName, credentialspecResource) + + credentialspecTaskResource, ok := task.GetCredentialSpecResource() + assert.True(t, ok) + assert.NotEmpty(t, credentialspecTaskResource) +} + +func TestInitializeAndGetCredentialSpecResource(t *testing.T) { + hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" + container := &apicontainer.Container{ + Name: "myName", + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + } + container.DockerConfig.HostConfig = &hostConfig + + task := &Task{ + Arn: "test", + Containers: []*apicontainer.Container{container}, + ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + AWSRegion: "test-aws-region", + } + + credentialsManager := mock_credentials.NewMockManager(ctrl) + ssmClientCreator := mock_ssm_factory.NewMockSSMClientCreator(ctrl) + s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) + + resFields := &taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, + }, + } + + task.initializeCredentialSpecResource(cfg, credentialsManager, resFields) + + resourceDep := apicontainer.ResourceDependency{ + Name: credentialspec.ResourceName, + RequiredStatus: resourcestatus.ResourceStatus(credentialspec.CredentialSpecCreated), + } + + assert.Equal(t, resourceDep, task.Containers[0].TransitionDependenciesMap[apicontainerstatus.ContainerCreated].ResourceDependencies[0]) + + _, ok := task.GetCredentialSpecResource() + assert.True(t, ok) +} diff --git a/agent/api/task/task_test_utils.go b/agent/api/task/task_test_utils.go index 66707a2b5c1..d8c3ec97012 100644 --- a/agent/api/task/task_test_utils.go +++ b/agent/api/task/task_test_utils.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/task/task_unsupported.go b/agent/api/task/task_unsupported.go index 33c3636e9ff..c4671dceae6 100644 --- a/agent/api/task/task_unsupported.go +++ b/agent/api/task/task_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -19,6 +20,8 @@ import ( "time" "github.com/aws/amazon-ecs-agent/agent/ecscni" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" @@ -42,6 +45,16 @@ func (task *Task) adjustForPlatform(cfg *config.Config) { } func (task *Task) initializeCgroupResourceSpec(cgroupPath string, cGroupCPUPeriod time.Duration, resourceFields *taskresource.ResourceFields) error { + if !task.MemoryCPULimitsEnabled { + if task.CPU > 0 || task.Memory > 0 { + // Client-side validation/warning if a task with task-level CPU/memory limits specified somehow lands on an instance + // where agent does not support it. These limits will be ignored. + logger.Warn("Ignoring task-level CPU/memory limits since agent does not support the TaskCPUMemLimits capability", logger.Fields{ + field.TaskID: task.GetID(), + }) + } + return nil + } return nil } @@ -93,8 +106,13 @@ func (task *Task) initializeFSxWindowsFileServerResource(cfg *config.Config, cre return errors.New("task with FSx for Windows File Server volumes is only supported on Windows container instance") } -// BuildCNIConfig builds the configuration for the CNI plugins +// BuildCNIConfigAwsvpc builds the configuration for the CNI plugins // On unsupported platforms, we will not support this functionality -func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { +func (task *Task) BuildCNIConfigAwsvpc(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { + return nil, errors.New("unsupported platform") +} + +// BuildCNIConfigBridgeMode builds a list of CNI network configurations for a task in docker bridge mode. +func (task *Task) BuildCNIConfigBridgeMode(cniConfig *ecscni.Config, containerName string) (*ecscni.Config, error) { return nil, errors.New("unsupported platform") } diff --git a/agent/api/task/task_windows.go b/agent/api/task/task_windows.go index 766a48555d8..b131baf7072 100644 --- a/agent/api/task/task_windows.go +++ b/agent/api/task/task_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -20,17 +21,16 @@ import ( "time" "github.com/aws/amazon-ecs-agent/agent/ecscni" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/containernetworking/cni/libcni" - apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/taskresource" - "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" "github.com/aws/amazon-ecs-agent/agent/taskresource/fsxwindowsfileserver" - resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" resourcetype "github.com/aws/amazon-ecs-agent/agent/taskresource/types" taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/cihub/seelog" @@ -123,64 +123,17 @@ func (task *Task) dockerCPUShares(containerCPU uint) int64 { } func (task *Task) initializeCgroupResourceSpec(cgroupPath string, cGroupCPUPeriod time.Duration, resourceFields *taskresource.ResourceFields) error { - return errors.New("unsupported platform") -} - -// requiresCredentialSpecResource returns true if at least one container in the task -// needs a valid credentialspec resource -func (task *Task) requiresCredentialSpecResource() bool { - for _, container := range task.Containers { - if container.RequiresCredentialSpec() { - return true - } - } - return false -} - -// initializeCredentialSpecResource builds the resource dependency map for the credentialspec resource -func (task *Task) initializeCredentialSpecResource(config *config.Config, credentialsManager credentials.Manager, - resourceFields *taskresource.ResourceFields) error { - credentialspecResource, err := credentialspec.NewCredentialSpecResource(task.Arn, config.AWSRegion, task.getAllCredentialSpecRequirements(), - task.ExecutionCredentialsID, credentialsManager, resourceFields.SSMClientCreator, resourceFields.S3ClientCreator) - if err != nil { - return err - } - - task.AddResource(credentialspec.ResourceName, credentialspecResource) - - // for every container that needs credential spec vending, it needs to wait for all credential spec resources - for _, container := range task.Containers { - if container.RequiresCredentialSpec() { - container.BuildResourceDependency(credentialspecResource.GetName(), - resourcestatus.ResourceStatus(credentialspec.CredentialSpecCreated), - apicontainerstatus.ContainerCreated) - } - } - - return nil -} - -// getAllCredentialSpecRequirements is used to build all the credential spec requirements for the task -func (task *Task) getAllCredentialSpecRequirements() []string { - reqs := []string{} - - for _, container := range task.Containers { - credentialSpec, err := container.GetCredentialSpec() - if err == nil && credentialSpec != "" && !utils.StrSliceContains(reqs, credentialSpec) { - reqs = append(reqs, credentialSpec) + if !task.MemoryCPULimitsEnabled { + if task.CPU > 0 || task.Memory > 0 { + // Client-side validation/warning if a task with task-level CPU/memory limits specified somehow lands on an instance + // where agent does not support it. These limits will be ignored. + logger.Warn("Ignoring task-level CPU/memory limits since agent does not support the TaskCPUMemLimits capability", logger.Fields{ + field.TaskID: task.GetID(), + }) } + return nil } - - return reqs -} - -// GetCredentialSpecResource retrieves credentialspec resource from resource map -func (task *Task) GetCredentialSpecResource() ([]taskresource.TaskResource, bool) { - task.lock.RLock() - defer task.lock.RUnlock() - - res, ok := task.ResourcesMapUnsafe[credentialspec.ResourceName] - return res, ok + return errors.New("unsupported platform") } func enableIPv6SysctlSetting(hostConfig *dockercontainer.HostConfig) { @@ -250,8 +203,8 @@ func (task *Task) addFSxWindowsFileServerResource( return nil } -// BuildCNIConfig builds a list of CNI network configurations for the task. -func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { +// BuildCNIConfigAwsvpc builds a list of CNI network configurations for the task. +func (task *Task) BuildCNIConfigAwsvpc(includeIPAMConfig bool, cniConfig *ecscni.Config) (*ecscni.Config, error) { if !task.IsNetworkModeAWSVPC() { return nil, errors.New("task config: task network mode is not awsvpc") } @@ -278,7 +231,7 @@ func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Confi // IfName is expected by the plugin but is not used. cniConfig.NetworkConfigs = append(cniConfig.NetworkConfigs, &ecscni.NetworkConfig{ - IfName: eni.ID, + IfName: ecscni.DefaultENIName, CNINetworkConfig: netconf, }) } @@ -295,3 +248,8 @@ func (task *Task) BuildCNIConfig(includeIPAMConfig bool, cniConfig *ecscni.Confi return cniConfig, nil } + +// BuildCNIConfigBridgeMode builds a list of CNI network configurations for a task in docker bridge mode. +func (task *Task) BuildCNIConfigBridgeMode(cniConfig *ecscni.Config, containerName string) (*ecscni.Config, error) { + return nil, errors.New("unsupported platform") +} diff --git a/agent/api/task/task_windows_test.go b/agent/api/task/task_windows_test.go index cde35b42024..06e22262490 100644 --- a/agent/api/task/task_windows_test.go +++ b/agent/api/task/task_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -29,9 +30,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/ecscni" "github.com/aws/amazon-ecs-agent/agent/taskresource" - "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" "github.com/aws/amazon-ecs-agent/agent/taskresource/fsxwindowsfileserver" - resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/golang/mock/gomock" @@ -44,7 +43,6 @@ import ( mock_asm_factory "github.com/aws/amazon-ecs-agent/agent/asm/factory/mocks" mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" mock_fsx_factory "github.com/aws/amazon-ecs-agent/agent/fsx/factory/mocks" - mock_s3_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" mock_ssm_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" ) @@ -121,19 +119,20 @@ func TestPostUnmarshalWindowsCanonicalPaths(t *testing.T) { task.PostUnmarshalTask(&cfg, nil, nil, nil, nil) for _, container := range task.Containers { // remove v3 endpoint from each container because it's randomly generated - removeV3andV4EndpointConfig(container) + removeEndpointConfigFromEnvironment(container) } assert.Equal(t, expectedTask.Containers, task.Containers, "Containers should be equal") assert.Equal(t, expectedTask.Volumes, task.Volumes, "Volumes should be equal") } -// removeV3EndpointConfig removes the v3 endpoint id and the injected env for a container +// removeEndpointConfigFromEnvironment removes the v3 endpoint id and the injected env for a container // so that checking all other fields can be easier -func removeV3andV4EndpointConfig(container *apicontainer.Container) { +func removeEndpointConfigFromEnvironment(container *apicontainer.Container) { container.SetV3EndpointID("") if container.Environment != nil { delete(container.Environment, apicontainer.MetadataURIEnvironmentVariableName) delete(container.Environment, apicontainer.MetadataURIEnvVarNameV4) + delete(container.Environment, apicontainer.AgentURIEnvVarName) } if len(container.Environment) == 0 { container.Environment = nil @@ -353,172 +352,6 @@ func TestGetCanonicalPath(t *testing.T) { } } -func TestRequiresCredentialSpecResource(t *testing.T) { - container1 := &apicontainer.Container{} - task1 := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{container1}, - } - - hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" - container2 := &apicontainer.Container{} - container2.DockerConfig.HostConfig = &hostConfig - task2 := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{container2}, - } - - testCases := []struct { - name string - task *Task - expectedOutput bool - }{ - { - name: "missing_credentialspec", - task: task1, - expectedOutput: false, - }, - { - name: "valid_credentialspec", - task: task2, - expectedOutput: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expectedOutput, tc.task.requiresCredentialSpecResource()) - }) - } - -} - -func TestGetAllCredentialSpecRequirements(t *testing.T) { - hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" - container := &apicontainer.Container{} - container.DockerConfig.HostConfig = &hostConfig - - task := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{container}, - } - - allCredSpecReq := task.getAllCredentialSpecRequirements() - - credentialspec := "credentialspec:file://gmsa_gmsa-acct.json" - expectedCredSpecReq := []string{credentialspec} - - assert.EqualValues(t, expectedCredSpecReq, allCredSpecReq) -} - -func TestGetAllCredentialSpecRequirementsWithMultipleContainersUsingSameSpec(t *testing.T) { - hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" - c1 := &apicontainer.Container{} - c1.DockerConfig.HostConfig = &hostConfig - - c2 := &apicontainer.Container{} - c2.DockerConfig.HostConfig = &hostConfig - - task := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{c1, c2}, - } - - allCredSpecReq := task.getAllCredentialSpecRequirements() - - credentialspec := "credentialspec:file://gmsa_gmsa-acct.json" - expectedCredSpecReq := []string{credentialspec} - - assert.Equal(t, len(expectedCredSpecReq), len(allCredSpecReq)) - assert.EqualValues(t, expectedCredSpecReq, allCredSpecReq) -} - -func TestGetAllCredentialSpecRequirementsWithMultipleContainers(t *testing.T) { - hostConfig1 := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct-1.json\"]}" - hostConfig2 := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct-2.json\"]}" - - c1 := &apicontainer.Container{} - c1.DockerConfig.HostConfig = &hostConfig1 - - c2 := &apicontainer.Container{} - c2.DockerConfig.HostConfig = &hostConfig1 - - c3 := &apicontainer.Container{} - c3.DockerConfig.HostConfig = &hostConfig2 - - task := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{c1, c2, c3}, - } - - allCredSpecReq := task.getAllCredentialSpecRequirements() - - credentialspec1 := "credentialspec:file://gmsa_gmsa-acct-1.json" - credentialspec2 := "credentialspec:file://gmsa_gmsa-acct-2.json" - - expectedCredSpecReq := []string{credentialspec1, credentialspec2} - - assert.EqualValues(t, expectedCredSpecReq, allCredSpecReq) -} - -func TestInitializeAndGetCredentialSpecResource(t *testing.T) { - hostConfig := "{\"SecurityOpt\": [\"credentialspec:file://gmsa_gmsa-acct.json\"]}" - container := &apicontainer.Container{ - Name: "myName", - TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), - } - container.DockerConfig.HostConfig = &hostConfig - - task := &Task{ - Arn: "test", - Containers: []*apicontainer.Container{container}, - ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), - } - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - cfg := &config.Config{ - AWSRegion: "test-aws-region", - } - - credentialsManager := mock_credentials.NewMockManager(ctrl) - ssmClientCreator := mock_ssm_factory.NewMockSSMClientCreator(ctrl) - s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) - - resFields := &taskresource.ResourceFields{ - ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ - SSMClientCreator: ssmClientCreator, - CredentialsManager: credentialsManager, - }, - S3ClientCreator: s3ClientCreator, - } - - task.initializeCredentialSpecResource(cfg, credentialsManager, resFields) - - resourceDep := apicontainer.ResourceDependency{ - Name: credentialspec.ResourceName, - RequiredStatus: resourcestatus.ResourceStatus(credentialspec.CredentialSpecCreated), - } - - assert.Equal(t, resourceDep, task.Containers[0].TransitionDependenciesMap[apicontainerstatus.ContainerCreated].ResourceDependencies[0]) - - _, ok := task.GetCredentialSpecResource() - assert.True(t, ok) -} - -func TestGetCredentialSpecResource(t *testing.T) { - credentialspecResource := &credentialspec.CredentialSpecResource{} - task := &Task{ - ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), - } - task.AddResource(credentialspec.ResourceName, credentialspecResource) - - credentialspecTaskResource, ok := task.GetCredentialSpecResource() - assert.True(t, ok) - assert.NotEmpty(t, credentialspecTaskResource) -} - func TestRequiresFSxWindowsFileServerResource(t *testing.T) { task1 := &Task{ Arn: "test1", @@ -729,6 +562,7 @@ func TestPostUnmarshalTaskWithFSxWindowsFileServerVolumes(t *testing.T) { // TestBuildCNIConfig tests if the generated CNI config is correct func TestBuildCNIConfig(t *testing.T) { testTask := &Task{} + testTask.NetworkMode = AWSVPCNetworkMode testTask.AddTaskENI(&apieni.ENI{ ID: "TestBuildCNIConfig", MacAddress: mac, @@ -742,7 +576,7 @@ func TestBuildCNIConfig(t *testing.T) { }, }) - cniConfig, err := testTask.BuildCNIConfig(true, &ecscni.Config{ + cniConfig, err := testTask.BuildCNIConfigAwsvpc(true, &ecscni.Config{ MinSupportedCNIVersion: "latest", }) assert.NoError(t, err) diff --git a/agent/api/task/taskvolume_test.go b/agent/api/task/taskvolume_test.go index dac7a2bffaf..ed0f8ad9a91 100644 --- a/agent/api/task/taskvolume_test.go +++ b/agent/api/task/taskvolume_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -90,6 +91,7 @@ func TestMarshalTaskVolumesEFS(t *testing.T) { "Arn": "test", "Family": "", "Version": "", + "ServiceName": "", "Containers": null, "associations": null, "resources": null, diff --git a/agent/api/task/taskvolume_windows_test.go b/agent/api/task/taskvolume_windows_test.go index b85bcb44489..98070de3f3f 100644 --- a/agent/api/task/taskvolume_windows_test.go +++ b/agent/api/task/taskvolume_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -50,6 +51,7 @@ func TestMarshalTaskVolumeFSxWindowsFileServer(t *testing.T) { "Arn": "test", "Family": "", "Version": "", + "ServiceName": "", "Containers": null, "associations": null, "resources": null, diff --git a/agent/api/task/types_unmarshal_test.go b/agent/api/task/types_unmarshal_test.go index 04890be8890..bd2c3d069a8 100644 --- a/agent/api/task/types_unmarshal_test.go +++ b/agent/api/task/types_unmarshal_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/task/util_test.go b/agent/api/task/util_test.go index 1b17240e443..8c440971b8b 100644 --- a/agent/api/task/util_test.go +++ b/agent/api/task/util_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/api/testutils/container_equal_test.go b/agent/api/testutils/container_equal_test.go index b1162560bb0..a3519887974 100644 --- a/agent/api/testutils/container_equal_test.go +++ b/agent/api/testutils/container_equal_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -46,7 +47,7 @@ func TestContainerEqual(t *testing.T) { {apicontainer.Container{Memory: 1}, apicontainer.Container{Memory: 1}, true}, {apicontainer.Container{Links: []string{"1", "2"}}, apicontainer.Container{Links: []string{"1", "2"}}, true}, {apicontainer.Container{Links: []string{"1", "2"}}, apicontainer.Container{Links: []string{"2", "1"}}, true}, - {apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolTCP}}}, true}, + {apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, true}, {apicontainer.Container{Essential: true}, apicontainer.Container{Essential: true}, true}, {apicontainer.Container{EntryPoint: nil}, apicontainer.Container{EntryPoint: nil}, true}, {apicontainer.Container{EntryPoint: &[]string{"1", "2"}}, apicontainer.Container{EntryPoint: &[]string{"1", "2"}}, true}, @@ -65,9 +66,9 @@ func TestContainerEqual(t *testing.T) { {apicontainer.Container{CPU: 1}, apicontainer.Container{CPU: 2e2}, false}, {apicontainer.Container{Memory: 1}, apicontainer.Container{Memory: 2e2}, false}, {apicontainer.Container{Links: []string{"1", "2"}}, apicontainer.Container{Links: []string{"1", "二"}}, false}, - {apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "二", apicontainer.TransportProtocolTCP}}}, false}, - {apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 22, "1", apicontainer.TransportProtocolTCP}}}, false}, - {apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{1, 2, "1", apicontainer.TransportProtocolUDP}}}, false}, + {apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "二", Protocol: apicontainer.TransportProtocolTCP}}}, false}, + {apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 22, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, false}, + {apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolTCP}}}, apicontainer.Container{Ports: []apicontainer.PortBinding{{ContainerPort: 1, HostPort: 2, BindIP: "1", Protocol: apicontainer.TransportProtocolUDP}}}, false}, {apicontainer.Container{Essential: true}, apicontainer.Container{Essential: false}, false}, {apicontainer.Container{EntryPoint: nil}, apicontainer.Container{EntryPoint: &[]string{"nonnil"}}, false}, {apicontainer.Container{EntryPoint: &[]string{"1", "2"}}, apicontainer.Container{EntryPoint: &[]string{"2", "1"}}, false}, diff --git a/agent/api/testutils/task_equal_test.go b/agent/api/testutils/task_equal_test.go index 069e583e0bf..68300334d30 100644 --- a/agent/api/testutils/task_equal_test.go +++ b/agent/api/testutils/task_equal_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent.go b/agent/app/agent.go index 91658edcd14..420b31b25a5 100644 --- a/agent/app/agent.go +++ b/agent/app/agent.go @@ -48,6 +48,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ecscni" "github.com/aws/amazon-ecs-agent/agent/engine" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" + engineserviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eni/pause" "github.com/aws/amazon-ecs-agent/agent/eventhandler" "github.com/aws/amazon-ecs-agent/agent/eventstream" @@ -59,6 +60,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/taskresource" tcshandler "github.com/aws/amazon-ecs-agent/agent/tcs/handler" "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper" "github.com/aws/amazon-ecs-agent/agent/utils/retry" "github.com/aws/amazon-ecs-agent/agent/version" @@ -85,7 +87,7 @@ const ( instanceIdBackoffMax = time.Second * 5 instanceIdBackoffJitter = 0.2 instanceIdBackoffMultiple = 1.3 - instanceIdMaxRetryCount = 3 + instanceIdMaxRetryCount = 5 targetLifecycleBackoffMin = time.Second targetLifecycleBackoffMax = time.Second * 5 @@ -133,7 +135,8 @@ type ecsAgent struct { credentialProvider *aws_credentials.Credentials stateManagerFactory factory.StateManager saveableOptionFactory factory.SaveableOption - pauseLoader pause.Loader + pauseLoader loader.Loader + serviceconnectManager engineserviceconnect.Manager eniWatcher *watcher.ENIWatcher cniClient ecscni.CNIClient vpc string @@ -225,10 +228,11 @@ func newAgent(blackholeEC2Metadata bool, acceptInsecureCert *bool) (agent, error // We instantiate our own credentialProvider for use in acs/tcs. This tries // to mimic roughly the way it's instantiated by the SDK for a default // session. - credentialProvider: instancecreds.GetCredentials(), + credentialProvider: instancecreds.GetCredentials(cfg.External.Enabled()), stateManagerFactory: factory.NewStateManager(), saveableOptionFactory: factory.NewSaveableOption(), pauseLoader: pause.New(), + serviceconnectManager: engineserviceconnect.NewManager(), cniClient: ecscni.NewClient(cfg.CNIPluginsPath), metadataManager: metadataManager, terminationHandler: sighandlers.StartDefaultTerminationHandler, @@ -303,8 +307,8 @@ func (agent *ecsAgent) doStart(containerChangeEventStream *eventstream.EventStre } // Create the task engine - taskEngine, currentEC2InstanceID, err := agent.newTaskEngine(containerChangeEventStream, - credentialsManager, state, imageManager, execCmdMgr) + taskEngine, currentEC2InstanceID, err := agent.newTaskEngine( + containerChangeEventStream, credentialsManager, state, imageManager, execCmdMgr, agent.serviceconnectManager) if err != nil { seelog.Criticalf("Unable to initialize new task engine: %v", err) return exitcodes.ExitTerminal @@ -334,7 +338,7 @@ func (agent *ecsAgent) doStart(containerChangeEventStream *eventstream.EventStre if agent.cfg.TaskENIEnabled.Enabled() { // check pause container image load if loadPauseErr != nil { - if pause.IsNoSuchFileError(loadPauseErr) || pause.UnsupportedPlatform(loadPauseErr) { + if loader.IsNoSuchFileError(loadPauseErr) || loader.IsUnsupportedPlatform(loadPauseErr) { return exitcodes.ExitTerminal } else { return exitcodes.ExitError @@ -363,6 +367,24 @@ func (agent *ecsAgent) doStart(containerChangeEventStream *eventstream.EventStre } return exitcodes.ExitError } + } else if !agent.cfg.External.Enabled() { + // Set VPC and Subnet IDs for the EC2 instance + err, terminal := agent.setVPCSubnet() + switch err { + case nil: + // No error so do nothing + case instanceNotLaunchedInVPCError: + // We have ascertained that the EC2 Instance is not running in a VPC + // No need to stop the ECS Agent in this case + logger.Info("Unable to detect VPC ID for the instance as it was not launched in VPC mode.") + default: + // Encountered an error initializing VPC ID and Subnet + seelog.Criticalf("Unable to detect VPC ID and Subnet: %v", err) + if terminal { + return exitcodes.ExitTerminal + } + return exitcodes.ExitError + } } // Register the container instance @@ -373,6 +395,11 @@ func (agent *ecsAgent) doStart(containerChangeEventStream *eventstream.EventStre } return exitcodes.ExitTerminal } + scManager := agent.serviceconnectManager + scManager.SetECSClient(client, agent.containerInstanceARN) + if loaded, _ := scManager.IsLoaded(agent.dockerClient); loaded { + imageManager.AddImageToCleanUpExclusionList(agent.serviceconnectManager.GetLoadedImageName()) + } // Add container instance ARN to metadata manager if agent.cfg.ContainerMetadataEnabled.Enabled() { @@ -488,7 +515,8 @@ func (agent *ecsAgent) newTaskEngine(containerChangeEventStream *eventstream.Eve credentialsManager credentials.Manager, state dockerstate.TaskEngineState, imageManager engine.ImageManager, - execCmdMgr execcmd.Manager) (engine.TaskEngine, string, error) { + execCmdMgr execcmd.Manager, + serviceConnectManager engineserviceconnect.Manager) (engine.TaskEngine, string, error) { containerChangeEventStream.StartListening() @@ -496,10 +524,10 @@ func (agent *ecsAgent) newTaskEngine(containerChangeEventStream *eventstream.Eve seelog.Info("Checkpointing not enabled; a new container instance will be created each time the agent is run") return engine.NewTaskEngine(agent.cfg, agent.dockerClient, credentialsManager, containerChangeEventStream, imageManager, state, - agent.metadataManager, agent.resourceFields, execCmdMgr), "", nil + agent.metadataManager, agent.resourceFields, execCmdMgr, serviceConnectManager), "", nil } - savedData, err := agent.loadData(containerChangeEventStream, credentialsManager, state, imageManager, execCmdMgr) + savedData, err := agent.loadData(containerChangeEventStream, credentialsManager, state, imageManager, execCmdMgr, serviceConnectManager) if err != nil { seelog.Criticalf("Error loading previously saved state: %v", err) return nil, "", err @@ -512,6 +540,10 @@ func (agent *ecsAgent) newTaskEngine(containerChangeEventStream *eventstream.Eve } currentEC2InstanceID := agent.getEC2InstanceID() + if currentEC2InstanceID == "" { + currentEC2InstanceID = savedData.ec2InstanceID + seelog.Warnf("Not able to get EC2 Instance ID from IMDS, using EC2 Instance ID from saved state: '%s'", currentEC2InstanceID) + } if savedData.ec2InstanceID != "" && savedData.ec2InstanceID != currentEC2InstanceID { seelog.Warnf(instanceIDMismatchErrorFormat, savedData.ec2InstanceID, currentEC2InstanceID) @@ -521,7 +553,7 @@ func (agent *ecsAgent) newTaskEngine(containerChangeEventStream *eventstream.Eve // Reset taskEngine; all the other values are still default return engine.NewTaskEngine(agent.cfg, agent.dockerClient, credentialsManager, containerChangeEventStream, imageManager, state, agent.metadataManager, - agent.resourceFields, execCmdMgr), currentEC2InstanceID, nil + agent.resourceFields, execCmdMgr, serviceConnectManager), currentEC2InstanceID, nil } if savedData.cluster != "" { @@ -813,9 +845,9 @@ func (agent *ecsAgent) startAsyncRoutines( // Start serving the endpoint to fetch IAM Role credentials and other task metadata if agent.cfg.TaskMetadataAZDisabled { // send empty availability zone - go handlers.ServeTaskHTTPEndpoint(agent.ctx, credentialsManager, state, client, agent.containerInstanceARN, agent.cfg, statsEngine, "") + go handlers.ServeTaskHTTPEndpoint(agent.ctx, credentialsManager, state, client, agent.containerInstanceARN, agent.cfg, statsEngine, "", agent.vpc) } else { - go handlers.ServeTaskHTTPEndpoint(agent.ctx, credentialsManager, state, client, agent.containerInstanceARN, agent.cfg, statsEngine, agent.availabilityZone) + go handlers.ServeTaskHTTPEndpoint(agent.ctx, credentialsManager, state, client, agent.containerInstanceARN, agent.cfg, statsEngine, agent.availabilityZone, agent.vpc) } // Start sending events to the backend diff --git a/agent/app/agent_capability.go b/agent/app/agent_capability.go index cc90dc89d71..9041167c75d 100644 --- a/agent/app/agent_capability.go +++ b/agent/app/agent_capability.go @@ -23,6 +23,8 @@ import ( "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" "github.com/aws/aws-sdk-go/aws" "github.com/cihub/seelog" "github.com/pkg/errors" @@ -40,6 +42,7 @@ const ( appMeshAttributeSuffix = "aws-appmesh" cniPluginVersionSuffix = "cni-plugin-version" capabilityTaskCPUMemLimit = "task-cpu-mem-limit" + capabilityIncreasedTaskCPULimit = "increased-task-cpu-limit" capabilityDockerPluginInfix = "docker-plugin." attributeSeparator = "." capabilityPrivateRegistryAuthASM = "private-registry-authentication.secretsmanager" @@ -72,6 +75,11 @@ const ( capabilityExecConfigRelativePath = "config" capabilityExecCertsRelativePath = "certs" capabilityExternal = "external" + capabilityServiceConnect = "service-connect-v1" + + // network capabilities, going forward, please append "network." prefix to any new networking capability we introduce + networkCapabilityPrefix = "network." + capabilityContainerPortRange = networkCapabilityPrefix + "container-port-range" ) var ( @@ -95,6 +103,8 @@ var ( capabilityFullTaskSync, // ecs agent version 1.39.0 supports bulk loading env vars through environmentFiles in S3 capabilityEnvFilesS3, + // support container port range in container definition - port mapping field + capabilityContainerPortRange, } // use empty struct as value type to simulate set capabilityExecInvalidSsmVersions = map[string]struct{}{} @@ -113,6 +123,7 @@ var ( attributePrefix + appMeshAttributeSuffix, attributePrefix + taskEIAAttributeSuffix, attributePrefix + taskEIAWithOptimizedCPU, + attributePrefix + capabilityServiceConnect, } // List of capabilities that are only supported on external capaciity. Currently only one but keep as a list // for future proof and also align with externalUnsupportedCapabilities. @@ -128,53 +139,55 @@ var ( // capabilities returns the supported capabilities of this agent / docker-client pair. // Currently, the following capabilities are possible: // -// com.amazonaws.ecs.capability.privileged-container -// com.amazonaws.ecs.capability.docker-remote-api.1.17 -// com.amazonaws.ecs.capability.docker-remote-api.1.18 -// com.amazonaws.ecs.capability.docker-remote-api.1.19 -// com.amazonaws.ecs.capability.docker-remote-api.1.20 -// com.amazonaws.ecs.capability.logging-driver.json-file -// com.amazonaws.ecs.capability.logging-driver.syslog -// com.amazonaws.ecs.capability.logging-driver.fluentd -// com.amazonaws.ecs.capability.logging-driver.journald -// com.amazonaws.ecs.capability.logging-driver.gelf -// com.amazonaws.ecs.capability.logging-driver.none -// com.amazonaws.ecs.capability.selinux -// com.amazonaws.ecs.capability.apparmor -// com.amazonaws.ecs.capability.ecr-auth -// com.amazonaws.ecs.capability.task-iam-role -// com.amazonaws.ecs.capability.task-iam-role-network-host -// ecs.capability.docker-volume-driver.${driverName} -// ecs.capability.task-eni -// ecs.capability.task-eni-block-instance-metadata -// ecs.capability.execution-role-ecr-pull -// ecs.capability.execution-role-awslogs -// ecs.capability.container-health-check -// ecs.capability.private-registry-authentication.secretsmanager -// ecs.capability.secrets.ssm.environment-variables -// ecs.capability.secrets.ssm.bootstrap.log-driver -// ecs.capability.pid-ipc-namespace-sharing -// ecs.capability.ecr-endpoint -// ecs.capability.secrets.asm.environment-variables -// ecs.capability.secrets.asm.bootstrap.log-driver -// ecs.capability.aws-appmesh -// ecs.capability.task-eia -// ecs.capability.task-eni-trunking -// ecs.capability.task-eia.optimized-cpu -// ecs.capability.firelens.fluentd -// ecs.capability.firelens.fluentbit -// ecs.capability.efs -// com.amazonaws.ecs.capability.logging-driver.awsfirelens -// ecs.capability.logging-driver.awsfirelens.log-driver-buffer-limit -// ecs.capability.firelens.options.config.file -// ecs.capability.firelens.options.config.s3 -// ecs.capability.full-sync -// ecs.capability.gmsa -// ecs.capability.efsAuth -// ecs.capability.env-files.s3 -// ecs.capability.fsxWindowsFileServer -// ecs.capability.execute-command -// ecs.capability.external +// com.amazonaws.ecs.capability.privileged-container +// com.amazonaws.ecs.capability.docker-remote-api.1.17 +// com.amazonaws.ecs.capability.docker-remote-api.1.18 +// com.amazonaws.ecs.capability.docker-remote-api.1.19 +// com.amazonaws.ecs.capability.docker-remote-api.1.20 +// com.amazonaws.ecs.capability.logging-driver.json-file +// com.amazonaws.ecs.capability.logging-driver.syslog +// com.amazonaws.ecs.capability.logging-driver.fluentd +// com.amazonaws.ecs.capability.logging-driver.journald +// com.amazonaws.ecs.capability.logging-driver.gelf +// com.amazonaws.ecs.capability.logging-driver.none +// com.amazonaws.ecs.capability.selinux +// com.amazonaws.ecs.capability.apparmor +// com.amazonaws.ecs.capability.ecr-auth +// com.amazonaws.ecs.capability.task-iam-role +// com.amazonaws.ecs.capability.task-iam-role-network-host +// ecs.capability.docker-volume-driver.${driverName} +// ecs.capability.task-eni +// ecs.capability.task-eni-block-instance-metadata +// ecs.capability.execution-role-ecr-pull +// ecs.capability.execution-role-awslogs +// ecs.capability.container-health-check +// ecs.capability.private-registry-authentication.secretsmanager +// ecs.capability.secrets.ssm.environment-variables +// ecs.capability.secrets.ssm.bootstrap.log-driver +// ecs.capability.pid-ipc-namespace-sharing +// ecs.capability.ecr-endpoint +// ecs.capability.secrets.asm.environment-variables +// ecs.capability.secrets.asm.bootstrap.log-driver +// ecs.capability.aws-appmesh +// ecs.capability.task-eia +// ecs.capability.task-eni-trunking +// ecs.capability.task-eia.optimized-cpu +// ecs.capability.firelens.fluentd +// ecs.capability.firelens.fluentbit +// ecs.capability.efs +// com.amazonaws.ecs.capability.logging-driver.awsfirelens +// ecs.capability.logging-driver.awsfirelens.log-driver-buffer-limit +// ecs.capability.firelens.options.config.file +// ecs.capability.firelens.options.config.s3 +// ecs.capability.full-sync +// ecs.capability.gmsa +// ecs.capability.efsAuth +// ecs.capability.env-files.s3 +// ecs.capability.fsxWindowsFileServer +// ecs.capability.execute-command +// ecs.capability.external +// ecs.capability.service-connect-v1 +// ecs.capability.network.container-port-range func (agent *ecsAgent) capabilities() ([]*ecs.Attribute, error) { var capabilities []*ecs.Attribute @@ -210,6 +223,7 @@ func (agent *ecsAgent) capabilities() ([]*ecs.Attribute, error) { return nil, err } + capabilities = agent.appendIncreasedTaskCPULimitCapability(capabilities) capabilities = agent.appendTaskENICapabilities(capabilities) capabilities = agent.appendENITrunkingCapabilities(capabilities) capabilities = agent.appendDockerDependentCapabilities(capabilities, supportedVersions) @@ -269,6 +283,8 @@ func (agent *ecsAgent) capabilities() ([]*ecs.Attribute, error) { if err != nil { return nil, err } + // add service-connect capabilities if applicable + capabilities = agent.appendServiceConnectCapabilities(capabilities) if agent.cfg.External.Enabled() { // Add external specific capability; remove external unsupported capabilities. @@ -295,6 +311,14 @@ func (agent *ecsAgent) appendDockerDependentCapabilities(capabilities []*ecs.Att return capabilities } +func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { + if agent.cfg.GMSACapable { + return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityGMSA) + } + + return capabilities +} + func (agent *ecsAgent) appendLoggingDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { knownVersions := make(map[dockerclient.DockerVersion]struct{}) // Determine known API versions. Known versions are used exclusively for logging-driver enablement, since none of @@ -351,6 +375,17 @@ func (agent *ecsAgent) appendTaskCPUMemLimitCapabilities(capabilities []*ecs.Att return capabilities, nil } +func (agent *ecsAgent) appendIncreasedTaskCPULimitCapability(capabilities []*ecs.Attribute) []*ecs.Attribute { + if !agent.cfg.TaskCPUMemLimit.Enabled() { + // don't register the "increased-task-cpu-limit" capability if the "task-cpu-mem-limit" capability is disabled. + // "task-cpu-mem-limit" capability may be explicitly disabled or disabled due to unsupported docker version. + seelog.Warn("Increased Task CPU Limit capability is disabled since the Task CPU + Mem Limit capability is disabled.") + } else { + capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityIncreasedTaskCPULimit) + } + return capabilities +} + func (agent *ecsAgent) appendTaskENICapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { if agent.cfg.TaskENIEnabled.Enabled() { // The assumption here is that all of the dependencies for supporting the @@ -425,6 +460,32 @@ func (agent *ecsAgent) appendExecCapabilities(capabilities []*ecs.Attribute) ([] return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityExec), nil } +func (agent *ecsAgent) appendServiceConnectCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { + if loaded, _ := agent.serviceconnectManager.IsLoaded(agent.dockerClient); !loaded { + _, err := agent.serviceconnectManager.LoadImage(agent.ctx, agent.cfg, agent.dockerClient) + if err != nil { + logger.Error("ServiceConnect Capability: Failed to load appnet Agent container. This container instance will not be able to support ServiceConnect tasks", + logger.Fields{ + field.Error: err, + }, + ) + return capabilities + } + } + loadedVer, _ := agent.serviceconnectManager.GetLoadedAppnetVersion() + supportedAppnetInterfaceVerToCapabilities, _ := agent.serviceconnectManager.GetCapabilitiesForAppnetInterfaceVersion(loadedVer) + if supportedAppnetInterfaceVerToCapabilities == nil { + logger.Warn("ServiceConnect Capability: No service connect capabilities were found for Appnet version:", logger.Fields{ + field.Image: loadedVer, + }, + ) + } + for _, serviceConnectCapability := range supportedAppnetInterfaceVerToCapabilities { + capabilities = appendNameOnlyAttribute(capabilities, serviceConnectCapability) + } + return capabilities +} + func defaultGetSubDirectories(path string) ([]string, error) { var subDirectories []string diff --git a/agent/app/agent_capability_test.go b/agent/app/agent_capability_test.go index 0753f3910c7..974972551f3 100644 --- a/agent/app/agent_capability_test.go +++ b/agent/app/agent_capability_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -30,7 +31,8 @@ import ( mock_dockerapi "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi/mocks" "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" mock_ecscni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks" - mock_pause "github.com/aws/amazon-ecs-agent/agent/eni/pause/mocks" + mock_serviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" + mock_loader "github.com/aws/amazon-ecs-agent/agent/utils/loader/mocks" mock_mobypkgwrapper "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper/mocks" "github.com/aws/aws-sdk-go/aws" @@ -71,7 +73,7 @@ func TestCapabilities(t *testing.T) { cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ AvailableLoggingDrivers: []dockerclient.LoggingDriver{ dockerclient.JSONFileDriver, @@ -89,6 +91,12 @@ func TestCapabilities(t *testing.T) { } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes().Return([]string{"ecs.capability.service-connect-v1"}, nil) + // Scan() and ListPluginsWithFilters() are tested with // AnyTimes() because they are not called in windows. gomock.InOrder( @@ -131,6 +139,8 @@ func TestCapabilities(t *testing.T) { attributePrefix + capabilityEnvFilesS3, attributePrefix + taskENIBlockInstanceMetadataAttributeSuffix, attributePrefix + capabilityExec, + attributePrefix + capabilityServiceConnect, + attributePrefix + capabilityContainerPortRange, } var expectedCapabilities []*ecs.Attribute @@ -150,13 +160,14 @@ func TestCapabilities(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -218,12 +229,17 @@ func getCapabilitiesWithConfig(cfg *config.Config, t *testing.T) []*ecs.Attribut client := mock_dockerapi.NewMockDockerClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() // CNI plugins are platform dependent. Therefore return version for any plugin query. mockCNIClient.EXPECT().Version(gomock.Any()).Return("v1", nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -243,13 +259,14 @@ func getCapabilitiesWithConfig(cfg *config.Config, t *testing.T) []*ecs.Attribut // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: cfg, - dockerClient: client, - pauseLoader: mockPauseLoader, - cniClient: mockCNIClient, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: cfg, + dockerClient: client, + pauseLoader: mockPauseLoader, + cniClient: mockCNIClient, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() require.NoError(t, err) @@ -271,18 +288,24 @@ func TestCapabilitiesECR(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - pauseLoader: mockPauseLoader, - dockerClient: client, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + pauseLoader: mockPauseLoader, + dockerClient: client, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -317,18 +340,24 @@ func TestCapabilitiesTaskIAMRoleForSupportedDockerVersion(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -360,18 +389,24 @@ func TestCapabilitiesTaskIAMRoleForUnSupportedDockerVersion(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -404,18 +439,24 @@ func TestCapabilitiesTaskIAMRoleNetworkHostForSupportedDockerVersion(t *testing. client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -448,18 +489,24 @@ func TestCapabilitiesTaskIAMRoleNetworkHostForUnSupportedDockerVersion(t *testin client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -481,7 +528,7 @@ func TestAWSVPCBlockInstanceMetadataWhenTaskENIIsDisabled(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ AvailableLoggingDrivers: []dockerclient.LoggingDriver{ dockerclient.JSONFileDriver, @@ -492,6 +539,11 @@ func TestAWSVPCBlockInstanceMetadataWhenTaskENIIsDisabled(t *testing.T) { mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -524,13 +576,14 @@ func TestAWSVPCBlockInstanceMetadataWhenTaskENIIsDisabled(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -572,19 +625,24 @@ func TestCapabilitiesExecutionRoleAWSLogs(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -607,8 +665,12 @@ func TestCapabilitiesTaskResourceLimit(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_22} mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -620,11 +682,12 @@ func TestCapabilitiesTaskResourceLimit(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } expectedCapability := attributePrefix + capabilityTaskCPUMemLimit @@ -650,8 +713,12 @@ func TestCapabilitesTaskResourceLimitDisabledByMissingDockerVersion(t *testing.T client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_19} mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -663,11 +730,12 @@ func TestCapabilitesTaskResourceLimitDisabledByMissingDockerVersion(t *testing.T // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } unexpectedCapability := attributePrefix + capabilityTaskCPUMemLimit @@ -692,8 +760,13 @@ func TestCapabilitesTaskResourceLimitErrorCase(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_19} - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -702,10 +775,11 @@ func TestCapabilitesTaskResourceLimitErrorCase(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - pauseLoader: mockPauseLoader, - dockerClient: client, + ctx: ctx, + cfg: conf, + pauseLoader: mockPauseLoader, + dockerClient: client, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -713,6 +787,84 @@ func TestCapabilitesTaskResourceLimitErrorCase(t *testing.T) { assert.Error(t, err, "An error should be thrown when TaskCPUMemLimit is explicitly enabled") } +func TestCapabilitiesIncreasedTaskCPULimit(t *testing.T) { + testCases := []struct { + testName string + taskCPUMemLimitValue config.Conditional + dockerVersion dockerclient.DockerVersion + expectedIncreasedTaskCPULimitEnabled bool + }{ + { + testName: "enabled by default", + taskCPUMemLimitValue: config.NotSet, + dockerVersion: dockerclient.Version_1_22, + expectedIncreasedTaskCPULimitEnabled: true, + }, + { + testName: "disabled, unsupportedDockerVersion", + taskCPUMemLimitValue: config.NotSet, + dockerVersion: dockerclient.Version_1_19, + expectedIncreasedTaskCPULimitEnabled: false, + }, + { + testName: "disabled, taskCPUMemLimit explicitly disabled", + taskCPUMemLimitValue: config.ExplicitlyDisabled, + dockerVersion: dockerclient.Version_1_22, + expectedIncreasedTaskCPULimitEnabled: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + conf := &config.Config{ + TaskCPUMemLimit: config.BooleanDefaultTrue{Value: tc.taskCPUMemLimitValue}, + } + + client := mock_dockerapi.NewMockDockerClient(ctrl) + versionList := []dockerclient.DockerVersion{tc.dockerVersion} + mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) + mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + gomock.InOrder( + client.EXPECT().SupportedVersions().Return(versionList), + client.EXPECT().KnownVersions().Return(versionList), + mockMobyPlugins.EXPECT().Scan().AnyTimes().Return([]string{}, nil), + client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any()).AnyTimes().Return([]string{}, nil), + ) + ctx, cancel := context.WithCancel(context.TODO()) + // Cancel the context to cancel async routines + defer cancel() + agent := &ecsAgent{ + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, + } + + capability := attributePrefix + capabilityIncreasedTaskCPULimit + capabilities, err := agent.capabilities() + assert.NoError(t, err) + + capMap := make(map[string]bool) + for _, capability := range capabilities { + capMap[aws.StringValue(capability.Name)] = true + } + + _, ok := capMap[capability] + assert.Equal(t, tc.expectedIncreasedTaskCPULimitEnabled, ok) + }) + } +} + func TestCapabilitiesContainerHealth(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -728,18 +880,23 @@ func TestCapabilitiesContainerHealth(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &config.Config{}, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &config.Config{}, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -769,18 +926,23 @@ func TestCapabilitiesContainerHealthDisabled(t *testing.T) { client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]string{}, nil) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() ctx, cancel := context.WithCancel(context.TODO()) // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &config.Config{DisableDockerHealthCheck: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}}, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &config.Config{DisableDockerHealthCheck: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}}, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -801,8 +963,12 @@ func TestCapabilitesListPluginsErrorCase(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_19} - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -814,11 +980,12 @@ func TestCapabilitesListPluginsErrorCase(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &config.Config{}, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &config.Config{}, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -838,8 +1005,12 @@ func TestCapabilitesScanPluginsErrorCase(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_19} - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -851,11 +1022,12 @@ func TestCapabilitesScanPluginsErrorCase(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &config.Config{}, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &config.Config{}, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -945,8 +1117,12 @@ func TestCapabilitiesExecuteCommand(t *testing.T) { mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) client := mock_dockerapi.NewMockDockerClient(ctrl) versionList := []dockerclient.DockerVersion{dockerclient.Version_1_19} - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return(versionList), client.EXPECT().KnownVersions().Return(versionList), @@ -958,11 +1134,12 @@ func TestCapabilitiesExecuteCommand(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &config.Config{}, - dockerClient: client, - pauseLoader: mockPauseLoader, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &config.Config{}, + dockerClient: client, + pauseLoader: mockPauseLoader, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() @@ -979,6 +1156,130 @@ func TestCapabilitiesExecuteCommand(t *testing.T) { } } +func TestCapabilitiesNoServiceConnect(t *testing.T) { + mockPathExists(true) + defer mockPathExists(false) + getSubDirectories = func(path string) ([]string, error) { + // appendExecCapabilities() requires at least 1 version to exist + return []string{"3.0.236.0"}, nil + } + defer func() { + getSubDirectories = defaultGetSubDirectories + }() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := mock_dockerapi.NewMockDockerClient(ctrl) + cniClient := mock_ecscni.NewMockCNIClient(ctrl) + mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) + mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) + conf := &config.Config{ + AvailableLoggingDrivers: []dockerclient.LoggingDriver{ + dockerclient.JSONFileDriver, + dockerclient.SyslogDriver, + dockerclient.JournaldDriver, + dockerclient.GelfDriver, + dockerclient.FluentdDriver, + }, + PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyDisabled}, + SELinuxCapable: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, + AppArmorCapable: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, + TaskENIEnabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, + AWSVPCBlockInstanceMetdata: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, + TaskCleanupWaitDuration: config.DefaultConfig().TaskCleanupWaitDuration, + } + + mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() + mockServiceConnectManager.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("No File")).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + + // Scan() and ListPluginsWithFilters() are tested with + // AnyTimes() because they are not called in windows. + gomock.InOrder( + client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ + dockerclient.Version_1_17, + dockerclient.Version_1_18, + }), + client.EXPECT().KnownVersions().Return([]dockerclient.DockerVersion{ + dockerclient.Version_1_17, + dockerclient.Version_1_18, + dockerclient.Version_1_19, + }), + // CNI plugins are platform dependent. + // Therefore, for any version query for any plugin return an appropriate version + cniClient.EXPECT().Version(gomock.Any()).Return("v1", nil), + mockMobyPlugins.EXPECT().Scan().AnyTimes().Return([]string{}, nil), + client.EXPECT().ListPluginsWithFilters(gomock.Any(), gomock.Any(), gomock.Any(), + gomock.Any()).AnyTimes().Return([]string{}, nil), + ) + + expectedNameOnlyCapabilities := []string{ + capabilityPrefix + "privileged-container", + capabilityPrefix + "docker-remote-api.1.17", + capabilityPrefix + "docker-remote-api.1.18", + capabilityPrefix + "logging-driver.json-file", + capabilityPrefix + "logging-driver.syslog", + capabilityPrefix + "logging-driver.journald", + capabilityPrefix + "selinux", + capabilityPrefix + "apparmor", + attributePrefix + "docker-plugin.local", + attributePrefix + taskENIAttributeSuffix, + attributePrefix + capabilityPrivateRegistryAuthASM, + attributePrefix + capabilitySecretEnvSSM, + attributePrefix + capabilitySecretLogDriverSSM, + attributePrefix + capabilityECREndpoint, + attributePrefix + capabilitySecretEnvASM, + attributePrefix + capabilitySecretLogDriverASM, + attributePrefix + capabilityContainerOrdering, + attributePrefix + capabilityFullTaskSync, + attributePrefix + capabilityEnvFilesS3, + attributePrefix + taskENIBlockInstanceMetadataAttributeSuffix, + attributePrefix + capabilityExec, + attributePrefix + capabilityContainerPortRange, + } + + var expectedCapabilities []*ecs.Attribute + for _, name := range expectedNameOnlyCapabilities { + expectedCapabilities = append(expectedCapabilities, + &ecs.Attribute{Name: aws.String(name)}) + } + expectedCapabilities = append(expectedCapabilities, + []*ecs.Attribute{ + { + Name: aws.String(attributePrefix + cniPluginVersionSuffix), + Value: aws.String("v1"), + }, + }...) + + ctx, cancel := context.WithCancel(context.TODO()) + // Cancel the context to cancel async routines + defer cancel() + agent := &ecsAgent{ + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, + } + capabilities, err := agent.capabilities() + assert.NoError(t, err) + + for _, expected := range expectedCapabilities { + assert.Contains(t, capabilities, &ecs.Attribute{ + Name: expected.Name, + Value: expected.Value, + }) + } +} + func TestDefaultGetSubDirectories(t *testing.T) { rootDir, err := ioutil.TempDir(os.TempDir(), testTempDirPrefix) if err != nil { @@ -1117,3 +1418,29 @@ func TestAppendAndRemoveAttributes(t *testing.T) { Name: aws.String("cap-2"), }) } + +func TestAppendGMSACapabilities(t *testing.T) { + var inputCapabilities []*ecs.Attribute + var expectedCapabilities []*ecs.Attribute + + expectedCapabilities = append(expectedCapabilities, + []*ecs.Attribute{ + { + Name: aws.String(attributePrefix + capabilityGMSA), + }, + }...) + + agent := &ecsAgent{ + cfg: &config.Config{ + GMSACapable: true, + }, + } + + capabilities := agent.appendGMSACapabilities(inputCapabilities) + + assert.Equal(t, len(expectedCapabilities), len(capabilities)) + for i, expected := range expectedCapabilities { + assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(capabilities[i].Name)) + assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value)) + } +} diff --git a/agent/app/agent_capability_unix.go b/agent/app/agent_capability_unix.go index b7fed896c07..51b43935dba 100644 --- a/agent/app/agent_capability_unix.go +++ b/agent/app/agent_capability_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -199,10 +200,6 @@ func (agent *ecsAgent) appendFirelensConfigCapabilities(capabilities []*ecs.Attr return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensConfigS3) } -func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { - return capabilities -} - func (agent *ecsAgent) appendIPv6Capability(capabilities []*ecs.Attribute) []*ecs.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+taskENIIPv6AttributeSuffix) } diff --git a/agent/app/agent_capability_unix_test.go b/agent/app/agent_capability_unix_test.go index 98f281d7c39..89ec2ca6820 100644 --- a/agent/app/agent_capability_unix_test.go +++ b/agent/app/agent_capability_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -22,8 +23,6 @@ import ( "path/filepath" "testing" - mock_pause "github.com/aws/amazon-ecs-agent/agent/eni/pause/mocks" - app_mocks "github.com/aws/amazon-ecs-agent/agent/app/mocks" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient" @@ -31,9 +30,11 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/ecscni" mock_ecscni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks" + mock_serviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" "github.com/aws/amazon-ecs-agent/agent/gpu" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/utils" + mock_loader "github.com/aws/amazon-ecs-agent/agent/utils/loader/mocks" mock_mobypkgwrapper "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper/mocks" "github.com/aws/aws-sdk-go/aws" aws_credentials "github.com/aws/aws-sdk-go/aws/credentials" @@ -53,7 +54,7 @@ func TestVolumeDriverCapabilitiesUnix(t *testing.T) { cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ AvailableLoggingDrivers: []dockerclient.LoggingDriver{ dockerclient.JSONFileDriver, @@ -71,6 +72,10 @@ func TestVolumeDriverCapabilitiesUnix(t *testing.T) { } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -122,13 +127,14 @@ func TestVolumeDriverCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -148,13 +154,17 @@ func TestNvidiaDriverCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, GPUSupportEnabled: true, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -198,6 +208,7 @@ func TestNvidiaDriverCapabilitiesUnix(t *testing.T) { DriverVersion: "396.44", }, }, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -217,13 +228,17 @@ func TestEmptyNvidiaDriverCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, GPUSupportEnabled: true, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -265,6 +280,7 @@ func TestEmptyNvidiaDriverCapabilitiesUnix(t *testing.T) { DriverVersion: "", }, }, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -285,7 +301,7 @@ func TestENITrunkingCapabilitiesUnix(t *testing.T) { cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, TaskENIEnabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, @@ -293,6 +309,10 @@ func TestENITrunkingCapabilitiesUnix(t *testing.T) { } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -341,13 +361,14 @@ func TestENITrunkingCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -369,7 +390,7 @@ func TestNoENITrunkingCapabilitiesUnix(t *testing.T) { cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, TaskENIEnabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, @@ -377,6 +398,10 @@ func TestNoENITrunkingCapabilitiesUnix(t *testing.T) { } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -417,13 +442,14 @@ func TestNoENITrunkingCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -443,12 +469,16 @@ func TestPIDAndIPCNamespaceSharingCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -474,6 +504,7 @@ func TestPIDAndIPCNamespaceSharingCapabilitiesUnix(t *testing.T) { attributePrefix + capabilityFullTaskSync, attributePrefix + capabilityEnvFilesS3, attributePrefix + capabiltyPIDAndIPCNamespaceSharing, + attributePrefix + capabilityContainerPortRange, } var expectedCapabilities []*ecs.Attribute @@ -485,12 +516,13 @@ func TestPIDAndIPCNamespaceSharingCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -510,12 +542,16 @@ func TestPIDAndIPCNamespaceSharingCapabilitiesNoPauseContainer(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, errors.New("mock error")) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -551,12 +587,13 @@ func TestPIDAndIPCNamespaceSharingCapabilitiesNoPauseContainer(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -576,12 +613,16 @@ func TestAppMeshCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -608,6 +649,7 @@ func TestAppMeshCapabilitiesUnix(t *testing.T) { attributePrefix + capabilityEnvFilesS3, attributePrefix + capabiltyPIDAndIPCNamespaceSharing, attributePrefix + appMeshAttributeSuffix, + attributePrefix + capabilityContainerPortRange, } var expectedCapabilities []*ecs.Attribute @@ -620,12 +662,13 @@ func TestAppMeshCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -650,12 +693,16 @@ func TestTaskEIACapabilitiesNoOptimizedCPU(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -672,12 +719,13 @@ func TestTaskEIACapabilitiesNoOptimizedCPU(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -692,7 +740,7 @@ func TestTaskEIACapabilitiesWithOptimizedCPU(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, @@ -704,6 +752,10 @@ func TestTaskEIACapabilitiesWithOptimizedCPU(t *testing.T) { defer resetOpenFile() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -720,12 +772,13 @@ func TestTaskEIACapabilitiesWithOptimizedCPU(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -742,13 +795,17 @@ func TestCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, VolumePluginCapabilities: []string{capabilityEFSAuth}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -781,6 +838,7 @@ func TestCapabilitiesUnix(t *testing.T) { capabilityPrefix + capabilityFirelensLoggingDriver, attributePrefix + capabilityFirelensLoggingDriver + capabilityFireLensLoggingDriverConfigBufferLimitSuffix, attributePrefix + capabilityEnvFilesS3, + attributePrefix + capabilityContainerPortRange, } var expectedCapabilities []*ecs.Attribute @@ -792,12 +850,13 @@ func TestCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -816,12 +875,16 @@ func TestFirelensConfigCapabilitiesUnix(t *testing.T) { client := mock_dockerapi.NewMockDockerClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) conf := &config.Config{ PrivilegedDisabled: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled}, } mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() gomock.InOrder( client.EXPECT().SupportedVersions().Return([]dockerclient.DockerVersion{ dockerclient.Version_1_17, @@ -838,12 +901,13 @@ func TestFirelensConfigCapabilitiesUnix(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -852,16 +916,6 @@ func TestFirelensConfigCapabilitiesUnix(t *testing.T) { assert.Contains(t, capabilities, &ecs.Attribute{Name: aws.String(attributePrefix + capabilityFirelensConfigS3)}) } -func TestAppendGMSACapabilities(t *testing.T) { - var inputCapabilities []*ecs.Attribute - - agent := &ecsAgent{} - - capabilities := agent.appendGMSACapabilities(inputCapabilities) - assert.Equal(t, len(inputCapabilities), len(capabilities)) - assert.EqualValues(t, capabilities, inputCapabilities) -} - func TestAppendFSxWindowsFileServerCapabilities(t *testing.T) { var inputCapabilities []*ecs.Attribute diff --git a/agent/app/agent_capability_unspecified.go b/agent/app/agent_capability_unspecified.go index 28cfb80d45a..de0c37f8858 100644 --- a/agent/app/agent_capability_unspecified.go +++ b/agent/app/agent_capability_unspecified.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent_capability_windows.go b/agent/app/agent_capability_windows.go index 99225922e2a..77f34138e4b 100644 --- a/agent/app/agent_capability_windows.go +++ b/agent/app/agent_capability_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -99,14 +100,6 @@ func (agent *ecsAgent) appendFirelensConfigCapabilities(capabilities []*ecs.Attr return capabilities } -func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute { - if agent.cfg.GMSACapable { - return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityGMSA) - } - - return capabilities -} - func (agent *ecsAgent) appendEFSVolumePluginCapabilities(capabilities []*ecs.Attribute, pluginCapability string) []*ecs.Attribute { return capabilities } diff --git a/agent/app/agent_capability_windows_test.go b/agent/app/agent_capability_windows_test.go index 747a4db32f1..54139f4e9cd 100644 --- a/agent/app/agent_capability_windows_test.go +++ b/agent/app/agent_capability_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -26,6 +27,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/ecscni" mock_ecscni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks" + "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" mock_mobypkgwrapper "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper/mocks" "github.com/aws/aws-sdk-go/aws" @@ -106,12 +108,13 @@ func TestVolumeDriverCapabilitiesWindows(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: serviceconnect.NewManager(), } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -183,7 +186,9 @@ func TestSupportedCapabilitiesWindows(t *testing.T) { attributePrefix + capabilityContainerOrdering, attributePrefix + capabilityFullTaskSync, attributePrefix + capabilityEnvFilesS3, - attributePrefix + taskENIBlockInstanceMetadataAttributeSuffix} + attributePrefix + taskENIBlockInstanceMetadataAttributeSuffix, + attributePrefix + capabilityContainerPortRange, + } var expectedCapabilities []*ecs.Attribute for _, name := range expectedCapabilityNames { @@ -202,12 +207,13 @@ func TestSupportedCapabilitiesWindows(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: conf, - dockerClient: client, - cniClient: cniClient, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: conf, + dockerClient: client, + cniClient: cniClient, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: serviceconnect.NewManager(), } capabilities, err := agent.capabilities() assert.NoError(t, err) @@ -221,32 +227,6 @@ func TestSupportedCapabilitiesWindows(t *testing.T) { } } -func TestAppendGMSACapabilities(t *testing.T) { - var inputCapabilities []*ecs.Attribute - var expectedCapabilities []*ecs.Attribute - - expectedCapabilities = append(expectedCapabilities, - []*ecs.Attribute{ - { - Name: aws.String(attributePrefix + capabilityGMSA), - }, - }...) - - agent := &ecsAgent{ - cfg: &config.Config{ - GMSACapable: true, - }, - } - - capabilities := agent.appendGMSACapabilities(inputCapabilities) - - assert.Equal(t, len(expectedCapabilities), len(capabilities)) - for i, expected := range expectedCapabilities { - assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(capabilities[i].Name)) - assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value)) - } -} - func TestAppendGMSACapabilitiesFalse(t *testing.T) { var inputCapabilities []*ecs.Attribute var expectedCapabilities []*ecs.Attribute diff --git a/agent/app/agent_compatibility_linux.go b/agent/app/agent_compatibility_linux.go index 4c493ff1b14..716b91f8db9 100644 --- a/agent/app/agent_compatibility_linux.go +++ b/agent/app/agent_compatibility_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent_compatibility_linux_test.go b/agent/app/agent_compatibility_linux_test.go index f20c4af0f9f..d2a6539b07c 100644 --- a/agent/app/agent_compatibility_linux_test.go +++ b/agent/app/agent_compatibility_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -37,7 +38,7 @@ func init() { } func TestCompatibilityEnabledSuccess(t *testing.T) { - ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager := mock_statemanager.NewMockStateManager(ctrl) @@ -64,14 +65,14 @@ func TestCompatibilityEnabledSuccess(t *testing.T) { defer cancel() containerChangeEventStream := eventstream.NewEventStream("events", ctx) - _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr) + _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr, serviceConnectManager) assert.NoError(t, err) assert.True(t, cfg.TaskCPUMemLimit.Enabled()) } func TestCompatibilityNotSetFail(t *testing.T) { - ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager := mock_statemanager.NewMockStateManager(ctrl) @@ -88,6 +89,7 @@ func TestCompatibilityNotSetFail(t *testing.T) { require.NoError(t, dataClient.SaveTask(task)) } + cfg.Cluster = "test-cluster" agent := &ecsAgent{ cfg: &cfg, dataClient: dataClient, @@ -105,14 +107,14 @@ func TestCompatibilityNotSetFail(t *testing.T) { defer cancel() containerChangeEventStream := eventstream.NewEventStream("events", ctx) - _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr) + _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr, serviceConnectManager) assert.NoError(t, err) assert.False(t, cfg.TaskCPUMemLimit.Enabled()) } func TestCompatibilityExplicitlyEnabledFail(t *testing.T) { - ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + ctrl, creds, _, images, _, _, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager := mock_statemanager.NewMockStateManager(ctrl) @@ -146,7 +148,7 @@ func TestCompatibilityExplicitlyEnabledFail(t *testing.T) { defer cancel() containerChangeEventStream := eventstream.NewEventStream("events", ctx) - _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr) + _, _, err := agent.newTaskEngine(containerChangeEventStream, creds, dockerstate.NewTaskEngineState(), images, execCmdMgr, serviceConnectManager) assert.Error(t, err) } diff --git a/agent/app/agent_compatibility_unspecified.go b/agent/app/agent_compatibility_unspecified.go index dce18e76689..0911e725f29 100644 --- a/agent/app/agent_compatibility_unspecified.go +++ b/agent/app/agent_compatibility_unspecified.go @@ -1,4 +1,5 @@ //go:build !linux +// +build !linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent_integ_test.go b/agent/app/agent_integ_test.go index a158a739b88..416b10771b7 100644 --- a/agent/app/agent_integ_test.go +++ b/agent/app/agent_integ_test.go @@ -1,4 +1,5 @@ //go:build integration +// +build integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent_test.go b/agent/app/agent_test.go index 79210cb8569..85764bad29c 100644 --- a/agent/app/agent_test.go +++ b/agent/app/agent_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -44,11 +45,12 @@ import ( mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" mock_execcmdagent "github.com/aws/amazon-ecs-agent/agent/engine/execcmd/mocks" mock_engine "github.com/aws/amazon-ecs-agent/agent/engine/mocks" - mock_pause "github.com/aws/amazon-ecs-agent/agent/eni/pause/mocks" + mock_serviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" "github.com/aws/amazon-ecs-agent/agent/statemanager" mock_statemanager "github.com/aws/amazon-ecs-agent/agent/statemanager/mocks" + mock_loader "github.com/aws/amazon-ecs-agent/agent/utils/loader/mocks" mock_mobypkgwrapper "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper/mocks" "github.com/aws/amazon-ecs-agent/agent/version" "github.com/aws/aws-sdk-go/aws" @@ -87,7 +89,8 @@ func setup(t *testing.T) (*gomock.Controller, *mock_dockerapi.MockDockerClient, *mock_factory.MockStateManager, *mock_factory.MockSaveableOption, - *mock_execcmdagent.MockManager) { + *mock_execcmdagent.MockManager, + *mock_serviceconnect.MockManager) { ctrl := gomock.NewController(t) @@ -99,12 +102,13 @@ func setup(t *testing.T) (*gomock.Controller, mock_dockerapi.NewMockDockerClient(ctrl), mock_factory.NewMockStateManager(ctrl), mock_factory.NewMockSaveableOption(ctrl), - mock_execcmdagent.NewMockManager(ctrl) + mock_execcmdagent.NewMockManager(ctrl), + mock_serviceconnect.NewMockManager(ctrl) } func TestDoStartMinimumSupportedDockerVersionTerminal(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t) defer ctrl.Finish() oldAPIVersions := []dockerclient.DockerVersion{ @@ -134,7 +138,7 @@ func TestDoStartMinimumSupportedDockerVersionTerminal(t *testing.T) { func TestDoStartMinimumSupportedDockerVersionError(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t) defer ctrl.Finish() gomock.InOrder( @@ -161,7 +165,7 @@ func TestDoStartMinimumSupportedDockerVersionError(t *testing.T) { func TestDoStartNewTaskEngineError(t *testing.T) { ctrl, credentialsManager, _, imageManager, client, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t) defer ctrl.Finish() ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) @@ -203,16 +207,24 @@ func TestDoStartNewTaskEngineError(t *testing.T) { func TestDoStartRegisterContainerInstanceErrorTerminal(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockEC2Metadata.EXPECT().PrimaryENIMAC().Return("mac", nil) + mockEC2Metadata.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + mockEC2Metadata.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( dockerClient.EXPECT().SupportedVersions().Return(apiVersions), mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -242,6 +254,7 @@ func TestDoStartRegisterContainerInstanceErrorTerminal(t *testing.T) { ec2MetadataClient: mockEC2Metadata, terminationHandler: func(taskEngineState dockerstate.TaskEngineState, dataClient data.Client, taskEngine engine.TaskEngine, cancel context.CancelFunc) { }, + serviceconnectManager: mockServiceConnectManager, } exitCode := agent.doStart(eventstream.NewEventStream("events", ctx), @@ -251,15 +264,23 @@ func TestDoStartRegisterContainerInstanceErrorTerminal(t *testing.T) { func TestDoStartRegisterContainerInstanceErrorNonTerminal(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockEC2Metadata.EXPECT().PrimaryENIMAC().Return("mac", nil) + mockEC2Metadata.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + mockEC2Metadata.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( dockerClient.EXPECT().SupportedVersions().Return(apiVersions), mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -288,6 +309,7 @@ func TestDoStartRegisterContainerInstanceErrorNonTerminal(t *testing.T) { ec2MetadataClient: mockEC2Metadata, terminationHandler: func(taskEngineState dockerstate.TaskEngineState, dataClient data.Client, taskEngine engine.TaskEngine, cancel context.CancelFunc) { }, + serviceconnectManager: mockServiceConnectManager, } exitCode := agent.doStart(eventstream.NewEventStream("events", ctx), @@ -297,7 +319,7 @@ func TestDoStartRegisterContainerInstanceErrorNonTerminal(t *testing.T) { func TestDoStartWarmPoolsError(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) gomock.InOrder( @@ -337,20 +359,24 @@ func TestDoStartWarmPoolsError(t *testing.T) { } func TestDoStartHappyPath(t *testing.T) { - testDoStartHappyPathWithConditions(t, false, false) + testDoStartHappyPathWithConditions(t, false, false, false) } func TestDoStartWarmPoolsEnabled(t *testing.T) { - testDoStartHappyPathWithConditions(t, false, true) + testDoStartHappyPathWithConditions(t, false, true, false) } func TestDoStartWarmPoolsBlackholed(t *testing.T) { - testDoStartHappyPathWithConditions(t, true, true) + testDoStartHappyPathWithConditions(t, true, true, false) } -func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPoolsEnv bool) { +func TestDoStartHappyPathExternal(t *testing.T) { + testDoStartHappyPathWithConditions(t, false, false, true) +} + +func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPoolsEnv bool, isExternalLaunchType bool) { ctrl, credentialsManager, _, imageManager, client, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t) defer ctrl.Finish() saveableOptionFactory.EXPECT().AddSaveable(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() @@ -362,6 +388,13 @@ func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPools ec2MetadataClient.EXPECT().PublicIPv4Address().Return(hostPublicIPv4Address, nil) ec2MetadataClient.EXPECT().OutpostARN().Return("", nil) + if !isExternalLaunchType { + // VPC and Subnet should not be initizalied for external launch type + ec2MetadataClient.EXPECT().PrimaryENIMAC().Return("mac", nil) + ec2MetadataClient.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + ec2MetadataClient.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) + } + if blackholed { if warmPoolsEnv { ec2MetadataClient.EXPECT().TargetLifecycleState().Return("", errors.New("blackholed")).Times(targetLifecycleMaxRetryCount) @@ -381,10 +414,18 @@ func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPools dockerClient.EXPECT().Version(gomock.Any(), gomock.Any()).AnyTimes() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) containermetadata := mock_containermetadata.NewMockManager(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() + mockServiceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedImageName().Return("service_connect_agent:v1").AnyTimes() + imageManager.EXPECT().AddImageToCleanUpExclusionList(gomock.Eq("service_connect_agent:v1")).Times(1) imageManager.EXPECT().StartImageCleanupProcess(gomock.Any()).MaxTimes(1) dockerClient.EXPECT().ListContainers(gomock.Any(), gomock.Any(), gomock.Any()).Return( dockerapi.ListContainersResponse{}).AnyTimes() @@ -424,6 +465,9 @@ func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPools if warmPoolsEnv { cfg.WarmPoolsSupport = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} } + if isExternalLaunchType { + cfg.External = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} + } cfg.Cluster = clusterName ctx, cancel := context.WithCancel(context.TODO()) @@ -445,6 +489,7 @@ func testDoStartHappyPathWithConditions(t *testing.T, blackholed bool, warmPools stateManagerFactory: stateManagerFactory, ec2MetadataClient: ec2MetadataClient, saveableOptionFactory: saveableOptionFactory, + serviceconnectManager: mockServiceConnectManager, } var agentW sync.WaitGroup @@ -476,11 +521,11 @@ func assertMetadata(t *testing.T, key, expectedVal string, dataClient data.Clien func TestNewTaskEngineRestoreFromCheckpointNoEC2InstanceIDToLoadHappyPath(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} @@ -522,7 +567,7 @@ func TestNewTaskEngineRestoreFromCheckpointNoEC2InstanceIDToLoadHappyPath(t *tes } _, instanceID, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) assert.Equal(t, expectedInstanceID, instanceID) assert.Equal(t, "prev-container-inst", agent.containerInstanceARN) @@ -530,11 +575,11 @@ func TestNewTaskEngineRestoreFromCheckpointNoEC2InstanceIDToLoadHappyPath(t *tes func TestNewTaskEngineRestoreFromCheckpointPreviousEC2InstanceIDLoadedHappyPath(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} @@ -586,7 +631,7 @@ func TestNewTaskEngineRestoreFromCheckpointPreviousEC2InstanceIDLoadedHappyPath( } _, instanceID, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) assert.Equal(t, expectedInstanceID, instanceID) assert.NotEqual(t, "prev-container-inst", agent.containerInstanceARN) @@ -595,11 +640,11 @@ func TestNewTaskEngineRestoreFromCheckpointPreviousEC2InstanceIDLoadedHappyPath( func TestNewTaskEngineRestoreFromCheckpointClusterIDMismatch(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} @@ -648,21 +693,21 @@ func TestNewTaskEngineRestoreFromCheckpointClusterIDMismatch(t *testing.T) { } _, _, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.Error(t, err) assert.IsType(t, clusterMismatchError{}, err) } func TestNewTaskEngineRestoreFromCheckpointNewStateManagerError(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) gomock.InOrder( saveableOptionFactory.EXPECT().AddSaveable("TaskEngine", gomock.Any()).Return(nil), saveableOptionFactory.EXPECT().AddSaveable("ContainerInstanceArn", gomock.Any()).Return(nil), @@ -694,21 +739,21 @@ func TestNewTaskEngineRestoreFromCheckpointNewStateManagerError(t *testing.T) { } _, _, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.Error(t, err) assert.False(t, isTransient(err)) } func TestNewTaskEngineRestoreFromCheckpointStateLoadError(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager := mock_statemanager.NewMockStateManager(ctrl) cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) gomock.InOrder( saveableOptionFactory.EXPECT().AddSaveable("TaskEngine", gomock.Any()).Return(nil), @@ -741,21 +786,21 @@ func TestNewTaskEngineRestoreFromCheckpointStateLoadError(t *testing.T) { } _, _, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.Error(t, err) assert.False(t, isTransient(err)) } func TestNewTaskEngineRestoreFromCheckpoint(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) cfg := getTestConfig() cfg.Checkpoint = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} cfg.Cluster = testCluster - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) ec2MetadataClient.EXPECT().InstanceID().Return(testEC2InstanceID, nil) @@ -780,7 +825,7 @@ func TestNewTaskEngineRestoreFromCheckpoint(t *testing.T) { state := dockerstate.NewTaskEngineState() _, instanceID, err := agent.newTaskEngine(eventstream.NewEventStream("events", ctx), - credentialsManager, state, imageManager, execCmdMgr) + credentialsManager, state, imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) assert.Equal(t, testEC2InstanceID, instanceID) @@ -846,6 +891,8 @@ func TestGetEC2InstanceIDIIDError(t *testing.T) { ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) agent := &ecsAgent{ec2MetadataClient: ec2MetadataClient} + ec2MetadataClient.EXPECT().InstanceID().Return("", errors.New("error")) + ec2MetadataClient.EXPECT().InstanceID().Return("", errors.New("error")) ec2MetadataClient.EXPECT().InstanceID().Return("", errors.New("error")) ec2MetadataClient.EXPECT().InstanceID().Return("", errors.New("error")) ec2MetadataClient.EXPECT().InstanceID().Return("", errors.New("error")) @@ -872,10 +919,15 @@ func TestReregisterContainerInstanceHappyPath(t *testing.T) { mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), mockDockerClient.EXPECT().SupportedVersions().Return(nil), @@ -895,13 +947,14 @@ func TestReregisterContainerInstanceHappyPath(t *testing.T) { mockEC2Metadata.EXPECT().OutpostARN().Return("", nil) agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - dockerClient: mockDockerClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, - ec2MetadataClient: mockEC2Metadata, + ctx: ctx, + cfg: &cfg, + dockerClient: mockDockerClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + ec2MetadataClient: mockEC2Metadata, + serviceconnectManager: mockServiceConnectManager, } agent.containerInstanceARN = containerInstanceARN agent.availabilityZone = availabilityZone @@ -919,10 +972,15 @@ func TestReregisterContainerInstanceInstanceTypeChanged(t *testing.T) { mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), mockDockerClient.EXPECT().SupportedVersions().Return(nil), @@ -943,13 +1001,14 @@ func TestReregisterContainerInstanceInstanceTypeChanged(t *testing.T) { mockEC2Metadata.EXPECT().OutpostARN().Return("", nil) agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - dockerClient: mockDockerClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - ec2MetadataClient: mockEC2Metadata, - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + dockerClient: mockDockerClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + ec2MetadataClient: mockEC2Metadata, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } agent.containerInstanceARN = containerInstanceARN agent.availabilityZone = availabilityZone @@ -968,10 +1027,15 @@ func TestReregisterContainerInstanceAttributeError(t *testing.T) { mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -991,13 +1055,14 @@ func TestReregisterContainerInstanceAttributeError(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - ec2MetadataClient: mockEC2Metadata, - dockerClient: mockDockerClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + ec2MetadataClient: mockEC2Metadata, + dockerClient: mockDockerClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } agent.containerInstanceARN = containerInstanceARN agent.availabilityZone = availabilityZone @@ -1016,10 +1081,15 @@ func TestReregisterContainerInstanceNonTerminalError(t *testing.T) { mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), mockDockerClient.EXPECT().SupportedVersions().Return(nil), @@ -1038,13 +1108,14 @@ func TestReregisterContainerInstanceNonTerminalError(t *testing.T) { // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - dockerClient: mockDockerClient, - ec2MetadataClient: mockEC2Metadata, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + dockerClient: mockDockerClient, + ec2MetadataClient: mockEC2Metadata, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } agent.containerInstanceARN = containerInstanceARN agent.availabilityZone = availabilityZone @@ -1064,10 +1135,15 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetHappyPath(t *t mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), mockDockerClient.EXPECT().SupportedVersions().Return(nil), @@ -1086,13 +1162,14 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetHappyPath(t *t // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - dockerClient: mockDockerClient, - ec2MetadataClient: mockEC2Metadata, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + dockerClient: mockDockerClient, + ec2MetadataClient: mockEC2Metadata, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } err := agent.registerContainerInstance(client, nil) assert.NoError(t, err) @@ -1109,10 +1186,15 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetCanRetryError( mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() retriableError := apierrors.NewRetriableError(apierrors.NewRetriable(true), errors.New("error")) gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -1132,13 +1214,14 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetCanRetryError( // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - dockerClient: mockDockerClient, - ec2MetadataClient: mockEC2Metadata, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + dockerClient: mockDockerClient, + ec2MetadataClient: mockEC2Metadata, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } err := agent.registerContainerInstance(client, nil) @@ -1155,10 +1238,15 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetCannotRetryErr mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() cannotRetryError := apierrors.NewRetriableError(apierrors.NewRetriable(false), errors.New("error")) gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -1178,13 +1266,14 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetCannotRetryErr // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - ec2MetadataClient: mockEC2Metadata, - dockerClient: mockDockerClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + ec2MetadataClient: mockEC2Metadata, + dockerClient: mockDockerClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } err := agent.registerContainerInstance(client, nil) @@ -1201,10 +1290,15 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetAttributeError mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), mockDockerClient.EXPECT().SupportedVersions().Return(nil), @@ -1223,13 +1317,14 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetAttributeError // Cancel the context to cancel async routines defer cancel() agent := &ecsAgent{ - ctx: ctx, - cfg: &cfg, - ec2MetadataClient: mockEC2Metadata, - dockerClient: mockDockerClient, - pauseLoader: mockPauseLoader, - credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), - mobyPlugins: mockMobyPlugins, + ctx: ctx, + cfg: &cfg, + ec2MetadataClient: mockEC2Metadata, + dockerClient: mockDockerClient, + pauseLoader: mockPauseLoader, + credentialProvider: aws_credentials.NewCredentials(mockCredentialsProvider), + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } err := agent.registerContainerInstance(client, nil) @@ -1239,16 +1334,24 @@ func TestRegisterContainerInstanceWhenContainerInstanceARNIsNotSetAttributeError func TestRegisterContainerInstanceInvalidParameterTerminalError(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) mockEC2Metadata := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(false, nil).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + mockEC2Metadata.EXPECT().PrimaryENIMAC().Return("mac", nil) + mockEC2Metadata.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + mockEC2Metadata.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() gomock.InOrder( dockerClient.EXPECT().SupportedVersions().Return(apiVersions), mockCredentialsProvider.EXPECT().Retrieve().Return(aws_credentials.Value{}, nil), @@ -1276,6 +1379,7 @@ func TestRegisterContainerInstanceInvalidParameterTerminalError(t *testing.T) { mobyPlugins: mockMobyPlugins, terminationHandler: func(taskEngineState dockerstate.TaskEngineState, dataClient data.Client, taskEngine engine.TaskEngine, cancel context.CancelFunc) { }, + serviceconnectManager: mockServiceConnectManager, } exitCode := agent.doStart(eventstream.NewEventStream("events", ctx), diff --git a/agent/app/agent_unix.go b/agent/app/agent_unix.go index 9dda0d1bf50..24a863c5388 100644 --- a/agent/app/agent_unix.go +++ b/agent/app/agent_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -27,6 +28,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/eni/watcher" "github.com/aws/amazon-ecs-agent/agent/gpu" + s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" "github.com/aws/amazon-ecs-agent/agent/statechange" @@ -156,6 +158,7 @@ func (agent *ecsAgent) initializeResourceFields(credentialsManager credentials.M IOUtil: ioutilwrapper.NewIOUtil(), ASMClientCreator: asmfactory.NewClientCreator(), SSMClientCreator: ssmfactory.NewSSMClientCreator(), + S3ClientCreator: s3factory.NewS3ClientCreator(), CredentialsManager: credentialsManager, EC2InstanceID: agent.getEC2InstanceID(), }, @@ -175,7 +178,7 @@ func (agent *ecsAgent) cgroupInit() error { if agent.cfg.TaskCPUMemLimit.Value == config.ExplicitlyEnabled { return errors.Wrapf(err, "unable to setup '/ecs' cgroup") } - seelog.Warnf("Disabling TaskCPUMemLimit because agent is unabled to setup '/ecs' cgroup: %v", err) + seelog.Warnf("Disabling TaskCPUMemLimit because agent is unable to setup '/ecs' cgroup: %v", err) agent.cfg.TaskCPUMemLimit.Value = config.ExplicitlyDisabled return nil } diff --git a/agent/app/agent_unix_test.go b/agent/app/agent_unix_test.go index 25933a72653..32149e574cf 100644 --- a/agent/app/agent_unix_test.go +++ b/agent/app/agent_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -34,7 +35,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" mock_engine "github.com/aws/amazon-ecs-agent/agent/engine/mocks" - mock_pause "github.com/aws/amazon-ecs-agent/agent/eni/pause/mocks" + mock_serviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" mock_udev "github.com/aws/amazon-ecs-agent/agent/eni/udevwrapper/mocks" "github.com/aws/amazon-ecs-agent/agent/eni/watcher" "github.com/aws/amazon-ecs-agent/agent/eventstream" @@ -42,6 +43,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/sighandlers/exitcodes" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/mock_control" + mock_loader "github.com/aws/amazon-ecs-agent/agent/utils/loader/mocks" mock_mobypkgwrapper "github.com/aws/amazon-ecs-agent/agent/utils/mobypkgwrapper/mocks" "github.com/aws/aws-sdk-go/aws" @@ -63,7 +65,7 @@ func resetGetpid() { func TestDoStartTaskENIHappyPath(t *testing.T) { ctrl, credentialsManager, _, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() cniCapabilities := []string{ecscni.CapabilityAWSVPCNetworkingMode} @@ -72,7 +74,7 @@ func TestDoStartTaskENIHappyPath(t *testing.T) { cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockUdevMonitor := mock_udev.NewMockUdev(ctrl) mockMetadata := mock_ec2.NewMockEC2MetadataClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) @@ -105,6 +107,14 @@ func TestDoStartTaskENIHappyPath(t *testing.T) { mockMetadata.EXPECT().OutpostARN().Return("", nil) mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() + mockServiceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedImageName().Return("service_connect_agent:v1").AnyTimes() + imageManager.EXPECT().AddImageToCleanUpExclusionList(gomock.Eq("service_connect_agent:v1")).Times(1) mockUdevMonitor.EXPECT().Monitor(gomock.Any()).Return(monitoShutdownEvents).AnyTimes() gomock.InOrder( @@ -164,7 +174,8 @@ func TestDoStartTaskENIHappyPath(t *testing.T) { ec2MetadataClient: mockMetadata, terminationHandler: func(state dockerstate.TaskEngineState, dataClient data.Client, taskEngine engine.TaskEngine, cancel context.CancelFunc) { }, - mobyPlugins: mockMobyPlugins, + mobyPlugins: mockMobyPlugins, + serviceconnectManager: mockServiceConnectManager, } getPid = func() int { @@ -420,12 +431,12 @@ func TestInitializeTaskENIDependenciesQueryCNICapabilitiesError(t *testing.T) { func TestDoStartCgroupInitHappyPath(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockControl := mock_control.NewMockControl(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) var discoverEndpointsInvoked sync.WaitGroup discoverEndpointsInvoked.Add(2) containerChangeEvents := make(chan dockerapi.DockerContainerChangeEvent) @@ -435,9 +446,20 @@ func TestDoStartCgroupInitHappyPath(t *testing.T) { dockerClient.EXPECT().SupportedVersions().Return(apiVersions) imageManager.EXPECT().StartImageCleanupProcess(gomock.Any()).MaxTimes(1) mockCredentialsProvider.EXPECT().IsExpired().Return(false).AnyTimes() + ec2MetadataClient.EXPECT().PrimaryENIMAC().Return("mac", nil) + ec2MetadataClient.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + ec2MetadataClient.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) ec2MetadataClient.EXPECT().OutpostARN().Return("", nil) mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() + mockServiceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedImageName().Return("service_connect_agent:v1").AnyTimes() + imageManager.EXPECT().AddImageToCleanUpExclusionList(gomock.Eq("service_connect_agent:v1")).Times(1) gomock.InOrder( mockControl.EXPECT().Init().Return(nil), @@ -485,6 +507,7 @@ func TestDoStartCgroupInitHappyPath(t *testing.T) { resourceFields: &taskresource.ResourceFields{ Control: mockControl, }, + serviceconnectManager: mockServiceConnectManager, } var agentW sync.WaitGroup @@ -506,12 +529,12 @@ func TestDoStartCgroupInitHappyPath(t *testing.T) { func TestDoStartCgroupInitErrorPath(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockControl := mock_control.NewMockControl(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) var discoverEndpointsInvoked sync.WaitGroup discoverEndpointsInvoked.Add(2) @@ -521,6 +544,11 @@ func TestDoStartCgroupInitErrorPath(t *testing.T) { mockCredentialsProvider.EXPECT().IsExpired().Return(false).AnyTimes() mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() mockControl.EXPECT().Init().Return(errors.New("test error")) @@ -541,6 +569,7 @@ func TestDoStartCgroupInitErrorPath(t *testing.T) { resourceFields: &taskresource.ResourceFields{ Control: mockControl, }, + serviceconnectManager: mockServiceConnectManager, } status := agent.doStart(eventstream.NewEventStream("events", ctx), @@ -551,13 +580,13 @@ func TestDoStartCgroupInitErrorPath(t *testing.T) { func TestDoStartGPUManagerHappyPath(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockGPUManager := mock_gpu.NewMockGPUManager(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) ec2MetadataClient := mock_ec2.NewMockEC2MetadataClient(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) devices := []*ecs.PlatformDevice{ { @@ -581,9 +610,20 @@ func TestDoStartGPUManagerHappyPath(t *testing.T) { dockerClient.EXPECT().SupportedVersions().Return(apiVersions) imageManager.EXPECT().StartImageCleanupProcess(gomock.Any()).MaxTimes(1) mockCredentialsProvider.EXPECT().IsExpired().Return(false).AnyTimes() + ec2MetadataClient.EXPECT().PrimaryENIMAC().Return("mac", nil) + ec2MetadataClient.EXPECT().VPCID(gomock.Eq("mac")).Return("vpc-id", nil) + ec2MetadataClient.EXPECT().SubnetID(gomock.Eq("mac")).Return("subnet-id", nil) ec2MetadataClient.EXPECT().OutpostARN().Return("", nil) mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() + mockServiceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedImageName().Return("service_connect_agent:v1").AnyTimes() + imageManager.EXPECT().AddImageToCleanUpExclusionList(gomock.Eq("service_connect_agent:v1")).Times(1) gomock.InOrder( mockGPUManager.EXPECT().Initialize().Return(nil), @@ -634,6 +674,7 @@ func TestDoStartGPUManagerHappyPath(t *testing.T) { resourceFields: &taskresource.ResourceFields{ NvidiaGPUManager: mockGPUManager, }, + serviceconnectManager: mockServiceConnectManager, } var agentW sync.WaitGroup @@ -655,12 +696,12 @@ func TestDoStartGPUManagerHappyPath(t *testing.T) { func TestDoStartGPUManagerInitError(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) mockGPUManager := mock_gpu.NewMockGPUManager(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) var discoverEndpointsInvoked sync.WaitGroup discoverEndpointsInvoked.Add(2) @@ -671,6 +712,11 @@ func TestDoStartGPUManagerInitError(t *testing.T) { mockGPUManager.EXPECT().Initialize().Return(errors.New("init error")) mockPauseLoader.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() mockPauseLoader.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager := mock_serviceconnect.NewMockManager(ctrl) + mockServiceConnectManager.EXPECT().IsLoaded(gomock.Any()).Return(true, nil).AnyTimes() + mockServiceConnectManager.EXPECT().GetLoadedAppnetVersion().AnyTimes() + mockServiceConnectManager.EXPECT().GetCapabilitiesForAppnetInterfaceVersion("").AnyTimes() + mockServiceConnectManager.EXPECT().SetECSClient(gomock.Any(), gomock.Any()).AnyTimes() cfg := getTestConfig() cfg.GPUSupportEnabled = true @@ -688,6 +734,7 @@ func TestDoStartGPUManagerInitError(t *testing.T) { resourceFields: &taskresource.ResourceFields{ NvidiaGPUManager: mockGPUManager, }, + serviceconnectManager: mockServiceConnectManager, } status := agent.doStart(eventstream.NewEventStream("events", ctx), @@ -698,12 +745,12 @@ func TestDoStartGPUManagerInitError(t *testing.T) { func TestDoStartTaskENIPauseError(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, _, _, execCmdMgr := setup(t) + dockerClient, _, _, execCmdMgr, _ := setup(t) defer ctrl.Finish() cniClient := mock_ecscni.NewMockCNIClient(ctrl) mockCredentialsProvider := app_mocks.NewMockProvider(ctrl) - mockPauseLoader := mock_pause.NewMockLoader(ctrl) + mockPauseLoader := mock_loader.NewMockLoader(ctrl) mockMetadata := mock_ec2.NewMockEC2MetadataClient(ctrl) mockMobyPlugins := mock_mobypkgwrapper.NewMockPlugins(ctrl) diff --git a/agent/app/agent_unspecified.go b/agent/app/agent_unspecified.go index 34bf5d351fa..876bfefd6a5 100644 --- a/agent/app/agent_unspecified.go +++ b/agent/app/agent_unspecified.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/app/agent_windows.go b/agent/app/agent_windows.go index 5f1421e5221..43ab36d156d 100644 --- a/agent/app/agent_windows.go +++ b/agent/app/agent_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -306,12 +307,12 @@ func (agent *ecsAgent) initializeResourceFields(credentialsManager credentials.M ASMClientCreator: asmfactory.NewClientCreator(), SSMClientCreator: ssmfactory.NewSSMClientCreator(), FSxClientCreator: fsxfactory.NewFSxClientCreator(), + S3ClientCreator: s3factory.NewS3ClientCreator(), CredentialsManager: credentialsManager, }, - Ctx: agent.ctx, - DockerClient: agent.dockerClient, - S3ClientCreator: s3factory.NewS3ClientCreator(), - NetworkUtils: networkutils.New(), + Ctx: agent.ctx, + DockerClient: agent.dockerClient, + NetworkUtils: networkutils.New(), } } diff --git a/agent/app/agent_windows_test.go b/agent/app/agent_windows_test.go index 55952d01cd6..c1657ef3c12 100644 --- a/agent/app/agent_windows_test.go +++ b/agent/app/agent_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -288,7 +289,7 @@ func TestHandler_Execute_AgentStops(t *testing.T) { func TestDoStartTaskLimitsFail(t *testing.T) { ctrl, credentialsManager, state, imageManager, client, - dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr := setup(t) + dockerClient, stateManagerFactory, saveableOptionFactory, execCmdMgr, _ := setup(t) defer ctrl.Finish() cfg := getTestConfig() diff --git a/agent/app/data.go b/agent/app/data.go index ea5930478c8..b0c9602572f 100644 --- a/agent/app/data.go +++ b/agent/app/data.go @@ -22,6 +22,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/pkg/errors" @@ -46,29 +47,30 @@ type savedData struct { // load from boltdb, and if it doesn't get anything, it tries to load from state file and then save data it loaded to // boltdb. Behavior of three cases are considered: // -// 1. Agent starts from fresh instance (no previous state): -// (1) Try to load from boltdb, get nothing; -// (2) Try to load from state file, get nothing; -// (3) Return empty data. +// 1. Agent starts from fresh instance (no previous state): +// (1) Try to load from boltdb, get nothing; +// (2) Try to load from state file, get nothing; +// (3) Return empty data. // -// 2. Agent starts with previous state stored in boltdb: -// (1) Try to load from boltdb, get the data; -// (2) Return loaded data. +// 2. Agent starts with previous state stored in boltdb: +// (1) Try to load from boltdb, get the data; +// (2) Return loaded data. // -// 3. Agent starts with previous state stored in state file (i.e. it was just upgraded from an old agent that uses state file): -// (1) Try to load from boltdb, get nothing; -// (2) Try to load from state file, get something; -// (3) Save loaded data to boltdb; -// (4) Return loaded data. +// 3. Agent starts with previous state stored in state file (i.e. it was just upgraded from an old agent that uses state file): +// (1) Try to load from boltdb, get nothing; +// (2) Try to load from state file, get something; +// (3) Save loaded data to boltdb; +// (4) Return loaded data. func (agent *ecsAgent) loadData(containerChangeEventStream *eventstream.EventStream, credentialsManager credentials.Manager, state dockerstate.TaskEngineState, imageManager engine.ImageManager, - execCmdMgr execcmd.Manager) (*savedData, error) { + execCmdMgr execcmd.Manager, + serviceConnectManager serviceconnect.Manager) (*savedData, error) { s := &savedData{ taskEngine: engine.NewTaskEngine(agent.cfg, agent.dockerClient, credentialsManager, containerChangeEventStream, imageManager, state, - agent.metadataManager, agent.resourceFields, execCmdMgr), + agent.metadataManager, agent.resourceFields, execCmdMgr, serviceConnectManager), } s.taskEngine.SetDataClient(agent.dataClient) diff --git a/agent/app/data_test.go b/agent/app/data_test.go index d201f49e2f5..74af421302a 100644 --- a/agent/app/data_test.go +++ b/agent/app/data_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -85,7 +86,7 @@ var ( func TestLoadDataNoPreviousState(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - _, stateManagerFactory, _, execCmdMgr := setup(t) + _, stateManagerFactory, _, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager, dataClient, cleanup := newTestClient(t) @@ -113,13 +114,13 @@ func TestLoadDataNoPreviousState(t *testing.T) { } _, err := agent.loadData(eventstream.NewEventStream("events", ctx), - credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr) + credentialsManager, dockerstate.NewTaskEngineState(), imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) } func TestLoadDataLoadFromBoltDB(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - _, stateManagerFactory, _, execCmdMgr := setup(t) + _, stateManagerFactory, _, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() _, dataClient, cleanup := newTestClient(t) @@ -143,14 +144,14 @@ func TestLoadDataLoadFromBoltDB(t *testing.T) { state := dockerstate.NewTaskEngineState() s, err := agent.loadData(eventstream.NewEventStream("events", ctx), - credentialsManager, state, imageManager, execCmdMgr) + credentialsManager, state, imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) checkLoadedData(state, s, t) } func TestLoadDataLoadFromStateFile(t *testing.T) { ctrl, credentialsManager, _, imageManager, _, - _, stateManagerFactory, _, execCmdMgr := setup(t) + _, stateManagerFactory, _, execCmdMgr, serviceConnectManager := setup(t) defer ctrl.Finish() stateManager, dataClient, cleanup := newTestClient(t) @@ -181,7 +182,7 @@ func TestLoadDataLoadFromStateFile(t *testing.T) { state := dockerstate.NewTaskEngineState() s, err := agent.loadData(eventstream.NewEventStream("events", ctx), - credentialsManager, state, imageManager, execCmdMgr) + credentialsManager, state, imageManager, execCmdMgr, serviceConnectManager) assert.NoError(t, err) checkLoadedData(state, s, t) diff --git a/agent/asm/asm_test.go b/agent/asm/asm_test.go index d3e18bfd12a..95bb3cfc0cf 100644 --- a/agent/asm/asm_test.go +++ b/agent/asm/asm_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/async/lru_cache_test.go b/agent/async/lru_cache_test.go index 12591307ffc..7e863acd00d 100644 --- a/agent/async/lru_cache_test.go +++ b/agent/async/lru_cache_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/conditional.go b/agent/config/conditional.go index b6f8be049b8..f6cd5568ff6 100644 --- a/agent/config/conditional.go +++ b/agent/config/conditional.go @@ -71,7 +71,7 @@ type BooleanDefaultFalse struct { Value Conditional } -/// Enabled is a convenience function for when consumers don't care if the value is implicit or explicit +// / Enabled is a convenience function for when consumers don't care if the value is implicit or explicit func (b BooleanDefaultFalse) Enabled() bool { return b.Value == ExplicitlyEnabled } diff --git a/agent/config/conditional_test.go b/agent/config/conditional_test.go index 601bf94e1fb..e9cda03b300 100644 --- a/agent/config/conditional_test.go +++ b/agent/config/conditional_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/config.go b/agent/config/config.go index b7e296df33a..d8c3d98f697 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -187,6 +187,9 @@ var ( // DefaultPauseContainerTag is the tag for the pause container image. The linker's load // flags are used to populate this value from the Makefile DefaultPauseContainerTag = "" + + // CgroupV2 Specifies whether or not to run in Cgroups V2 mode. + CgroupV2 = false ) // Merge merges two config files, preferring the ones on the left. Any nil or @@ -593,6 +596,7 @@ func environmentConfig() (Config, error) { EnableRuntimeStats: parseBooleanDefaultFalseConfig("ECS_ENABLE_RUNTIME_STATS"), ShouldExcludeIPv6PortBinding: parseBooleanDefaultTrueConfig("ECS_EXCLUDE_IPV6_PORTBINDING"), WarmPoolsSupport: parseBooleanDefaultFalseConfig("ECS_WARM_POOLS_CHECK"), + DynamicHostPortRange: parseDynamicHostPortRange("ECS_DYNAMIC_HOST_PORT_RANGE"), }, err } diff --git a/agent/config/config_linux.go b/agent/config/config_linux.go new file mode 100644 index 00000000000..9c21038f337 --- /dev/null +++ b/agent/config/config_linux.go @@ -0,0 +1,27 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package config + +import ( + "github.com/containerd/cgroups" +) + +func init() { + if cgroups.Mode() == cgroups.Unified { + CgroupV2 = true + } +} diff --git a/agent/config/config_test.go b/agent/config/config_test.go index c2d14738364..9c563ff8c0e 100644 --- a/agent/config/config_test.go +++ b/agent/config/config_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -18,6 +19,7 @@ package config import ( "encoding/json" "errors" + "fmt" "os" "testing" "time" @@ -25,6 +27,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/ec2" mock_ec2 "github.com/aws/amazon-ecs-agent/agent/ec2/mocks" + "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/golang/mock/gomock" @@ -159,6 +162,7 @@ func TestEnvironmentConfig(t *testing.T) { defer setTestEnv("ECS_ENABLE_RUNTIME_STATS", "true")() defer setTestEnv("ECS_EXCLUDE_IPV6_PORTBINDING", "true")() defer setTestEnv("ECS_WARM_POOLS_CHECK", "false")() + defer setTestEnv("ECS_DYNAMIC_HOST_PORT_RANGE", "200-300")() additionalLocalRoutesJSON := `["1.2.3.4/22","5.6.7.8/32"]` setTestEnv("ECS_AWSVPC_ADDITIONAL_LOCAL_ROUTES", additionalLocalRoutesJSON) setTestEnv("ECS_ENABLE_CONTAINER_METADATA", "true") @@ -218,6 +222,7 @@ func TestEnvironmentConfig(t *testing.T) { assert.True(t, conf.EnableRuntimeStats.Enabled(), "Wrong value for EnableRuntimeStats") assert.True(t, conf.ShouldExcludeIPv6PortBinding.Enabled(), "Wrong value for ShouldExcludeIPv6PortBinding") assert.False(t, conf.WarmPoolsSupport.Enabled(), "Wrong value for WarmPoolsSupport") + assert.Equal(t, "200-300", conf.DynamicHostPortRange) } func TestTrimWhitespaceWhenCreating(t *testing.T) { @@ -493,6 +498,69 @@ func TestValidFormatParseEnvVariableDuration(t *testing.T) { assert.Equal(t, 1*time.Second, duration, "Unexpected value parsed in parseEnvVariableDuration.") } +func TestParseDynamicHostPortRange(t *testing.T) { + testCases := []struct { + testName string + testDynamicHostPortRangeVal string + expectedPortRangeVal string + expectedErrorDynamicHostPortRange error + expectedErrorEphemeralHostPortRange error + }{ + { + testName: "Parse DynamicHostPortRange for valid DynamicHostPortRange value", + testDynamicHostPortRangeVal: "200-300", + expectedPortRangeVal: "200-300", + expectedErrorDynamicHostPortRange: nil, + expectedErrorEphemeralHostPortRange: nil, + }, + { + testName: "Parse DynamicHostPortRange for valid case when config option is not set or is empty", + testDynamicHostPortRangeVal: "", + expectedPortRangeVal: "300-400", + expectedErrorDynamicHostPortRange: nil, + expectedErrorEphemeralHostPortRange: nil, + }, + { + testName: "Parse DynamicHostPortRange for Invalid DynamicHostPortRange value", + testDynamicHostPortRangeVal: "test1", + expectedPortRangeVal: "300-400", + expectedErrorDynamicHostPortRange: errors.New("Invalid DynamicHostPortRange"), + expectedErrorEphemeralHostPortRange: nil, + }, + { + testName: "Invalid DynamicHostPortRange value and error on getDynamicHostPortRange value", + testDynamicHostPortRangeVal: "test2", + expectedPortRangeVal: fmt.Sprintf("%d-%d", utils.DefaultPortRangeStart, utils.DefaultPortRangeEnd), + expectedErrorDynamicHostPortRange: nil, + expectedErrorEphemeralHostPortRange: errors.New("Error getting EphemeralHostPortRange"), + }, + } + defer func() { + getDynamicHostPortRange = utils.GetDynamicHostPortRange + }() + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + defer setTestRegion()() + defer setTestEnv("ECS_DYNAMIC_HOST_PORT_RANGE", tc.testDynamicHostPortRangeVal)() + + if tc.testDynamicHostPortRangeVal == "" || tc.expectedErrorDynamicHostPortRange != nil { + getDynamicHostPortRange = func() (start int, end int, err error) { + return 300, 400, nil + } + } + + if tc.expectedErrorEphemeralHostPortRange != nil { + getDynamicHostPortRange = func() (start int, end int, err error) { + return 10, 20, errors.New("test default values") + } + } + + dynamicHostPortRange := parseDynamicHostPortRange("ECS_DYNAMIC_HOST_PORT_RANGE") + assert.Equal(t, tc.expectedPortRangeVal, dynamicHostPortRange) + }) + } +} + func TestInvalidTaskCleanupTimeoutOverridesToThreeHours(t *testing.T) { defer setTestRegion()() setTestEnv("ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION", "1ms") diff --git a/agent/config/config_unix.go b/agent/config/config_unix.go index 506c67f7fa8..8886a92ce31 100644 --- a/agent/config/config_unix.go +++ b/agent/config/config_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -33,8 +34,13 @@ const ( // defaultRuntimeStatsLogFile stores the path where the golang runtime stats are periodically logged defaultRuntimeStatsLogFile = `/log/agent-runtime-stats.log` - // DefaultTaskCgroupPrefix is default cgroup prefix for ECS tasks - DefaultTaskCgroupPrefix = "/ecs" + // DefaultTaskCgroupV1Prefix is default cgroup v1 prefix for ECS tasks + DefaultTaskCgroupV1Prefix = "/ecs" + // DefaultTaskCgroupV2Prefix is default cgroup v2 prefix for ECS tasks + // ecstasks is used because this creates a systemd "slice", and using just + // ecs would create a confusing name conflict with the ecs systemd service. + // (we would have both ecs.service and ecs.slice in /sys/fs/cgroup). + DefaultTaskCgroupV2Prefix = "ecstasks" // Default cgroup memory system root path, this is the default used if the // path has not been configured through ECS_CGROUP_PATH @@ -94,7 +100,7 @@ func DefaultConfig() Config { PollingMetricsWaitDuration: DefaultPollingMetricsWaitDuration, NvidiaRuntime: DefaultNvidiaRuntime, CgroupCPUPeriod: defaultCgroupCPUPeriod, - GMSACapable: false, + GMSACapable: parseGMSACapability(), FSxWindowsFileServerCapable: false, RuntimeStatsLogFile: defaultRuntimeStatsLogFile, EnableRuntimeStats: BooleanDefaultFalse{Value: NotSet}, diff --git a/agent/config/config_unix_test.go b/agent/config/config_unix_test.go index d39a0966082..e35d9bbf0e3 100644 --- a/agent/config/config_unix_test.go +++ b/agent/config/config_unix_test.go @@ -1,4 +1,5 @@ //go:build !windows && unit +// +build !windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/config_windows.go b/agent/config/config_windows.go index 181c9545af9..d35ec9606f4 100644 --- a/agent/config/config_windows.go +++ b/agent/config/config_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/config_windows_test.go b/agent/config/config_windows_test.go index f4ad9898178..f2c4a323678 100644 --- a/agent/config/config_windows_test.go +++ b/agent/config/config_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/const_linux.go b/agent/config/const_linux.go index 2b9289e1dde..1ea8885bbbd 100644 --- a/agent/config/const_linux.go +++ b/agent/config/const_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/const_unknown.go b/agent/config/const_unknown.go index 37f31d03e76..53841f47eec 100644 --- a/agent/config/const_unknown.go +++ b/agent/config/const_unknown.go @@ -1,4 +1,5 @@ //go:build !windows && !linux +// +build !windows,!linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/const_windows.go b/agent/config/const_windows.go index 71ab60ced9c..3606476fbaf 100644 --- a/agent/config/const_windows.go +++ b/agent/config/const_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/doc.go b/agent/config/doc.go index d67369a5d5c..e41fe25125b 100644 --- a/agent/config/doc.go +++ b/agent/config/doc.go @@ -15,7 +15,7 @@ Package config handles loading configuration data, warning on missing data, and setting sane defaults. -Configuration Sources +# Configuration Sources Configuration data is loaded from two sources currently: the environment and a json config file. diff --git a/agent/config/os_family_windows.go b/agent/config/os_family_windows.go index 5bc773a7ce9..d7b2218d849 100644 --- a/agent/config/os_family_windows.go +++ b/agent/config/os_family_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/os_family_windows_test.go b/agent/config/os_family_windows_test.go index 68e14f16f5c..9b6208cd9bb 100644 --- a/agent/config/os_family_windows_test.go +++ b/agent/config/os_family_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/parse.go b/agent/config/parse.go index ba37b28a926..8ce2e721d10 100644 --- a/agent/config/parse.go +++ b/agent/config/parse.go @@ -23,8 +23,18 @@ import ( "time" "github.com/aws/amazon-ecs-agent/agent/dockerclient" + "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/cihub/seelog" cnitypes "github.com/containernetworking/cni/pkg/types" + "github.com/docker/go-connections/nat" +) + +const ( + // envSkipDomainJoinCheck is an environment setting that can be used to skip + // domain join check validation. This is useful for integration and + // functional-tests but should not be set for any non-test use-case. + envSkipDomainJoinCheck = "ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION" ) func parseCheckpoint(dataDir string) BooleanDefaultFalse { @@ -365,3 +375,29 @@ func parseCgroupCPUPeriod() time.Duration { return defaultCgroupCPUPeriod } + +var getDynamicHostPortRange = utils.GetDynamicHostPortRange + +func parseDynamicHostPortRange(dynamicHostPortRangeEnv string) string { + dynamicHostPortRange := os.Getenv(dynamicHostPortRangeEnv) + if dynamicHostPortRange != "" { + _, _, err := nat.ParsePortRangeToInt(dynamicHostPortRange) + if err != nil { + seelog.Warnf("Invalid dynamicHostPortRange value from config: %s, err: %v", dynamicHostPortRange, err) + return getDefaultDynamicHostPortRange() + } + } else { + return getDefaultDynamicHostPortRange() + } + return dynamicHostPortRange +} + +func getDefaultDynamicHostPortRange() string { + startHostPortRange, endHostPortRange, err := getDynamicHostPortRange() + if err != nil { + seelog.Warnf("Unable to read the ephemeral host port range, "+ + "falling back to the default range: %v-%v", utils.DefaultPortRangeStart, utils.DefaultPortRangeEnd) + return fmt.Sprintf("%d-%d", utils.DefaultPortRangeStart, utils.DefaultPortRangeEnd) + } + return fmt.Sprintf("%d-%d", startHostPortRange, endHostPortRange) +} diff --git a/agent/config/parse_linux.go b/agent/config/parse_linux.go index 255163decb8..ccf9de5e04f 100644 --- a/agent/config/parse_linux.go +++ b/agent/config/parse_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -6,7 +7,7 @@ // not use this file except in compliance with the License. A copy of the // License is located at // -// httpaws.amazon.com/apache2.0/ +// http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either @@ -17,10 +18,52 @@ package config import ( "errors" + "os" "strings" + + "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/cihub/seelog" ) func parseGMSACapability() bool { + envStatus := utils.ParseBool(os.Getenv("ECS_GMSA_SUPPORTED"), true) + if envStatus { + // Check if domain join check override is present + skipDomainJoinCheck := utils.ParseBool(os.Getenv(envSkipDomainJoinCheck), false) + if skipDomainJoinCheck { + seelog.Infof("Skipping domain join validation based on environment override") + return true + } + + // check if the credentials fetcher socket is created and exists + // this env variable is set in ecs-init module + if credentialsfetcherHostDir := os.Getenv("CREDENTIALS_FETCHER_HOST_DIR"); credentialsfetcherHostDir != "" { + _, err := os.Stat(credentialsfetcherHostDir) + if err != nil { + if os.IsNotExist(err) { + seelog.Errorf("CREDENTIALS_FETCHER_HOST_DIR not found, err: %v", err) + return false + } + } + + //skip domain join check if the domainless gMSA is supported by setting the env variable + domainlessGMSAUser := os.Getenv("CREDENTIALS_FETCHER_SECRET_NAME_FOR_DOMAINLESS_GMSA") + if domainlessGMSAUser != "" && len(domainlessGMSAUser) > 0 { + seelog.Info("domainless gMSA support is enabled") + return true + } + + // returns true if the container instance is domain joined + // this env variable is set in ecs-init module + isDomainJoined := utils.ParseBool(os.Getenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE"), false) + + if !isDomainJoined { + seelog.Error("gMSA on linux requires domain joined instance. Did not find expected env var ECS_DOMAIN_JOINED_LINUX_INSTANCE=true") + } + return isDomainJoined + } + } + seelog.Debug("env variables to support gMSA are not set") return false } diff --git a/agent/config/parse_linux_test.go b/agent/config/parse_linux_test.go new file mode 100644 index 00000000000..cabc2596195 --- /dev/null +++ b/agent/config/parse_linux_test.go @@ -0,0 +1,51 @@ +//go:build linux && unit +// +build linux,unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGMSACapabilitySupported(t *testing.T) { + t.Setenv("ECS_GMSA_SUPPORTED", "True") + t.Setenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE", "True") + t.Setenv("CREDENTIALS_FETCHER_HOST_DIR", "/var/run") + + assert.True(t, parseGMSACapability()) +} + +func TestParseGMSACapabilityNonDomainJoined(t *testing.T) { + t.Setenv("ECS_GMSA_SUPPORTED", "True") + t.Setenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE", "False") + + assert.False(t, parseGMSACapability()) +} + +func TestParseGMSACapabilityUnsupported(t *testing.T) { + t.Setenv("ECS_GMSA_SUPPORTED", "False") + + assert.False(t, parseGMSACapability()) +} + +func TestSkipDomainJoinCheckParseGMSACapability(t *testing.T) { + t.Setenv("ECS_GMSA_SUPPORTED", "True") + t.Setenv("ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION", "True") + + assert.True(t, parseGMSACapability()) +} diff --git a/agent/config/parse_unsupported.go b/agent/config/parse_unsupported.go index 732fae352b3..45d711563a8 100644 --- a/agent/config/parse_unsupported.go +++ b/agent/config/parse_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/parse_windows.go b/agent/config/parse_windows.go index fbdc2ade0ec..c9c6dec403b 100644 --- a/agent/config/parse_windows.go +++ b/agent/config/parse_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -27,11 +28,6 @@ import ( ) const ( - // envSkipDomainJoinCheck is an environment setting that can be used to skip - // domain join check validation. This is useful for integration and - // functional-tests but should not be set for any non-test use-case. - envSkipDomainJoinCheck = "ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION" - // envSkipWindowsServerVersionCheck is an environment setting that can be used // to skip the windows server version check. This is useful for testing and // should not be set for any non-test use-case. diff --git a/agent/config/parse_windows_test.go b/agent/config/parse_windows_test.go index 618ae726d0a..d4604840555 100644 --- a/agent/config/parse_windows_test.go +++ b/agent/config/parse_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/sensitive_test.go b/agent/config/sensitive_test.go index a5c00fe66da..f0f13b2a01b 100644 --- a/agent/config/sensitive_test.go +++ b/agent/config/sensitive_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/types.go b/agent/config/types.go index 2e533f41bf7..68c6eee6cd5 100644 --- a/agent/config/types.go +++ b/agent/config/types.go @@ -33,6 +33,7 @@ type Config struct { // ClusterArn is the Name or full ARN of a Cluster to register into. It has // been deprecated (and will eventually be removed) in favor of Cluster ClusterArn string `deprecated:"Please use Cluster instead"` + // Cluster can either be the Name or full ARN of a Cluster. This is the // cluster the agent should register this ContainerInstance into. If this // value is not set, it will default to "default" @@ -101,8 +102,9 @@ type Config struct { // on the instance DisableDockerHealthCheck BooleanDefaultFalse - // ReservedMemory specifies the amount of memory (in MB) to reserve for things - // other than containers managed by ECS + // ReservedMemory specifies Reduction, in MiB, of the memory capacity of the instance + // that is reported to Amazon ECS. Used by Amazon ECS when placing tasks on container instances. + // This doesn't reserve memory usage on the instance ReservedMemory uint16 // DockerStopTimeout specifies the amount of time before a SIGKILL is issued to @@ -358,4 +360,9 @@ type Config struct { // WarmPoolsSupport specifies whether the agent should poll IMDS to check the target lifecycle state for a starting // instance WarmPoolsSupport BooleanDefaultFalse + + // DynamicHostPortRange specifies the dynamic host port range that the agent + // uses to assign host ports from, for a container port range mapping. + // This defaults to the platform specific ephemeral host port range + DynamicHostPortRange string } diff --git a/agent/config/types_unix.go b/agent/config/types_unix.go index 887e35ea535..164fa4aaaf2 100644 --- a/agent/config/types_unix.go +++ b/agent/config/types_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/config/types_windows.go b/agent/config/types_windows.go index 90ca8b250c6..df04eedac0f 100644 --- a/agent/config/types_windows.go +++ b/agent/config/types_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/manager_test.go b/agent/containermetadata/manager_test.go index cfb1f7b1bed..737dbf55042 100644 --- a/agent/containermetadata/manager_test.go +++ b/agent/containermetadata/manager_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/manager_unix_test.go b/agent/containermetadata/manager_unix_test.go index 2c787e401a0..ad3399ccf35 100644 --- a/agent/containermetadata/manager_unix_test.go +++ b/agent/containermetadata/manager_unix_test.go @@ -1,4 +1,5 @@ //go:build unit && !windows +// +build unit,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/manager_windows_test.go b/agent/containermetadata/manager_windows_test.go index 8174b4a0e07..296f5621341 100644 --- a/agent/containermetadata/manager_windows_test.go +++ b/agent/containermetadata/manager_windows_test.go @@ -1,4 +1,5 @@ //go:build unit && windows +// +build unit,windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/parse_metadata_test.go b/agent/containermetadata/parse_metadata_test.go index 73770de927d..7a3357f7b9b 100644 --- a/agent/containermetadata/parse_metadata_test.go +++ b/agent/containermetadata/parse_metadata_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/types_test.go b/agent/containermetadata/types_test.go index 12b25486d12..5aebbd84bbf 100644 --- a/agent/containermetadata/types_test.go +++ b/agent/containermetadata/types_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/utils_test.go b/agent/containermetadata/utils_test.go index dba39d36984..df3ad60c291 100644 --- a/agent/containermetadata/utils_test.go +++ b/agent/containermetadata/utils_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/write_metadata_test.go b/agent/containermetadata/write_metadata_test.go index b3ab5e0b745..adeeb1c8378 100644 --- a/agent/containermetadata/write_metadata_test.go +++ b/agent/containermetadata/write_metadata_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/write_metadata_unix.go b/agent/containermetadata/write_metadata_unix.go index d3b32ddef94..1c9e799034f 100644 --- a/agent/containermetadata/write_metadata_unix.go +++ b/agent/containermetadata/write_metadata_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/write_metadata_unix_test.go b/agent/containermetadata/write_metadata_unix_test.go index 9603e71f8fa..5d6eb54e90d 100644 --- a/agent/containermetadata/write_metadata_unix_test.go +++ b/agent/containermetadata/write_metadata_unix_test.go @@ -1,4 +1,5 @@ //go:build unit && !windows +// +build unit,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/write_metadata_windows.go b/agent/containermetadata/write_metadata_windows.go index 357ff630657..85efa3c672a 100644 --- a/agent/containermetadata/write_metadata_windows.go +++ b/agent/containermetadata/write_metadata_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/containermetadata/write_metadata_windows_test.go b/agent/containermetadata/write_metadata_windows_test.go index 2e86c31bf3d..3a906b3d768 100644 --- a/agent/containermetadata/write_metadata_windows_test.go +++ b/agent/containermetadata/write_metadata_windows_test.go @@ -1,4 +1,5 @@ //go:build unit && windows +// +build unit,windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/credentials/instancecreds/instancecreds.go b/agent/credentials/instancecreds/instancecreds.go index 70947c92c2d..256dd843221 100644 --- a/agent/credentials/instancecreds/instancecreds.go +++ b/agent/credentials/instancecreds/instancecreds.go @@ -16,42 +16,10 @@ package instancecreds import ( "sync" - "github.com/aws/amazon-ecs-agent/agent/credentials/providers" "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/cihub/seelog" ) var ( credentialChain *credentials.Credentials mu sync.Mutex ) - -// GetCredentials returns the instance credentials chain. This is the default chain -// credentials plus the "rotating shared credentials provider", so credentials will -// be checked in this order: -// 1. Env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). -// 2. Shared credentials file (https://docs.aws.amazon.com/ses/latest/DeveloperGuide/create-shared-credentials-file.html) (file at ~/.aws/credentials containing access key id and secret access key). -// 3. EC2 role credentials. This is an IAM role that the user specifies when they launch their EC2 container instance (ie ecsInstanceRole (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html)). -// 4. Rotating shared credentials file located at /rotatingcreds/credentials -func GetCredentials() *credentials.Credentials { - mu.Lock() - if credentialChain == nil { - credProviders := defaults.CredProviders(defaults.Config(), defaults.Handlers()) - credProviders = append(credProviders, providers.NewRotatingSharedCredentialsProvider()) - credentialChain = credentials.NewCredentials(&credentials.ChainProvider{ - VerboseErrors: false, - Providers: credProviders, - }) - } - mu.Unlock() - - // credentials.Credentials is concurrency-safe, so lock not needed here - v, err := credentialChain.Get() - if err != nil { - seelog.Errorf("Error getting ECS instance credentials from default chain: %s", err) - } else { - seelog.Infof("Successfully got ECS instance credentials from provider: %s", v.ProviderName) - } - return credentialChain -} diff --git a/agent/credentials/instancecreds/instancecreds_linux.go b/agent/credentials/instancecreds/instancecreds_linux.go new file mode 100644 index 00000000000..b94452635ad --- /dev/null +++ b/agent/credentials/instancecreds/instancecreds_linux.go @@ -0,0 +1,52 @@ +//go:build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package instancecreds + +import ( + "github.com/aws/amazon-ecs-agent/agent/credentials/providers" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/cihub/seelog" +) + +// GetCredentials returns the instance credentials chain. This is the default chain +// credentials plus the "rotating shared credentials provider", so credentials will +// be checked in this order: +// 1. Env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). +// 2. Shared credentials file (https://docs.aws.amazon.com/ses/latest/DeveloperGuide/create-shared-credentials-file.html) (file at ~/.aws/credentials containing access key id and secret access key). +// 3. EC2 role credentials. This is an IAM role that the user specifies when they launch their EC2 container instance (ie ecsInstanceRole (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html)). +// 4. Rotating shared credentials file located at /rotatingcreds/credentials +func GetCredentials(isExternal bool) *credentials.Credentials { + mu.Lock() + if credentialChain == nil { + credProviders := defaults.CredProviders(defaults.Config(), defaults.Handlers()) + credProviders = append(credProviders, providers.NewRotatingSharedCredentialsProvider()) + credentialChain = credentials.NewCredentials(&credentials.ChainProvider{ + VerboseErrors: false, + Providers: credProviders, + }) + } + mu.Unlock() + + // credentials.Credentials is concurrency-safe, so lock not needed here + v, err := credentialChain.Get() + if err != nil { + seelog.Errorf("Error getting ECS instance credentials from default chain: %s", err) + } else { + seelog.Infof("Successfully got ECS instance credentials from provider: %s", v.ProviderName) + } + return credentialChain +} diff --git a/agent/credentials/instancecreds/instancecreds_test.go b/agent/credentials/instancecreds/instancecreds_test.go index 4c4f6428645..7eae7486869 100644 --- a/agent/credentials/instancecreds/instancecreds_test.go +++ b/agent/credentials/instancecreds/instancecreds_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -25,11 +26,10 @@ import ( func TestGetCredentials(t *testing.T) { credentialChain = nil - credsA := GetCredentials() + credsA := GetCredentials(false) require.NotNil(t, credsA) - credsB := GetCredentials() + credsB := GetCredentials(true) require.NotNil(t, credsB) - require.Equal(t, credsA, credsB) } // test that env vars override all other provider types @@ -44,7 +44,7 @@ func TestGetCredentials_EnvVars(t *testing.T) { defer os.Setenv("AWS_ACCESS_KEY_ID", origAKID) defer os.Setenv("AWS_SECRET_ACCESS_KEY", origSecret) - creds := GetCredentials() + creds := GetCredentials(false) require.NotNil(t, creds) v, err := creds.Get() require.NoError(t, err) @@ -81,7 +81,7 @@ aws_secret_access_key = TESTFILESECRET // reset before exiting defer os.Setenv("AWS_SHARED_CREDENTIALS_FILE", origEnv) - creds := GetCredentials() + creds := GetCredentials(false) require.NotNil(t, creds) v, err := creds.Get() require.NoError(t, err) diff --git a/agent/credentials/instancecreds/instancecreds_unsupported.go b/agent/credentials/instancecreds/instancecreds_unsupported.go new file mode 100644 index 00000000000..c7c27d44987 --- /dev/null +++ b/agent/credentials/instancecreds/instancecreds_unsupported.go @@ -0,0 +1,32 @@ +//go:build !linux && !windows +// +build !linux,!windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package instancecreds + +import ( + "github.com/aws/aws-sdk-go/aws/credentials" +) + +// GetCredentials returns the instance credentials chain. This is the default chain +// credentials plus the "rotating shared credentials provider", so credentials will +// be checked in this order: +// 1. Env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). +// 2. Shared credentials file (https://docs.aws.amazon.com/ses/latest/DeveloperGuide/create-shared-credentials-file.html) (file at ~/.aws/credentials containing access key id and secret access key). +// 3. EC2 role credentials. This is an IAM role that the user specifies when they launch their EC2 container instance (ie ecsInstanceRole (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html)). +// 4. Rotating shared credentials file located at /rotatingcreds/credentials +func GetCredentials(isExternal bool) *credentials.Credentials { + return nil +} diff --git a/agent/credentials/instancecreds/instancecreds_windows.go b/agent/credentials/instancecreds/instancecreds_windows.go new file mode 100644 index 00000000000..efcd1f9aa74 --- /dev/null +++ b/agent/credentials/instancecreds/instancecreds_windows.go @@ -0,0 +1,69 @@ +//go:build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package instancecreds + +import ( + "github.com/aws/amazon-ecs-agent/agent/credentials/providers" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/cihub/seelog" +) + +// GetCredentials returns the instance credentials chain. This is the default chain +// credentials plus the "rotating shared credentials provider", so credentials will +// be checked in this order: +// +// 1. Env vars (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY). +// +// 2. Shared credentials file (https://docs.aws.amazon.com/ses/latest/DeveloperGuide/create-shared-credentials-file.html) (file at ~/.aws/credentials containing access key id and secret access key). +// +// 3. EC2 role credentials. This is an IAM role that the user specifies when they launch their EC2 container instance (ie ecsInstanceRole (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html)). +// +// 4. Rotating shared credentials file located at /rotatingcreds/credentials +// +// The default credential chain provided by the SDK includes: +// * EnvProvider +// * SharedCredentialsProvider +// * RemoteCredProvider (EC2RoleProvider) +// +// In the case of ECS-A on Windows, the `SharedCredentialsProvider` takes +// precedence over the `RotatingSharedCredentialsProvider` and this results +// in the credentials not being refreshed. To mitigate this issue, we will +// reorder the credential chain and ensure that `RotatingSharedCredentialsProvider` +// takes precedence over the `SharedCredentialsProvider` for ECS-A. +func GetCredentials(isExternal bool) *credentials.Credentials { + mu.Lock() + credProviders := defaults.CredProviders(defaults.Config(), defaults.Handlers()) + if isExternal { + credProviders = append(credProviders[:1], append([]credentials.Provider{providers.NewRotatingSharedCredentialsProvider()}, credProviders[1:]...)...) + } else { + credProviders = append(credProviders, providers.NewRotatingSharedCredentialsProvider()) + } + credentialChain = credentials.NewCredentials(&credentials.ChainProvider{ + VerboseErrors: false, + Providers: credProviders, + }) + mu.Unlock() + + // credentials.Credentials is concurrency-safe, so lock not needed here + v, err := credentialChain.Get() + if err != nil { + seelog.Errorf("Error getting ECS instance credentials from default chain: %s", err) + } else { + seelog.Infof("Successfully got ECS instance credentials from provider: %s", v.ProviderName) + } + return credentialChain +} diff --git a/agent/credentials/manager.go b/agent/credentials/manager.go index 18ff5046cb4..12530d04cd0 100644 --- a/agent/credentials/manager.go +++ b/agent/credentials/manager.go @@ -68,14 +68,10 @@ type IAMRoleCredentials struct { type TaskIAMRoleCredentials struct { ARN string IAMRoleCredentials IAMRoleCredentials - lock sync.RWMutex } // GetIAMRoleCredentials returns the IAM role credentials in the task IAM role struct func (role *TaskIAMRoleCredentials) GetIAMRoleCredentials() IAMRoleCredentials { - role.lock.RLock() - defer role.lock.RUnlock() - return role.IAMRoleCredentials } diff --git a/agent/credentials/manager_test.go b/agent/credentials/manager_test.go index b30284c0980..40f3ca75fc1 100644 --- a/agent/credentials/manager_test.go +++ b/agent/credentials/manager_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/credentials/providers/credentials_filename_linux.go b/agent/credentials/providers/credentials_filename_linux.go new file mode 100644 index 00000000000..965eebf15a0 --- /dev/null +++ b/agent/credentials/providers/credentials_filename_linux.go @@ -0,0 +1,22 @@ +//go:build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package providers + +const ( + // defaultRotatingCredentialsFilename is the default location of the credentials file + // for RotatingSharedCredentialsProvider. + defaultRotatingCredentialsFilename = "/rotatingcreds/credentials" +) diff --git a/agent/credentials/providers/credentials_filename_unsupported.go b/agent/credentials/providers/credentials_filename_unsupported.go new file mode 100644 index 00000000000..5995402c26d --- /dev/null +++ b/agent/credentials/providers/credentials_filename_unsupported.go @@ -0,0 +1,23 @@ +//go:build !linux && !windows +// +build !linux,!windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package providers + +const ( + // defaultRotatingCredentialsFilename is the default location of the credentials file + // for RotatingSharedCredentialsProvider. + defaultRotatingCredentialsFilename = "/unsupported/rotatingcreds/credentials" +) diff --git a/agent/credentials/providers/credentials_filename_windows.go b/agent/credentials/providers/credentials_filename_windows.go new file mode 100644 index 00000000000..58fbdc47da4 --- /dev/null +++ b/agent/credentials/providers/credentials_filename_windows.go @@ -0,0 +1,20 @@ +//go:build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package providers + +// defaultRotatingCredentialsFilename is the default location of the credentials file +// for RotatingSharedCredentialsProvider. +const defaultRotatingCredentialsFilename = "C:\\Windows\\System32\\config\\systemprofile\\.aws\\credentials" diff --git a/agent/credentials/providers/rotating_shared_credentials_provider.go b/agent/credentials/providers/rotating_shared_credentials_provider.go index 7878891fff5..93f52b78754 100644 --- a/agent/credentials/providers/rotating_shared_credentials_provider.go +++ b/agent/credentials/providers/rotating_shared_credentials_provider.go @@ -15,6 +15,7 @@ package providers import ( "fmt" + "os" "time" "github.com/aws/aws-sdk-go/aws/credentials" @@ -22,10 +23,10 @@ import ( ) const ( + ALTERNATE_CREDENTIAL_PROFILE_ENV_VAR = "ECS_ALTERNATE_CREDENTIAL_PROFILE" + DEFAULT_CREDENTIAL_PROFILE = "default" // defaultRotationInterval is how frequently to expire and re-retrieve the credentials from file. defaultRotationInterval = time.Minute - // defaultFilename is the default location of the credentials file within the container. - defaultFilename = "/rotatingcreds/credentials" // RotatingSharedCredentialsProviderName is the name of this provider RotatingSharedCredentialsProviderName = "RotatingSharedCredentialsProvider" ) @@ -43,11 +44,17 @@ type RotatingSharedCredentialsProvider struct { // NewRotatingSharedCredentials returns a rotating shared credentials provider // with default values set. func NewRotatingSharedCredentialsProvider() *RotatingSharedCredentialsProvider { + var credentialProfile = DEFAULT_CREDENTIAL_PROFILE + if alternateCredentialProfile := os.Getenv(ALTERNATE_CREDENTIAL_PROFILE_ENV_VAR); alternateCredentialProfile != "" { + seelog.Infof("Overriding %s credential profile; using: %s.", DEFAULT_CREDENTIAL_PROFILE, alternateCredentialProfile) + credentialProfile = alternateCredentialProfile + } + return &RotatingSharedCredentialsProvider{ RotationInterval: defaultRotationInterval, sharedCredentialsProvider: &credentials.SharedCredentialsProvider{ - Filename: defaultFilename, - Profile: "default", + Filename: defaultRotatingCredentialsFilename, + Profile: credentialProfile, }, } } diff --git a/agent/credentials/providers/rotating_shared_credentials_provider_test.go b/agent/credentials/providers/rotating_shared_credentials_provider_test.go index 404701cfec3..6f7b521b524 100644 --- a/agent/credentials/providers/rotating_shared_credentials_provider_test.go +++ b/agent/credentials/providers/rotating_shared_credentials_provider_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -29,7 +30,16 @@ func TestNewRotatingSharedCredentialsProvider(t *testing.T) { p := NewRotatingSharedCredentialsProvider() require.Equal(t, time.Minute, p.RotationInterval) require.Equal(t, "default", p.sharedCredentialsProvider.Profile) - require.Equal(t, "/rotatingcreds/credentials", p.sharedCredentialsProvider.Filename) + require.Equal(t, defaultRotatingCredentialsFilename, p.sharedCredentialsProvider.Filename) +} + +func TestNewRotatingSharedCredentialsProviderExternal(t *testing.T) { + os.Setenv("ECS_ALTERNATE_CREDENTIAL_PROFILE", "external") + defer os.Unsetenv("ECS_ALTERNATE_CREDENTIAL_PROFILE") + p := NewRotatingSharedCredentialsProvider() + require.Equal(t, time.Minute, p.RotationInterval) + require.Equal(t, "external", p.sharedCredentialsProvider.Profile) + require.Equal(t, defaultRotatingCredentialsFilename, p.sharedCredentialsProvider.Filename) } func TestRotatingSharedCredentialsProvider_RetrieveFail_BadPath(t *testing.T) { diff --git a/agent/data/client_test.go b/agent/data/client_test.go index 27b8a399139..ab5d0d918cb 100644 --- a/agent/data/client_test.go +++ b/agent/data/client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/container_client_test.go b/agent/data/container_client_test.go index 9d1c325cbe8..cd11908adf8 100644 --- a/agent/data/container_client_test.go +++ b/agent/data/container_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/eniattachment_client_test.go b/agent/data/eniattachment_client_test.go index 58fbd80a56b..cac248e8de1 100644 --- a/agent/data/eniattachment_client_test.go +++ b/agent/data/eniattachment_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/helpers_test.go b/agent/data/helpers_test.go index efae4878b04..fad4799a35b 100644 --- a/agent/data/helpers_test.go +++ b/agent/data/helpers_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/imagestate_client_test.go b/agent/data/imagestate_client_test.go index c5ddaacb5aa..696191314f6 100644 --- a/agent/data/imagestate_client_test.go +++ b/agent/data/imagestate_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/metadata_client_test.go b/agent/data/metadata_client_test.go index 6d5b32721a9..f9a6b754548 100644 --- a/agent/data/metadata_client_test.go +++ b/agent/data/metadata_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/data/task_client_test.go b/agent/data/task_client_test.go index 5134b1edad4..6d72782764c 100644 --- a/agent/data/task_client_test.go +++ b/agent/data/task_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerapi/docker_client.go b/agent/dockerclient/dockerapi/docker_client.go index c5781393666..0837a9f913c 100644 --- a/agent/dockerclient/dockerapi/docker_client.go +++ b/agent/dockerclient/dockerapi/docker_client.go @@ -214,20 +214,21 @@ type DockerClient interface { // DockerGoClient wraps the underlying go-dockerclient and docker/docker library. // It exists primarily for the following four purposes: -// 1) Provide an abstraction over inputs and outputs, -// a) Inputs: Trims them down to what we actually need (largely unchanged tbh) -// b) Outputs: Unifies error handling and the common 'start->inspect' -// pattern by having a consistent error output. This error output -// contains error data with a given Name that aims to be presentable as a -// 'reason' in state changes. It also filters out the information about a -// container that is of interest, such as network bindings, while -// ignoring the rest. -// 2) Timeouts: It adds timeouts everywhere, mostly as a reaction to -// pull-related issues in the Docker daemon. -// 3) Versioning: It abstracts over multiple client versions to allow juggling -// appropriately there. -// 4) Allows for both the go-dockerclient client and Docker SDK client to live -// side-by-side until migration to the Docker SDK is complete. +// 1. Provide an abstraction over inputs and outputs, +// a) Inputs: Trims them down to what we actually need (largely unchanged tbh) +// b) Outputs: Unifies error handling and the common 'start->inspect' +// pattern by having a consistent error output. This error output +// contains error data with a given Name that aims to be presentable as a +// 'reason' in state changes. It also filters out the information about a +// container that is of interest, such as network bindings, while +// ignoring the rest. +// 2. Timeouts: It adds timeouts everywhere, mostly as a reaction to +// pull-related issues in the Docker daemon. +// 3. Versioning: It abstracts over multiple client versions to allow juggling +// appropriately there. +// 4. Allows for both the go-dockerclient client and Docker SDK client to live +// side-by-side until migration to the Docker SDK is complete. +// // Implements DockerClient // TODO Remove clientfactory field once all API calls are migrated to sdkclientFactory type dockerGoClient struct { diff --git a/agent/dockerclient/dockerapi/docker_client_test.go b/agent/dockerclient/dockerapi/docker_client_test.go index 49a09232e9f..2442e971266 100644 --- a/agent/dockerclient/dockerapi/docker_client_test.go +++ b/agent/dockerclient/dockerapi/docker_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerapi/docker_events_buffer_test.go b/agent/dockerclient/dockerapi/docker_events_buffer_test.go index 1f9042fdda4..4ca0003f736 100644 --- a/agent/dockerclient/dockerapi/docker_events_buffer_test.go +++ b/agent/dockerclient/dockerapi/docker_events_buffer_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerapi/errors_test.go b/agent/dockerclient/dockerapi/errors_test.go index 6aa593750bf..1bdd65a15fb 100644 --- a/agent/dockerclient/dockerapi/errors_test.go +++ b/agent/dockerclient/dockerapi/errors_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerapi/inactivity_timeout_handler_test.go b/agent/dockerclient/dockerapi/inactivity_timeout_handler_test.go index 4fb9e5b2e06..eaea8ec1bfc 100644 --- a/agent/dockerclient/dockerapi/inactivity_timeout_handler_test.go +++ b/agent/dockerclient/dockerapi/inactivity_timeout_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerapi_compare_versions_test.go b/agent/dockerclient/dockerapi_compare_versions_test.go index 792e4b94992..46e077ef9a3 100644 --- a/agent/dockerclient/dockerapi_compare_versions_test.go +++ b/agent/dockerclient/dockerapi_compare_versions_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerauth/doc.go b/agent/dockerclient/dockerauth/doc.go index 2ee606d8a07..8d43c81e0ee 100644 --- a/agent/dockerclient/dockerauth/doc.go +++ b/agent/dockerclient/dockerauth/doc.go @@ -15,7 +15,7 @@ Package dockerauth handles storing auth configuration information for Docker registries. -Usage +# Usage This package pulls authentication information from the passed configuration. A user should set the "EngineAuthType" and "EngineAuthData" configuration @@ -24,7 +24,7 @@ keys to values indicated below. These keys may be set by either setting the environment variables "ECS_ENGINE_AUTH_TYPE" and "ECS_ENGINE_AUTH_DATA" or by setting the keys "EngineAuthData" and "EngineAuthType" in the JSON configuration file located at the configured "ECS_AGENT_CONFIG_FILE_PATH" (see http://godoc.org/github.com/aws/amazon-ecs-agent/agent/config) -Auth Types +# Auth Types The two currently supported auth types are "docker" and "dockercfg". @@ -34,6 +34,7 @@ The auth type "docker" is intended to work most naturally with a JSON configuration file. The "AuthData" is a structured JSON object which specifies values for the docker "AuthConfig" structure. The "AuthData" should be an object similar to the following: + { "my.registry.example.com": { "username": "myUsername", @@ -46,13 +47,13 @@ similar to the following: } } - Dockercfg: The auth type "dockercfg" is intended to allow easy use of an existing ".dockercfg" file generated by running "docker login". This auth type expects the "AuthData" to be a string containing the contents of that file. The contents of your ".dockercfg" will generally be a string of the following form: + '{"http://myregistry.com/v1/":{"auth":"dXNlcjpzd29yZGZpc2g=","email":"email"}}' */ package dockerauth diff --git a/agent/dockerclient/dockerauth/dockerauth_test.go b/agent/dockerclient/dockerauth/dockerauth_test.go index f8fa5072537..f1d9fb01749 100644 --- a/agent/dockerclient/dockerauth/dockerauth_test.go +++ b/agent/dockerclient/dockerauth/dockerauth_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/dockerauth/ecr_test.go b/agent/dockerclient/dockerauth/ecr_test.go index 2f6ca9786a6..d394421dcbe 100644 --- a/agent/dockerclient/dockerauth/ecr_test.go +++ b/agent/dockerclient/dockerauth/ecr_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/sdkclientfactory/sdkclientfactory_test.go b/agent/dockerclient/sdkclientfactory/sdkclientfactory_test.go index e983af5fbe2..bc6664d6d77 100644 --- a/agent/dockerclient/sdkclientfactory/sdkclientfactory_test.go +++ b/agent/dockerclient/sdkclientfactory/sdkclientfactory_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/sdkclientfactory/sdkclientfactory_unix_test.go b/agent/dockerclient/sdkclientfactory/sdkclientfactory_unix_test.go index 82d4732fe93..4c88e87101a 100644 --- a/agent/dockerclient/sdkclientfactory/sdkclientfactory_unix_test.go +++ b/agent/dockerclient/sdkclientfactory/sdkclientfactory_unix_test.go @@ -1,4 +1,5 @@ //go:build unit && !windows +// +build unit,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/sdkclientfactory/sdkclientfactory_windows_test.go b/agent/dockerclient/sdkclientfactory/sdkclientfactory_windows_test.go index 3c0effd4137..23aba519006 100644 --- a/agent/dockerclient/sdkclientfactory/sdkclientfactory_windows_test.go +++ b/agent/dockerclient/sdkclientfactory/sdkclientfactory_windows_test.go @@ -1,4 +1,5 @@ //go:build unit && windows +// +build unit,windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/sdkclientfactory/versionsupport_unix.go b/agent/dockerclient/sdkclientfactory/versionsupport_unix.go index 884dc94c40b..9117a7d7290 100644 --- a/agent/dockerclient/sdkclientfactory/versionsupport_unix.go +++ b/agent/dockerclient/sdkclientfactory/versionsupport_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/dockerclient/sdkclientfactory/versionsupport_windows.go b/agent/dockerclient/sdkclientfactory/versionsupport_windows.go index 5857b085295..afc83054a2b 100644 --- a/agent/dockerclient/sdkclientfactory/versionsupport_windows.go +++ b/agent/dockerclient/sdkclientfactory/versionsupport_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ec2/ec2_client.go b/agent/ec2/ec2_client.go index 0a2cccd07ec..e60eb1af6bc 100644 --- a/agent/ec2/ec2_client.go +++ b/agent/ec2/ec2_client.go @@ -53,7 +53,7 @@ type ClientImpl struct { func NewClientImpl(awsRegion string) Client { ec2Config := aws.NewConfig().WithMaxRetries(clientRetriesNum) ec2Config.Region = aws.String(awsRegion) - ec2Config.Credentials = instancecreds.GetCredentials() + ec2Config.Credentials = instancecreds.GetCredentials(false) client := ec2sdk.New(session.New(), ec2Config) return &ClientImpl{ client: client, diff --git a/agent/ec2/ec2_client_test.go b/agent/ec2/ec2_client_test.go index 089b487a06b..00ed5d5c92a 100644 --- a/agent/ec2/ec2_client_test.go +++ b/agent/ec2/ec2_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ec2/ec2_metadata_client.go b/agent/ec2/ec2_metadata_client.go index 3486a4d8c8c..90afa0fb2f5 100644 --- a/agent/ec2/ec2_metadata_client.go +++ b/agent/ec2/ec2_metadata_client.go @@ -27,7 +27,7 @@ import ( ) const ( - SecurityCrednetialsResource = "iam/security-credentials/" + SecurityCredentialsResource = "iam/security-credentials/" InstanceIdentityDocumentResource = "instance-identity/document" InstanceIdentityDocumentSignatureResource = "instance-identity/signature" MacResource = "mac" @@ -94,7 +94,7 @@ type ec2MetadataClientImpl struct { func NewEC2MetadataClient(client HttpClient) EC2MetadataClient { if client == nil { config := aws.NewConfig().WithMaxRetries(metadataRetries) - config.Credentials = instancecreds.GetCredentials() + config.Credentials = instancecreds.GetCredentials(false) return &ec2MetadataClientImpl{ client: ec2metadata.New(session.New(), config), } @@ -105,7 +105,7 @@ func NewEC2MetadataClient(client HttpClient) EC2MetadataClient { // DefaultCredentials returns the credentials associated with the instance iam role func (c *ec2MetadataClientImpl) DefaultCredentials() (*RoleCredentials, error) { - securityCredential, err := c.client.GetMetadata(SecurityCrednetialsResource) + securityCredential, err := c.client.GetMetadata(SecurityCredentialsResource) if err != nil { return nil, err } @@ -117,7 +117,7 @@ func (c *ec2MetadataClientImpl) DefaultCredentials() (*RoleCredentials, error) { defaultCredentialName := securityCredentialList[0] - defaultCredentialStr, err := c.client.GetMetadata(SecurityCrednetialsResource + defaultCredentialName) + defaultCredentialStr, err := c.client.GetMetadata(SecurityCredentialsResource + defaultCredentialName) if err != nil { return nil, err } diff --git a/agent/ec2/ec2_metadata_client_test.go b/agent/ec2/ec2_metadata_client_test.go index ed87ad10064..4d0db7a6db8 100644 --- a/agent/ec2/ec2_metadata_client_test.go +++ b/agent/ec2/ec2_metadata_client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -97,8 +98,8 @@ func TestDefaultCredentials(t *testing.T) { mockGetter := mock_ec2.NewMockHttpClient(ctrl) testClient := ec2.NewEC2MetadataClient(mockGetter) - mockGetter.EXPECT().GetMetadata(ec2.SecurityCrednetialsResource).Return(testRoleName, nil) - mockGetter.EXPECT().GetMetadata(ec2.SecurityCrednetialsResource+testRoleName).Return( + mockGetter.EXPECT().GetMetadata(ec2.SecurityCredentialsResource).Return(testRoleName, nil) + mockGetter.EXPECT().GetMetadata(ec2.SecurityCredentialsResource+testRoleName).Return( string(ignoreError(json.Marshal(makeTestRoleCredentials())).([]byte)), nil) credentials, err := testClient.DefaultCredentials() diff --git a/agent/ecr/client_test.go b/agent/ecr/client_test.go index d56d833236d..17e729b6cac 100644 --- a/agent/ecr/client_test.go +++ b/agent/ecr/client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecr/factory.go b/agent/ecr/factory.go index 08975399aa5..3ddf50cb7cb 100644 --- a/agent/ecr/factory.go +++ b/agent/ecr/factory.go @@ -75,7 +75,7 @@ func getClientConfig(httpClient *http.Client, authData *apicontainer.ECRAuthData authData.GetPullCredentials().SessionToken) cfg = cfg.WithCredentials(creds) } else { - cfg = cfg.WithCredentials(instancecreds.GetCredentials()) + cfg = cfg.WithCredentials(instancecreds.GetCredentials(false)) } return cfg, nil diff --git a/agent/ecr/factory_test.go b/agent/ecr/factory_test.go index 5eac709a5f6..324ec90e8df 100644 --- a/agent/ecr/factory_test.go +++ b/agent/ecr/factory_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecr/model/ecr/api.go b/agent/ecr/model/ecr/api.go index 9a03b27f87e..6c6fa60d572 100644 --- a/agent/ecr/model/ecr/api.go +++ b/agent/ecr/model/ecr/api.go @@ -41,14 +41,13 @@ const opGetAuthorizationToken = "GetAuthorizationToken" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the GetAuthorizationTokenRequest method. +// req, resp := client.GetAuthorizationTokenRequest(params) // -// // Example sending a request using the GetAuthorizationTokenRequest method. -// req, resp := client.GetAuthorizationTokenRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECR) GetAuthorizationTokenRequest(input *GetAuthorizationTokenInput) (req *request.Request, output *GetAuthorizationTokenOutput) { op := &request.Operation{ Name: opGetAuthorizationToken, @@ -75,10 +74,10 @@ func (c *ECR) GetAuthorizationTokenRequest(input *GetAuthorizationTokenInput) (r // API operation GetAuthorizationToken for usage and error information. // // Returned Error Types: -// * ServerException // -// * InvalidParameterException +// - ServerException // +// - InvalidParameterException func (c *ECR) GetAuthorizationToken(input *GetAuthorizationTokenInput) (*GetAuthorizationTokenOutput, error) { req, out := c.GetAuthorizationTokenRequest(input) return out, req.Send() diff --git a/agent/ecr/model/ecr/service.go b/agent/ecr/model/ecr/service.go index c58f433ab7f..e9477a64107 100644 --- a/agent/ecr/model/ecr/service.go +++ b/agent/ecr/model/ecr/service.go @@ -53,13 +53,14 @@ const ( // aws.Config parameter to add your extra config. // // Example: -// mySession := session.Must(session.NewSession()) // -// // Create a ECR client from just a session. -// svc := ecr.New(mySession) +// mySession := session.Must(session.NewSession()) // -// // Create a ECR client with additional configuration -// svc := ecr.New(mySession, aws.NewConfig().WithRegion("us-west-2")) +// // Create a ECR client from just a session. +// svc := ecr.New(mySession) +// +// // Create a ECR client with additional configuration +// svc := ecr.New(mySession, aws.NewConfig().WithRegion("us-west-2")) func New(p client.ConfigProvider, cfgs ...*aws.Config) *ECR { c := p.ClientConfig(EndpointsID, cfgs...) if c.SigningNameDerived || len(c.SigningName) == 0 { diff --git a/agent/ecs_client/model/api/api-2.json b/agent/ecs_client/model/api/api-2.json index 6bd3502e205..f1e062d7d0b 100644 --- a/agent/ecs_client/model/api/api-2.json +++ b/agent/ecs_client/model/api/api-2.json @@ -223,6 +223,24 @@ {"shape":"ClientException"} ] }, + "GetTaskProtection":{ + "name":"GetTaskProtection", + "http":{ + "method":"POST", + "requestUri":"/" + }, + "input":{"shape":"GetTaskProtectionRequest"}, + "output":{"shape":"GetTaskProtectionResponse"}, + "errors":[ + {"shape":"AccessDeniedException"}, + {"shape":"ClientException"}, + {"shape":"ClusterNotFoundException"}, + {"shape":"InvalidParameterException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ServerException"}, + {"shape":"UnsupportedFeatureException"} + ] + }, "ListAttributes":{ "name":"ListAttributes", "http":{ @@ -541,6 +559,24 @@ {"shape":"PlatformTaskDefinitionIncompatibilityException"}, {"shape":"AccessDeniedException"} ] + }, + "UpdateTaskProtection":{ + "name":"UpdateTaskProtection", + "http":{ + "method":"POST", + "requestUri":"/" + }, + "input":{"shape":"UpdateTaskProtectionRequest"}, + "output":{"shape":"UpdateTaskProtectionResponse"}, + "errors":[ + {"shape":"AccessDeniedException"}, + {"shape":"ClientException"}, + {"shape":"ClusterNotFoundException"}, + {"shape":"InvalidParameterException"}, + {"shape":"ResourceNotFoundException"}, + {"shape":"ServerException"}, + {"shape":"UnsupportedFeatureException"} + ] } }, "shapes":{ @@ -1148,6 +1184,7 @@ "type":"structure", "members":{ "endpoint":{"shape":"String"}, + "serviceConnectEndpoint":{"shape": "String"}, "telemetryEndpoint":{"shape":"String"} } }, @@ -1230,13 +1267,29 @@ "type":"structure", "members":{ "arn":{"shape":"String"}, - "reason":{"shape":"String"} + "reason":{"shape":"String"}, + "detail":{"shape":"String"} } }, "Failures":{ "type":"list", "member":{"shape":"Failure"} }, + "GetTaskProtectionRequest":{ + "type":"structure", + "required":["cluster"], + "members":{ + "cluster":{"shape":"String"}, + "tasks":{"shape":"StringList"} + } + }, + "GetTaskProtectionResponse":{ + "type":"structure", + "members":{ + "protectedTasks":{"shape":"ProtectedTasks"}, + "failures":{"shape":"Failures"} + } + }, "HealthCheck":{ "type":"structure", "required":["command"], @@ -1592,7 +1645,9 @@ "members":{ "bindIP":{"shape":"String"}, "containerPort":{"shape":"BoxedInteger"}, + "containerPortRange":{"shape":"String"}, "hostPort":{"shape":"BoxedInteger"}, + "hostPortRange":{"shape":"String"}, "protocol":{"shape":"TransportProtocol"} } }, @@ -1708,6 +1763,7 @@ "type":"structure", "members":{ "containerPort":{"shape":"BoxedInteger"}, + "containerPortRange":{"shape":"String"}, "hostPort":{"shape":"BoxedInteger"}, "protocol":{"shape":"TransportProtocol"} } @@ -1716,6 +1772,18 @@ "type":"list", "member":{"shape":"PortMapping"} }, + "ProtectedTask":{ + "type":"structure", + "members":{ + "taskArn":{"shape":"String"}, + "protectionEnabled":{"shape":"Boolean"}, + "expirationDate":{"shape":"Timestamp"} + } + }, + "ProtectedTasks":{ + "type":"list", + "member":{"shape":"ProtectedTask"} + }, "ProxyConfiguration":{ "type":"structure", "required":["containerName"], @@ -2402,6 +2470,27 @@ "service":{"shape":"Service"} } }, + "UpdateTaskProtectionRequest":{ + "type":"structure", + "required":[ + "cluster", + "tasks", + "protectionEnabled" + ], + "members":{ + "cluster":{"shape":"String"}, + "tasks":{"shape":"StringList"}, + "protectionEnabled":{"shape":"Boolean"}, + "expiresInMinutes":{"shape":"BoxedInteger"} + } + }, + "UpdateTaskProtectionResponse":{ + "type":"structure", + "members":{ + "protectedTasks":{"shape":"ProtectedTasks"}, + "failures":{"shape":"Failures"} + } + }, "VersionInfo":{ "type":"structure", "members":{ diff --git a/agent/ecs_client/model/api/docs-2.json b/agent/ecs_client/model/api/docs-2.json index 45574139415..175bb52e794 100644 --- a/agent/ecs_client/model/api/docs-2.json +++ b/agent/ecs_client/model/api/docs-2.json @@ -1141,6 +1141,7 @@ "DiscoverPollEndpointRequest$containerInstance": "

The container instance ID or full ARN of the container instance. The ARN contains the arn:aws:ecs namespace, followed by the Region of the container instance, the AWS account ID of the container instance owner, the container-instance namespace, and then the container instance ID. For example, arn:aws:ecs:region:aws_account_id:container-instance/container_instance_ID .

", "DiscoverPollEndpointRequest$cluster": "

The short name or full Amazon Resource Name (ARN) of the cluster that the container instance belongs to.

", "DiscoverPollEndpointResponse$endpoint": "

The endpoint for the Amazon ECS agent to poll.

", + "DiscoverPollEndpointResponse$serviceConnectEndpoint": "

The endpoint for the ServiceConnect Relay to connect to.

", "DiscoverPollEndpointResponse$telemetryEndpoint": "

The telemetry endpoint for the Amazon ECS agent.

", "DockerLabelsMap$key": null, "DockerLabelsMap$value": null, diff --git a/agent/ecs_client/model/ecs/api.go b/agent/ecs_client/model/ecs/api.go index bd9ff756a23..f4314d94fa9 100644 --- a/agent/ecs_client/model/ecs/api.go +++ b/agent/ecs_client/model/ecs/api.go @@ -41,14 +41,13 @@ const opCreateCluster = "CreateCluster" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the CreateClusterRequest method. +// req, resp := client.CreateClusterRequest(params) // -// // Example sending a request using the CreateClusterRequest method. -// req, resp := client.CreateClusterRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) CreateClusterRequest(input *CreateClusterInput) (req *request.Request, output *CreateClusterOutput) { op := &request.Operation{ Name: opCreateCluster, @@ -87,18 +86,18 @@ func (c *ECS) CreateClusterRequest(input *CreateClusterInput) (req *request.Requ // API operation CreateCluster for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) CreateCluster(input *CreateClusterInput) (*CreateClusterOutput, error) { req, out := c.CreateClusterRequest(input) return out, req.Send() @@ -136,14 +135,13 @@ const opCreateService = "CreateService" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the CreateServiceRequest method. +// req, resp := client.CreateServiceRequest(params) // -// // Example sending a request using the CreateServiceRequest method. -// req, resp := client.CreateServiceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) CreateServiceRequest(input *CreateServiceInput) (req *request.Request, output *CreateServiceOutput) { op := &request.Operation{ Name: opCreateService, @@ -208,20 +206,20 @@ func (c *ECS) CreateServiceRequest(input *CreateServiceInput) (req *request.Requ // When the service scheduler launches new tasks, it determines task placement // in your cluster using the following logic: // -// * Determine which of the container instances in your cluster can support -// your service's task definition (for example, they have the required CPU, -// memory, ports, and container instance attributes). -// -// * By default, the service scheduler attempts to balance tasks across Availability -// Zones in this manner (although you can choose a different placement strategy) -// with the placementStrategy parameter): Sort the valid container instances, -// giving priority to instances that have the fewest number of running tasks -// for this service in their respective Availability Zone. For example, if -// zone A has one running service task and zones B and C each have zero, -// valid container instances in either zone B or C are considered optimal -// for placement. Place the new service task on a valid container instance -// in an optimal Availability Zone (based on the previous steps), favoring -// container instances with the fewest number of running tasks for this service. +// - Determine which of the container instances in your cluster can support +// your service's task definition (for example, they have the required CPU, +// memory, ports, and container instance attributes). +// +// - By default, the service scheduler attempts to balance tasks across Availability +// Zones in this manner (although you can choose a different placement strategy) +// with the placementStrategy parameter): Sort the valid container instances, +// giving priority to instances that have the fewest number of running tasks +// for this service in their respective Availability Zone. For example, if +// zone A has one running service task and zones B and C each have zero, +// valid container instances in either zone B or C are considered optimal +// for placement. Place the new service task on a valid container instance +// in an optimal Availability Zone (based on the previous steps), favoring +// container instances with the fewest number of running tasks for this service. // // Returns awserr.Error for service API and SDK errors. Use runtime type assertions // with awserr.Error's Code and Message methods to get detailed information about @@ -231,35 +229,35 @@ func (c *ECS) CreateServiceRequest(input *CreateServiceInput) (req *request.Requ // API operation CreateService for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * UnsupportedFeatureException -// The specified task is not supported in this region. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * PlatformUnknownException -// The specified platform version does not exist. +// - UnsupportedFeatureException +// The specified task is not supported in this region. // -// * PlatformTaskDefinitionIncompatibilityException -// The specified platform version does not satisfy the task definition's required -// capabilities. +// - PlatformUnknownException +// The specified platform version does not exist. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - PlatformTaskDefinitionIncompatibilityException +// The specified platform version does not satisfy the task definition's required +// capabilities. // +// - AccessDeniedException +// You do not have authorization to perform the requested action. func (c *ECS) CreateService(input *CreateServiceInput) (*CreateServiceOutput, error) { req, out := c.CreateServiceRequest(input) return out, req.Send() @@ -297,14 +295,13 @@ const opDeleteAccountSetting = "DeleteAccountSetting" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeleteAccountSettingRequest method. +// req, resp := client.DeleteAccountSettingRequest(params) // -// // Example sending a request using the DeleteAccountSettingRequest method. -// req, resp := client.DeleteAccountSettingRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeleteAccountSettingRequest(input *DeleteAccountSettingInput) (req *request.Request, output *DeleteAccountSettingOutput) { op := &request.Operation{ Name: opDeleteAccountSetting, @@ -331,18 +328,18 @@ func (c *ECS) DeleteAccountSettingRequest(input *DeleteAccountSettingInput) (req // API operation DeleteAccountSetting for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) DeleteAccountSetting(input *DeleteAccountSettingInput) (*DeleteAccountSettingOutput, error) { req, out := c.DeleteAccountSettingRequest(input) return out, req.Send() @@ -380,14 +377,13 @@ const opDeleteAttributes = "DeleteAttributes" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeleteAttributesRequest method. +// req, resp := client.DeleteAttributesRequest(params) // -// // Example sending a request using the DeleteAttributesRequest method. -// req, resp := client.DeleteAttributesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeleteAttributesRequest(input *DeleteAttributesInput) (req *request.Request, output *DeleteAttributesOutput) { op := &request.Operation{ Name: opDeleteAttributes, @@ -416,19 +412,19 @@ func (c *ECS) DeleteAttributesRequest(input *DeleteAttributesInput) (req *reques // API operation DeleteAttributes for usage and error information. // // Returned Error Types: -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. // -// * TargetNotFoundException -// The specified target could not be found. You can view your available container -// instances with ListContainerInstances. Amazon ECS container instances are -// cluster-specific and region-specific. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - TargetNotFoundException +// The specified target could not be found. You can view your available container +// instances with ListContainerInstances. Amazon ECS container instances are +// cluster-specific and region-specific. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) DeleteAttributes(input *DeleteAttributesInput) (*DeleteAttributesOutput, error) { req, out := c.DeleteAttributesRequest(input) return out, req.Send() @@ -466,14 +462,13 @@ const opDeleteCluster = "DeleteCluster" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeleteClusterRequest method. +// req, resp := client.DeleteClusterRequest(params) // -// // Example sending a request using the DeleteClusterRequest method. -// req, resp := client.DeleteClusterRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeleteClusterRequest(input *DeleteClusterInput) (req *request.Request, output *DeleteClusterOutput) { op := &request.Operation{ Name: opDeleteCluster, @@ -504,35 +499,35 @@ func (c *ECS) DeleteClusterRequest(input *DeleteClusterInput) (req *request.Requ // API operation DeleteCluster for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * ClusterContainsContainerInstancesException -// You cannot delete a cluster that has registered container instances. You -// must first deregister the container instances before you can delete the cluster. -// For more information, see DeregisterContainerInstance. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * ClusterContainsServicesException -// You cannot delete a cluster that contains services. You must first update -// the service to reduce its desired task count to 0 and then delete the service. -// For more information, see UpdateService and DeleteService. +// - ClusterContainsContainerInstancesException +// You cannot delete a cluster that has registered container instances. You +// must first deregister the container instances before you can delete the cluster. +// For more information, see DeregisterContainerInstance. // -// * ClusterContainsTasksException -// You cannot delete a cluster that has active tasks. +// - ClusterContainsServicesException +// You cannot delete a cluster that contains services. You must first update +// the service to reduce its desired task count to 0 and then delete the service. +// For more information, see UpdateService and DeleteService. // +// - ClusterContainsTasksException +// You cannot delete a cluster that has active tasks. func (c *ECS) DeleteCluster(input *DeleteClusterInput) (*DeleteClusterOutput, error) { req, out := c.DeleteClusterRequest(input) return out, req.Send() @@ -570,14 +565,13 @@ const opDeleteService = "DeleteService" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeleteServiceRequest method. +// req, resp := client.DeleteServiceRequest(params) // -// // Example sending a request using the DeleteServiceRequest method. -// req, resp := client.DeleteServiceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeleteServiceRequest(input *DeleteServiceInput) (req *request.Request, output *DeleteServiceOutput) { op := &request.Operation{ Name: opDeleteService, @@ -619,26 +613,26 @@ func (c *ECS) DeleteServiceRequest(input *DeleteServiceInput) (req *request.Requ // API operation DeleteService for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * ServiceNotFoundException -// The specified service could not be found. You can view your available services -// with ListServices. Amazon ECS services are cluster-specific and region-specific. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // +// - ServiceNotFoundException +// The specified service could not be found. You can view your available services +// with ListServices. Amazon ECS services are cluster-specific and region-specific. func (c *ECS) DeleteService(input *DeleteServiceInput) (*DeleteServiceOutput, error) { req, out := c.DeleteServiceRequest(input) return out, req.Send() @@ -676,14 +670,13 @@ const opDeregisterContainerInstance = "DeregisterContainerInstance" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeregisterContainerInstanceRequest method. +// req, resp := client.DeregisterContainerInstanceRequest(params) // -// // Example sending a request using the DeregisterContainerInstanceRequest method. -// req, resp := client.DeregisterContainerInstanceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeregisterContainerInstanceRequest(input *DeregisterContainerInstanceInput) (req *request.Request, output *DeregisterContainerInstanceOutput) { op := &request.Operation{ Name: opDeregisterContainerInstance, @@ -726,22 +719,22 @@ func (c *ECS) DeregisterContainerInstanceRequest(input *DeregisterContainerInsta // API operation DeregisterContainerInstance for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) DeregisterContainerInstance(input *DeregisterContainerInstanceInput) (*DeregisterContainerInstanceOutput, error) { req, out := c.DeregisterContainerInstanceRequest(input) return out, req.Send() @@ -779,14 +772,13 @@ const opDeregisterTaskDefinition = "DeregisterTaskDefinition" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DeregisterTaskDefinitionRequest method. +// req, resp := client.DeregisterTaskDefinitionRequest(params) // -// // Example sending a request using the DeregisterTaskDefinitionRequest method. -// req, resp := client.DeregisterTaskDefinitionRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DeregisterTaskDefinitionRequest(input *DeregisterTaskDefinitionInput) (req *request.Request, output *DeregisterTaskDefinitionOutput) { op := &request.Operation{ Name: opDeregisterTaskDefinition, @@ -829,18 +821,18 @@ func (c *ECS) DeregisterTaskDefinitionRequest(input *DeregisterTaskDefinitionInp // API operation DeregisterTaskDefinition for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) DeregisterTaskDefinition(input *DeregisterTaskDefinitionInput) (*DeregisterTaskDefinitionOutput, error) { req, out := c.DeregisterTaskDefinitionRequest(input) return out, req.Send() @@ -878,14 +870,13 @@ const opDescribeClusters = "DescribeClusters" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DescribeClustersRequest method. +// req, resp := client.DescribeClustersRequest(params) // -// // Example sending a request using the DescribeClustersRequest method. -// req, resp := client.DescribeClustersRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DescribeClustersRequest(input *DescribeClustersInput) (req *request.Request, output *DescribeClustersOutput) { op := &request.Operation{ Name: opDescribeClusters, @@ -914,18 +905,18 @@ func (c *ECS) DescribeClustersRequest(input *DescribeClustersInput) (req *reques // API operation DescribeClusters for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) DescribeClusters(input *DescribeClustersInput) (*DescribeClustersOutput, error) { req, out := c.DescribeClustersRequest(input) return out, req.Send() @@ -963,14 +954,13 @@ const opDescribeContainerInstances = "DescribeContainerInstances" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DescribeContainerInstancesRequest method. +// req, resp := client.DescribeContainerInstancesRequest(params) // -// // Example sending a request using the DescribeContainerInstancesRequest method. -// req, resp := client.DescribeContainerInstancesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DescribeContainerInstancesRequest(input *DescribeContainerInstancesInput) (req *request.Request, output *DescribeContainerInstancesOutput) { op := &request.Operation{ Name: opDescribeContainerInstances, @@ -1000,22 +990,22 @@ func (c *ECS) DescribeContainerInstancesRequest(input *DescribeContainerInstance // API operation DescribeContainerInstances for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) DescribeContainerInstances(input *DescribeContainerInstancesInput) (*DescribeContainerInstancesOutput, error) { req, out := c.DescribeContainerInstancesRequest(input) return out, req.Send() @@ -1053,14 +1043,13 @@ const opDescribeServices = "DescribeServices" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DescribeServicesRequest method. +// req, resp := client.DescribeServicesRequest(params) // -// // Example sending a request using the DescribeServicesRequest method. -// req, resp := client.DescribeServicesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DescribeServicesRequest(input *DescribeServicesInput) (req *request.Request, output *DescribeServicesOutput) { op := &request.Operation{ Name: opDescribeServices, @@ -1089,22 +1078,22 @@ func (c *ECS) DescribeServicesRequest(input *DescribeServicesInput) (req *reques // API operation DescribeServices for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) DescribeServices(input *DescribeServicesInput) (*DescribeServicesOutput, error) { req, out := c.DescribeServicesRequest(input) return out, req.Send() @@ -1142,14 +1131,13 @@ const opDescribeTaskDefinition = "DescribeTaskDefinition" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DescribeTaskDefinitionRequest method. +// req, resp := client.DescribeTaskDefinitionRequest(params) // -// // Example sending a request using the DescribeTaskDefinitionRequest method. -// req, resp := client.DescribeTaskDefinitionRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DescribeTaskDefinitionRequest(input *DescribeTaskDefinitionInput) (req *request.Request, output *DescribeTaskDefinitionOutput) { op := &request.Operation{ Name: opDescribeTaskDefinition, @@ -1183,18 +1171,18 @@ func (c *ECS) DescribeTaskDefinitionRequest(input *DescribeTaskDefinitionInput) // API operation DescribeTaskDefinition for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) DescribeTaskDefinition(input *DescribeTaskDefinitionInput) (*DescribeTaskDefinitionOutput, error) { req, out := c.DescribeTaskDefinitionRequest(input) return out, req.Send() @@ -1232,14 +1220,13 @@ const opDescribeTasks = "DescribeTasks" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DescribeTasksRequest method. +// req, resp := client.DescribeTasksRequest(params) // -// // Example sending a request using the DescribeTasksRequest method. -// req, resp := client.DescribeTasksRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DescribeTasksRequest(input *DescribeTasksInput) (req *request.Request, output *DescribeTasksOutput) { op := &request.Operation{ Name: opDescribeTasks, @@ -1268,22 +1255,22 @@ func (c *ECS) DescribeTasksRequest(input *DescribeTasksInput) (req *request.Requ // API operation DescribeTasks for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) DescribeTasks(input *DescribeTasksInput) (*DescribeTasksOutput, error) { req, out := c.DescribeTasksRequest(input) return out, req.Send() @@ -1321,14 +1308,13 @@ const opDiscoverPollEndpoint = "DiscoverPollEndpoint" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the DiscoverPollEndpointRequest method. +// req, resp := client.DiscoverPollEndpointRequest(params) // -// // Example sending a request using the DiscoverPollEndpointRequest method. -// req, resp := client.DiscoverPollEndpointRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) DiscoverPollEndpointRequest(input *DiscoverPollEndpointInput) (req *request.Request, output *DiscoverPollEndpointOutput) { op := &request.Operation{ Name: opDiscoverPollEndpoint, @@ -1347,7 +1333,6 @@ func (c *ECS) DiscoverPollEndpointRequest(input *DiscoverPollEndpointInput) (req // DiscoverPollEndpoint API operation for Amazon Elastic Container Service. // -// // This action is only used by the Amazon ECS agent, and it is not intended // for use outside of the agent. // @@ -1361,14 +1346,14 @@ func (c *ECS) DiscoverPollEndpointRequest(input *DiscoverPollEndpointInput) (req // API operation DiscoverPollEndpoint for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. func (c *ECS) DiscoverPollEndpoint(input *DiscoverPollEndpointInput) (*DiscoverPollEndpointOutput, error) { req, out := c.DiscoverPollEndpointRequest(input) return out, req.Send() @@ -1390,6 +1375,100 @@ func (c *ECS) DiscoverPollEndpointWithContext(ctx aws.Context, input *DiscoverPo return out, req.Send() } +const opGetTaskProtection = "GetTaskProtection" + +// GetTaskProtectionRequest generates a "aws/request.Request" representing the +// client's request for the GetTaskProtection operation. The "output" return +// value will be populated with the request's response once the request completes +// successfully. +// +// Use "Send" method on the returned Request to send the API call to the service. +// the "output" return value is not valid until after Send returns without error. +// +// See GetTaskProtection for more information on using the GetTaskProtection +// API call, and error handling. +// +// This method is useful when you want to inject custom logic or configuration +// into the SDK's request lifecycle. Such as custom headers, or retry logic. +// +// // Example sending a request using the GetTaskProtectionRequest method. +// req, resp := client.GetTaskProtectionRequest(params) +// +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } +func (c *ECS) GetTaskProtectionRequest(input *GetTaskProtectionInput) (req *request.Request, output *GetTaskProtectionOutput) { + op := &request.Operation{ + Name: opGetTaskProtection, + HTTPMethod: "POST", + HTTPPath: "/", + } + + if input == nil { + input = &GetTaskProtectionInput{} + } + + output = &GetTaskProtectionOutput{} + req = c.newRequest(op, input, output) + return +} + +// GetTaskProtection API operation for Amazon Elastic Container Service. +// +// Returns awserr.Error for service API and SDK errors. Use runtime type assertions +// with awserr.Error's Code and Message methods to get detailed information about +// the error. +// +// See the AWS API reference guide for Amazon Elastic Container Service's +// API operation GetTaskProtection for usage and error information. +// +// Returned Error Types: +// +// - AccessDeniedException +// You do not have authorization to perform the requested action. +// +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. +// +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. +// +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. +// +// - ResourceNotFoundException +// +// - ServerException +// These errors are usually caused by a server issue. +// +// - UnsupportedFeatureException +// The specified task is not supported in this region. +func (c *ECS) GetTaskProtection(input *GetTaskProtectionInput) (*GetTaskProtectionOutput, error) { + req, out := c.GetTaskProtectionRequest(input) + return out, req.Send() +} + +// GetTaskProtectionWithContext is the same as GetTaskProtection with the addition of +// the ability to pass a context and additional request options. +// +// See GetTaskProtection for details on how to use this API operation. +// +// The context must be non-nil and will be used for request cancellation. If +// the context is nil a panic will occur. In the future the SDK may create +// sub-contexts for http.Requests. See https://golang.org/pkg/context/ +// for more information on using Contexts. +func (c *ECS) GetTaskProtectionWithContext(ctx aws.Context, input *GetTaskProtectionInput, opts ...request.Option) (*GetTaskProtectionOutput, error) { + req, out := c.GetTaskProtectionRequest(input) + req.SetContext(ctx) + req.ApplyOptions(opts...) + return out, req.Send() +} + const opListAttributes = "ListAttributes" // ListAttributesRequest generates a "aws/request.Request" representing the @@ -1406,14 +1485,13 @@ const opListAttributes = "ListAttributes" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListAttributesRequest method. +// req, resp := client.ListAttributesRequest(params) // -// // Example sending a request using the ListAttributesRequest method. -// req, resp := client.ListAttributesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListAttributesRequest(input *ListAttributesInput) (req *request.Request, output *ListAttributesOutput) { op := &request.Operation{ Name: opListAttributes, @@ -1448,14 +1526,14 @@ func (c *ECS) ListAttributesRequest(input *ListAttributesInput) (req *request.Re // API operation ListAttributes for usage and error information. // // Returned Error Types: -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) ListAttributes(input *ListAttributesInput) (*ListAttributesOutput, error) { req, out := c.ListAttributesRequest(input) return out, req.Send() @@ -1493,14 +1571,13 @@ const opListClusters = "ListClusters" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListClustersRequest method. +// req, resp := client.ListClustersRequest(params) // -// // Example sending a request using the ListClustersRequest method. -// req, resp := client.ListClustersRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListClustersRequest(input *ListClustersInput) (req *request.Request, output *ListClustersOutput) { op := &request.Operation{ Name: opListClusters, @@ -1535,18 +1612,18 @@ func (c *ECS) ListClustersRequest(input *ListClustersInput) (req *request.Reques // API operation ListClusters for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) ListClusters(input *ListClustersInput) (*ListClustersOutput, error) { req, out := c.ListClustersRequest(input) return out, req.Send() @@ -1576,15 +1653,14 @@ func (c *ECS) ListClustersWithContext(ctx aws.Context, input *ListClustersInput, // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListClusters operation. -// pageNum := 0 -// err := client.ListClustersPages(params, -// func(page *ecs.ListClustersOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListClusters operation. +// pageNum := 0 +// err := client.ListClustersPages(params, +// func(page *ecs.ListClustersOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListClustersPages(input *ListClustersInput, fn func(*ListClustersOutput, bool) bool) error { return c.ListClustersPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -1636,14 +1712,13 @@ const opListContainerInstances = "ListContainerInstances" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListContainerInstancesRequest method. +// req, resp := client.ListContainerInstancesRequest(params) // -// // Example sending a request using the ListContainerInstancesRequest method. -// req, resp := client.ListContainerInstancesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListContainerInstancesRequest(input *ListContainerInstancesInput) (req *request.Request, output *ListContainerInstancesOutput) { op := &request.Operation{ Name: opListContainerInstances, @@ -1682,22 +1757,22 @@ func (c *ECS) ListContainerInstancesRequest(input *ListContainerInstancesInput) // API operation ListContainerInstances for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) ListContainerInstances(input *ListContainerInstancesInput) (*ListContainerInstancesOutput, error) { req, out := c.ListContainerInstancesRequest(input) return out, req.Send() @@ -1727,15 +1802,14 @@ func (c *ECS) ListContainerInstancesWithContext(ctx aws.Context, input *ListCont // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListContainerInstances operation. -// pageNum := 0 -// err := client.ListContainerInstancesPages(params, -// func(page *ecs.ListContainerInstancesOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListContainerInstances operation. +// pageNum := 0 +// err := client.ListContainerInstancesPages(params, +// func(page *ecs.ListContainerInstancesOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListContainerInstancesPages(input *ListContainerInstancesInput, fn func(*ListContainerInstancesOutput, bool) bool) error { return c.ListContainerInstancesPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -1787,14 +1861,13 @@ const opListServices = "ListServices" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListServicesRequest method. +// req, resp := client.ListServicesRequest(params) // -// // Example sending a request using the ListServicesRequest method. -// req, resp := client.ListServicesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListServicesRequest(input *ListServicesInput) (req *request.Request, output *ListServicesOutput) { op := &request.Operation{ Name: opListServices, @@ -1829,22 +1902,22 @@ func (c *ECS) ListServicesRequest(input *ListServicesInput) (req *request.Reques // API operation ListServices for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) ListServices(input *ListServicesInput) (*ListServicesOutput, error) { req, out := c.ListServicesRequest(input) return out, req.Send() @@ -1874,15 +1947,14 @@ func (c *ECS) ListServicesWithContext(ctx aws.Context, input *ListServicesInput, // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListServices operation. -// pageNum := 0 -// err := client.ListServicesPages(params, -// func(page *ecs.ListServicesOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListServices operation. +// pageNum := 0 +// err := client.ListServicesPages(params, +// func(page *ecs.ListServicesOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListServicesPages(input *ListServicesInput, fn func(*ListServicesOutput, bool) bool) error { return c.ListServicesPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -1934,14 +2006,13 @@ const opListTagsForResource = "ListTagsForResource" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListTagsForResourceRequest method. +// req, resp := client.ListTagsForResourceRequest(params) // -// // Example sending a request using the ListTagsForResourceRequest method. -// req, resp := client.ListTagsForResourceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListTagsForResourceRequest(input *ListTagsForResourceInput) (req *request.Request, output *ListTagsForResourceOutput) { op := &request.Operation{ Name: opListTagsForResource, @@ -1968,22 +2039,22 @@ func (c *ECS) ListTagsForResourceRequest(input *ListTagsForResourceInput) (req * // API operation ListTagsForResource for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) ListTagsForResource(input *ListTagsForResourceInput) (*ListTagsForResourceOutput, error) { req, out := c.ListTagsForResourceRequest(input) return out, req.Send() @@ -2021,14 +2092,13 @@ const opListTaskDefinitionFamilies = "ListTaskDefinitionFamilies" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListTaskDefinitionFamiliesRequest method. +// req, resp := client.ListTaskDefinitionFamiliesRequest(params) // -// // Example sending a request using the ListTaskDefinitionFamiliesRequest method. -// req, resp := client.ListTaskDefinitionFamiliesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListTaskDefinitionFamiliesRequest(input *ListTaskDefinitionFamiliesInput) (req *request.Request, output *ListTaskDefinitionFamiliesOutput) { op := &request.Operation{ Name: opListTaskDefinitionFamilies, @@ -2069,18 +2139,18 @@ func (c *ECS) ListTaskDefinitionFamiliesRequest(input *ListTaskDefinitionFamilie // API operation ListTaskDefinitionFamilies for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) ListTaskDefinitionFamilies(input *ListTaskDefinitionFamiliesInput) (*ListTaskDefinitionFamiliesOutput, error) { req, out := c.ListTaskDefinitionFamiliesRequest(input) return out, req.Send() @@ -2110,15 +2180,14 @@ func (c *ECS) ListTaskDefinitionFamiliesWithContext(ctx aws.Context, input *List // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListTaskDefinitionFamilies operation. -// pageNum := 0 -// err := client.ListTaskDefinitionFamiliesPages(params, -// func(page *ecs.ListTaskDefinitionFamiliesOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListTaskDefinitionFamilies operation. +// pageNum := 0 +// err := client.ListTaskDefinitionFamiliesPages(params, +// func(page *ecs.ListTaskDefinitionFamiliesOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListTaskDefinitionFamiliesPages(input *ListTaskDefinitionFamiliesInput, fn func(*ListTaskDefinitionFamiliesOutput, bool) bool) error { return c.ListTaskDefinitionFamiliesPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -2170,14 +2239,13 @@ const opListTaskDefinitions = "ListTaskDefinitions" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListTaskDefinitionsRequest method. +// req, resp := client.ListTaskDefinitionsRequest(params) // -// // Example sending a request using the ListTaskDefinitionsRequest method. -// req, resp := client.ListTaskDefinitionsRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListTaskDefinitionsRequest(input *ListTaskDefinitionsInput) (req *request.Request, output *ListTaskDefinitionsOutput) { op := &request.Operation{ Name: opListTaskDefinitions, @@ -2214,18 +2282,18 @@ func (c *ECS) ListTaskDefinitionsRequest(input *ListTaskDefinitionsInput) (req * // API operation ListTaskDefinitions for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) ListTaskDefinitions(input *ListTaskDefinitionsInput) (*ListTaskDefinitionsOutput, error) { req, out := c.ListTaskDefinitionsRequest(input) return out, req.Send() @@ -2255,15 +2323,14 @@ func (c *ECS) ListTaskDefinitionsWithContext(ctx aws.Context, input *ListTaskDef // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListTaskDefinitions operation. -// pageNum := 0 -// err := client.ListTaskDefinitionsPages(params, -// func(page *ecs.ListTaskDefinitionsOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListTaskDefinitions operation. +// pageNum := 0 +// err := client.ListTaskDefinitionsPages(params, +// func(page *ecs.ListTaskDefinitionsOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListTaskDefinitionsPages(input *ListTaskDefinitionsInput, fn func(*ListTaskDefinitionsOutput, bool) bool) error { return c.ListTaskDefinitionsPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -2315,14 +2382,13 @@ const opListTasks = "ListTasks" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the ListTasksRequest method. +// req, resp := client.ListTasksRequest(params) // -// // Example sending a request using the ListTasksRequest method. -// req, resp := client.ListTasksRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) ListTasksRequest(input *ListTasksInput) (req *request.Request, output *ListTasksOutput) { op := &request.Operation{ Name: opListTasks, @@ -2362,26 +2428,26 @@ func (c *ECS) ListTasksRequest(input *ListTasksInput) (req *request.Request, out // API operation ListTasks for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * ServiceNotFoundException -// The specified service could not be found. You can view your available services -// with ListServices. Amazon ECS services are cluster-specific and region-specific. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // +// - ServiceNotFoundException +// The specified service could not be found. You can view your available services +// with ListServices. Amazon ECS services are cluster-specific and region-specific. func (c *ECS) ListTasks(input *ListTasksInput) (*ListTasksOutput, error) { req, out := c.ListTasksRequest(input) return out, req.Send() @@ -2411,15 +2477,14 @@ func (c *ECS) ListTasksWithContext(ctx aws.Context, input *ListTasksInput, opts // // Note: This operation can generate multiple requests to a service. // -// // Example iterating over at most 3 pages of a ListTasks operation. -// pageNum := 0 -// err := client.ListTasksPages(params, -// func(page *ecs.ListTasksOutput, lastPage bool) bool { -// pageNum++ -// fmt.Println(page) -// return pageNum <= 3 -// }) -// +// // Example iterating over at most 3 pages of a ListTasks operation. +// pageNum := 0 +// err := client.ListTasksPages(params, +// func(page *ecs.ListTasksOutput, lastPage bool) bool { +// pageNum++ +// fmt.Println(page) +// return pageNum <= 3 +// }) func (c *ECS) ListTasksPages(input *ListTasksInput, fn func(*ListTasksOutput, bool) bool) error { return c.ListTasksPagesWithContext(aws.BackgroundContext(), input, fn) } @@ -2471,14 +2536,13 @@ const opPutAccountSetting = "PutAccountSetting" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the PutAccountSettingRequest method. +// req, resp := client.PutAccountSettingRequest(params) // -// // Example sending a request using the PutAccountSettingRequest method. -// req, resp := client.PutAccountSettingRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) PutAccountSettingRequest(input *PutAccountSettingInput) (req *request.Request, output *PutAccountSettingOutput) { op := &request.Operation{ Name: opPutAccountSetting, @@ -2505,18 +2569,18 @@ func (c *ECS) PutAccountSettingRequest(input *PutAccountSettingInput) (req *requ // API operation PutAccountSetting for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) PutAccountSetting(input *PutAccountSettingInput) (*PutAccountSettingOutput, error) { req, out := c.PutAccountSettingRequest(input) return out, req.Send() @@ -2554,14 +2618,13 @@ const opPutAttributes = "PutAttributes" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the PutAttributesRequest method. +// req, resp := client.PutAttributesRequest(params) // -// // Example sending a request using the PutAttributesRequest method. -// req, resp := client.PutAttributesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) PutAttributesRequest(input *PutAttributesInput) (req *request.Request, output *PutAttributesOutput) { op := &request.Operation{ Name: opPutAttributes, @@ -2594,24 +2657,24 @@ func (c *ECS) PutAttributesRequest(input *PutAttributesInput) (req *request.Requ // API operation PutAttributes for usage and error information. // // Returned Error Types: -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. // -// * TargetNotFoundException -// The specified target could not be found. You can view your available container -// instances with ListContainerInstances. Amazon ECS container instances are -// cluster-specific and region-specific. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * AttributeLimitExceededException -// You can apply up to 10 custom attributes per resource. You can view the attributes -// of a resource with ListAttributes. You can remove existing attributes on -// a resource with DeleteAttributes. +// - TargetNotFoundException +// The specified target could not be found. You can view your available container +// instances with ListContainerInstances. Amazon ECS container instances are +// cluster-specific and region-specific. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - AttributeLimitExceededException +// You can apply up to 10 custom attributes per resource. You can view the attributes +// of a resource with ListAttributes. You can remove existing attributes on +// a resource with DeleteAttributes. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) PutAttributes(input *PutAttributesInput) (*PutAttributesOutput, error) { req, out := c.PutAttributesRequest(input) return out, req.Send() @@ -2649,14 +2712,13 @@ const opRegisterContainerInstance = "RegisterContainerInstance" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the RegisterContainerInstanceRequest method. +// req, resp := client.RegisterContainerInstanceRequest(params) // -// // Example sending a request using the RegisterContainerInstanceRequest method. -// req, resp := client.RegisterContainerInstanceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) RegisterContainerInstanceRequest(input *RegisterContainerInstanceInput) (req *request.Request, output *RegisterContainerInstanceOutput) { op := &request.Operation{ Name: opRegisterContainerInstance, @@ -2675,7 +2737,6 @@ func (c *ECS) RegisterContainerInstanceRequest(input *RegisterContainerInstanceI // RegisterContainerInstance API operation for Amazon Elastic Container Service. // -// // This action is only used by the Amazon ECS agent, and it is not intended // for use outside of the agent. // @@ -2690,18 +2751,18 @@ func (c *ECS) RegisterContainerInstanceRequest(input *RegisterContainerInstanceI // API operation RegisterContainerInstance for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) RegisterContainerInstance(input *RegisterContainerInstanceInput) (*RegisterContainerInstanceOutput, error) { req, out := c.RegisterContainerInstanceRequest(input) return out, req.Send() @@ -2739,14 +2800,13 @@ const opRegisterTaskDefinition = "RegisterTaskDefinition" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the RegisterTaskDefinitionRequest method. +// req, resp := client.RegisterTaskDefinitionRequest(params) // -// // Example sending a request using the RegisterTaskDefinitionRequest method. -// req, resp := client.RegisterTaskDefinitionRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) RegisterTaskDefinitionRequest(input *RegisterTaskDefinitionInput) (req *request.Request, output *RegisterTaskDefinitionOutput) { op := &request.Operation{ Name: opRegisterTaskDefinition, @@ -2795,18 +2855,18 @@ func (c *ECS) RegisterTaskDefinitionRequest(input *RegisterTaskDefinitionInput) // API operation RegisterTaskDefinition for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) RegisterTaskDefinition(input *RegisterTaskDefinitionInput) (*RegisterTaskDefinitionOutput, error) { req, out := c.RegisterTaskDefinitionRequest(input) return out, req.Send() @@ -2844,14 +2904,13 @@ const opRunTask = "RunTask" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the RunTaskRequest method. +// req, resp := client.RunTaskRequest(params) // -// // Example sending a request using the RunTaskRequest method. -// req, resp := client.RunTaskRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) RunTaskRequest(input *RunTaskInput) (req *request.Request, output *RunTaskOutput) { op := &request.Operation{ Name: opRunTask, @@ -2889,17 +2948,17 @@ func (c *ECS) RunTaskRequest(input *RunTaskInput) (req *request.Request, output // // To manage eventual consistency, you can do the following: // -// * Confirm the state of the resource before you run a command to modify -// it. Run the DescribeTasks command using an exponential backoff algorithm -// to ensure that you allow enough time for the previous command to propagate -// through the system. To do this, run the DescribeTasks command repeatedly, -// starting with a couple of seconds of wait time and increasing gradually -// up to five minutes of wait time. +// - Confirm the state of the resource before you run a command to modify +// it. Run the DescribeTasks command using an exponential backoff algorithm +// to ensure that you allow enough time for the previous command to propagate +// through the system. To do this, run the DescribeTasks command repeatedly, +// starting with a couple of seconds of wait time and increasing gradually +// up to five minutes of wait time. // -// * Add wait time between subsequent commands, even if the DescribeTasks -// command returns an accurate response. Apply an exponential backoff algorithm -// starting with a couple of seconds of wait time, and increase gradually -// up to about five minutes of wait time. +// - Add wait time between subsequent commands, even if the DescribeTasks +// command returns an accurate response. Apply an exponential backoff algorithm +// starting with a couple of seconds of wait time, and increase gradually +// up to about five minutes of wait time. // // Returns awserr.Error for service API and SDK errors. Use runtime type assertions // with awserr.Error's Code and Message methods to get detailed information about @@ -2909,39 +2968,39 @@ func (c *ECS) RunTaskRequest(input *RunTaskInput) (req *request.Request, output // API operation RunTask for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * UnsupportedFeatureException -// The specified task is not supported in this region. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * PlatformUnknownException -// The specified platform version does not exist. +// - UnsupportedFeatureException +// The specified task is not supported in this region. // -// * PlatformTaskDefinitionIncompatibilityException -// The specified platform version does not satisfy the task definition's required -// capabilities. +// - PlatformUnknownException +// The specified platform version does not exist. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - PlatformTaskDefinitionIncompatibilityException +// The specified platform version does not satisfy the task definition's required +// capabilities. // -// * BlockedException -// Your AWS account has been blocked. Contact AWS Support (http://aws.amazon.com/contact-us/) -// for more information. +// - AccessDeniedException +// You do not have authorization to perform the requested action. // +// - BlockedException +// Your AWS account has been blocked. Contact AWS Support (http://aws.amazon.com/contact-us/) +// for more information. func (c *ECS) RunTask(input *RunTaskInput) (*RunTaskOutput, error) { req, out := c.RunTaskRequest(input) return out, req.Send() @@ -2979,14 +3038,13 @@ const opStartTask = "StartTask" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the StartTaskRequest method. +// req, resp := client.StartTaskRequest(params) // -// // Example sending a request using the StartTaskRequest method. -// req, resp := client.StartTaskRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) StartTaskRequest(input *StartTaskInput) (req *request.Request, output *StartTaskOutput) { op := &request.Operation{ Name: opStartTask, @@ -3020,22 +3078,22 @@ func (c *ECS) StartTaskRequest(input *StartTaskInput) (req *request.Request, out // API operation StartTask for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) StartTask(input *StartTaskInput) (*StartTaskOutput, error) { req, out := c.StartTaskRequest(input) return out, req.Send() @@ -3073,14 +3131,13 @@ const opStopTask = "StopTask" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the StopTaskRequest method. +// req, resp := client.StopTaskRequest(params) // -// // Example sending a request using the StopTaskRequest method. -// req, resp := client.StopTaskRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) StopTaskRequest(input *StopTaskInput) (req *request.Request, output *StopTaskOutput) { op := &request.Operation{ Name: opStopTask, @@ -3120,22 +3177,22 @@ func (c *ECS) StopTaskRequest(input *StopTaskInput) (req *request.Request, outpu // API operation StopTask for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) StopTask(input *StopTaskInput) (*StopTaskOutput, error) { req, out := c.StopTaskRequest(input) return out, req.Send() @@ -3173,14 +3230,13 @@ const opSubmitAttachmentStateChanges = "SubmitAttachmentStateChanges" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the SubmitAttachmentStateChangesRequest method. +// req, resp := client.SubmitAttachmentStateChangesRequest(params) // -// // Example sending a request using the SubmitAttachmentStateChangesRequest method. -// req, resp := client.SubmitAttachmentStateChangesRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) SubmitAttachmentStateChangesRequest(input *SubmitAttachmentStateChangesInput) (req *request.Request, output *SubmitAttachmentStateChangesOutput) { op := &request.Operation{ Name: opSubmitAttachmentStateChanges, @@ -3207,21 +3263,21 @@ func (c *ECS) SubmitAttachmentStateChangesRequest(input *SubmitAttachmentStateCh // API operation SubmitAttachmentStateChanges for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - AccessDeniedException +// You do not have authorization to perform the requested action. // +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. func (c *ECS) SubmitAttachmentStateChanges(input *SubmitAttachmentStateChangesInput) (*SubmitAttachmentStateChangesOutput, error) { req, out := c.SubmitAttachmentStateChangesRequest(input) return out, req.Send() @@ -3259,14 +3315,13 @@ const opSubmitContainerStateChange = "SubmitContainerStateChange" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the SubmitContainerStateChangeRequest method. +// req, resp := client.SubmitContainerStateChangeRequest(params) // -// // Example sending a request using the SubmitContainerStateChangeRequest method. -// req, resp := client.SubmitContainerStateChangeRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) SubmitContainerStateChangeRequest(input *SubmitContainerStateChangeInput) (req *request.Request, output *SubmitContainerStateChangeOutput) { op := &request.Operation{ Name: opSubmitContainerStateChange, @@ -3285,7 +3340,6 @@ func (c *ECS) SubmitContainerStateChangeRequest(input *SubmitContainerStateChang // SubmitContainerStateChange API operation for Amazon Elastic Container Service. // -// // This action is only used by the Amazon ECS agent, and it is not intended // for use outside of the agent. // @@ -3299,17 +3353,17 @@ func (c *ECS) SubmitContainerStateChangeRequest(input *SubmitContainerStateChang // API operation SubmitContainerStateChange for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - AccessDeniedException +// You do not have authorization to perform the requested action. func (c *ECS) SubmitContainerStateChange(input *SubmitContainerStateChangeInput) (*SubmitContainerStateChangeOutput, error) { req, out := c.SubmitContainerStateChangeRequest(input) return out, req.Send() @@ -3347,14 +3401,13 @@ const opSubmitTaskStateChange = "SubmitTaskStateChange" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the SubmitTaskStateChangeRequest method. +// req, resp := client.SubmitTaskStateChangeRequest(params) // -// // Example sending a request using the SubmitTaskStateChangeRequest method. -// req, resp := client.SubmitTaskStateChangeRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) SubmitTaskStateChangeRequest(input *SubmitTaskStateChangeInput) (req *request.Request, output *SubmitTaskStateChangeOutput) { op := &request.Operation{ Name: opSubmitTaskStateChange, @@ -3373,7 +3426,6 @@ func (c *ECS) SubmitTaskStateChangeRequest(input *SubmitTaskStateChangeInput) (r // SubmitTaskStateChange API operation for Amazon Elastic Container Service. // -// // This action is only used by the Amazon ECS agent, and it is not intended // for use outside of the agent. // @@ -3387,17 +3439,17 @@ func (c *ECS) SubmitTaskStateChangeRequest(input *SubmitTaskStateChangeInput) (r // API operation SubmitTaskStateChange for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // +// - AccessDeniedException +// You do not have authorization to perform the requested action. func (c *ECS) SubmitTaskStateChange(input *SubmitTaskStateChangeInput) (*SubmitTaskStateChangeOutput, error) { req, out := c.SubmitTaskStateChangeRequest(input) return out, req.Send() @@ -3435,14 +3487,13 @@ const opUpdateContainerAgent = "UpdateContainerAgent" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the UpdateContainerAgentRequest method. +// req, resp := client.UpdateContainerAgentRequest(params) // -// // Example sending a request using the UpdateContainerAgentRequest method. -// req, resp := client.UpdateContainerAgentRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) UpdateContainerAgentRequest(input *UpdateContainerAgentInput) (req *request.Request, output *UpdateContainerAgentOutput) { op := &request.Operation{ Name: opUpdateContainerAgent, @@ -3481,40 +3532,40 @@ func (c *ECS) UpdateContainerAgentRequest(input *UpdateContainerAgentInput) (req // API operation UpdateContainerAgent for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. -// -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. -// -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. -// -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. -// -// * UpdateInProgressException -// There is already a current Amazon ECS container agent update in progress -// on the specified container instance. If the container agent becomes disconnected -// while it is in a transitional stage, such as PENDING or STAGING, the update -// process can get stuck in that state. However, when the agent reconnects, -// it resumes where it stopped previously. -// -// * NoUpdateAvailableException -// There is no update available for this Amazon ECS container agent. This could -// be because the agent is already running the latest version, or it is so old -// that there is no update path to the current version. -// -// * MissingVersionException -// Amazon ECS is unable to determine the current version of the Amazon ECS container -// agent on the container instance and does not have enough information to proceed -// with an update. This could be because the agent running on the container -// instance is an older or custom version that does not use our version information. // +// - ServerException +// These errors are usually caused by a server issue. +// +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. +// +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. +// +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. +// +// - UpdateInProgressException +// There is already a current Amazon ECS container agent update in progress +// on the specified container instance. If the container agent becomes disconnected +// while it is in a transitional stage, such as PENDING or STAGING, the update +// process can get stuck in that state. However, when the agent reconnects, +// it resumes where it stopped previously. +// +// - NoUpdateAvailableException +// There is no update available for this Amazon ECS container agent. This could +// be because the agent is already running the latest version, or it is so old +// that there is no update path to the current version. +// +// - MissingVersionException +// Amazon ECS is unable to determine the current version of the Amazon ECS container +// agent on the container instance and does not have enough information to proceed +// with an update. This could be because the agent running on the container +// instance is an older or custom version that does not use our version information. func (c *ECS) UpdateContainerAgent(input *UpdateContainerAgentInput) (*UpdateContainerAgentOutput, error) { req, out := c.UpdateContainerAgentRequest(input) return out, req.Send() @@ -3552,14 +3603,13 @@ const opUpdateContainerInstancesState = "UpdateContainerInstancesState" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the UpdateContainerInstancesStateRequest method. +// req, resp := client.UpdateContainerInstancesStateRequest(params) // -// // Example sending a request using the UpdateContainerInstancesStateRequest method. -// req, resp := client.UpdateContainerInstancesStateRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) UpdateContainerInstancesStateRequest(input *UpdateContainerInstancesStateInput) (req *request.Request, output *UpdateContainerInstancesStateOutput) { op := &request.Operation{ Name: opUpdateContainerInstancesState, @@ -3595,24 +3645,24 @@ func (c *ECS) UpdateContainerInstancesStateRequest(input *UpdateContainerInstanc // parameters, minimumHealthyPercent and maximumPercent. You can change the // deployment configuration of your service using UpdateService. // -// * If minimumHealthyPercent is below 100%, the scheduler can ignore desiredCount -// temporarily during task replacement. For example, desiredCount is four -// tasks, a minimum of 50% allows the scheduler to stop two existing tasks -// before starting two new tasks. If the minimum is 100%, the service scheduler -// can't remove existing tasks until the replacement tasks are considered -// healthy. Tasks for services that do not use a load balancer are considered -// healthy if they are in the RUNNING state. Tasks for services that use -// a load balancer are considered healthy if they are in the RUNNING state -// and the container instance they are hosted on is reported as healthy by -// the load balancer. -// -// * The maximumPercent parameter represents an upper limit on the number -// of running tasks during task replacement, which enables you to define -// the replacement batch size. For example, if desiredCount of four tasks, -// a maximum of 200% starts four new tasks before stopping the four tasks -// to be drained (provided that the cluster resources required to do this -// are available). If the maximum is 100%, then replacement tasks can't start -// until the draining tasks have stopped. +// - If minimumHealthyPercent is below 100%, the scheduler can ignore desiredCount +// temporarily during task replacement. For example, desiredCount is four +// tasks, a minimum of 50% allows the scheduler to stop two existing tasks +// before starting two new tasks. If the minimum is 100%, the service scheduler +// can't remove existing tasks until the replacement tasks are considered +// healthy. Tasks for services that do not use a load balancer are considered +// healthy if they are in the RUNNING state. Tasks for services that use +// a load balancer are considered healthy if they are in the RUNNING state +// and the container instance they are hosted on is reported as healthy by +// the load balancer. +// +// - The maximumPercent parameter represents an upper limit on the number +// of running tasks during task replacement, which enables you to define +// the replacement batch size. For example, if desiredCount of four tasks, +// a maximum of 200% starts four new tasks before stopping the four tasks +// to be drained (provided that the cluster resources required to do this +// are available). If the maximum is 100%, then replacement tasks can't start +// until the draining tasks have stopped. // // Any PENDING or RUNNING tasks that do not belong to a service are not affected; // you must wait for them to finish or stop them manually. @@ -3631,22 +3681,22 @@ func (c *ECS) UpdateContainerInstancesStateRequest(input *UpdateContainerInstanc // API operation UpdateContainerInstancesState for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. func (c *ECS) UpdateContainerInstancesState(input *UpdateContainerInstancesStateInput) (*UpdateContainerInstancesStateOutput, error) { req, out := c.UpdateContainerInstancesStateRequest(input) return out, req.Send() @@ -3684,14 +3734,13 @@ const opUpdateService = "UpdateService" // This method is useful when you want to inject custom logic or configuration // into the SDK's request lifecycle. Such as custom headers, or retry logic. // +// // Example sending a request using the UpdateServiceRequest method. +// req, resp := client.UpdateServiceRequest(params) // -// // Example sending a request using the UpdateServiceRequest method. -// req, resp := client.UpdateServiceRequest(params) -// -// err := req.Send() -// if err == nil { // resp is now filled -// fmt.Println(resp) -// } +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } func (c *ECS) UpdateServiceRequest(input *UpdateServiceInput) (req *request.Request, output *UpdateServiceOutput) { op := &request.Operation{ Name: opUpdateService, @@ -3734,20 +3783,20 @@ func (c *ECS) UpdateServiceRequest(input *UpdateServiceInput) (req *request.Requ // uses the deployment configuration parameters, minimumHealthyPercent and maximumPercent, // to determine the deployment strategy. // -// * If minimumHealthyPercent is below 100%, the scheduler can ignore desiredCount -// temporarily during a deployment. For example, if desiredCount is four -// tasks, a minimum of 50% allows the scheduler to stop two existing tasks -// before starting two new tasks. Tasks for services that do not use a load -// balancer are considered healthy if they are in the RUNNING state. Tasks -// for services that use a load balancer are considered healthy if they are -// in the RUNNING state and the container instance they are hosted on is -// reported as healthy by the load balancer. -// -// * The maximumPercent parameter represents an upper limit on the number -// of running tasks during a deployment, which enables you to define the -// deployment batch size. For example, if desiredCount is four tasks, a maximum -// of 200% starts four new tasks before stopping the four older tasks (provided -// that the cluster resources required to do this are available). +// - If minimumHealthyPercent is below 100%, the scheduler can ignore desiredCount +// temporarily during a deployment. For example, if desiredCount is four +// tasks, a minimum of 50% allows the scheduler to stop two existing tasks +// before starting two new tasks. Tasks for services that do not use a load +// balancer are considered healthy if they are in the RUNNING state. Tasks +// for services that use a load balancer are considered healthy if they are +// in the RUNNING state and the container instance they are hosted on is +// reported as healthy by the load balancer. +// +// - The maximumPercent parameter represents an upper limit on the number +// of running tasks during a deployment, which enables you to define the +// deployment batch size. For example, if desiredCount is four tasks, a maximum +// of 200% starts four new tasks before stopping the four older tasks (provided +// that the cluster resources required to do this are available). // // When UpdateService stops a task during a deployment, the equivalent of docker // stop is issued to the containers running in the task. This results in a SIGTERM @@ -3758,31 +3807,31 @@ func (c *ECS) UpdateServiceRequest(input *UpdateServiceInput) (req *request.Requ // When the service scheduler launches new tasks, it determines task placement // in your cluster with the following logic: // -// * Determine which of the container instances in your cluster can support -// your service's task definition (for example, they have the required CPU, -// memory, ports, and container instance attributes). +// - Determine which of the container instances in your cluster can support +// your service's task definition (for example, they have the required CPU, +// memory, ports, and container instance attributes). // -// * By default, the service scheduler attempts to balance tasks across Availability -// Zones in this manner (although you can choose a different placement strategy): -// Sort the valid container instances by the fewest number of running tasks -// for this service in the same Availability Zone as the instance. For example, -// if zone A has one running service task and zones B and C each have zero, -// valid container instances in either zone B or C are considered optimal -// for placement. Place the new service task on a valid container instance -// in an optimal Availability Zone (based on the previous steps), favoring -// container instances with the fewest number of running tasks for this service. +// - By default, the service scheduler attempts to balance tasks across Availability +// Zones in this manner (although you can choose a different placement strategy): +// Sort the valid container instances by the fewest number of running tasks +// for this service in the same Availability Zone as the instance. For example, +// if zone A has one running service task and zones B and C each have zero, +// valid container instances in either zone B or C are considered optimal +// for placement. Place the new service task on a valid container instance +// in an optimal Availability Zone (based on the previous steps), favoring +// container instances with the fewest number of running tasks for this service. // // When the service scheduler stops running tasks, it attempts to maintain balance // across the Availability Zones in your cluster using the following logic: // -// * Sort the container instances by the largest number of running tasks -// for this service in the same Availability Zone as the instance. For example, -// if zone A has one running service task and zones B and C each have two, -// container instances in either zone B or C are considered optimal for termination. +// - Sort the container instances by the largest number of running tasks +// for this service in the same Availability Zone as the instance. For example, +// if zone A has one running service task and zones B and C each have two, +// container instances in either zone B or C are considered optimal for termination. // -// * Stop the task on a container instance in an optimal Availability Zone -// (based on the previous steps), favoring container instances with the largest -// number of running tasks for this service. +// - Stop the task on a container instance in an optimal Availability Zone +// (based on the previous steps), favoring container instances with the largest +// number of running tasks for this service. // // Returns awserr.Error for service API and SDK errors. Use runtime type assertions // with awserr.Error's Code and Message methods to get detailed information about @@ -3792,40 +3841,40 @@ func (c *ECS) UpdateServiceRequest(input *UpdateServiceInput) (req *request.Requ // API operation UpdateService for usage and error information. // // Returned Error Types: -// * ServerException -// These errors are usually caused by a server issue. // -// * ClientException -// These errors are usually caused by a client action, such as using an action -// or resource on behalf of a user that doesn't have permissions to use the -// action or resource, or specifying an identifier that is not valid. +// - ServerException +// These errors are usually caused by a server issue. // -// * InvalidParameterException -// The specified parameter is invalid. Review the available parameters for the -// API request. +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. // -// * ClusterNotFoundException -// The specified cluster could not be found. You can view your available clusters -// with ListClusters. Amazon ECS clusters are region-specific. +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. // -// * ServiceNotFoundException -// The specified service could not be found. You can view your available services -// with ListServices. Amazon ECS services are cluster-specific and region-specific. +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. // -// * ServiceNotActiveException -// The specified service is not active. You can't update a service that is inactive. -// If you have previously deleted a service, you can re-create it with CreateService. +// - ServiceNotFoundException +// The specified service could not be found. You can view your available services +// with ListServices. Amazon ECS services are cluster-specific and region-specific. // -// * PlatformUnknownException -// The specified platform version does not exist. +// - ServiceNotActiveException +// The specified service is not active. You can't update a service that is inactive. +// If you have previously deleted a service, you can re-create it with CreateService. // -// * PlatformTaskDefinitionIncompatibilityException -// The specified platform version does not satisfy the task definition's required -// capabilities. +// - PlatformUnknownException +// The specified platform version does not exist. // -// * AccessDeniedException -// You do not have authorization to perform the requested action. +// - PlatformTaskDefinitionIncompatibilityException +// The specified platform version does not satisfy the task definition's required +// capabilities. // +// - AccessDeniedException +// You do not have authorization to perform the requested action. func (c *ECS) UpdateService(input *UpdateServiceInput) (*UpdateServiceOutput, error) { req, out := c.UpdateServiceRequest(input) return out, req.Send() @@ -3847,6 +3896,100 @@ func (c *ECS) UpdateServiceWithContext(ctx aws.Context, input *UpdateServiceInpu return out, req.Send() } +const opUpdateTaskProtection = "UpdateTaskProtection" + +// UpdateTaskProtectionRequest generates a "aws/request.Request" representing the +// client's request for the UpdateTaskProtection operation. The "output" return +// value will be populated with the request's response once the request completes +// successfully. +// +// Use "Send" method on the returned Request to send the API call to the service. +// the "output" return value is not valid until after Send returns without error. +// +// See UpdateTaskProtection for more information on using the UpdateTaskProtection +// API call, and error handling. +// +// This method is useful when you want to inject custom logic or configuration +// into the SDK's request lifecycle. Such as custom headers, or retry logic. +// +// // Example sending a request using the UpdateTaskProtectionRequest method. +// req, resp := client.UpdateTaskProtectionRequest(params) +// +// err := req.Send() +// if err == nil { // resp is now filled +// fmt.Println(resp) +// } +func (c *ECS) UpdateTaskProtectionRequest(input *UpdateTaskProtectionInput) (req *request.Request, output *UpdateTaskProtectionOutput) { + op := &request.Operation{ + Name: opUpdateTaskProtection, + HTTPMethod: "POST", + HTTPPath: "/", + } + + if input == nil { + input = &UpdateTaskProtectionInput{} + } + + output = &UpdateTaskProtectionOutput{} + req = c.newRequest(op, input, output) + return +} + +// UpdateTaskProtection API operation for Amazon Elastic Container Service. +// +// Returns awserr.Error for service API and SDK errors. Use runtime type assertions +// with awserr.Error's Code and Message methods to get detailed information about +// the error. +// +// See the AWS API reference guide for Amazon Elastic Container Service's +// API operation UpdateTaskProtection for usage and error information. +// +// Returned Error Types: +// +// - AccessDeniedException +// You do not have authorization to perform the requested action. +// +// - ClientException +// These errors are usually caused by a client action, such as using an action +// or resource on behalf of a user that doesn't have permissions to use the +// action or resource, or specifying an identifier that is not valid. +// +// - ClusterNotFoundException +// The specified cluster could not be found. You can view your available clusters +// with ListClusters. Amazon ECS clusters are region-specific. +// +// - InvalidParameterException +// The specified parameter is invalid. Review the available parameters for the +// API request. +// +// - ResourceNotFoundException +// +// - ServerException +// These errors are usually caused by a server issue. +// +// - UnsupportedFeatureException +// The specified task is not supported in this region. +func (c *ECS) UpdateTaskProtection(input *UpdateTaskProtectionInput) (*UpdateTaskProtectionOutput, error) { + req, out := c.UpdateTaskProtectionRequest(input) + return out, req.Send() +} + +// UpdateTaskProtectionWithContext is the same as UpdateTaskProtection with the addition of +// the ability to pass a context and additional request options. +// +// See UpdateTaskProtection for details on how to use this API operation. +// +// The context must be non-nil and will be used for request cancellation. If +// the context is nil a panic will occur. In the future the SDK may create +// sub-contexts for http.Requests. See https://golang.org/pkg/context/ +// for more information on using Contexts. +func (c *ECS) UpdateTaskProtectionWithContext(ctx aws.Context, input *UpdateTaskProtectionInput, opts ...request.Option) (*UpdateTaskProtectionOutput, error) { + req, out := c.UpdateTaskProtectionRequest(input) + req.SetContext(ctx) + req.ApplyOptions(opts...) + return out, req.Send() +} + // You do not have authorization to perform the requested action. type AccessDeniedException struct { _ struct{} `type:"structure"` @@ -7404,6 +7547,9 @@ type DiscoverPollEndpointOutput struct { // The endpoint for the Amazon ECS agent to poll. Endpoint *string `locationName:"endpoint" type:"string"` + // The endpoint for the ServiceConnect Relay to connect to. + ServiceConnectEndpoint *string `locationName:"serviceConnectEndpoint" type:"string"` + // The telemetry endpoint for the Amazon ECS agent. TelemetryEndpoint *string `locationName:"telemetryEndpoint" type:"string"` } @@ -7424,6 +7570,12 @@ func (s *DiscoverPollEndpointOutput) SetEndpoint(v string) *DiscoverPollEndpoint return s } +// SetServiceConnectEndpoint sets the ServiceConnectEndpoint field's value. +func (s *DiscoverPollEndpointOutput) SetServiceConnectEndpoint(v string) *DiscoverPollEndpointOutput { + s.ServiceConnectEndpoint = &v + return s +} + // SetTelemetryEndpoint sets the TelemetryEndpoint field's value. func (s *DiscoverPollEndpointOutput) SetTelemetryEndpoint(v string) *DiscoverPollEndpointOutput { s.TelemetryEndpoint = &v @@ -7698,6 +7850,8 @@ type Failure struct { // The Amazon Resource Name (ARN) of the failed resource. Arn *string `locationName:"arn" type:"string"` + Detail *string `locationName:"detail" type:"string"` + // The reason for the failure. Reason *string `locationName:"reason" type:"string"` } @@ -7718,6 +7872,12 @@ func (s *Failure) SetArn(v string) *Failure { return s } +// SetDetail sets the Detail field's value. +func (s *Failure) SetDetail(v string) *Failure { + s.Detail = &v + return s +} + // SetReason sets the Reason field's value. func (s *Failure) SetReason(v string) *Failure { s.Reason = &v @@ -7768,6 +7928,80 @@ func (s *FirelensConfiguration) SetType(v string) *FirelensConfiguration { return s } +type GetTaskProtectionInput struct { + _ struct{} `type:"structure"` + + // Cluster is a required field + Cluster *string `locationName:"cluster" type:"string" required:"true"` + + Tasks []*string `locationName:"tasks" type:"list"` +} + +// String returns the string representation +func (s GetTaskProtectionInput) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s GetTaskProtectionInput) GoString() string { + return s.String() +} + +// Validate inspects the fields of the type to determine if they are valid. +func (s *GetTaskProtectionInput) Validate() error { + invalidParams := request.ErrInvalidParams{Context: "GetTaskProtectionInput"} + if s.Cluster == nil { + invalidParams.Add(request.NewErrParamRequired("Cluster")) + } + + if invalidParams.Len() > 0 { + return invalidParams + } + return nil +} + +// SetCluster sets the Cluster field's value. +func (s *GetTaskProtectionInput) SetCluster(v string) *GetTaskProtectionInput { + s.Cluster = &v + return s +} + +// SetTasks sets the Tasks field's value. +func (s *GetTaskProtectionInput) SetTasks(v []*string) *GetTaskProtectionInput { + s.Tasks = v + return s +} + +type GetTaskProtectionOutput struct { + _ struct{} `type:"structure"` + + Failures []*Failure `locationName:"failures" type:"list"` + + ProtectedTasks []*ProtectedTask `locationName:"protectedTasks" type:"list"` +} + +// String returns the string representation +func (s GetTaskProtectionOutput) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s GetTaskProtectionOutput) GoString() string { + return s.String() +} + +// SetFailures sets the Failures field's value. +func (s *GetTaskProtectionOutput) SetFailures(v []*Failure) *GetTaskProtectionOutput { + s.Failures = v + return s +} + +// SetProtectedTasks sets the ProtectedTasks field's value. +func (s *GetTaskProtectionOutput) SetProtectedTasks(v []*ProtectedTask) *GetTaskProtectionOutput { + s.ProtectedTasks = v + return s +} + // An object representing a container health check. Health check parameters // that are specified in a container definition override any Docker health checks // that exist in the container image (such as those specified in a parent image @@ -9536,9 +9770,13 @@ type NetworkBinding struct { // The port number on the container that is used with the network binding. ContainerPort *int64 `locationName:"containerPort" type:"integer"` + ContainerPortRange *string `locationName:"containerPortRange" type:"string"` + // The port number on the host that is used with the network binding. HostPort *int64 `locationName:"hostPort" type:"integer"` + HostPortRange *string `locationName:"hostPortRange" type:"string"` + // The protocol used for the network binding. Protocol *string `locationName:"protocol" type:"string" enum:"TransportProtocol"` } @@ -9565,12 +9803,24 @@ func (s *NetworkBinding) SetContainerPort(v int64) *NetworkBinding { return s } +// SetContainerPortRange sets the ContainerPortRange field's value. +func (s *NetworkBinding) SetContainerPortRange(v string) *NetworkBinding { + s.ContainerPortRange = &v + return s +} + // SetHostPort sets the HostPort field's value. func (s *NetworkBinding) SetHostPort(v int64) *NetworkBinding { s.HostPort = &v return s } +// SetHostPortRange sets the HostPortRange field's value. +func (s *NetworkBinding) SetHostPortRange(v string) *NetworkBinding { + s.HostPortRange = &v + return s +} + // SetProtocol sets the Protocol field's value. func (s *NetworkBinding) SetProtocol(v string) *NetworkBinding { s.Protocol = &v @@ -9976,6 +10226,8 @@ type PortMapping struct { // the 100 reserved ports limit of a container instance. ContainerPort *int64 `locationName:"containerPort" type:"integer"` + ContainerPortRange *string `locationName:"containerPortRange" type:"string"` + // The port number on the container instance to reserve for your container. // // If using containers in a task with the awsvpc or host network mode, the hostPort @@ -10028,6 +10280,12 @@ func (s *PortMapping) SetContainerPort(v int64) *PortMapping { return s } +// SetContainerPortRange sets the ContainerPortRange field's value. +func (s *PortMapping) SetContainerPortRange(v string) *PortMapping { + s.ContainerPortRange = &v + return s +} + // SetHostPort sets the HostPort field's value. func (s *PortMapping) SetHostPort(v int64) *PortMapping { s.HostPort = &v @@ -10040,6 +10298,44 @@ func (s *PortMapping) SetProtocol(v string) *PortMapping { return s } +type ProtectedTask struct { + _ struct{} `type:"structure"` + + ExpirationDate *time.Time `locationName:"expirationDate" type:"timestamp"` + + ProtectionEnabled *bool `locationName:"protectionEnabled" type:"boolean"` + + TaskArn *string `locationName:"taskArn" type:"string"` +} + +// String returns the string representation +func (s ProtectedTask) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s ProtectedTask) GoString() string { + return s.String() +} + +// SetExpirationDate sets the ExpirationDate field's value. +func (s *ProtectedTask) SetExpirationDate(v time.Time) *ProtectedTask { + s.ExpirationDate = &v + return s +} + +// SetProtectionEnabled sets the ProtectionEnabled field's value. +func (s *ProtectedTask) SetProtectionEnabled(v bool) *ProtectedTask { + s.ProtectionEnabled = &v + return s +} + +// SetTaskArn sets the TaskArn field's value. +func (s *ProtectedTask) SetTaskArn(v string) *ProtectedTask { + s.TaskArn = &v + return s +} + type ProxyConfiguration struct { _ struct{} `type:"structure"` @@ -10856,6 +11152,61 @@ func (s *Resource) SetType(v string) *Resource { return s } +type ResourceNotFoundException struct { + _ struct{} `type:"structure"` + RespMetadata protocol.ResponseMetadata `json:"-" xml:"-"` + + Message_ *string `locationName:"message" type:"string"` +} + +// String returns the string representation +func (s ResourceNotFoundException) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s ResourceNotFoundException) GoString() string { + return s.String() +} + +func newErrorResourceNotFoundException(v protocol.ResponseMetadata) error { + return &ResourceNotFoundException{ + RespMetadata: v, + } +} + +// Code returns the exception type name. +func (s *ResourceNotFoundException) Code() string { + return "ResourceNotFoundException" +} + +// Message returns the exception's message. +func (s *ResourceNotFoundException) Message() string { + if s.Message_ != nil { + return *s.Message_ + } + return "" +} + +// OrigErr always returns nil, satisfies awserr.Error interface. +func (s *ResourceNotFoundException) OrigErr() error { + return nil +} + +func (s *ResourceNotFoundException) Error() string { + return fmt.Sprintf("%s: %s", s.Code(), s.Message()) +} + +// Status code returns the HTTP status code for the request's response error. +func (s *ResourceNotFoundException) StatusCode() int { + return s.RespMetadata.StatusCode +} + +// RequestID returns the service's response RequestID for request. +func (s *ResourceNotFoundException) RequestID() string { + return s.RespMetadata.RequestID +} + type ResourceRequirement struct { _ struct{} `type:"structure"` @@ -13744,6 +14095,104 @@ func (s *UpdateServiceOutput) SetService(v *Service) *UpdateServiceOutput { return s } +type UpdateTaskProtectionInput struct { + _ struct{} `type:"structure"` + + // Cluster is a required field + Cluster *string `locationName:"cluster" type:"string" required:"true"` + + ExpiresInMinutes *int64 `locationName:"expiresInMinutes" type:"integer"` + + // ProtectionEnabled is a required field + ProtectionEnabled *bool `locationName:"protectionEnabled" type:"boolean" required:"true"` + + // Tasks is a required field + Tasks []*string `locationName:"tasks" type:"list" required:"true"` +} + +// String returns the string representation +func (s UpdateTaskProtectionInput) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s UpdateTaskProtectionInput) GoString() string { + return s.String() +} + +// Validate inspects the fields of the type to determine if they are valid. +func (s *UpdateTaskProtectionInput) Validate() error { + invalidParams := request.ErrInvalidParams{Context: "UpdateTaskProtectionInput"} + if s.Cluster == nil { + invalidParams.Add(request.NewErrParamRequired("Cluster")) + } + if s.ProtectionEnabled == nil { + invalidParams.Add(request.NewErrParamRequired("ProtectionEnabled")) + } + if s.Tasks == nil { + invalidParams.Add(request.NewErrParamRequired("Tasks")) + } + + if invalidParams.Len() > 0 { + return invalidParams + } + return nil +} + +// SetCluster sets the Cluster field's value. +func (s *UpdateTaskProtectionInput) SetCluster(v string) *UpdateTaskProtectionInput { + s.Cluster = &v + return s +} + +// SetExpiresInMinutes sets the ExpiresInMinutes field's value. +func (s *UpdateTaskProtectionInput) SetExpiresInMinutes(v int64) *UpdateTaskProtectionInput { + s.ExpiresInMinutes = &v + return s +} + +// SetProtectionEnabled sets the ProtectionEnabled field's value. +func (s *UpdateTaskProtectionInput) SetProtectionEnabled(v bool) *UpdateTaskProtectionInput { + s.ProtectionEnabled = &v + return s +} + +// SetTasks sets the Tasks field's value. +func (s *UpdateTaskProtectionInput) SetTasks(v []*string) *UpdateTaskProtectionInput { + s.Tasks = v + return s +} + +type UpdateTaskProtectionOutput struct { + _ struct{} `type:"structure"` + + Failures []*Failure `locationName:"failures" type:"list"` + + ProtectedTasks []*ProtectedTask `locationName:"protectedTasks" type:"list"` +} + +// String returns the string representation +func (s UpdateTaskProtectionOutput) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s UpdateTaskProtectionOutput) GoString() string { + return s.String() +} + +// SetFailures sets the Failures field's value. +func (s *UpdateTaskProtectionOutput) SetFailures(v []*Failure) *UpdateTaskProtectionOutput { + s.Failures = v + return s +} + +// SetProtectedTasks sets the ProtectedTasks field's value. +func (s *UpdateTaskProtectionOutput) SetProtectedTasks(v []*ProtectedTask) *UpdateTaskProtectionOutput { + s.ProtectedTasks = v + return s +} + // The Docker and Amazon ECS container agent version information about a container // instance. type VersionInfo struct { diff --git a/agent/ecs_client/model/ecs/errors.go b/agent/ecs_client/model/ecs/errors.go index 0a06c5366ec..9a1fa393f5d 100644 --- a/agent/ecs_client/model/ecs/errors.go +++ b/agent/ecs_client/model/ecs/errors.go @@ -116,6 +116,10 @@ const ( // The specified platform version does not exist. ErrCodePlatformUnknownException = "PlatformUnknownException" + // ErrCodeResourceNotFoundException for service response error code + // "ResourceNotFoundException". + ErrCodeResourceNotFoundException = "ResourceNotFoundException" + // ErrCodeServerException for service response error code // "ServerException". // @@ -175,6 +179,7 @@ var exceptionFromCode = map[string]func(protocol.ResponseMetadata) error{ "NoUpdateAvailableException": newErrorNoUpdateAvailableException, "PlatformTaskDefinitionIncompatibilityException": newErrorPlatformTaskDefinitionIncompatibilityException, "PlatformUnknownException": newErrorPlatformUnknownException, + "ResourceNotFoundException": newErrorResourceNotFoundException, "ServerException": newErrorServerException, "ServiceNotActiveException": newErrorServiceNotActiveException, "ServiceNotFoundException": newErrorServiceNotFoundException, diff --git a/agent/ecs_client/model/ecs/service.go b/agent/ecs_client/model/ecs/service.go index 0dc4c59775c..fa30959a632 100644 --- a/agent/ecs_client/model/ecs/service.go +++ b/agent/ecs_client/model/ecs/service.go @@ -53,13 +53,14 @@ const ( // aws.Config parameter to add your extra config. // // Example: -// mySession := session.Must(session.NewSession()) // -// // Create a ECS client from just a session. -// svc := ecs.New(mySession) +// mySession := session.Must(session.NewSession()) // -// // Create a ECS client with additional configuration -// svc := ecs.New(mySession, aws.NewConfig().WithRegion("us-west-2")) +// // Create a ECS client from just a session. +// svc := ecs.New(mySession) +// +// // Create a ECS client with additional configuration +// svc := ecs.New(mySession, aws.NewConfig().WithRegion("us-west-2")) func New(p client.ConfigProvider, cfgs ...*aws.Config) *ECS { c := p.ClientConfig(EndpointsID, cfgs...) return newClient(*c.Config, c.Handlers, c.PartitionID, c.Endpoint, c.SigningRegion, c.SigningName) diff --git a/agent/ecscni/mocks_cnitypes/result_mocks.go b/agent/ecscni/mocks_cnitypes/result_mocks.go index a2d18b09d11..b998d2c28c7 100644 --- a/agent/ecscni/mocks_cnitypes/result_mocks.go +++ b/agent/ecscni/mocks_cnitypes/result_mocks.go @@ -92,20 +92,6 @@ func (mr *MockResultMockRecorder) PrintTo(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTo", reflect.TypeOf((*MockResult)(nil).PrintTo), arg0) } -// String mocks base method -func (m *MockResult) String() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "String") - ret0, _ := ret[0].(string) - return ret0 -} - -// String indicates an expected call of String -func (mr *MockResultMockRecorder) String() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockResult)(nil).String)) -} - // Version mocks base method func (m *MockResult) Version() string { m.ctrl.T.Helper() diff --git a/agent/ecscni/mocks_libcni/libcni_mocks.go b/agent/ecscni/mocks_libcni/libcni_mocks.go index e75ceb98aa4..d72f03889d6 100644 --- a/agent/ecscni/mocks_libcni/libcni_mocks.go +++ b/agent/ecscni/mocks_libcni/libcni_mocks.go @@ -136,6 +136,22 @@ func (mr *MockCNIMockRecorder) DelNetworkList(arg0, arg1, arg2 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelNetworkList", reflect.TypeOf((*MockCNI)(nil).DelNetworkList), arg0, arg1, arg2) } +// GetNetworkCachedConfig mocks base method +func (m *MockCNI) GetNetworkCachedConfig(arg0 *libcni.NetworkConfig, arg1 *libcni.RuntimeConf) ([]byte, *libcni.RuntimeConf, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNetworkCachedConfig", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*libcni.RuntimeConf) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNetworkCachedConfig indicates an expected call of GetNetworkCachedConfig +func (mr *MockCNIMockRecorder) GetNetworkCachedConfig(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkCachedConfig", reflect.TypeOf((*MockCNI)(nil).GetNetworkCachedConfig), arg0, arg1) +} + // GetNetworkCachedResult mocks base method func (m *MockCNI) GetNetworkCachedResult(arg0 *libcni.NetworkConfig, arg1 *libcni.RuntimeConf) (types.Result, error) { m.ctrl.T.Helper() @@ -151,6 +167,22 @@ func (mr *MockCNIMockRecorder) GetNetworkCachedResult(arg0, arg1 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkCachedResult", reflect.TypeOf((*MockCNI)(nil).GetNetworkCachedResult), arg0, arg1) } +// GetNetworkListCachedConfig mocks base method +func (m *MockCNI) GetNetworkListCachedConfig(arg0 *libcni.NetworkConfigList, arg1 *libcni.RuntimeConf) ([]byte, *libcni.RuntimeConf, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNetworkListCachedConfig", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*libcni.RuntimeConf) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetNetworkListCachedConfig indicates an expected call of GetNetworkListCachedConfig +func (mr *MockCNIMockRecorder) GetNetworkListCachedConfig(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkListCachedConfig", reflect.TypeOf((*MockCNI)(nil).GetNetworkListCachedConfig), arg0, arg1) +} + // GetNetworkListCachedResult mocks base method func (m *MockCNI) GetNetworkListCachedResult(arg0 *libcni.NetworkConfigList, arg1 *libcni.RuntimeConf) (types.Result, error) { m.ctrl.T.Helper() diff --git a/agent/ecscni/namespace_helper_linux.go b/agent/ecscni/namespace_helper_linux.go index ff508cfd274..7f4479ef012 100644 --- a/agent/ecscni/namespace_helper_linux.go +++ b/agent/ecscni/namespace_helper_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/namespace_helper_unsupported.go b/agent/ecscni/namespace_helper_unsupported.go index 19d5666de45..bc10fce10eb 100644 --- a/agent/ecscni/namespace_helper_unsupported.go +++ b/agent/ecscni/namespace_helper_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/namespace_helper_windows.go b/agent/ecscni/namespace_helper_windows.go index 763dcc4a306..a552e148738 100644 --- a/agent/ecscni/namespace_helper_windows.go +++ b/agent/ecscni/namespace_helper_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/namespace_helper_windows_test.go b/agent/ecscni/namespace_helper_windows_test.go index 8d4300178b7..a777d39a19b 100644 --- a/agent/ecscni/namespace_helper_windows_test.go +++ b/agent/ecscni/namespace_helper_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/netconfig.go b/agent/ecscni/netconfig.go index dda77a8f907..53e9b90516d 100644 --- a/agent/ecscni/netconfig.go +++ b/agent/ecscni/netconfig.go @@ -16,7 +16,8 @@ package ecscni import ( "encoding/json" - "github.com/cihub/seelog" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/containernetworking/cni/libcni" cnitypes "github.com/containernetworking/cni/pkg/types" ) @@ -25,7 +26,11 @@ import ( func newNetworkConfig(netcfg interface{}, plugin string, cniVersion string) (*libcni.NetworkConfig, error) { configBytes, err := json.Marshal(netcfg) if err != nil { - seelog.Errorf("[ECSCNI] Marshal configuration for plugin %s failed, error: %v", plugin, err) + logger.Error("[ECSCNI] Marshal configuration failed", logger.Fields{ + "netcfg": netcfg, + "plugin": plugin, + "cniVersion": cniVersion, + }) return nil, err } @@ -33,6 +38,7 @@ func newNetworkConfig(netcfg interface{}, plugin string, cniVersion string) (*li Network: &cnitypes.NetConf{ Type: plugin, CNIVersion: cniVersion, + Name: defaultNetworkName, }, Bytes: configBytes, } diff --git a/agent/ecscni/netconfig_linux.go b/agent/ecscni/netconfig_linux.go index b9596d97f5b..49f080960b3 100644 --- a/agent/ecscni/netconfig_linux.go +++ b/agent/ecscni/netconfig_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,15 +17,17 @@ package ecscni import ( + "fmt" "net" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/api/appmesh" "github.com/aws/amazon-ecs-agent/agent/api/eni" "github.com/cihub/seelog" "github.com/containernetworking/cni/libcni" cnitypes "github.com/containernetworking/cni/pkg/types" - "github.com/pkg/errors" ) // NewBridgeNetworkConfig creates the config of bridge for ADD command, where @@ -45,7 +48,7 @@ func NewBridgeNetworkConfig(cfg *Config, includeIPAM bool) (string, *libcni.Netw if includeIPAM { ipamConfig, err := newIPAMConfig(cfg) if err != nil { - return "", nil, errors.Wrap(err, "NewBridgeNetworkConfig: create ipam configuration failed") + return "", nil, fmt.Errorf("NewBridgeNetworkConfig: create ipam configuration failed: %w", err) } bridgeConfig.IPAM = ipamConfig @@ -53,7 +56,7 @@ func NewBridgeNetworkConfig(cfg *Config, includeIPAM bool) (string, *libcni.Netw networkConfig, err := newNetworkConfig(bridgeConfig, ECSBridgePluginName, cfg.MinSupportedCNIVersion) if err != nil { - return "", nil, errors.Wrap(err, "NewBridgeNetworkConfig: construct bridge and ipam network configuration failed") + return "", nil, fmt.Errorf("NewBridgeNetworkConfig: construct bridge and ipam network configuration failed: %w", err) } return defaultVethName, networkConfig, nil @@ -63,7 +66,7 @@ func NewBridgeNetworkConfig(cfg *Config, includeIPAM bool) (string, *libcni.Netw func NewIPAMNetworkConfig(cfg *Config) (string, *libcni.NetworkConfig, error) { ipamConfig, err := newIPAMConfig(cfg) if err != nil { - return defaultVethName, nil, errors.Wrap(err, "NewIPAMNetworkConfig: create ipam network configuration failed") + return defaultVethName, nil, fmt.Errorf("NewIPAMNetworkConfig: create ipam network configuration failed: %w", err) } ipamNetworkConfig := IPAMNetworkConfig{ @@ -74,7 +77,7 @@ func NewIPAMNetworkConfig(cfg *Config) (string, *libcni.NetworkConfig, error) { networkConfig, err := newNetworkConfig(ipamNetworkConfig, ECSIPAMPluginName, cfg.MinSupportedCNIVersion) if err != nil { - return "", nil, errors.Wrap(err, "NewIPAMNetworkConfig: construct ipam network configuration failed") + return "", nil, fmt.Errorf("NewIPAMNetworkConfig: construct ipam network configuration failed: %w", err) } return defaultVethName, networkConfig, nil @@ -122,7 +125,7 @@ func NewENINetworkConfig(eni *eni.ENI, cfg *Config) (string, *libcni.NetworkConf networkConfig, err := newNetworkConfig(eniConf, ECSENIPluginName, cfg.MinSupportedCNIVersion) if err != nil { - return "", nil, errors.Wrap(err, "cni config: failed to create configuration") + return "", nil, fmt.Errorf("cni config: failed to create configuration: %w", err) } return defaultENIName, networkConfig, nil @@ -143,7 +146,7 @@ func NewBranchENINetworkConfig(eni *eni.ENI, cfg *Config) (string, *libcni.Netwo networkConfig, err := newNetworkConfig(eniConf, ECSBranchENIPluginName, cfg.MinSupportedCNIVersion) if err != nil { - return "", nil, errors.Wrap(err, "NewBranchENINetworkConfig: construct the eni network configuration failed") + return "", nil, fmt.Errorf("NewBranchENINetworkConfig: construct the eni network configuration failed: %w", err) } return defaultENIName, networkConfig, nil @@ -164,8 +167,75 @@ func NewAppMeshConfig(appMesh *appmesh.AppMesh, cfg *Config) (string, *libcni.Ne networkConfig, err := newNetworkConfig(appMeshConfig, ECSAppMeshPluginName, cfg.MinSupportedCNIVersion) if err != nil { - return "", nil, errors.Wrap(err, "NewAppMeshConfig: construct the app mesh network configuration failed") + return "", nil, fmt.Errorf("NewAppMeshConfig: construct the app mesh network configuration failed: %w", err) } return defaultAppMeshIfName, networkConfig, nil } + +// NewServiceConnectNetworkConfig creates a new ServiceConnect CNI network configuration +func NewServiceConnectNetworkConfig( + scConfig *serviceconnect.Config, + redirectMode RedirectMode, + shouldIncludeRedirectIP bool, + enableIPv4 bool, + enableIPv6 bool, + cfg *Config) (string, *libcni.NetworkConfig, error) { + var ingressConfig []IngressConfigJSONEntry + for _, ic := range scConfig.IngressConfig { + newEntry := IngressConfigJSONEntry{ + ListenerPort: ic.ListenerPort, + } + if ic.InterceptPort != nil { + newEntry.InterceptPort = *ic.InterceptPort + } + ingressConfig = append(ingressConfig, newEntry) + } + + var egressConfig *EgressConfigJSON + if scConfig.EgressConfig != nil { + egressConfig = &EgressConfigJSON{ + RedirectMode: string(redirectMode), + VIP: VIPConfigJSON{ + IPv4CIDR: scConfig.EgressConfig.VIP.IPV4CIDR, + IPv6CIDR: scConfig.EgressConfig.VIP.IPV6CIDR, + }, + } + switch redirectMode { + case NAT: + // NAT redirect mode is for awsvpc tasks, where the one and only pause container netns will have a NAT redirect rule. + egressConfig.ListenerPort = scConfig.EgressConfig.ListenerPort + case TPROXY: // bridge + // TPROXY redirect mode is used for bridge-mode tasks. There are two use cases: + // 1. SC pause container netns will set up TPROXY that requires the Egress port + // 2. Other task pause container netns will add a route for traffic destined for SC VIP-CIDR to go to SC container. + // In that case the configuration requires the SC (pause) container IP. + if shouldIncludeRedirectIP { + scNetworkConfig := scConfig.NetworkConfig + egressConfig.RedirectIP = &RedirectIPJson{ + IPv4: scNetworkConfig.SCPauseIPv4Addr, + IPv6: scNetworkConfig.SCPauseIPv6Addr, + } + } else { + // for sc pause container, pass egress listener port for setting up tproxy + egressConfig.ListenerPort = scConfig.EgressConfig.ListenerPort + } + default: + return "", nil, fmt.Errorf("NewServiceConnectNetworkConfig: unknown redirect mode %s", string(redirectMode)) + } + } + + scNetworkConfig := ServiceConnectConfig{ + Name: ECSServiceConnectPluginName, + Type: ECSServiceConnectPluginName, + IngressConfig: ingressConfig, + EgressConfig: egressConfig, + EnableIPv4: enableIPv4, + EnableIPv6: enableIPv6, + } + networkConfig, err := newNetworkConfig(scNetworkConfig, ECSServiceConnectPluginName, cfg.MinSupportedCNIVersion) + if err != nil { + return "", nil, fmt.Errorf("NewServiceConnectNetworkConfig: construct the service connect network configuration failed: %w", err) + } + return defaultServiceConnectIfName, networkConfig, nil +} diff --git a/agent/ecscni/netconfig_windows.go b/agent/ecscni/netconfig_windows.go index 2949379f78d..4d266d7e9a1 100644 --- a/agent/ecscni/netconfig_windows.go +++ b/agent/ecscni/netconfig_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/netconfig_windows_test.go b/agent/ecscni/netconfig_windows_test.go index 86a35953dd3..4f81fa9b6fa 100644 --- a/agent/ecscni/netconfig_windows_test.go +++ b/agent/ecscni/netconfig_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/plugin_linux.go b/agent/ecscni/plugin_linux.go index df9f86c8c65..cb13d41fb87 100644 --- a/agent/ecscni/plugin_linux.go +++ b/agent/ecscni/plugin_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -70,15 +71,18 @@ func (client *cniClient) setupNS(ctx context.Context, cfg *Config) (*current.Res if cniNetworkConfig.Network.Type == ECSBridgePluginName { bridgeResult = result } - seelog.Debugf("[ECSCNI] Completed adding network %s type %s in the container namespace %s", cniNetworkConfig.Network.Name, cniNetworkConfig.Network.Type, cfg.ContainerID) } - seelog.Debugf("[ECSCNI] Completed setting up the container namespace: %s", bridgeResult.String()) + seelog.Debugf("[ECSCNI] Completed setting up the container namespace: %s", cfg.ContainerID) + if bridgeResult == nil { + // Not every netns setup involves ECS Bridge Plugin + return nil, nil + } if _, err := bridgeResult.GetAsVersion(currentCNISpec); err != nil { seelog.Warnf("[ECSCNI] Unable to convert result to spec version %s; error: %v; result is of version: %s", currentCNISpec, err, bridgeResult.Version()) @@ -88,8 +92,7 @@ func (client *cniClient) setupNS(ctx context.Context, cfg *Config) (*current.Res curResult, ok := bridgeResult.(*current.Result) if !ok { return nil, errors.Errorf( - "cni setup: unable to convert result to expected version '%s'", - bridgeResult.String()) + "cni setup: unable to convert result to expected version '%v'", bridgeResult) } return curResult, nil diff --git a/agent/ecscni/plugin_linux_test.go b/agent/ecscni/plugin_linux_test.go index e7744fa6073..3b5ea8a3e4a 100644 --- a/agent/ecscni/plugin_linux_test.go +++ b/agent/ecscni/plugin_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -24,6 +25,8 @@ import ( "testing" "time" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + "github.com/aws/amazon-ecs-agent/agent/api/appmesh" "github.com/aws/amazon-ecs-agent/agent/api/eni" mock_libcni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks_libcni" @@ -46,6 +49,10 @@ const ( eniSubnetGatewayIPV4AddressWithoutBlockSize = "172.31.1.1" trunkENIMACAddress = "02:7b:64:49:b2:40" branchENIVLANID = "42" + testIngressListenerPort = uint16(11111) + testEgressConfigListenerPort = uint16(22222) + testSCPauseIPv4Addr = "172.0.0.2" + testSCPauseIPv6Addr = "fd00::4:120" ) func TestSetupNS(t *testing.T) { @@ -231,6 +238,77 @@ func appMeshNetworkConfig(config *Config) *NetworkConfig { return &NetworkConfig{CNINetworkConfig: appMeshNetworkConfig} } +func TestSetupNSServiceConnectEnabled(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ecscniClient := NewClient("") + libcniClient := mock_libcni.NewMockCNI(ctrl) + ecscniClient.(*cniClient).libcni = libcniClient + + additionalRoutesJson := `["169.254.172.1/32", "10.11.12.13/32"]` + var additionalRoutes []cnitypes.IPNet + err := json.Unmarshal([]byte(additionalRoutesJson), &additionalRoutes) + assert.NoError(t, err) + + gomock.InOrder( + // ENI plugin was called first + libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(¤t.Result{}, nil).Do( + func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) { + assert.Equal(t, ECSENIPluginName, net.Network.Type, "first plugin should be eni") + }), + // Bridge plugin was called second + libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(¤t.Result{}, nil).Do( + func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) { + assert.Equal(t, ECSBridgePluginName, net.Network.Type, "second plugin should be bridge") + var bridgeConfig BridgeConfig + err := json.Unmarshal(net.Bytes, &bridgeConfig) + assert.NoError(t, err, "unmarshal BridgeConfig") + assert.Len(t, bridgeConfig.IPAM.IPV4Routes, 3, "default route plus two extra routes") + }), + // ServiceConnect plugin was called third + libcniClient.EXPECT().AddNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(¤t.Result{}, nil).Do( + func(ctx context.Context, net *libcni.NetworkConfig, rt *libcni.RuntimeConf) { + assert.Equal(t, ECSServiceConnectPluginName, net.Network.Type, "third plugin should be service connect") + }), + ) + config := &Config{ + AdditionalLocalRoutes: additionalRoutes, + NetworkConfigs: []*NetworkConfig{}, + } + config.NetworkConfigs = append(config.NetworkConfigs, eniNetworkConfig(config)) + config.NetworkConfigs = append(config.NetworkConfigs, bridgeConfigWithIPAM(config)) + config.NetworkConfigs = append(config.NetworkConfigs, serviceConnectNetworkConfig(config)) + _, err = ecscniClient.SetupNS(context.TODO(), config, time.Second) + assert.NoError(t, err) +} + +func serviceConnectNetworkConfig(config *Config) *NetworkConfig { + _, serviceConnectNetworkConfig, _ := NewServiceConnectNetworkConfig(defaultTestServiceConnectConfig(), NAT, false, true, false, config) + return &NetworkConfig{CNINetworkConfig: serviceConnectNetworkConfig} +} + +func defaultTestServiceConnectConfig() *serviceconnect.Config { + return &serviceconnect.Config{ + IngressConfig: []serviceconnect.IngressConfigEntry{{ + ListenerName: "test ingress listener", + ListenerPort: testIngressListenerPort, + }}, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: "test egress listener", + ListenerPort: testEgressConfigListenerPort, + VIP: serviceconnect.VIP{ + IPV4CIDR: "169.254.0.0/16", + }, + }, + DNSConfig: nil, + NetworkConfig: serviceconnect.NetworkConfig{ + SCPauseIPv4Addr: testSCPauseIPv4Addr, + SCPauseIPv6Addr: testSCPauseIPv6Addr, + }, + } +} + func TestSetupNSTimeout(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -346,6 +424,27 @@ func TestCleanupNSAppMeshEnabled(t *testing.T) { assert.NoError(t, err) } +func TestCleanupNSServiceConnectEnabled(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ecscniClient := NewClient("") + libcniClient := mock_libcni.NewMockCNI(ctrl) + ecscniClient.(*cniClient).libcni = libcniClient + + // This will be called for both bridge and eni plugin + libcniClient.EXPECT().DelNetwork(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(3) + + config := &Config{ + NetworkConfigs: []*NetworkConfig{}, + } + config.NetworkConfigs = append(config.NetworkConfigs, eniNetworkConfig(config)) + config.NetworkConfigs = append(config.NetworkConfigs, bridgeConfigWithIPAM(config)) + config.NetworkConfigs = append(config.NetworkConfigs, serviceConnectNetworkConfig(config)) + err := ecscniClient.CleanupNS(context.TODO(), config, time.Second) + assert.NoError(t, err) +} + func TestCleanupNSTimeout(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -556,6 +655,201 @@ func TestConstructIPAMNetworkConfig(t *testing.T) { assert.Equal(t, expectedConfigBytes, networkConfig.Bytes) } +func TestConstructServiceConnectNetworkConfig(t *testing.T) { + testCases := []struct { + redirectMode RedirectMode + shouldIncludeRedirectIP bool + }{ + { + redirectMode: NAT, + shouldIncludeRedirectIP: false, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: true, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: false, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("redirectMode: %s, shouldIncludeRedirectIP: %t", string(tc.redirectMode), tc.shouldIncludeRedirectIP), func(t *testing.T) { + + config := defaultTestServiceConnectConfig() + scIfName, netConfig, err := NewServiceConnectNetworkConfig(config, tc.redirectMode, tc.shouldIncludeRedirectIP, true, false, &Config{}) + require.NoError(t, err, "Failed to construct service connect network config") + assert.Equal(t, defaultServiceConnectIfName, scIfName) + + var scNetworkConfig ServiceConnectConfig + err = json.Unmarshal(netConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, 1, len(scNetworkConfig.IngressConfig)) + assert.Equal(t, testIngressListenerPort, scNetworkConfig.IngressConfig[0].ListenerPort) + assert.Equal(t, uint16(0), scNetworkConfig.IngressConfig[0].InterceptPort) + assert.NotNil(t, scNetworkConfig.EgressConfig) + assert.Equal(t, "169.254.0.0/16", scNetworkConfig.EgressConfig.VIP.IPv4CIDR) + assert.Equal(t, "", scNetworkConfig.EgressConfig.VIP.IPv6CIDR) + assert.Equal(t, true, scNetworkConfig.EnableIPv4) + assert.Equal(t, false, scNetworkConfig.EnableIPv6) + // For Egress config, only one of RedirectIP and Egress ListenerPort can be specified. + // Only Bridge mode application pause container should specify RedirectIP. + if tc.redirectMode == TPROXY && tc.shouldIncludeRedirectIP { + assert.NotNil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testSCPauseIPv4Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv4) + assert.Equal(t, testSCPauseIPv6Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv6) + assert.Equal(t, uint16(0), scNetworkConfig.EgressConfig.ListenerPort) + } else { + assert.Nil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testEgressConfigListenerPort, scNetworkConfig.EgressConfig.ListenerPort) + } + }) + } +} + +func TestConstructServiceConnectNetworkConfig_EmptyEgress(t *testing.T) { + testCases := []struct { + redirectMode RedirectMode + shouldIncludeRedirectIP bool + }{ + { + redirectMode: NAT, + shouldIncludeRedirectIP: false, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: true, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: false, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("redirectMode: %s, shouldIncludeRedirectIP: %t", string(tc.redirectMode), tc.shouldIncludeRedirectIP), func(t *testing.T) { + config := defaultTestServiceConnectConfig() + config.EgressConfig = nil + scIfName, netConfig, err := NewServiceConnectNetworkConfig(config, tc.redirectMode, tc.shouldIncludeRedirectIP, true, true, &Config{}) + require.NoError(t, err, "Failed to construct service connect network config") + assert.Equal(t, defaultServiceConnectIfName, scIfName) + + var scNetworkConfig ServiceConnectConfig + err = json.Unmarshal(netConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, 1, len(scNetworkConfig.IngressConfig)) + assert.Equal(t, testIngressListenerPort, scNetworkConfig.IngressConfig[0].ListenerPort) + assert.Equal(t, uint16(0), scNetworkConfig.IngressConfig[0].InterceptPort) + assert.Nil(t, scNetworkConfig.EgressConfig) + assert.Equal(t, true, scNetworkConfig.EnableIPv4) + assert.Equal(t, true, scNetworkConfig.EnableIPv6) + }) + } +} + +func TestConstructServiceConnectNetworkConfig_MultipleIngress(t *testing.T) { + testCases := []struct { + redirectMode RedirectMode + shouldIncludeRedirectIP bool + }{ + { + redirectMode: NAT, + shouldIncludeRedirectIP: false, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: true, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: false, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("redirectMode: %s, shouldIncludeRedirectIP: %t", string(tc.redirectMode), tc.shouldIncludeRedirectIP), func(t *testing.T) { + config := defaultTestServiceConnectConfig() + interceptPort := uint16(44444) + config.IngressConfig = append(config.IngressConfig, serviceconnect.IngressConfigEntry{ + ListenerName: "test listener 2", + ListenerPort: uint16(33333), + InterceptPort: &interceptPort, + }) + scIfName, netConfig, err := NewServiceConnectNetworkConfig(config, tc.redirectMode, tc.shouldIncludeRedirectIP, true, true, &Config{}) + require.NoError(t, err, "Failed to construct service connect network config") + assert.Equal(t, defaultServiceConnectIfName, scIfName) + + var scNetworkConfig ServiceConnectConfig + err = json.Unmarshal(netConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, 2, len(scNetworkConfig.IngressConfig)) + assert.Equal(t, testIngressListenerPort, scNetworkConfig.IngressConfig[0].ListenerPort) + assert.Equal(t, uint16(0), scNetworkConfig.IngressConfig[0].InterceptPort) + assert.Equal(t, uint16(33333), scNetworkConfig.IngressConfig[1].ListenerPort) + assert.Equal(t, uint16(44444), scNetworkConfig.IngressConfig[1].InterceptPort) + // For Egress config, only one of RedirectIP and Egress ListenerPort can be specified. + // Only Bridge mode application pause container should specify RedirectIP. + if tc.redirectMode == TPROXY && tc.shouldIncludeRedirectIP { + assert.NotNil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testSCPauseIPv4Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv4) + assert.Equal(t, testSCPauseIPv6Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv6) + assert.Equal(t, uint16(0), scNetworkConfig.EgressConfig.ListenerPort) + } else { + assert.Nil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testEgressConfigListenerPort, scNetworkConfig.EgressConfig.ListenerPort) + } + assert.Equal(t, true, scNetworkConfig.EnableIPv4) + assert.Equal(t, true, scNetworkConfig.EnableIPv6) + }) + } +} + +func TestConstructServiceConnectNetworkConfig_EmptyIngress(t *testing.T) { + testCases := []struct { + redirectMode RedirectMode + shouldIncludeRedirectIP bool + }{ + { + redirectMode: NAT, + shouldIncludeRedirectIP: false, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: true, + }, + { + redirectMode: TPROXY, + shouldIncludeRedirectIP: false, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("redirectMode: %s, shouldIncludeRedirectIP: %t", string(tc.redirectMode), tc.shouldIncludeRedirectIP), func(t *testing.T) { + config := defaultTestServiceConnectConfig() + config.IngressConfig = []serviceconnect.IngressConfigEntry{} + scIfName, netConfig, err := NewServiceConnectNetworkConfig(config, tc.redirectMode, tc.shouldIncludeRedirectIP, true, false, &Config{}) + require.NoError(t, err, "Failed to construct service connect network config") + assert.Equal(t, defaultServiceConnectIfName, scIfName) + + var scNetworkConfig ServiceConnectConfig + err = json.Unmarshal(netConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, 0, len(scNetworkConfig.IngressConfig)) + // For Egress config, only one of RedirectIP and Egress ListenerPort can be specified. + // Only Bridge mode application pause container should specify RedirectIP. + if tc.redirectMode == TPROXY && tc.shouldIncludeRedirectIP { + assert.NotNil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testSCPauseIPv4Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv4) + assert.Equal(t, testSCPauseIPv6Addr, scNetworkConfig.EgressConfig.RedirectIP.IPv6) + assert.Equal(t, uint16(0), scNetworkConfig.EgressConfig.ListenerPort) + } else { + assert.Nil(t, scNetworkConfig.EgressConfig.RedirectIP) + assert.Equal(t, testEgressConfigListenerPort, scNetworkConfig.EgressConfig.ListenerPort) + } + assert.Equal(t, true, scNetworkConfig.EnableIPv4) + assert.Equal(t, false, scNetworkConfig.EnableIPv6) + }) + } +} + // TestConstructBridgeNetworkConfigWithIPAM tests createBridgeNetworkConfigWithIPAM // creates the correct configuration for bridge and ipam plugin func TestConstructNetworkConfig(t *testing.T) { diff --git a/agent/ecscni/plugin_test.go b/agent/ecscni/plugin_test.go index 963b5eebdd1..be359f77e3c 100644 --- a/agent/ecscni/plugin_test.go +++ b/agent/ecscni/plugin_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -28,14 +29,14 @@ import ( const ( // ECSCNIVersion, ECSCNIGitHash, VPCCNIGitHash needs to be updated every time CNI plugin is updated. currentECSCNIVersion = "2020.09.0" - currentECSCNIGitHash = "55b2ae77ee0bf22321b14f2d4ebbcc04f77322e1" - currentVPCCNIGitHash = "199bfc65cced4951cbb6a38e6e828afa8c2b023c" + currentECSCNIGitHash = "13a9b8de6bbcbeab9624a66b4c7ad7008f02bcb6" + currentVPCCNIGitHash = "a2168cc8d07c97dc8dbc930b92bd14cb817531c0" ) // Asserts that CNI plugin version matches the expected version func TestCNIPluginVersionNumber(t *testing.T) { versionStr := getCNIVersionString(t) - assert.Equal(t, versionStr, currentECSCNIVersion) + assert.Equal(t, currentECSCNIVersion, versionStr) } // Asserts that CNI plugin version is upgraded when new commits are made to CNI plugin submodule diff --git a/agent/ecscni/plugin_unsupported.go b/agent/ecscni/plugin_unsupported.go index 67b2ba87548..e156b75c03e 100644 --- a/agent/ecscni/plugin_unsupported.go +++ b/agent/ecscni/plugin_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/plugin_windows.go b/agent/ecscni/plugin_windows.go index 948715e9542..48487026c2f 100644 --- a/agent/ecscni/plugin_windows.go +++ b/agent/ecscni/plugin_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -116,8 +117,7 @@ func (client *cniClient) doSetupNS(ctx context.Context, cfg *Config) (*current.R curResult, ok := ecsBridgeResult.(*current.Result) if !ok { return nil, errors.Errorf( - "cni setup: unable to convert result to expected version '%s'", - ecsBridgeResult.String()) + "cni setup: unable to convert result to expected version '%v'", ecsBridgeResult) } return curResult, nil diff --git a/agent/ecscni/plugin_windows_test.go b/agent/ecscni/plugin_windows_test.go index e82541f7ff5..04420ba7a7b 100644 --- a/agent/ecscni/plugin_windows_test.go +++ b/agent/ecscni/plugin_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/ecscni/types_linux.go b/agent/ecscni/types_linux.go index 1a07be23e0b..6d6ce353fb7 100644 --- a/agent/ecscni/types_linux.go +++ b/agent/ecscni/types_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -28,6 +29,11 @@ const ( // defaultAppMeshIfName is the default name of app mesh to setup iptable rules // for app mesh container. IfName is mandatory field to invoke CNI plugin. defaultAppMeshIfName = "aws-appmesh" + // defaultServiceConnectIfName is the default ifname used for invoking ServiceConnect CNI plugin. + // Even though the actual SC netns configuration does not require IfName, we still need to pass in a placeholder + // value because IfName is a mandatory field to invoke any CNI plugin. + // Additionally, the API spec requires that ifName length to be <= 15 chars. + defaultServiceConnectIfName = "ecs-sc" // ECSIPAMPluginName is the binary of the ipam plugin ECSIPAMPluginName = "ecs-ipam" // ECSBridgePluginName is the binary of the bridge plugin @@ -38,11 +44,17 @@ const ( ECSAppMeshPluginName = "aws-appmesh" // ECSBranchENIPluginName is the binary of the branch-eni plugin ECSBranchENIPluginName = "vpc-branch-eni" + // ECSServiceConnectPluginName is the binary of the service connect plugin + ECSServiceConnectPluginName = "ecs-serviceconnect" // NetnsFormat is used to construct the path to cotainer network namespace NetnsFormat = "/host/proc/%s/ns/net" + // Starting with CNI plugin v0.8.0 (this PR https://github.com/containernetworking/cni/pull/698) + // NetworkName has to be non-empty field for network config. + // We do not actually make use of the field, hence passing in a placeholder string to fulfill the API spec + defaultNetworkName = "network-name" ) -//IPAMNetworkConfig is the config format accepted by the plugin +// IPAMNetworkConfig is the config format accepted by the plugin type IPAMNetworkConfig struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` @@ -159,3 +171,55 @@ type BranchENIConfig struct { // InterfaceType is the type of the interface to connect the branch ENI to InterfaceType string `json:"interfaceType,omitempty"` } + +type ServiceConnectConfig struct { + // CNIVersion is the CNI spec version to use + CNIVersion string `json:"cniVersion,omitempty"` + // Name is the CNI network name + Name string `json:"name,omitempty"` + // Type is the CNI plugin name + Type string `json:"type,omitempty"` + + // IngressConfig (optional) specifies the netfilter rules to be set for incoming requests. + IngressConfig []IngressConfigJSONEntry `json:"ingressConfig,omitempty"` + // EgressConfig (optional) specifies the netfilter rules to be set for outgoing requests. + EgressConfig *EgressConfigJSON `json:"egressConfig,omitempty"` + // EnableIPv4 (optional) specifies whether to set the rules in IPV4 table. Default value is false. + EnableIPv4 bool `json:"enableIPv4,omitempty"` + // EnableIPv6 (optional) specifies whether to set the rules in IPV6 table. Default value is false. + EnableIPv6 bool `json:"enableIPv6,omitempty"` +} + +// IngressConfig defines the ingress network config in JSON format for the ecs-serviceconnect CNI plugin. +type IngressConfigJSONEntry struct { + ListenerPort uint16 `json:"listenerPort"` + InterceptPort uint16 `json:"interceptPort,omitempty"` +} + +// RedirectMode defines the type of redirection of traffic to be used. +type RedirectMode string + +const ( + NAT RedirectMode = "nat" + TPROXY RedirectMode = "tproxy" +) + +// EgressConfig defines the egress network config in JSON format for the ecs-serviceconnect CNI plugin. +type EgressConfigJSON struct { + ListenerPort uint16 `json:"listenerPort"` + RedirectIP *RedirectIPJson `json:"redirectIP"` + RedirectMode string `json:"redirectMode"` + VIP VIPConfigJSON `json:"vip"` +} + +// RedirectIPJson defines the IP to be redirected in JSON format for the ecs-serviceconnect CNI plugin. +type RedirectIPJson struct { + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` +} + +// VIPConfigJSON defines the EgressVIP network config in JSON format for the ecs-serviceconnect CNI plugin. +type VIPConfigJSON struct { + IPv4CIDR string `json:"ipv4Cidr,omitempty"` + IPv6CIDR string `json:"ipv6Cidr,omitempty"` +} diff --git a/agent/ecscni/types_windows.go b/agent/ecscni/types_windows.go index 61871805a1b..69312c5a922 100644 --- a/agent/ecscni/types_windows.go +++ b/agent/ecscni/types_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -30,6 +31,12 @@ const ( TaskHNSNetworkNamePrefix = "task" // ECSBridgeNetworkName is the name of the HNS network used as ecs-bridge. ECSBridgeNetworkName = "nat" + // Starting with CNI plugin v0.8.0 (this PR https://github.com/containernetworking/cni/pull/698) + // NetworkName has to be non-empty field for network config. + // We do not actually make use of the field, hence passing in a placeholder string to fulfill the API spec + defaultNetworkName = "network-name" + // DefaultENIName is the name of eni interface name in the container namespace + DefaultENIName = "eth0" ) var ( diff --git a/agent/engine/common_integ_test.go b/agent/engine/common_integ_test.go index 1e6b65ec522..e16430bc791 100644 --- a/agent/engine/common_integ_test.go +++ b/agent/engine/common_integ_test.go @@ -1,4 +1,5 @@ //go:build sudo || integration +// +build sudo integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -39,8 +40,12 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ec2" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + engineserviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" + s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" + ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" "github.com/aws/amazon-ecs-agent/agent/statechange" + "github.com/aws/amazon-ecs-agent/agent/taskresource" log "github.com/cihub/seelog" "github.com/stretchr/testify/assert" ) @@ -84,6 +89,42 @@ func setupIntegTestLogs(t *testing.T) string { return testLogDir } +func setupGMSALinux(cfg *config.Config, state dockerstate.TaskEngineState, t *testing.T) (TaskEngine, func(), credentials.Manager) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + skipIntegTestIfApplicable(t) + + sdkClientFactory := sdkclientfactory.NewFactory(ctx, dockerEndpoint) + dockerClient, err := dockerapi.NewDockerGoClient(sdkClientFactory, cfg, context.Background()) + if err != nil { + t.Fatalf("Error creating Docker client: %v", err) + } + credentialsManager := credentials.NewManager() + if state == nil { + state = dockerstate.NewTaskEngineState() + } + imageManager := NewImageManager(cfg, dockerClient, state) + imageManager.SetDataClient(data.NewNoopClient()) + metadataManager := containermetadata.NewManager(dockerClient, cfg) + + resourceFields := &taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmfactory.NewSSMClientCreator(), + S3ClientCreator: s3factory.NewS3ClientCreator(), + }, + DockerClient: dockerClient, + } + + taskEngine := NewDockerTaskEngine(cfg, dockerClient, credentialsManager, + eventstream.NewEventStream("ENGINEINTEGTEST", context.Background()), imageManager, state, metadataManager, + resourceFields, execcmd.NewManager(), engineserviceconnect.NewManager()) + taskEngine.MustInit(context.TODO()) + return taskEngine, func() { + taskEngine.Shutdown() + }, credentialsManager +} + func loggerConfigIntegrationTest(logfile string) string { config := fmt.Sprintf(` @@ -168,7 +209,7 @@ func setup(cfg *config.Config, state dockerstate.TaskEngineState, t *testing.T) taskEngine := NewDockerTaskEngine(cfg, dockerClient, credentialsManager, eventstream.NewEventStream("ENGINEINTEGTEST", context.Background()), imageManager, state, metadataManager, - nil, execcmd.NewManager()) + nil, execcmd.NewManager(), engineserviceconnect.NewManager()) taskEngine.MustInit(context.TODO()) return taskEngine, func() { taskEngine.Shutdown() @@ -211,13 +252,14 @@ func waitForTaskCleanup(t *testing.T, taskEngine TaskEngine, taskArn string, sec // Organized first by EventType (Task or Container), // then by StatusType (i.e. RUNNING, STOPPED, etc) // then by Task/Container identifying string (TaskARN or ContainerName) -// EventType -// / \ -// TaskEvent ContainerEvent -// / \ / \ -// RUNNING STOPPED RUNNING STOPPED -// / \ / \ | | -// ARN1 ARN2 ARN3 ARN4 ARN:Cont1 ARN:Cont2 +// +// EventType +// / \ +// TaskEvent ContainerEvent +// / \ / \ +// RUNNING STOPPED RUNNING STOPPED +// / \ / \ | | +// ARN1 ARN2 ARN3 ARN4 ARN:Cont1 ARN:Cont2 type EventSet map[statechange.EventType]statusToName // Type definition for mapping a Status to a TaskARN/ContainerName diff --git a/agent/engine/common_test.go b/agent/engine/common_test.go index 5e160117e50..4e99aca4427 100644 --- a/agent/engine/common_test.go +++ b/agent/engine/common_test.go @@ -174,6 +174,8 @@ func validateContainerRunWorkflow(t *testing.T, dockerConfig.Env = append(dockerConfig.Env, "ECS_CONTAINER_METADATA_URI="+metadataEndpointEnvValue) metadataEndpointEnvValueV4 := fmt.Sprintf(apicontainer.MetadataURIFormatV4, v3EndpointID) dockerConfig.Env = append(dockerConfig.Env, "ECS_CONTAINER_METADATA_URI_V4="+metadataEndpointEnvValueV4) + agentAPIEndpointEnvValue := fmt.Sprintf(apicontainer.AgentURIFormat, v3EndpointID) + dockerConfig.Env = append(dockerConfig.Env, "ECS_AGENT_URI="+agentAPIEndpointEnvValue) } // Container config should get updated with this during CreateContainer dockerConfig.Labels["com.amazonaws.ecs.task-arn"] = task.Arn @@ -297,10 +299,10 @@ func waitForStopEvents(t *testing.T, stateChangeEvents <-chan statechange.Event, event := <-stateChangeEvents if cont := event.(api.ContainerStateChange); cont.Status != apicontainerstatus.ContainerStopped { - t.Fatal("Expected container to stop first") if verifyExitCode { assert.Equal(t, *cont.ExitCode, 1, "Exit code should be present") } + t.Fatal("Expected container to stop first") } event = <-stateChangeEvents assert.Equal(t, event.(api.TaskStateChange).Status, apitaskstatus.TaskStopped, "Expected task to be STOPPED") diff --git a/agent/engine/common_unix_integ_test.go b/agent/engine/common_unix_integ_test.go index 53d81706003..0b83295f7a6 100644 --- a/agent/engine/common_unix_integ_test.go +++ b/agent/engine/common_unix_integ_test.go @@ -1,4 +1,6 @@ //go:build linux && (sudo || integration) +// +build linux +// +build sudo integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/data_test.go b/agent/engine/data_test.go index 7b29e975632..049c190d31e 100644 --- a/agent/engine/data_test.go +++ b/agent/engine/data_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/default.go b/agent/engine/default.go index 27ef92eb28c..7e097ab17d3 100644 --- a/agent/engine/default.go +++ b/agent/engine/default.go @@ -22,6 +22,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/aws/amazon-ecs-agent/agent/taskresource" ) @@ -33,11 +34,12 @@ func NewTaskEngine(cfg *config.Config, client dockerapi.DockerClient, imageManager ImageManager, state dockerstate.TaskEngineState, metadataManager containermetadata.Manager, resourceFields *taskresource.ResourceFields, - execCmdMgr execcmd.Manager) TaskEngine { + execCmdMgr execcmd.Manager, + serviceConnectManager serviceconnect.Manager) TaskEngine { taskEngine := NewDockerTaskEngine(cfg, client, credentialsManager, containerChangeEventStream, imageManager, - state, metadataManager, resourceFields, execCmdMgr) + state, metadataManager, resourceFields, execCmdMgr, serviceConnectManager) return taskEngine } diff --git a/agent/engine/dependencygraph/graph.go b/agent/engine/dependencygraph/graph.go index 4444a0dcb98..7ef8d64f1ea 100644 --- a/agent/engine/dependencygraph/graph.go +++ b/agent/engine/dependencygraph/graph.go @@ -47,24 +47,45 @@ const ( var ( // CredentialsNotResolvedErr is the error where a container needs to wait for // credentials before it can process by agent - CredentialsNotResolvedErr = errors.New("dependency graph: container execution credentials not available") + CredentialsNotResolvedErr = &dependencyError{err: errors.New("dependency graph: container execution credentials not available")} // DependentContainerNotResolvedErr is the error where a dependent container isn't in expected state - DependentContainerNotResolvedErr = errors.New("dependency graph: dependent container not in expected state") + DependentContainerNotResolvedErr = &dependencyError{err: errors.New("dependency graph: dependent container not in expected state")} // ContainerPastDesiredStatusErr is the error where the container status is bigger than desired status - ContainerPastDesiredStatusErr = errors.New("container transition: container status is equal or greater than desired status") + ContainerPastDesiredStatusErr = &dependencyError{err: errors.New("container transition: container status is equal or greater than desired status")} // ErrContainerDependencyNotResolved is when the container's dependencies // on other containers are not resolved - ErrContainerDependencyNotResolved = errors.New("dependency graph: dependency on containers not resolved") + ErrContainerDependencyNotResolved = &dependencyError{err: errors.New("dependency graph: dependency on containers not resolved")} // ErrResourceDependencyNotResolved is when the container's dependencies // on task resources are not resolved - ErrResourceDependencyNotResolved = errors.New("dependency graph: dependency on resources not resolved") + ErrResourceDependencyNotResolved = &dependencyError{err: errors.New("dependency graph: dependency on resources not resolved")} // ResourcePastDesiredStatusErr is the error where the task resource known status is bigger than desired status - ResourcePastDesiredStatusErr = errors.New("task resource transition: task resource status is equal or greater than desired status") + ResourcePastDesiredStatusErr = &dependencyError{err: errors.New("task resource transition: task resource status is equal or greater than desired status")} // ErrContainerDependencyNotResolvedForResource is when the resource's dependencies // on other containers are not resolved - ErrContainerDependencyNotResolvedForResource = errors.New("dependency graph: resource's dependency on containers not resolved") + ErrContainerDependencyNotResolvedForResource = &dependencyError{err: errors.New("dependency graph: resource's dependency on containers not resolved")} ) +// DependencyError represents an error of a container dependency. These errors can be either terminal or non-terminal. +// Terminal dependency errors indicate that a given dependency can never be fulfilled (e.g. a container with a SUCCESS +// dependency has stopped with an exit code other than zero). +type DependencyError interface { + Error() string + IsTerminal() bool +} + +type dependencyError struct { + err error + isTerminal bool +} + +func (de *dependencyError) Error() string { + return de.err.Error() +} + +func (de *dependencyError) IsTerminal() bool { + return de.isTerminal +} + // ValidDependencies takes a task and verifies that it is possible to allow all // containers within it to reach the desired status by proceeding in some // order. @@ -124,7 +145,7 @@ func DependenciesAreResolved(target *apicontainer.Container, id string, manager credentials.Manager, resources []taskresource.TaskResource, - cfg *config.Config) (*apicontainer.DependsOn, error) { + cfg *config.Config) (*apicontainer.DependsOn, DependencyError) { if !executionCredentialsResolved(target, id, manager) { return nil, CredentialsNotResolvedErr } @@ -244,7 +265,7 @@ func verifyStatusResolvable(target *apicontainer.Container, existingContainers m // (map from name to container). The `resolves` function passed should return true if the named container is resolved. func verifyContainerOrderingStatusResolvable(target *apicontainer.Container, existingContainers map[string]*apicontainer.Container, - cfg *config.Config, resolves func(*apicontainer.Container, *apicontainer.Container, string, *config.Config) bool) (*apicontainer.DependsOn, error) { + cfg *config.Config, resolves func(*apicontainer.Container, *apicontainer.Container, string, *config.Config) bool) (*apicontainer.DependsOn, DependencyError) { targetGoal := target.GetDesiredStatus() targetKnown := target.GetKnownStatus() @@ -260,7 +281,7 @@ func verifyContainerOrderingStatusResolvable(target *apicontainer.Container, exi for _, dependency := range targetDependencies { dependencyContainer, ok := existingContainers[dependency.ContainerName] if !ok { - return nil, fmt.Errorf("dependency graph: container ordering dependency [%v] for target [%v] does not exist.", dependencyContainer, target) + return nil, &dependencyError{err: fmt.Errorf("dependency graph: container ordering dependency [%v] for target [%v] does not exist.", dependencyContainer, target), isTerminal: true} } // We want to check whether the dependency container has timed out only if target has not been created yet. @@ -268,7 +289,7 @@ func verifyContainerOrderingStatusResolvable(target *apicontainer.Container, exi // However, if dependency container has already stopped, then it cannot time out. if targetKnown < apicontainerstatus.ContainerCreated && dependencyContainer.GetKnownStatus() != apicontainerstatus.ContainerStopped { if hasDependencyTimedOut(dependencyContainer, dependency.Condition) { - return nil, fmt.Errorf("dependency graph: container ordering dependency [%v] for target [%v] has timed out.", dependencyContainer, target) + return nil, &dependencyError{err: fmt.Errorf("dependency graph: container ordering dependency [%v] for target [%v] has timed out.", dependencyContainer, target), isTerminal: true} } } @@ -276,13 +297,13 @@ func verifyContainerOrderingStatusResolvable(target *apicontainer.Container, exi // can then never progress to its desired state when the dependency condition is 'SUCCESS' if dependency.Condition == successCondition && dependencyContainer.GetKnownStatus() == apicontainerstatus.ContainerStopped && !hasDependencyStoppedSuccessfully(dependencyContainer) { - return nil, fmt.Errorf("dependency graph: failed to resolve container ordering dependency [%v] for target [%v] as dependency did not exit successfully.", dependencyContainer, target) + return nil, &dependencyError{err: fmt.Errorf("dependency graph: failed to resolve container ordering dependency [%v] for target [%v] as dependency did not exit successfully.", dependencyContainer, target), isTerminal: true} } // For any of the dependency conditions - START/COMPLETE/SUCCESS/HEALTHY, if the dependency container has // not started and will not start in the future, this dependency can never be resolved. if dependencyContainer.HasNotAndWillNotStart() { - return nil, fmt.Errorf("dependency graph: failed to resolve container ordering dependency [%v] for target [%v] because dependency will never start", dependencyContainer, target) + return nil, &dependencyError{err: fmt.Errorf("dependency graph: failed to resolve container ordering dependency [%v] for target [%v] because dependency will never start", dependencyContainer, target), isTerminal: true} } if !resolves(target, dependencyContainer, dependency.Condition, cfg) { @@ -290,14 +311,14 @@ func verifyContainerOrderingStatusResolvable(target *apicontainer.Container, exi } } if blockedDependency != nil { - return blockedDependency, fmt.Errorf("dependency graph: failed to resolve the container ordering dependency [%v] for target [%v]", blockedDependency, target) + return blockedDependency, &dependencyError{err: fmt.Errorf("dependency graph: failed to resolve the container ordering dependency [%v] for target [%v]", blockedDependency, target)} } return nil, nil } func verifyTransitionDependenciesResolved(target *apicontainer.Container, existingContainers map[string]*apicontainer.Container, - existingResources map[string]taskresource.TaskResource) error { + existingResources map[string]taskresource.TaskResource) DependencyError { if !verifyContainerDependenciesResolved(target, existingContainers) { return ErrContainerDependencyNotResolved @@ -471,7 +492,7 @@ func verifyContainerOrderingStatus(dependsOnContainer *apicontainer.Container) b dependsOnContainerDesiredStatus == dependsOnContainer.GetSteadyStateStatus() } -func verifyShutdownOrder(target *apicontainer.Container, existingContainers map[string]*apicontainer.Container) error { +func verifyShutdownOrder(target *apicontainer.Container, existingContainers map[string]*apicontainer.Container) DependencyError { // We considered adding this to the task state, but this will be at most 45 loops, // so we err'd on the side of having less state. missingShutdownDependencies := []string{} @@ -493,8 +514,8 @@ func verifyShutdownOrder(target *apicontainer.Container, existingContainers map[ return nil } - return fmt.Errorf("dependency graph: target %s needs other containers stopped before it can stop: [%s]", - target.Name, strings.Join(missingShutdownDependencies, "], [")) + return &dependencyError{err: fmt.Errorf("dependency graph: target %s needs other containers stopped before it can stop: [%s]", + target.Name, strings.Join(missingShutdownDependencies, "], ["))} } func onSteadyStateCanResolve(target *apicontainer.Container, run *apicontainer.Container) bool { diff --git a/agent/engine/dependencygraph/graph_test.go b/agent/engine/dependencygraph/graph_test.go index bf3fad7a446..af6d8d218f4 100644 --- a/agent/engine/dependencygraph/graph_test.go +++ b/agent/engine/dependencygraph/graph_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/dependencygraph/graph_unix_test.go b/agent/engine/dependencygraph/graph_unix_test.go index b777d4bec77..0c173e59e0e 100644 --- a/agent/engine/dependencygraph/graph_unix_test.go +++ b/agent/engine/dependencygraph/graph_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/docker_image_manager.go b/agent/engine/docker_image_manager.go index 77b8ae94903..263d0cc46d9 100644 --- a/agent/engine/docker_image_manager.go +++ b/agent/engine/docker_image_manager.go @@ -49,6 +49,7 @@ type ImageManager interface { GetImageStateFromImageName(containerImageName string) (*image.ImageState, bool) StartImageCleanupProcess(ctx context.Context) SetDataClient(dataClient data.Client) + AddImageToCleanUpExclusionList(image string) } // dockerImageManager accounts all the images and their states in the instance. @@ -112,6 +113,13 @@ func buildImageCleanupExclusionList(cfg *config.Config) []string { return excludedImages } +func (imageManager *dockerImageManager) AddImageToCleanUpExclusionList(image string) { + imageManager.imageCleanupExclusionList = append(imageManager.imageCleanupExclusionList, image) + logger.Info("Image excluded from cleanup", logger.Fields{ + field.Image: image, + }) +} + func (imageManager *dockerImageManager) AddAllImageStates(imageStates []*image.ImageState) { imageManager.updateLock.Lock() defer imageManager.updateLock.Unlock() @@ -300,7 +308,7 @@ func (imageManager *dockerImageManager) isImageOldEnough(imageState *image.Image return ageOfImage > imageManager.minimumAgeBeforeDeletion } -//TODO: change image createdTime to image lastUsedTime when docker support it in the future +// TODO: change image createdTime to image lastUsedTime when docker support it in the future func (imageManager *dockerImageManager) nonECSImageOldEnough(NonECSImage ImageWithSizeID) bool { ageOfImage := time.Since(NonECSImage.createdTime) return ageOfImage > imageManager.nonECSMinimumAgeBeforeDeletion diff --git a/agent/engine/docker_image_manager_data_test.go b/agent/engine/docker_image_manager_data_test.go index 05354ec8ae5..d322d52f16f 100644 --- a/agent/engine/docker_image_manager_data_test.go +++ b/agent/engine/docker_image_manager_data_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/docker_image_manager_integ_test.go b/agent/engine/docker_image_manager_integ_test.go index 1cd64a07e1a..031a7bd1415 100644 --- a/agent/engine/docker_image_manager_integ_test.go +++ b/agent/engine/docker_image_manager_integ_test.go @@ -1,4 +1,5 @@ //go:build integration +// +build integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -46,15 +47,16 @@ const ( ) // Deletion of images in the order of LRU time: Happy path -// a. This includes starting up agent, pull images, start containers, -// account them in image manager,  stop containers, remove containers, account this in image manager, -// b. Simulate the pulled time (so that it passes the minimum age criteria -// for getting chosen for deletion ) -// c. Start image cleanup , ensure that ONLY the top 2 eligible LRU images -// are removed from the instance,  and those deleted images’ image states are removed from image manager. -// d. Ensure images that do not pass the ‘minimumAgeForDeletion’ criteria are not removed. -// e. Image has not passed the ‘hasNoAssociatedContainers’ criteria. -// f. Ensure that that if not eligible, image is not deleted from the instance and image reference in ImageManager is not removed. +// +// a. This includes starting up agent, pull images, start containers, +// account them in image manager,  stop containers, remove containers, account this in image manager, +// b. Simulate the pulled time (so that it passes the minimum age criteria +// for getting chosen for deletion ) +// c. Start image cleanup , ensure that ONLY the top 2 eligible LRU images +// are removed from the instance,  and those deleted images’ image states are removed from image manager. +// d. Ensure images that do not pass the ‘minimumAgeForDeletion’ criteria are not removed. +// e. Image has not passed the ‘hasNoAssociatedContainers’ criteria. +// f. Ensure that that if not eligible, image is not deleted from the instance and image reference in ImageManager is not removed. func TestIntegImageCleanupHappyCase(t *testing.T) { if runtime.GOOS == "windows" { t.Skip(`Skipping this test because of error: level=error time=2020-05-27T20:20:03Z msg="Error removing` + @@ -163,9 +165,10 @@ func TestIntegImageCleanupHappyCase(t *testing.T) { } // Test that images not falling in the image deletion eligibility criteria are not removed: -// a. Ensure images that do not pass the ‘minimumAgeForDeletion’ criteria are not removed. -// b. Image has not passed the ‘hasNoAssociatedContainers’ criteria. -// c. Ensure that the image is not deleted from the instance and image reference in ImageManager is not removed. +// +// a. Ensure images that do not pass the ‘minimumAgeForDeletion’ criteria are not removed. +// b. Image has not passed the ‘hasNoAssociatedContainers’ criteria. +// c. Ensure that the image is not deleted from the instance and image reference in ImageManager is not removed. func TestIntegImageCleanupThreshold(t *testing.T) { cfg := defaultTestConfigIntegTest() cfg.TaskCleanupWaitDuration = 1 * time.Second diff --git a/agent/engine/docker_image_manager_test.go b/agent/engine/docker_image_manager_test.go index 06662ea9d59..91ca9a9e601 100644 --- a/agent/engine/docker_image_manager_test.go +++ b/agent/engine/docker_image_manager_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/docker_image_manager_unix_integ_test.go b/agent/engine/docker_image_manager_unix_integ_test.go index 849b9127d04..b9871ebb01e 100644 --- a/agent/engine/docker_image_manager_unix_integ_test.go +++ b/agent/engine/docker_image_manager_unix_integ_test.go @@ -1,4 +1,5 @@ //go:build !windows && integration +// +build !windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/docker_image_manager_windows_integ_test.go b/agent/engine/docker_image_manager_windows_integ_test.go index cade220651e..77a53c7c735 100644 --- a/agent/engine/docker_image_manager_windows_integ_test.go +++ b/agent/engine/docker_image_manager_windows_integ_test.go @@ -1,4 +1,5 @@ //go:build windows && integration +// +build windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/docker_task_engine.go b/agent/engine/docker_task_engine.go index 69d1e2d0d41..85a11a7d46a 100644 --- a/agent/engine/docker_task_engine.go +++ b/agent/engine/docker_task_engine.go @@ -25,12 +25,8 @@ import ( "sync" "time" - "github.com/aws/aws-sdk-go/aws" - - "github.com/aws/amazon-ecs-agent/agent/logger" - "github.com/aws/amazon-ecs-agent/agent/logger/field" - "github.com/aws/amazon-ecs-agent/agent/api" + "github.com/aws/amazon-ecs-agent/agent/api/appnet" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" @@ -46,7 +42,10 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dependencygraph" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" "github.com/aws/amazon-ecs-agent/agent/metrics" "github.com/aws/amazon-ecs-agent/agent/statechange" "github.com/aws/amazon-ecs-agent/agent/taskresource" @@ -56,9 +55,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/utils/retry" utilsync "github.com/aws/amazon-ecs-agent/agent/utils/sync" "github.com/aws/amazon-ecs-agent/agent/utils/ttime" - dockercontainer "github.com/docker/docker/api/types/container" - + "github.com/aws/aws-sdk-go/aws" "github.com/docker/docker/api/types" + dockercontainer "github.com/docker/docker/api/types/container" "github.com/pkg/errors" ) @@ -141,9 +140,10 @@ type DockerTaskEngine struct { events <-chan dockerapi.DockerContainerChangeEvent stateChangeEvents chan statechange.Event - client dockerapi.DockerClient - dataClient data.Client - cniClient ecscni.CNIClient + client dockerapi.DockerClient + dataClient data.Client + cniClient ecscni.CNIClient + appnetClient api.AppnetClient containerChangeEventStream *eventstream.EventStream @@ -161,6 +161,8 @@ type DockerTaskEngine struct { imageManager ImageManager containerStatusToTransitionFunction map[apicontainerstatus.ContainerStatus]transitionApplyFunc metadataManager containermetadata.Manager + serviceconnectManager serviceconnect.Manager + serviceconnectRelay *apitask.Task // taskSteadyStatePollInterval is the duration that a managed task waits // once the task gets into steady state before polling the state of all of @@ -196,7 +198,8 @@ func NewDockerTaskEngine(cfg *config.Config, state dockerstate.TaskEngineState, metadataManager containermetadata.Manager, resourceFields *taskresource.ResourceFields, - execCmdMgr execcmd.Manager) *DockerTaskEngine { + execCmdMgr execcmd.Manager, + serviceConnectManager serviceconnect.Manager) *DockerTaskEngine { dockerTaskEngine := &DockerTaskEngine{ cfg: cfg, client: client, @@ -212,8 +215,10 @@ func NewDockerTaskEngine(cfg *config.Config, containerChangeEventStream: containerChangeEventStream, imageManager: imageManager, cniClient: ecscni.NewClient(cfg.CNIPluginsPath), + appnetClient: appnet.Client(), metadataManager: metadataManager, + serviceconnectManager: serviceConnectManager, taskSteadyStatePollInterval: defaultTaskSteadyStatePollInterval, taskSteadyStatePollIntervalJitter: defaultTaskSteadyStatePollIntervalJitter, resourceFields: resourceFields, @@ -276,6 +281,7 @@ func (engine *DockerTaskEngine) Init(ctx context.Context) error { go engine.handleDockerEvents(derivedCtx) engine.initialized = true go engine.startPeriodicExecAgentsMonitoring(derivedCtx) + go engine.watchAppNetImage(derivedCtx) return nil } @@ -494,7 +500,8 @@ func (engine *DockerTaskEngine) filterTasksToStartUnsafe(tasks []*apitask.Task) return tasksToStart } -// updateContainerMetadata sets the container metadata from the docker inspect +// updateContainerMetadata sets the container metadata from the docker inspect, +// and update port mappings for bridge mode containers with service connect enabled func updateContainerMetadata(metadata *dockerapi.DockerContainerMetadata, container *apicontainer.Container, task *apitask.Task) { container.SetCreatedAt(metadata.CreatedAt) container.SetStartedAt(metadata.StartedAt) @@ -523,6 +530,25 @@ func updateContainerMetadata(metadata *dockerapi.DockerContainerMetadata, contai if len(metadata.PortBindings) != 0 && len(container.GetKnownPortBindings()) == 0 { container.SetKnownPortBindings(metadata.PortBindings) } + + // update port mappings for service connect bridge mode. + // For the bridge-mode ServiceConnect-enabled task, port mappings are applied to the pause container + // (~internal-ecs-pause-<$APP_CONTAINER>) instead of the application container (<$APP_CONTAINER>); therefore, + // we need to remap the port mappings from the associated pause container (~internal-ecs-pause-<$APP_CONTAINER>) + // to the application container (<$APP_CONTAINER>). + if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() && + !container.IsInternal() && len(container.Name) > 0 { + pauseContainer, err := task.GetBridgeModePauseContainerForTaskContainer(container) + if err != nil { + logger.Error("Error resolving pause container for bridge mode SC container", logger.Fields{ + field.Container: container.Name, + field.Error: err, + }) + } else { + container.SetKnownPortBindings(pauseContainer.GetKnownPortBindings()) + } + } + // update the container health information if container.HealthStatusShouldBeReported() { container.SetHealthStatus(metadata.Health) @@ -701,12 +727,22 @@ func (engine *DockerTaskEngine) deleteTask(task *apitask.Task) { } } + tID := task.GetID() if execcmd.IsExecEnabledTask(task) { // cleanup host exec agent log dirs - tID := task.GetID() if err := removeAll(filepath.Join(execcmd.ECSAgentExecLogDir, tID)); err != nil { logger.Warn("Unable to remove ExecAgent host logs for task", logger.Fields{ - field.TaskID: task.GetID(), + field.TaskID: tID, + field.Error: err, + }) + } + } + + if task.IsServiceConnectEnabled() { + serviceconnectConfig := task.GetServiceConnectRuntimeConfig() + if err := removeAll(filepath.Dir(serviceconnectConfig.AdminSocketPath)); err != nil { + logger.Warn("Unable to remove service-connect UDS bind mount path for task", logger.Fields{ + field.TaskID: tID, field.Error: err, }) } @@ -919,6 +955,26 @@ func (engine *DockerTaskEngine) AddTask(task *apitask.Task) { return } + // Check if ServiceConnect is Needed + if task.IsServiceConnectEnabled() { + if engine.serviceconnectRelay == nil { + engine.serviceconnectRelay, err = engine.serviceconnectManager.CreateInstanceTask(engine.cfg) + + if err != nil { + logger.Error("Unable to start relay for task in the engine", logger.Fields{ + field.TaskID: task.GetID(), + field.Error: err, + }) + task.SetKnownStatus(apitaskstatus.TaskStopped) + task.SetDesiredStatus(apitaskstatus.TaskStopped) + engine.emitTaskEvent(task, err.Error()) + return + } + engine.AddTask(engine.serviceconnectRelay) + logger.Info("docker_task_engine: Added AppNet Relay task to engine") + } + } + engine.tasksLock.Lock() defer engine.tasksLock.Unlock() @@ -960,8 +1016,12 @@ func (engine *DockerTaskEngine) GetTaskByArn(arn string) (*apitask.Task, bool) { func (engine *DockerTaskEngine) pullContainer(task *apitask.Task, container *apicontainer.Container) dockerapi.DockerContainerMetadata { switch container.Type { - case apicontainer.ContainerCNIPause, apicontainer.ContainerNamespacePause: - // pause images are managed at startup + case apicontainer.ContainerCNIPause, apicontainer.ContainerNamespacePause, apicontainer.ContainerServiceConnectRelay: + // pause images and AppNet relay image are managed at startup + return dockerapi.DockerContainerMetadata{} + } + // AppNet Agent container image is also managed at start up (it uses the same image as AppNet Relay container) + if task.IsServiceConnectEnabled() && container == task.GetServiceConnectContainer() { return dockerapi.DockerContainerMetadata{} } @@ -1019,6 +1079,12 @@ func (engine *DockerTaskEngine) imagePullRequired(imagePullBehavior config.Image // by inspecting the image. _, err := engine.client.InspectImage(container.Image) if err != nil { + logger.Info("Image inspect returned error, going to pull image for container", logger.Fields{ + field.TaskID: taskId, + field.Container: container.Name, + field.Image: container.Image, + field.Error: err.Error(), + }) return true } logger.Info("Found cached image, use it directly for container", logger.Fields{ @@ -1252,6 +1318,20 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a return dockerapi.DockerContainerMetadata{Error: apierrors.NamedError(hcerr)} } + // Add Service Connect modifications if needed + if task.IsServiceConnectEnabled() { + err := engine.serviceconnectManager.AugmentTaskContainer(task, container, hostConfig) + if err != nil { + return dockerapi.DockerContainerMetadata{Error: apierrors.NewNamedError(err)} + } + } + if container.Type == apicontainer.ContainerServiceConnectRelay { + err := engine.serviceconnectManager.AugmentInstanceContainer(task, container, hostConfig) + if err != nil { + return dockerapi.DockerContainerMetadata{Error: apierrors.NewNamedError(err)} + } + } + if container.AWSLogAuthExecutionRole() { err := task.ApplyExecutionRoleLogsAuth(hostConfig, engine.credentialsManager) if err != nil { @@ -1333,29 +1413,14 @@ func (engine *DockerTaskEngine) createContainer(task *apitask.Task, container *a containerCredSpec, err := container.GetCredentialSpec() if err == nil && containerCredSpec != "" { - // CredentialSpec mapping: input := credentialspec:file://test.json, output := credentialspec=file://test.json + // on windows CredentialSpec mapping: input := credentialspec:file://test.json, output := credentialspec=file://test.json + // on linux CredentialSpec mapping: input := ssm/asm arn, output := /var/credentials-fetcher/krbdir/123456/ccname_webapp01_xyz desiredCredSpecInjection, err := credSpecResource.GetTargetMapping(containerCredSpec) if err != nil || desiredCredSpecInjection == "" { missingErr := &apierrors.DockerClientConfigError{Msg: "unable to fetch valid credentialspec mapping"} return dockerapi.DockerContainerMetadata{Error: apierrors.NamedError(missingErr)} } - - // Inject containers' hostConfig.SecurityOpt with the credentialspec resource - logger.Info("Injecting container with credentialspec", logger.Fields{ - field.TaskID: task.GetID(), - field.Container: container.Name, - "credentialSpec": desiredCredSpecInjection, - }) - if len(hostConfig.SecurityOpt) == 0 { - hostConfig.SecurityOpt = []string{desiredCredSpecInjection} - } else { - for idx, opt := range hostConfig.SecurityOpt { - if strings.HasPrefix(opt, "credentialspec:") { - hostConfig.SecurityOpt[idx] = desiredCredSpecInjection - } - } - } - + engine.updateCredentialSpecMapping(task.GetID(), container.Name, desiredCredSpecInjection, hostConfig) } else { emptyErr := &apierrors.DockerClientConfigError{Msg: "unable to fetch valid credentialspec: " + err.Error()} return dockerapi.DockerContainerMetadata{Error: apierrors.NamedError(emptyErr)} @@ -1626,10 +1691,22 @@ func (engine *DockerTaskEngine) startContainer(task *apitask.Task, container *ap if err != nil { return dockerapi.DockerContainerMetadata{ Error: ContainerNetworkingError{ - fromError: errors.Wrapf(err, "startContainer: cni plugin invocation failed"), + fromError: fmt.Errorf("startContainer: cni plugin invocation failed: %+v", err), + }, + } + } + } + + if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() && task.IsContainerServiceConnectPause(container.Name) { + ipv4Addr, ipv6Addr := getBridgeModeContainerIP(dockerContainerMD.NetworkSettings) + if ipv4Addr == "" && ipv6Addr == "" { + return dockerapi.DockerContainerMetadata{ + Error: ContainerNetworkingError{ + fromError: fmt.Errorf("startContainer: failed to resolve container IP for SC bridge mode pause container"), }, } } + task.PopulateServiceConnectNetworkConfig(ipv4Addr, ipv6Addr) } return dockerContainerMD @@ -1640,24 +1717,33 @@ func (engine *DockerTaskEngine) provisionContainerResources(task *apitask.Task, field.TaskID: task.GetID(), field.Container: container.Name, }) + if task.IsNetworkModeAWSVPC() { + return engine.provisionContainerResourcesAwsvpc(task, container) + } else if task.IsNetworkModeBridge() { + return engine.provisionContainerResourcesBridgeMode(task, container) + } + return dockerapi.DockerContainerMetadata{} +} + +func (engine *DockerTaskEngine) provisionContainerResourcesAwsvpc(task *apitask.Task, container *apicontainer.Container) dockerapi.DockerContainerMetadata { containerInspectOutput, err := engine.inspectContainer(task, container) if err != nil { return dockerapi.DockerContainerMetadata{ Error: ContainerNetworkingError{ - fromError: errors.Wrap(err, - "container resource provisioning: cannot setup task network namespace due to error inspecting pause container"), + fromError: fmt.Errorf( + "container resource provisioning: cannot setup task network namespace due to error inspecting pause container: %+v", err), }, } } task.SetPausePIDInVolumeResources(strconv.Itoa(containerInspectOutput.State.Pid)) - cniConfig, err := engine.buildCNIConfigFromTaskContainer(task, containerInspectOutput, true) + cniConfig, err := engine.buildCNIConfigFromTaskContainerAwsvpc(task, containerInspectOutput, true) if err != nil { return dockerapi.DockerContainerMetadata{ Error: ContainerNetworkingError{ - fromError: errors.Wrap(err, - "container resource provisioning: unable to build cni configuration"), + fromError: fmt.Errorf( + "container resource provisioning: unable to build cni configuration, %+v", err), }, } } @@ -1671,8 +1757,19 @@ func (engine *DockerTaskEngine) provisionContainerResources(task *apitask.Task, }) return dockerapi.DockerContainerMetadata{ DockerID: cniConfig.ContainerID, - Error: ContainerNetworkingError{errors.Wrap(err, - "container resource provisioning: failed to setup network namespace")}, + Error: ContainerNetworkingError{fmt.Errorf( + "container resource provisioning: failed to setup network namespace: %+v", err)}, + } + } + + if result == nil { + logger.Error("Expect non-empty result from network namespace setup", logger.Fields{ + field.TaskID: task.GetID(), + }) + return dockerapi.DockerContainerMetadata{ + DockerID: cniConfig.ContainerID, + Error: ContainerNetworkingError{fmt.Errorf( + "container resource provisioning: empty result from network namespace setup")}, } } @@ -1695,20 +1792,67 @@ func (engine *DockerTaskEngine) provisionContainerResources(task *apitask.Task, }) return dockerapi.DockerContainerMetadata{ DockerID: cniConfig.ContainerID, - Error: ContainerNetworkingError{errors.Wrapf(err, - "container resource provisioning: failed to setup network namespace")}, + Error: ContainerNetworkingError{fmt.Errorf( + "container resource provisioning: failed to setup network namespace: %+v", err)}, + } + } + + return dockerapi.MetadataFromContainer(containerInspectOutput) +} + +func (engine *DockerTaskEngine) provisionContainerResourcesBridgeMode(task *apitask.Task, container *apicontainer.Container) dockerapi.DockerContainerMetadata { + if !task.IsServiceConnectEnabled() || container.Type != apicontainer.ContainerCNIPause { + return dockerapi.DockerContainerMetadata{ + Error: ContainerNetworkingError{fromError: fmt.Errorf( + "container resource provisioning bridge mode: cannot setup netns - only valid for SC-enabled task pause container"), + }, + } + } + + containerInspectOutput, err := engine.inspectContainer(task, container) + if err != nil || containerInspectOutput == nil { + return dockerapi.DockerContainerMetadata{ + Error: ContainerNetworkingError{fromError: fmt.Errorf( + "container resource provisioning bridge mode: cannot setup netns - error inspecting container %s: %+v", container.Name, err), + }, } } - return dockerapi.DockerContainerMetadata{ - DockerID: cniConfig.ContainerID, + cniConfig, err := engine.buildCNIConfigFromTaskContainerBridgeMode(task, containerInspectOutput, container.Name) + if err != nil { + return dockerapi.DockerContainerMetadata{ + Error: ContainerNetworkingError{fromError: fmt.Errorf( + "container resource provisioning bridge mode: unable to build cni configuration for container %s: %+v", container.Name, err), + }, + } + } + + // Invoke the libcni to config the network namespace for the container + _, err = engine.cniClient.SetupNS(engine.ctx, cniConfig, cniSetupTimeout) + + if err != nil { + logger.Error("Unable to configure pause container namespace", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + field.Error: err, + }) + return dockerapi.DockerContainerMetadata{ + DockerID: cniConfig.ContainerID, + Error: ContainerNetworkingError{fmt.Errorf("container resource provisioning: failed to setup network namespace: %+v", err)}, + } } + + logger.Info("Successfully configured pause netns", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + }) + return dockerapi.MetadataFromContainer(containerInspectOutput) } // checkTearDownPauseContainer idempotently tears down the pause container network when the pause container's known -//or desired status is stopped. +// or desired status is stopped. func (engine *DockerTaskEngine) checkTearDownPauseContainer(task *apitask.Task) { - if !task.IsNetworkModeAWSVPC() { + if !task.IsNetworkModeAWSVPC() || (task.IsNetworkModeBridge() && !task.IsServiceConnectEnabled()) { return } for _, container := range task.Containers { @@ -1738,8 +1882,9 @@ func (engine *DockerTaskEngine) cleanupPauseContainerNetwork(task *apitask.Task, delay := time.Duration(engine.cfg.ENIPauseContainerCleanupDelaySeconds) * time.Second if engine.handleDelay != nil && delay > 0 { logger.Info("Waiting before cleaning up pause container", logger.Fields{ - field.TaskID: task.GetID(), - "wait": delay.String(), + field.TaskID: task.GetID(), + field.Container: container.Name, + "wait": delay.String(), }) engine.handleDelay(delay) } @@ -1749,9 +1894,19 @@ func (engine *DockerTaskEngine) cleanupPauseContainerNetwork(task *apitask.Task, } logger.Info("Cleaning up the network namespace", logger.Fields{ - field.TaskID: task.GetID(), + field.TaskID: task.GetID(), + field.Container: container.Name, }) - cniConfig, err := engine.buildCNIConfigFromTaskContainer(task, containerInspectOutput, false) + + var cniConfig *ecscni.Config + if task.IsNetworkModeAWSVPC() { + cniConfig, err = engine.buildCNIConfigFromTaskContainerAwsvpc(task, containerInspectOutput, false) + } else if task.IsNetworkModeBridge() && task.IsServiceConnectEnabled() { + cniConfig, err = engine.buildCNIConfigFromTaskContainerBridgeMode(task, containerInspectOutput, container.Name) + } else { + return nil + } + if err != nil { return errors.Wrapf(err, "engine: failed cleanup task network namespace, task: %s", task.String()) @@ -1764,13 +1919,14 @@ func (engine *DockerTaskEngine) cleanupPauseContainerNetwork(task *apitask.Task, container.SetContainerTornDown(true) logger.Info("Cleaned pause container network namespace", logger.Fields{ - field.TaskID: task.GetID(), + field.TaskID: task.GetID(), + field.Container: container.Name, }) return nil } -// buildCNIConfigFromTaskContainer builds a CNI config for the task and container. -func (engine *DockerTaskEngine) buildCNIConfigFromTaskContainer( +// buildCNIConfigFromTaskContainerAwsvpc builds a CNI config for the task and container in AWSVPC mode. +func (engine *DockerTaskEngine) buildCNIConfigFromTaskContainerAwsvpc( task *apitask.Task, containerInspectOutput *types.ContainerJSON, includeIPAMConfig bool) (*ecscni.Config, error) { @@ -1802,7 +1958,25 @@ func (engine *DockerTaskEngine) buildCNIConfigFromTaskContainer( return nil, errors.New("engine: failed to build cni configuration from the task due to invalid container network namespace") } - cniConfig, err := task.BuildCNIConfig(includeIPAMConfig, cniConfig) + cniConfig, err := task.BuildCNIConfigAwsvpc(includeIPAMConfig, cniConfig) + if err != nil { + return nil, errors.Wrapf(err, "engine: failed to build cni configuration from task") + } + + return cniConfig, nil +} + +// buildCNIConfigFromTaskContainerBridgeMode builds a CNI config for the task and container in docker bridge mode. +func (engine *DockerTaskEngine) buildCNIConfigFromTaskContainerBridgeMode( + task *apitask.Task, containerInspectOutput *types.ContainerJSON, containerName string) (*ecscni.Config, error) { + + containerPid := strconv.Itoa(containerInspectOutput.State.Pid) + cniConfig := &ecscni.Config{ + MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion, + ContainerPID: containerPid, + ContainerID: containerInspectOutput.ID, + } + cniConfig, err := task.BuildCNIConfigBridgeMode(cniConfig, containerName) if err != nil { return nil, errors.Wrapf(err, "engine: failed to build cni configuration from task") } @@ -1820,6 +1994,22 @@ func (engine *DockerTaskEngine) inspectContainer(task *apitask.Task, container * } func (engine *DockerTaskEngine) stopContainer(task *apitask.Task, container *apicontainer.Container) dockerapi.DockerContainerMetadata { + // Before attempting to stop any container, send drain signal for Appnet Agent to start draining connections + // (if not already in progress). + if task.IsServiceConnectEnabled() && !task.IsServiceConnectConnectionDraining() { + if err := engine.appnetClient.DrainInboundConnections(task.GetServiceConnectRuntimeConfig()); err != nil { + logger.Error("Error sending drain signal to Appnet Agent", logger.Fields{ + field.TaskID: task.GetID(), + field.Error: err, + }) + } else { + task.SetServiceConnectConnectionDraining(true) + logger.Debug("Successfully sent drain signal to Appnet Agent", logger.Fields{ + field.TaskID: task.GetID(), + }) + } + } + logger.Info("Stopping container", logger.Fields{ field.TaskID: task.GetID(), field.Container: container.Name, @@ -1835,12 +2025,15 @@ func (engine *DockerTaskEngine) stopContainer(task *apitask.Task, container *api // Cleanup the pause container network namespace before stop the container if container.Type == apicontainer.ContainerCNIPause { - err := engine.cleanupPauseContainerNetwork(task, container) - if err != nil { - logger.Error("Unable to cleanup pause container network namespace", logger.Fields{ - field.TaskID: task.GetID(), - field.Error: err, - }) + if task.IsNetworkModeAWSVPC() || (task.IsNetworkModeBridge() && task.IsServiceConnectEnabled()) { + err := engine.cleanupPauseContainerNetwork(task, container) + if err != nil { + logger.Error("Unable to cleanup pause container network namespace", logger.Fields{ + field.TaskID: task.GetID(), + field.Container: container.Name, + field.Error: err, + }) + } } } @@ -2036,6 +2229,16 @@ func getContainerHostIP(networkSettings *types.NetworkSettings) (string, bool) { return "", false } +func getBridgeModeContainerIP(networkSettings *types.NetworkSettings) (string, string) { + if networkSettings != nil && + networkSettings.Networks != nil && + networkSettings.Networks[apitask.BridgeNetworkMode] != nil { + return networkSettings.Networks[apitask.BridgeNetworkMode].IPAddress, + networkSettings.Networks[apitask.BridgeNetworkMode].GlobalIPv6Address + } + return "", "" +} + func (engine *DockerTaskEngine) getDockerID(task *apitask.Task, container *apicontainer.Container) (string, error) { runtimeID := container.GetRuntimeID() if runtimeID != "" { diff --git a/agent/engine/docker_task_engine_linux.go b/agent/engine/docker_task_engine_linux.go index 1d27300f34a..2a566db8964 100644 --- a/agent/engine/docker_task_engine_linux.go +++ b/agent/engine/docker_task_engine_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,16 +17,29 @@ package engine import ( + "context" + "fmt" + "strings" "time" + "github.com/fsnotify/fsnotify" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/utils" + dockercontainer "github.com/docker/docker/api/types/container" ) const ( // Constants for CNI timeout during setup and cleanup. cniSetupTimeout = 1 * time.Minute cniCleanupTimeout = 30 * time.Second + + defaultKerberosTicketBindPath = "/var/credentials-fetcher/krbdir" + readOnly = ":ro" ) // updateTaskENIDependencies updates the task's dependencies for awsvpc networking mode. @@ -38,3 +52,105 @@ func (engine *DockerTaskEngine) updateTaskENIDependencies(task *apitask.Task) { func (engine *DockerTaskEngine) invokePluginsForContainer(task *apitask.Task, container *apicontainer.Container) error { return nil } + +func (engine *DockerTaskEngine) watchAppNetImage(ctx context.Context) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + logger.Error(fmt.Sprintf("failed to initialize fsnotify NewWatcher, error: %v", err)) + } + appnetContainerTarballDir := engine.serviceconnectManager.GetAppnetContainerTarballDir() + err = watcher.Add(appnetContainerTarballDir) + if err != nil { + logger.Error(fmt.Sprintf("error adding %s to fsnotify watcher, error: %v", appnetContainerTarballDir, err)) + } + defer watcher.Close() + + // Start listening for events. + for { + select { + case event, ok := <-watcher.Events: + if !ok { + logger.Warn("fsnotify event watcher channel is closed") + return + } + // check if the event file operation is write or create + const writeOrCreateMask = fsnotify.Write | fsnotify.Create + if event.Op&writeOrCreateMask != 0 { + logger.Debug(fmt.Sprintf("new fsnotify watcher event: %s", event.Name)) + // reload the updated Appnet Agent image + if err := engine.reloadAppNetImage(); err == nil { + // restart the internal instance relay task with + // updated Appnet Agent image + engine.restartInstanceTask() + } + } + case err, ok := <-watcher.Errors: + if !ok { + logger.Warn("fsnotify event watcher channel is closed") + return + } + logger.Error(fmt.Sprintf("fsnotify watcher error: %v", err)) + case <-ctx.Done(): + return + } + } +} + +func (engine *DockerTaskEngine) reloadAppNetImage() error { + _, err := engine.serviceconnectManager.LoadImage(engine.ctx, engine.cfg, engine.client) + if err != nil { + logger.Error(fmt.Sprintf("engine: Failed to reload appnet Agent container, error: %v", err)) + return err + } + return nil +} + +func (engine *DockerTaskEngine) restartInstanceTask() { + if engine.serviceconnectRelay != nil { + serviceconnectRelayTask, err := engine.serviceconnectManager.CreateInstanceTask(engine.cfg) + if err != nil { + logger.Error(fmt.Sprintf("Unable to start relay for task in the engine: %v", err)) + return + } + // clean up instance relay task + for _, container := range engine.serviceconnectRelay.Containers { + if container.Type == apicontainer.ContainerServiceConnectRelay { + engine.stopContainer(engine.serviceconnectRelay, container) + } + } + engine.serviceconnectRelay.SetDesiredStatus(apitaskstatus.TaskStopped) + engine.sweepTask(engine.serviceconnectRelay) + engine.deleteTask(engine.serviceconnectRelay) + + engine.serviceconnectRelay = serviceconnectRelayTask + engine.AddTask(engine.serviceconnectRelay) + logger.Info("engine: Restarted AppNet Relay task") + } +} + +// updateCredentialSpecMapping is used to map the bind location of kerberos ticket to the target location on the application container +func (engine *DockerTaskEngine) updateCredentialSpecMapping(taskID string, containerName string, desiredCredSpecInjection string, hostConfig *dockercontainer.HostConfig) { + // Inject containers' hostConfig.Bind with the kerberos ticket bind + logger.Info("Injecting container with kerberos ticket bind", logger.Fields{ + field.TaskID: taskID, + field.Container: containerName, + "kerberos ticket path": desiredCredSpecInjection, + }) + + // Inject containers' hostConfig.BindMount with the kerberos ticket location + bindMountKerberosTickets := desiredCredSpecInjection + ":" + defaultKerberosTicketBindPath + readOnly + if len(hostConfig.Binds) == 0 { + hostConfig.Binds = []string{bindMountKerberosTickets} + } else { + hostConfig.Binds = append(hostConfig.Binds, bindMountKerberosTickets) + } + + if len(hostConfig.SecurityOpt) != 0 { + for idx, opt := range hostConfig.SecurityOpt { + // credentialspec security opt is not supported by docker on linux + if strings.HasPrefix(opt, "credentialspec:") { + hostConfig.SecurityOpt = utils.Remove(hostConfig.SecurityOpt, idx) + } + } + } +} diff --git a/agent/engine/docker_task_engine_linux_test.go b/agent/engine/docker_task_engine_linux_test.go index fb1efc4aded..49ca3aaf0c4 100644 --- a/agent/engine/docker_task_engine_linux_test.go +++ b/agent/engine/docker_task_engine_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -19,6 +20,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "strconv" "strings" "sync" @@ -29,28 +31,37 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + mock_api "github.com/aws/amazon-ecs-agent/agent/api/mocks" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/data" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + mock_dockerapi "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi/mocks" "github.com/aws/amazon-ecs-agent/agent/ecscni" mock_ecscni "github.com/aws/amazon-ecs-agent/agent/ecscni/mocks" mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" + mock_serviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" "github.com/aws/amazon-ecs-agent/agent/engine/testdata" + mock_s3_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" + mock_ssm_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup" "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/mock_control" + "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" "github.com/aws/amazon-ecs-agent/agent/taskresource/firelens" "github.com/aws/amazon-ecs-agent/agent/taskresource/ssmsecret" resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" mock_ioutilwrapper "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper/mocks" - "github.com/aws/aws-sdk-go/aws" - "github.com/golang/mock/gomock" + "github.com/aws/aws-sdk-go/aws" + "github.com/containernetworking/cni/pkg/types/current" "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/golang/mock/gomock" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,7 +85,7 @@ func init() { func TestResourceContainerProgression(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") @@ -86,8 +97,10 @@ func TestResourceContainerProgression(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) taskID := sleepTask.GetID() - cgroupMemoryPath := fmt.Sprintf("/sys/fs/cgroup/memory/ecs/%s/memory.use_hierarchy", taskID) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } cgroupResource := cgroup.NewCgroupResource(sleepTask.Arn, mockControl, mockIO, cgroupRoot, cgroupMountPath, specs.LinuxResources{}) sleepTask.ResourcesMapUnsafe = make(map[string][]taskresource.TaskResource) @@ -98,35 +111,70 @@ func TestResourceContainerProgression(t *testing.T) { // events are processed containerEventsWG := sync.WaitGroup{} client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) - gomock.InOrder( - // Ensure that the resource is created first - mockControl.EXPECT().Exists(gomock.Any()).Return(false), - mockControl.EXPECT().Create(gomock.Any()).Return(nil, nil), - mockIO.EXPECT().WriteFile(cgroupMemoryPath, gomock.Any(), gomock.Any()).Return(nil), - imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes(), - client.EXPECT().PullImage(gomock.Any(), sleepContainer.Image, nil, gomock.Any()).Return(dockerapi.DockerContainerMetadata{}), - imageManager.EXPECT().RecordContainerReference(sleepContainer).Return(nil), - imageManager.EXPECT().GetImageStateFromImageName(sleepContainer.Image).Return(nil, false), - client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), - client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( - func(ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, containerName string, z time.Duration) { - assert.True(t, strings.Contains(containerName, sleepContainer.Name)) - containerEventsWG.Add(1) - go func() { - eventStream <- createDockerEvent(apicontainerstatus.ContainerCreated) - containerEventsWG.Done() - }() - }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), - // Next, the sleep container is started - client.EXPECT().StartContainer(gomock.Any(), containerID+":"+sleepContainer.Name, defaultConfig.ContainerStartTimeout).Do( - func(ctx interface{}, id string, timeout time.Duration) { - containerEventsWG.Add(1) - go func() { - eventStream <- createDockerEvent(apicontainerstatus.ContainerRunning) - containerEventsWG.Done() - }() - }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), - ) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + + // Hierarchical memory accounting is always enabled in CgroupV2 and no controller file exists to configure it + if config.CgroupV2 { + gomock.InOrder( + // Ensure that the resource is created first + mockControl.EXPECT().Exists(gomock.Any()).Return(false), + mockControl.EXPECT().Create(gomock.Any()).Return(nil), + imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes(), + client.EXPECT().PullImage(gomock.Any(), sleepContainer.Image, nil, gomock.Any()).Return(dockerapi.DockerContainerMetadata{}), + imageManager.EXPECT().RecordContainerReference(sleepContainer).Return(nil), + imageManager.EXPECT().GetImageStateFromImageName(sleepContainer.Image).Return(nil, false), + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), + client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( + func(ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, containerName string, z time.Duration) { + assert.True(t, strings.Contains(containerName, sleepContainer.Name)) + containerEventsWG.Add(1) + go func() { + eventStream <- createDockerEvent(apicontainerstatus.ContainerCreated) + containerEventsWG.Done() + }() + }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), + // Next, the sleep container is started + client.EXPECT().StartContainer(gomock.Any(), containerID+":"+sleepContainer.Name, defaultConfig.ContainerStartTimeout).Do( + func(ctx interface{}, id string, timeout time.Duration) { + containerEventsWG.Add(1) + go func() { + eventStream <- createDockerEvent(apicontainerstatus.ContainerRunning) + containerEventsWG.Done() + }() + }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), + ) + } else { + cgroupMemoryPath := fmt.Sprintf("/sys/fs/cgroup/memory/ecs/%s/memory.use_hierarchy", taskID) + gomock.InOrder( + // Ensure that the resource is created first + mockControl.EXPECT().Exists(gomock.Any()).Return(false), + mockControl.EXPECT().Create(gomock.Any()).Return(nil), + mockIO.EXPECT().WriteFile(cgroupMemoryPath, gomock.Any(), gomock.Any()).Return(nil), + imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes(), + client.EXPECT().PullImage(gomock.Any(), sleepContainer.Image, nil, gomock.Any()).Return(dockerapi.DockerContainerMetadata{}), + imageManager.EXPECT().RecordContainerReference(sleepContainer).Return(nil), + imageManager.EXPECT().GetImageStateFromImageName(sleepContainer.Image).Return(nil, false), + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil), + client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( + func(ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, containerName string, z time.Duration) { + assert.True(t, strings.Contains(containerName, sleepContainer.Name)) + containerEventsWG.Add(1) + go func() { + eventStream <- createDockerEvent(apicontainerstatus.ContainerCreated) + containerEventsWG.Done() + }() + }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), + // Next, the sleep container is started + client.EXPECT().StartContainer(gomock.Any(), containerID+":"+sleepContainer.Name, defaultConfig.ContainerStartTimeout).Do( + func(ctx interface{}, id string, timeout time.Duration) { + containerEventsWG.Add(1) + go func() { + eventStream <- createDockerEvent(apicontainerstatus.ContainerRunning) + containerEventsWG.Done() + }() + }).Return(dockerapi.DockerContainerMetadata{DockerID: containerID + ":" + sleepContainer.Name}), + ) + } addTaskToEngine(t, ctx, taskEngine, sleepTask, mockTime, &containerEventsWG) cleanup := make(chan time.Time, 1) @@ -243,7 +291,7 @@ func TestDeleteTaskBranchENIEnabled(t *testing.T) { func TestResourceContainerProgressionFailure(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") sleepContainer := sleepTask.Containers[0] @@ -254,16 +302,20 @@ func TestResourceContainerProgressionFailure(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) taskID := sleepTask.GetID() cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } cgroupResource := cgroup.NewCgroupResource(sleepTask.Arn, mockControl, nil, cgroupRoot, cgroupMountPath, specs.LinuxResources{}) sleepTask.ResourcesMapUnsafe = make(map[string][]taskresource.TaskResource) sleepTask.AddResource("cgroup", cgroupResource) eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() gomock.InOrder( // resource creation failure mockControl.EXPECT().Exists(gomock.Any()).Return(false), - mockControl.EXPECT().Create(gomock.Any()).Return(nil, errors.New("cgroup create error")), + mockControl.EXPECT().Create(gomock.Any()).Return(errors.New("cgroup create error")), ) mockTime.EXPECT().Now().Return(time.Now()).AnyTimes() @@ -300,7 +352,7 @@ func TestTaskCPULimitHappyPath(t *testing.T) { metadataConfig.ContainerMetadataEnabled = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager := mocks( + ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager, serviceConnectManager := mocks( t, ctx, &metadataConfig) defer ctrl.Finish() @@ -321,6 +373,7 @@ func TestTaskCPULimitHappyPath(t *testing.T) { containerEventsWG := sync.WaitGroup{} client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() containerName := make(chan string) go func() { name := <-containerName @@ -329,7 +382,6 @@ func TestTaskCPULimitHappyPath(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) taskID := sleepTask.GetID() - cgroupMemoryPath := fmt.Sprintf("/sys/fs/cgroup/memory/ecs/%s/memory.use_hierarchy", taskID) if tc.taskCPULimit.Enabled() { // TODO Currently, the resource Setup() method gets invoked multiple // times for a task. This is really a bug and a fortunate occurrence @@ -345,8 +397,11 @@ func TestTaskCPULimitHappyPath(t *testing.T) { }, } mockControl.EXPECT().Exists(gomock.Any()).Return(false) - mockControl.EXPECT().Create(gomock.Any()).Return(nil, nil) - mockIO.EXPECT().WriteFile(cgroupMemoryPath, gomock.Any(), gomock.Any()).Return(nil) + mockControl.EXPECT().Create(gomock.Any()).Return(nil) + if !config.CgroupV2 { + cgroupMemoryPath := fmt.Sprintf("/sys/fs/cgroup/memory/ecs/%s/memory.use_hierarchy", taskID) + mockIO.EXPECT().WriteFile(cgroupMemoryPath, gomock.Any(), gomock.Any()).Return(nil) + } } for _, container := range sleepTask.Containers { @@ -398,6 +453,9 @@ func TestTaskCPULimitHappyPath(t *testing.T) { taskEngine.AddTask(sleepTaskStop) taskEngine.AddTask(sleepTaskStop) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } if tc.taskCPULimit.Enabled() { mockControl.EXPECT().Remove(cgroupRoot).Return(nil) } @@ -514,7 +572,7 @@ func TestCreateFirelensContainer(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockTime.EXPECT().Now().AnyTimes() @@ -540,11 +598,12 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { config := defaultConfig ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &config) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &config) defer ctrl.Finish() testTask := testdata.LoadTask("sleep5") testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode testTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -566,7 +625,7 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { }, } - cniConfig, err := taskEngine.(*DockerTaskEngine).buildCNIConfigFromTaskContainer(testTask, containerInspectOutput, true) + cniConfig, err := taskEngine.(*DockerTaskEngine).buildCNIConfigFromTaskContainerAwsvpc(testTask, containerInspectOutput, true) assert.NoError(t, err) assert.Equal(t, containerID, cniConfig.ContainerID) assert.Equal(t, strconv.Itoa(containerPid), cniConfig.ContainerPID) @@ -582,7 +641,7 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -608,6 +667,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { containerEventsWG := sync.WaitGroup{} client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() // We cannot rely on the order of pulls between images as they can still be downloaded in // parallel. The dependency graph enforcement comes into effect for CREATED transitions. // Hence, do not enforce the order of invocation of these calls @@ -622,6 +682,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, containerName string, z time.Duration) { sleepTask.AddTaskENI(mockENI) + sleepTask.NetworkMode = apitask.AWSVPCNetworkMode sleepTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -719,7 +780,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { func TestPauseContainerHappyPath(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() cniClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -734,6 +795,7 @@ func TestPauseContainerHappyPath(t *testing.T) { // Add eni information to the task so the task can add dependency of pause container sleepTask.AddTaskENI(mockENI) + sleepTask.NetworkMode = apitask.AWSVPCNetworkMode sleepTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, @@ -748,6 +810,7 @@ func TestPauseContainerHappyPath(t *testing.T) { }) dockerClient.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() sleepContainerID1 := containerID + "1" sleepContainerID2 := containerID + "2" @@ -861,3 +924,597 @@ func TestPauseContainerHappyPath(t *testing.T) { } wg.Wait() } + +// Create the mock calls for the lifecycle of a ServiceConnect agent injected Pause Container +func setupMockSCPauseContainer(name string, expectedId string, expectedPid int, networkMode dockercontainer.NetworkMode, dockerClient *mock_dockerapi.MockDockerClient, settings *types.NetworkSettings) (*gomock.Call, *gomock.Call, *gomock.Call, *gomock.Call) { + createContainer := dockerClient.EXPECT().CreateContainer( + gomock.Any(), gomock.Any(), gomock.Any(), testdata.DockerNameSubstr(name), gomock.Any()).Return(dockerapi.DockerContainerMetadata{DockerID: expectedId}).Times(1) + + startContainer := dockerClient.EXPECT().StartContainer(gomock.Any(), expectedId, defaultConfig.ContainerStartTimeout).Return( + dockerapi.DockerContainerMetadata{ + DockerID: expectedId, + Health: apicontainer.HealthStatus{Status: apicontainerstatus.ContainerHealthy}, + NetworkSettings: settings, + }).Times(1) + inspectContainer := dockerClient.EXPECT().InspectContainer(gomock.Any(), expectedId, gomock.Any()).Return( + &types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: expectedId, + State: &types.ContainerState{Pid: expectedPid}, + HostConfig: &dockercontainer.HostConfig{ + NetworkMode: networkMode, + }, + }, + }, nil).MinTimes(1) + stopContainer := dockerClient.EXPECT().StopContainer(gomock.Any(), expectedId, gomock.Any()).Return( + dockerapi.DockerContainerMetadata{DockerID: expectedId}) + + gomock.InOrder( + createContainer, + startContainer, + inspectContainer, + stopContainer, + ) + + return createContainer, startContainer, inspectContainer, stopContainer +} + +// Create the mock calls for the lifecycle of a ServiceConnect Task Container +func setupMockSCTaskContainer( + name string, container *apicontainer.Container, expectedId string, expectedPid int, networkMode dockercontainer.NetworkMode, serviceConnectManager *mock_serviceconnect.MockManager, dockerClient *mock_dockerapi.MockDockerClient, settings *types.NetworkSettings) (*gomock.Call, *gomock.Call, *gomock.Call) { + + createContainer, startContainer, inspectContainer, stopContainer := setupMockSCPauseContainer(name, expectedId, expectedPid, networkMode, dockerClient, settings) + + // A task container differs from Pause by having a specific call to AugmentTaskContainer + augmentTask := serviceConnectManager.EXPECT().AugmentTaskContainer(gomock.Any(), container, gomock.Any()).Return(nil).Times(1) + gomock.InOrder( + augmentTask, + createContainer, + ) + + // A task container isn't typically inspected + inspectContainer.MinTimes(0) + + return createContainer, startContainer, stopContainer +} + +func TestContainersWithServiceConnect(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + cniClient := mock_ecscni.NewMockCNIClient(ctrl) + appnetClient := mock_api.NewMockAppnetClient(ctrl) + taskEngine.(*DockerTaskEngine).cniClient = cniClient + taskEngine.(*DockerTaskEngine).appnetClient = appnetClient + taskEngine.(*DockerTaskEngine).taskSteadyStatePollInterval = taskSteadyStatePollInterval + taskEngine.(*DockerTaskEngine).serviceconnectRelay = &apitask.Task{Arn: "arn::::::/task"} + eventStream := make(chan dockerapi.DockerContainerChangeEvent) + sleepTask := testdata.LoadTask("sleep5TwoContainers") + sleepTask.NetworkMode = apitask.AWSVPCNetworkMode + sleepContainer1 := sleepTask.Containers[0] + sleepContainer1.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + sleepContainer2 := sleepTask.Containers[1] + sleepContainer2.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + + // Inject mock SC config + sleepTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: "service-connect", + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: "host1.my.corp", + Address: "169.254.1.1", + }, + { + HostName: "host1.my.corp", + Address: "ff06::c4", + }, + }, + } + dockerConfig := dockercontainer.Config{ + Healthcheck: &dockercontainer.HealthConfig{ + Test: []string{"echo", "ok"}, + Interval: time.Millisecond, + Timeout: time.Second, + Retries: 1, + }, + } + + rawConfig, err := json.Marshal(&dockerConfig) + if err != nil { + t.Fatal(err) + } + sleepTask.Containers = append(sleepTask.Containers, &apicontainer.Container{ + Name: sleepTask.ServiceConnectConfig.ContainerName, + HealthCheckType: apicontainer.DockerHealthCheckType, + DockerConfig: apicontainer.DockerConfig{ + Config: aws.String(string(rawConfig)), + }, + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + }) + + // Add eni information to the task so the task can add dependency of pause container + sleepTask.AddTaskENI(mockENI) + + dockerClient.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + + sleepContainerID1 := containerID + "1" + sleepContainerID2 := containerID + "2" + scContainerID := "serviceConnectID" + pauseContainerID := "pauseContainerID" + // Pause container will be launched first + internalCreate, _, internalInspect, internalStop := setupMockSCPauseContainer("internalecspause", pauseContainerID, containerPid, containerNetNS, dockerClient, nil) + gomock.InOrder( + serviceConnectManager.EXPECT().AugmentTaskContainer(gomock.Any(), gomock.Any(), gomock.Any()), + internalInspect, + ) + cniClient.EXPECT().SetupNS(gomock.Any(), gomock.Any(), gomock.Any()).Return(nsResult, nil) + + // For the other container + imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes() + dockerClient.EXPECT().PullImage(gomock.Any(), gomock.Any(), nil, gomock.Any()).Return(dockerapi.DockerContainerMetadata{}).Times(2) + imageManager.EXPECT().RecordContainerReference(gomock.Any()).Return(nil).Times(2) + imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil, false).Times(2) + dockerClient.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).Times(4) + + serviceConnectCreate, _, scStop := setupMockSCTaskContainer("service-connect", sleepTask.Containers[2], scContainerID, 1337, containerNetNS, serviceConnectManager, dockerClient, nil) + firstCreate, _, firstStop := setupMockSCTaskContainer("sleep5", sleepContainer1, sleepContainerID1, 5, containerNetNS, serviceConnectManager, dockerClient, nil) + // The container self stops via the test below + firstStop.MinTimes(0) + secondCreate, _, secondStop := setupMockSCTaskContainer("sleep5-2", sleepContainer2, sleepContainerID2, 52, containerNetNS, serviceConnectManager, dockerClient, nil) + + gomock.InOrder( + internalCreate, + serviceConnectCreate, + firstCreate, + ) + gomock.InOrder( + serviceConnectCreate, + secondCreate, + ) + + cleanup := make(chan time.Time) + defer close(cleanup) + mockTime.EXPECT().Now().Return(time.Now()).MinTimes(1) + mockTime.EXPECT().After(gomock.Any()).Return(cleanup).MinTimes(1) + dockerClient.EXPECT().DescribeContainer(gomock.Any(), scContainerID).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), sleepContainerID1).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), sleepContainerID2).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), pauseContainerID).AnyTimes() + + var wg sync.WaitGroup + wg.Add(1) + gomock.InOrder( + appnetClient.EXPECT().DrainInboundConnections(gomock.Any()).MaxTimes(1), + secondStop, + scStop, + cniClient.EXPECT().CleanupNS(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil), + internalStop, + + cniClient.EXPECT().ReleaseIPResource(gomock.Any(), gomock.Any(), gomock.Any()).Do( + func(ctx context.Context, cfg *ecscni.Config, timeout time.Duration) { + wg.Done() + }).Return(nil), + ) + + dockerClient.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(4) + imageManager.EXPECT().RemoveContainerReferenceFromImageState(gomock.Any()).Return(nil).Times(3) + + err = taskEngine.Init(ctx) + assert.NoError(t, err) + taskEngine.AddTask(sleepTask) + stateChangeEvents := taskEngine.StateChangeEvents() + verifyTaskIsRunning(stateChangeEvents, sleepTask) + + // Simulate a container stop event from docker + eventStream <- dockerapi.DockerContainerChangeEvent{ + Status: apicontainerstatus.ContainerStopped, + DockerContainerMetadata: dockerapi.DockerContainerMetadata{ + DockerID: sleepContainerID1, + ExitCode: aws.Int(exitCode), + }, + } + + verifyTaskIsStopped(stateChangeEvents, sleepTask) + + sleepTask.SetSentStatus(apitaskstatus.TaskStopped) + cleanup <- time.Now() + for { + tasks, _ := taskEngine.(*DockerTaskEngine).ListTasks() + if len(tasks) == 0 { + break + } + t.Logf("Found %d tasks in the engine; first task arn: %s", len(tasks), tasks[0].Arn) + fmt.Printf("Found %d tasks in the engine; first task arn: %s\n", len(tasks), tasks[0].Arn) + time.Sleep(5 * time.Millisecond) + } + wg.Wait() + +} + +// TestContainersWithServiceConnect_BridgeMode verifies the start/stop of a bridge mode SC task +func TestContainersWithServiceConnect_BridgeMode(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + cniClient := mock_ecscni.NewMockCNIClient(ctrl) + taskEngine.(*DockerTaskEngine).cniClient = cniClient + taskEngine.(*DockerTaskEngine).taskSteadyStatePollInterval = taskSteadyStatePollInterval + taskEngine.(*DockerTaskEngine).serviceconnectRelay = &apitask.Task{Arn: "arn::::::/task"} + eventStream := make(chan dockerapi.DockerContainerChangeEvent) + sleepTask := testdata.LoadTask("sleep5PortMappings") + sleepTask.NetworkMode = apitask.BridgeNetworkMode + sleepContainer := sleepTask.Containers[0] + sleepContainer.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + + // Inject mock SC config + sleepTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: "service-connect", + IngressConfig: []serviceconnect.IngressConfigEntry{ + { + ListenerName: "testListener1", // bridge mode default - ephemeral listener host port + ListenerPort: 15000, + }, + }, + EgressConfig: &serviceconnect.EgressConfig{ + ListenerName: "testEgressListener", + ListenerPort: 0, // Presently this should always get ephemeral port + }, + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: "host1.my.corp", + Address: "169.254.1.1", + }, + { + HostName: "host1.my.corp", + Address: "ff06::c4", + }, + }, + } + + // if we create a dockercontainer.Config.Healthcheck variable and marshal it, dockercontainer.Config.Env gets set to empty + // and will later override the internal env vars that Agent populates for the container. + // In real world, the container env vars in task def are marshaled into container.Environment isntead of docker Config.Env. + // it gets merged with internal env vars, and eventually get assigned to docker Config.Env + healthCheckString := "{\"Healthcheck\":{\"Test\":[\"echo\",\"ok\"],\"Interval\":1000000,\"Timeout\":1000000000,\"Retries\":1}}" + sleepTask.Containers = append(sleepTask.Containers, &apicontainer.Container{ + Name: sleepTask.ServiceConnectConfig.ContainerName, + HealthCheckType: apicontainer.DockerHealthCheckType, + DockerConfig: apicontainer.DockerConfig{Config: aws.String(healthCheckString)}, + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, + }) + + dockerClient.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() + + sleepContainerID := containerID + "1" + scContainerID := "serviceConnectID" + sleepPauseContainerID := "sleepPauseContainerID" + scPauseContainerID := "pauseContainerID" + + // For both pause containers + serviceConnectManager.EXPECT().AugmentTaskContainer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2) + dockerClient.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).Times(2) + + internalCreate, internalStart, _, _ := setupMockSCPauseContainer("internalecspause-sleep5", sleepPauseContainerID, containerPid, containerNetNS, dockerClient, + &types.NetworkSettings{ + DefaultNetworkSettings: types.DefaultNetworkSettings{IPAddress: "1.2.3.4"}, + }) + internalSCCreate, internalSCStart, _, _ := setupMockSCPauseContainer("internalecspause-service-connect", scPauseContainerID, containerPid, containerNetNS, dockerClient, + &types.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{apitask.BridgeNetworkMode: {IPAddress: "1.2.3.4"}}, + }) + + // Sleep and SC pause containers can be created and started in parallel, but sleepPause.RESOURCES_PROVISIONED depends on + // SCPause.RUNNING (verified in the InOrder block down below) + serviceConnectCreate, scStart, _ := setupMockSCTaskContainer("service-connect", sleepTask.Containers[1], scContainerID, 1337, containerNetNS, serviceConnectManager, dockerClient, nil) + firstCreate, firstStart, firstStop := setupMockSCTaskContainer("sleep5", sleepTask.Containers[0], sleepContainerID, 5, containerNetNS, serviceConnectManager, dockerClient, nil) + // The container self stops via the test below + firstStop.MinTimes(0) + + gomock.InOrder( + internalCreate, + serviceConnectCreate, + firstCreate, + ) + gomock.InOrder( + internalSCCreate, + serviceConnectCreate, + ) + + gomock.InOrder( + internalStart, + scStart, + firstStart, + ) + + gomock.InOrder( + internalSCStart, + scStart, + ) + + cniClient.EXPECT().SetupNS(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, cfg *ecscni.Config, timeout time.Duration) (*current.Result, error) { + assert.Equal(t, 1, len(cfg.NetworkConfigs)) + var scNetworkConfig ecscni.ServiceConnectConfig + err := json.Unmarshal(cfg.NetworkConfigs[0].CNINetworkConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, string(ecscni.TPROXY), scNetworkConfig.EgressConfig.RedirectMode) + return nil, nil + }).Times(2) + cniClient.EXPECT().CleanupNS(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2) + + // For SC and sleep container - those calls can happen in parallel + // Note that SC container won't trigger image-related calls as AppNet container images are cached and managed by Agent + imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes() + dockerClient.EXPECT().PullImage(gomock.Any(), gomock.Any(), nil, gomock.Any()).Return(dockerapi.DockerContainerMetadata{}).Times(1) + imageManager.EXPECT().RecordContainerReference(gomock.Any()).Return(nil).Times(1) + imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).Return(nil, false).Times(1) + dockerClient.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).Times(2) + + cleanup := make(chan time.Time) + defer close(cleanup) + mockTime.EXPECT().Now().Return(time.Now()).MinTimes(1) + mockTime.EXPECT().After(gomock.Any()).Return(cleanup).MinTimes(1) + dockerClient.EXPECT().DescribeContainer(gomock.Any(), scContainerID).Return(apicontainerstatus.ContainerRunning, dockerapi.DockerContainerMetadata{ + DockerID: scContainerID, + Health: apicontainer.HealthStatus{Status: apicontainerstatus.ContainerHealthy}, + }).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), sleepContainerID).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), scPauseContainerID).AnyTimes() + dockerClient.EXPECT().DescribeContainer(gomock.Any(), sleepPauseContainerID).AnyTimes() + + dockerClient.EXPECT().RemoveContainer(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(4) + imageManager.EXPECT().RemoveContainerReferenceFromImageState(gomock.Any()).Return(nil).AnyTimes() + + err := taskEngine.Init(ctx) + assert.NoError(t, err) + taskEngine.AddTask(sleepTask) + stateChangeEvents := taskEngine.StateChangeEvents() + verifyTaskIsRunning(stateChangeEvents, sleepTask) + + // Simulate a container stop event from docker + eventStream <- dockerapi.DockerContainerChangeEvent{ + Status: apicontainerstatus.ContainerStopped, + DockerContainerMetadata: dockerapi.DockerContainerMetadata{ + DockerID: sleepContainerID, + ExitCode: aws.Int(exitCode), + }, + } + + verifyTaskIsStopped(stateChangeEvents, sleepTask) + + sleepTask.SetSentStatus(apitaskstatus.TaskStopped) + cleanup <- time.Now() + for { + tasks, _ := taskEngine.(*DockerTaskEngine).ListTasks() + if len(tasks) == 0 { + break + } + t.Logf("Found %d tasks in the engine; first task arn: %s", len(tasks), tasks[0].Arn) + fmt.Printf("Found %d tasks in the engine; first task arn: %s\n", len(tasks), tasks[0].Arn) + time.Sleep(5 * time.Millisecond) + } +} + +func verifyServiceConnectSleepPauseContainerBridgeMode(t *testing.T, ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, y, z interface{}) { + name, ok := config.Labels[labelPrefix+"container-name"] + assert.True(t, ok) + assert.Equal(t, fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, "sleep5"), name) + // verify host config network mode + assert.Equal(t, dockercontainer.NetworkMode(apitask.BridgeNetworkMode), hostConfig.NetworkMode) + // verify host config port bindings + assert.NotNil(t, hostConfig.PortBindings) + assert.Equal(t, 1, len(hostConfig.PortBindings)) + bindings, ok := hostConfig.PortBindings["8080/tcp"] + assert.True(t, ok) + assert.Equal(t, 1, len(bindings)) + assert.Equal(t, "0", bindings[0].HostPort) + // verify container config port exposed + assert.NotNil(t, config.ExposedPorts) + assert.Equal(t, 1, len(config.ExposedPorts)) + _, ok = config.ExposedPorts["8080/tcp"] + assert.True(t, ok) +} + +func verifyServiceConnectPauseContainerBridgeMode(t *testing.T, ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, y, z interface{}) { + name, ok := config.Labels[labelPrefix+"container-name"] + assert.True(t, ok) + assert.Equal(t, fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, "service-connect"), name) + // verify host config network mode + assert.Equal(t, dockercontainer.NetworkMode(apitask.BridgeNetworkMode), hostConfig.NetworkMode) + // verify host config port bindings + assert.NotNil(t, hostConfig.PortBindings) + assert.Equal(t, 1, len(hostConfig.PortBindings)) + bindings, ok := hostConfig.PortBindings["15000/tcp"] + assert.True(t, ok) + assert.Equal(t, 1, len(bindings)) + assert.Equal(t, "0", bindings[0].HostPort) + // verify container config port exposed + assert.NotNil(t, config.ExposedPorts) + assert.Equal(t, 2, len(config.ExposedPorts)) // 2 because egress container port is also exposed + _, ok = config.ExposedPorts["15000/tcp"] + assert.True(t, ok) +} + +func TestProvisionContainerResourcesBridgeModeWithServiceConnect(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) + taskEngine.(*DockerTaskEngine).cniClient = mockCNIClient + testTask := testdata.LoadTask("sleep5PortMappings") + testTask.NetworkMode = apitask.BridgeNetworkMode + + // append SC pause, application container pause, SC container + scContainer := &apicontainer.Container{ + Name: serviceConnectContainerName, + Type: apicontainer.ContainerNormal, + } + scPauseContainer := &apicontainer.Container{ + Name: fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, serviceConnectContainerName), + Type: apicontainer.ContainerCNIPause, + } + appPauseContainer := &apicontainer.Container{ + Name: fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, "sleep5"), + Type: apicontainer.ContainerCNIPause, + } + testTask.Containers = append(testTask.Containers, scContainer, scPauseContainer, appPauseContainer) + + // add task SC config + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: serviceConnectContainerName, + IngressConfig: []serviceconnect.IngressConfigEntry{{ListenerPort: 11111}}, + EgressConfig: &serviceconnect.EgressConfig{ListenerPort: 22222}, + NetworkConfig: serviceconnect.NetworkConfig{ + SCPauseIPv4Addr: "172.0.0.1", + SCPauseIPv6Addr: "", + }, + } + taskEngine.(*DockerTaskEngine).State().AddTask(testTask) + taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ + DockerID: containerID, + DockerName: dockerContainerName, + Container: scContainer, + }, testTask) + taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ + DockerID: containerID + scPauseContainer.Name, + DockerName: dockerContainerName + scPauseContainer.Name, + Container: scPauseContainer, + }, testTask) + taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ + DockerID: containerID + appPauseContainer.Name, + DockerName: dockerContainerName + appPauseContainer.Name, + Container: appPauseContainer, + }, testTask) + + for _, cont := range []*apicontainer.Container{scPauseContainer, appPauseContainer} { + gomock.InOrder( + dockerClient.EXPECT().InspectContainer(gomock.Any(), containerID+cont.Name, gomock.Any()).Return(&types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: containerID + cont.Name, + State: &types.ContainerState{Pid: containerPid}, + HostConfig: &dockercontainer.HostConfig{ + NetworkMode: apitask.BridgeNetworkMode, + }, + }, + }, nil), + mockCNIClient.EXPECT().SetupNS(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, cfg *ecscni.Config, timeout time.Duration) (*current.Result, error) { + assert.Equal(t, 1, len(cfg.NetworkConfigs)) + var scNetworkConfig ecscni.ServiceConnectConfig + err := json.Unmarshal(cfg.NetworkConfigs[0].CNINetworkConfig.Bytes, &scNetworkConfig) + assert.NoError(t, err, "unmarshal ServiceConnect network config") + assert.Equal(t, string(ecscni.TPROXY), scNetworkConfig.EgressConfig.RedirectMode) + return nil, nil + }), + ) + require.Nil(t, taskEngine.(*DockerTaskEngine).provisionContainerResources(testTask, cont).Error) + } +} + +func TestWatchAppNetImage(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, _, _, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + tempServiceConnectAppnetAgenTarballDir := t.TempDir() + + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().Return(tempServiceConnectAppnetAgenTarballDir).AnyTimes() + serviceConnectManager.EXPECT().LoadImage(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + watcherCtx, watcherCancel := context.WithTimeout(context.Background(), time.Second) + defer watcherCancel() + go taskEngine.(*DockerTaskEngine).watchAppNetImage(watcherCtx) + _, err := os.CreateTemp(tempServiceConnectAppnetAgenTarballDir, "agent.tar") + assert.NoError(t, err) + + <-watcherCtx.Done() +} + +func TestCredentialSpecResourceTaskFile(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, client, mockTime, taskEngine, credentialsManager, _, _, _ := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + // metadata required for createContainer workflow validation + credentialSpecTaskARN := "credentialSpecTask" + credentialSpecTaskFamily := "credentialSpecFamily" + credentialSpecTaskVersion := "1" + credentialSpecTaskContainerName := "credentialSpecContainer" + + c := &apicontainer.Container{ + Name: credentialSpecTaskContainerName, + } + credentialspecFile := "credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + targetCredentialspecFile := "/var/credentials-fetcher/krbdir/123456/webap01" + hostConfig := "{\"SecurityOpt\": [\"credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json\"]}" + c.DockerConfig.HostConfig = &hostConfig + + // sample test + testTask := &apitask.Task{ + Arn: credentialSpecTaskARN, + Family: credentialSpecTaskFamily, + Version: credentialSpecTaskVersion, + Containers: []*apicontainer.Container{c}, + } + + // metadata required for execution role authentication workflow + credentialsID := "execution role" + + // configure the task and container to use execution role + testTask.SetExecutionRoleCredentialsID(credentialsID) + + // validate base config + expectedConfig, err := testTask.DockerConfig(testTask.Containers[0], defaultDockerClientAPIVersion) + if err != nil { + t.Fatal(err) + } + + expectedConfig.Labels = map[string]string{ + "com.amazonaws.ecs.task-arn": credentialSpecTaskARN, + "com.amazonaws.ecs.container-name": credentialSpecTaskContainerName, + "com.amazonaws.ecs.task-definition-family": credentialSpecTaskFamily, + "com.amazonaws.ecs.task-definition-version": credentialSpecTaskVersion, + "com.amazonaws.ecs.cluster": "", + } + + ssmClientCreator := mock_ssm_factory.NewMockSSMClientCreator(ctrl) + s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) + + credentialSpecRes, cerr := credentialspec.NewCredentialSpecResource( + testTask.Arn, + defaultConfig.AWSRegion, + credentialsID, + credentialsManager, + ssmClientCreator, + s3ClientCreator, + nil) + assert.NoError(t, cerr) + + credSpecdata := map[string]string{ + credentialspecFile: targetCredentialspecFile, + } + credentialSpecRes.CredSpecMap = credSpecdata + + testTask.ResourcesMapUnsafe = map[string][]taskresource.TaskResource{ + credentialspec.ResourceName: {credentialSpecRes}, + } + + mockTime.EXPECT().Now().AnyTimes() + client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).AnyTimes() + + client.EXPECT().CreateContainer(gomock.Any(), expectedConfig, gomock.Any(), gomock.Any(), gomock.Any()) + + ret := taskEngine.(*DockerTaskEngine).createContainer(testTask, testTask.Containers[0]) + assert.Nil(t, ret.Error) +} diff --git a/agent/engine/docker_task_engine_test.go b/agent/engine/docker_task_engine_test.go index d90fee5c2ed..99102834aa8 100644 --- a/agent/engine/docker_task_engine_test.go +++ b/agent/engine/docker_task_engine_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -34,6 +35,7 @@ import ( apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/asm" @@ -52,6 +54,7 @@ import ( mock_execcmdagent "github.com/aws/amazon-ecs-agent/agent/engine/execcmd/mocks" "github.com/aws/amazon-ecs-agent/agent/engine/image" mock_engine "github.com/aws/amazon-ecs-agent/agent/engine/mocks" + mock_engineserviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect/mock" "github.com/aws/amazon-ecs-agent/agent/engine/testdata" "github.com/aws/amazon-ecs-agent/agent/eventstream" mock_ssm_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" @@ -84,6 +87,7 @@ const ( ipv6 = "f0:234:23" dockerContainerName = "docker-container-name" containerPid = 123 + containerPid2 = 456 taskIP = "169.254.170.3" exitCode = 1 labelsTaskARN = "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe" @@ -104,6 +108,7 @@ const ( networkModeAWSVPC = "awsvpc" testTaskARN = "arn:aws:ecs:region:account-id:task/task-id" containerNetworkMode = "none" + serviceConnectContainerName = "service-connect" ) var ( @@ -159,7 +164,8 @@ func setCreatedContainerName(name string) { func mocks(t *testing.T, ctx context.Context, cfg *config.Config) (*gomock.Controller, *mock_dockerapi.MockDockerClient, *mock_ttime.MockTime, TaskEngine, - *mock_credentials.MockManager, *mock_engine.MockImageManager, *mock_containermetadata.MockManager) { + *mock_credentials.MockManager, *mock_engine.MockImageManager, *mock_containermetadata.MockManager, + *mock_engineserviceconnect.MockManager) { ctrl := gomock.NewController(t) client := mock_dockerapi.NewMockDockerClient(ctrl) mockTime := mock_ttime.NewMockTime(ctrl) @@ -172,12 +178,14 @@ func mocks(t *testing.T, ctx context.Context, cfg *config.Config) (*gomock.Contr execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) taskEngine := NewTaskEngine(cfg, client, credentialsManager, containerChangeEventStream, - imageManager, dockerstate.NewTaskEngineState(), metadataManager, nil, execCmdMgr) + imageManager, dockerstate.NewTaskEngineState(), metadataManager, nil, execCmdMgr, nil) taskEngine.(*DockerTaskEngine)._time = mockTime taskEngine.(*DockerTaskEngine).ctx = ctx taskEngine.(*DockerTaskEngine).stopContainerBackoffMin = time.Millisecond taskEngine.(*DockerTaskEngine).stopContainerBackoffMax = time.Millisecond * 2 - return ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager + serviceConnectManager := mock_engineserviceconnect.NewMockManager(ctrl) + taskEngine.(*DockerTaskEngine).serviceconnectManager = serviceConnectManager + return ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager, serviceConnectManager } func mockSetupNSResult() *current.Result { @@ -231,7 +239,7 @@ func TestBatchContainerHappyPath(t *testing.T) { metadataConfig.ContainerMetadataEnabled = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager := mocks( + ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, metadataManager, serviceConnectManager := mocks( t, ctx, &metadataConfig) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) taskEngine.(*DockerTaskEngine).execCmdMgr = execCmdMgr @@ -254,6 +262,7 @@ func TestBatchContainerHappyPath(t *testing.T) { containerEventsWG := sync.WaitGroup{} client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() containerName := make(chan string) go func() { name := <-containerName @@ -343,7 +352,7 @@ func TestBatchContainerHappyPath(t *testing.T) { func TestRemoveEvents(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") @@ -352,6 +361,7 @@ func TestRemoveEvents(t *testing.T) { // events are processed containerEventsWG := sync.WaitGroup{} client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() client.EXPECT().StopContainer(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() containerName := make(chan string) go func() { @@ -418,7 +428,7 @@ func TestRemoveEvents(t *testing.T) { func TestStartTimeoutThenStart(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, testTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, testTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") @@ -426,6 +436,7 @@ func TestStartTimeoutThenStart(t *testing.T) { testTime.EXPECT().Now().Return(time.Now()).AnyTimes() testTime.EXPECT().After(gomock.Any()) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil) for _, container := range sleepTask.Containers { imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes() @@ -471,7 +482,7 @@ func TestStartTimeoutThenStart(t *testing.T) { func TestSteadyStatePoll(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, testTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, testTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() taskEngine.(*DockerTaskEngine).taskSteadyStatePollInterval = taskSteadyStatePollInterval @@ -481,6 +492,7 @@ func TestSteadyStatePoll(t *testing.T) { eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() containerName := make(chan string) go func() { <-containerName @@ -547,7 +559,7 @@ func TestSteadyStatePoll(t *testing.T) { func TestStopWithPendingStops(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, testTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, testTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() testTime.EXPECT().Now().Return(time.Now()).AnyTimes() testTime.EXPECT().After(gomock.Any()).AnyTimes() @@ -559,6 +571,7 @@ func TestStopWithPendingStops(t *testing.T) { eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := taskEngine.Init(ctx) assert.NoError(t, err) stateChangeEvents := taskEngine.StateChangeEvents() @@ -596,7 +609,7 @@ func TestStopWithPendingStops(t *testing.T) { func TestCreateContainerSaveDockerIDAndName(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, privateTaskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() dataClient, cleanup := newTestDataClient(t) defer cleanup() @@ -645,7 +658,7 @@ func TestCreateContainerMetadata(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, _, metadataManager := mocks(t, ctx, &config.Config{}) + ctrl, client, _, privateTaskEngine, _, _, metadataManager, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) @@ -668,7 +681,7 @@ func TestCreateContainerMetadata(t *testing.T) { func TestCreateContainerMergesLabels(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() testTask := &apitask.Task{ @@ -706,7 +719,7 @@ func TestCreateContainerMergesLabels(t *testing.T) { func TestCreateContainerAddV3EndpointIDToState(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, privateTaskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) @@ -750,12 +763,13 @@ func TestCreateContainerAddV3EndpointIDToState(t *testing.T) { func TestTaskTransitionWhenStopContainerTimesout(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() mockTime.EXPECT().Now().Return(time.Now()).AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() containerStopTimeoutError := dockerapi.DockerContainerMetadata{ @@ -817,12 +831,13 @@ func TestTaskTransitionWhenStopContainerTimesout(t *testing.T) { func TestTaskTransitionWhenStopContainerReturnsUnretriableError(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() mockTime.EXPECT().Now().Return(time.Now()).AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() containerEventsWG := sync.WaitGroup{} @@ -890,7 +905,7 @@ func TestTaskTransitionWhenStopContainerReturnsUnretriableError(t *testing.T) { func TestTaskTransitionWhenStopContainerReturnsTransientErrorBeforeSucceeding(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() sleepTask := testdata.LoadTask("sleep5") @@ -898,6 +913,7 @@ func TestTaskTransitionWhenStopContainerReturnsTransientErrorBeforeSucceeding(t client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) mockTime.EXPECT().Now().Return(time.Now()).AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() containerStoppingError := dockerapi.DockerContainerMetadata{ Error: dockerapi.CannotStopContainerError{errors.New("Error stopping container")}, } @@ -943,13 +959,14 @@ func TestGetTaskByArn(t *testing.T) { defer cancel() // Need a mock client as AddTask not only adds a task to the engine, but // also causes the engine to progress the task. - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockTime.EXPECT().Now().Return(time.Now()).AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() imageManager.EXPECT().AddAllImageStates(gomock.Any()).AnyTimes() imageManager.EXPECT().RecordContainerReference(gomock.Any()).AnyTimes() imageManager.EXPECT().GetImageStateFromImageName(gomock.Any()).AnyTimes() @@ -970,10 +987,10 @@ func TestGetTaskByArn(t *testing.T) { assert.False(t, found, "Task with invalid arn found in the task engine") } -func TestProvisionContainerResourcesSetPausePIDInVolumeResources(t *testing.T) { +func TestProvisionContainerResourcesAwsvpcSetPausePIDInVolumeResources(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() dataClient, cleanup := newTestDataClient(t) @@ -992,6 +1009,7 @@ func TestProvisionContainerResourcesSetPausePIDInVolumeResources(t *testing.T) { } testTask.Containers = append(testTask.Containers, pauseContainer) testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode volRes := &taskresourcevolume.VolumeResource{} testTask.ResourcesMapUnsafe = map[string][]taskresource.TaskResource{ "dockerVolume": {volRes}, @@ -1025,10 +1043,10 @@ func TestProvisionContainerResourcesSetPausePIDInVolumeResources(t *testing.T) { assert.Len(t, savedTasks, 1) } -func TestProvisionContainerResourcesInspectError(t *testing.T) { +func TestProvisionContainerResourcesAwsvpcInspectError(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -1040,6 +1058,7 @@ func TestProvisionContainerResourcesInspectError(t *testing.T) { } testTask.Containers = append(testTask.Containers, pauseContainer) testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode taskEngine.(*DockerTaskEngine).State().AddTask(testTask) taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ DockerID: containerID, @@ -1052,12 +1071,52 @@ func TestProvisionContainerResourcesInspectError(t *testing.T) { assert.NotNil(t, taskEngine.(*DockerTaskEngine).provisionContainerResources(testTask, pauseContainer).Error) } -// TestStopPauseContainerCleanupCalled tests when stopping the pause container +func TestProvisionContainerResourcesAwsvpcMissingCNIResponseError(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) + taskEngine.(*DockerTaskEngine).cniClient = mockCNIClient + testTask := testdata.LoadTask("sleep5") + pauseContainer := &apicontainer.Container{ + Name: "pausecontainer", + Type: apicontainer.ContainerCNIPause, + } + testTask.Containers = append(testTask.Containers, pauseContainer) + testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode + taskEngine.(*DockerTaskEngine).State().AddTask(testTask) + taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ + DockerID: containerID, + DockerName: dockerContainerName, + Container: pauseContainer, + }, testTask) + + dockerClient.EXPECT().InspectContainer(gomock.Any(), containerID, gomock.Any()).Return(&types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: containerID, + State: &types.ContainerState{Pid: containerPid}, + HostConfig: &dockercontainer.HostConfig{ + NetworkMode: containerNetworkMode, + }, + }, + }, nil) + mockCNIClient.EXPECT().SetupNS(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) + + actualErr := taskEngine.(*DockerTaskEngine).provisionContainerResources(testTask, pauseContainer).Error + + assert.NotNil(t, actualErr) + assert.True(t, strings.Contains(actualErr.Error(), "empty result from network namespace setup")) +} + +// TestStopPauseContainerCleanupCalledAwsvpc tests when stopping the pause container // its network namespace should be cleaned up first -func TestStopPauseContainerCleanupCalled(t *testing.T) { +func TestStopPauseContainerCleanupCalledAwsvpc(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -1070,6 +1129,7 @@ func TestStopPauseContainerCleanupCalled(t *testing.T) { } testTask.Containers = append(testTask.Containers, pauseContainer) testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode testTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -1109,9 +1169,9 @@ func TestStopPauseContainerCleanupCalled(t *testing.T) { require.True(t, pauseContainer.IsContainerTornDown()) } -// TestStopPauseContainerCleanupCalled tests when stopping the pause container +// TestStopPauseContainerCleanupDelayAwsvpc tests when stopping the pause container // its network namespace should be cleaned up first -func TestStopPauseContainerCleanupDelay(t *testing.T) { +func TestStopPauseContainerCleanupDelayAwsvpc(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() @@ -1120,7 +1180,7 @@ func TestStopPauseContainerCleanupDelay(t *testing.T) { cfg.ENIPauseContainerCleanupDelaySeconds = expectedDelaySeconds delayedChan := make(chan time.Duration, 1) - ctrl, dockerClient, _, taskEngine, _, _, _ := mocks(t, ctx, &cfg) + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &cfg) taskEngine.(*DockerTaskEngine).handleDelay = func(d time.Duration) { delayedChan <- d } @@ -1135,6 +1195,7 @@ func TestStopPauseContainerCleanupDelay(t *testing.T) { } testTask.Containers = append(testTask.Containers, pauseContainer) testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode taskEngine.(*DockerTaskEngine).State().AddTask(testTask) taskEngine.(*DockerTaskEngine).State().AddContainer(&apicontainer.DockerContainer{ DockerID: containerID, @@ -1170,11 +1231,11 @@ func TestStopPauseContainerCleanupDelay(t *testing.T) { } } -// TestCheckTearDownPauseContainer that the pause container teardown works and is idempotent -func TestCheckTearDownPauseContainer(t *testing.T) { +// TestCheckTearDownPauseContainerAwsvpc that the pause container teardown works and is idempotent +func TestCheckTearDownPauseContainerAwsvpc(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -1187,6 +1248,7 @@ func TestCheckTearDownPauseContainer(t *testing.T) { } testTask.Containers = append(testTask.Containers, pauseContainer) testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode testTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -1230,10 +1292,11 @@ func TestCheckTearDownPauseContainer(t *testing.T) { func TestTaskWithCircularDependency(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() client.EXPECT().ContainerEvents(gomock.Any()) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() task := testdata.LoadTask("circular_dependency") @@ -1256,7 +1319,7 @@ func TestTaskWithCircularDependency(t *testing.T) { func TestCreateContainerOnAgentRestart(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, _, _ := mocks(t, ctx, &config.Config{}) + ctrl, client, _, privateTaskEngine, _, _, _, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) @@ -1280,7 +1343,7 @@ func TestCreateContainerOnAgentRestart(t *testing.T) { func TestPullCNIImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, privateTaskEngine, _, _, _ := mocks(t, ctx, &config.Config{}) + ctrl, _, _, privateTaskEngine, _, _, _, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) @@ -1297,7 +1360,7 @@ func TestPullCNIImage(t *testing.T) { func TestPullNormalImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, &config.Config{}) + ctrl, client, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) taskEngine._time = nil @@ -1339,7 +1402,7 @@ func TestPullImageWithImagePullOnceBehavior(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullOnceBehavior}) + ctrl, client, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullOnceBehavior}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) taskEngine._time = nil @@ -1369,7 +1432,7 @@ func TestPullImageWithImagePullOnceBehavior(t *testing.T) { func TestPullImageWithImagePullPreferCachedBehaviorWithCachedImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullPreferCachedBehavior}) + ctrl, client, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullPreferCachedBehavior}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) taskEngine._time = nil @@ -1394,7 +1457,7 @@ func TestPullImageWithImagePullPreferCachedBehaviorWithCachedImage(t *testing.T) func TestPullImageWithImagePullPreferCachedBehaviorWithoutCachedImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullPreferCachedBehavior}) + ctrl, client, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, &config.Config{ImagePullBehavior: config.ImagePullPreferCachedBehavior}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) taskEngine._time = nil @@ -1420,7 +1483,7 @@ func TestPullImageWithImagePullPreferCachedBehaviorWithoutCachedImage(t *testing func TestUpdateContainerReference(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, &config.Config{}) + ctrl, _, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) taskEngine._time = nil @@ -1445,12 +1508,13 @@ func TestUpdateContainerReference(t *testing.T) { // TestPullAndUpdateContainerReference checks whether a container is added to task engine state when // Test # | Image availability | DependentContainersPullUpfront | ImagePullBehavior // ----------------------------------------------------------------------------------- -// 1 | remote | enabled | default -// 2 | remote | disabled | default -// 3 | local | enabled | default -// 4 | local | enabled | once -// 5 | local | enabled | prefer-cached -// 6 | local | enabled | always +// +// 1 | remote | enabled | default +// 2 | remote | disabled | default +// 3 | local | enabled | default +// 4 | local | enabled | once +// 5 | local | enabled | prefer-cached +// 6 | local | enabled | always func TestPullAndUpdateContainerReference(t *testing.T) { testcases := []struct { Name string @@ -1534,7 +1598,7 @@ func TestPullAndUpdateContainerReference(t *testing.T) { DependentContainersPullUpfront: tc.ImagePullUpfront, ImagePullBehavior: tc.ImagePullBehavior, } - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, cfg) + ctrl, client, _, privateTaskEngine, _, imageManager, _, _ := mocks(t, ctx, cfg) defer ctrl.Finish() taskEngine, _ := privateTaskEngine.(*DockerTaskEngine) @@ -1580,7 +1644,7 @@ func TestMetadataFileUpdatedAgentRestart(t *testing.T) { conf.ContainerMetadataEnabled = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, metadataManager := mocks(t, ctx, conf) + ctrl, client, _, privateTaskEngine, _, imageManager, metadataManager, serviceConnectManager := mocks(t, ctx, conf) defer ctrl.Finish() var metadataUpdateWG sync.WaitGroup @@ -1603,6 +1667,10 @@ func TestMetadataFileUpdatedAgentRestart(t *testing.T) { state.AddContainer(dockerContainer, task) eventStream := make(chan dockerapi.DockerContainerChangeEvent) client.EXPECT().ContainerEvents(gomock.Any()).Return(eventStream, nil) + _, watcherCancel := context.WithTimeout(context.Background(), time.Second) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().Do(func() { + watcherCancel() + }).AnyTimes() client.EXPECT().DescribeContainer(gomock.Any(), gomock.Any()) imageManager.EXPECT().RecordContainerReference(gomock.Any()) @@ -1625,7 +1693,7 @@ func TestMetadataFileUpdatedAgentRestart(t *testing.T) { func TestTaskUseExecutionRolePullECRImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, _ := mocks( + ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, _, _ := mocks( t, ctx, &defaultConfig) defer ctrl.Finish() @@ -1672,7 +1740,7 @@ func TestTaskUseExecutionRolePullECRImage(t *testing.T) { func TestTaskUseExecutionRolePullPrivateRegistryImage(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, _ := mocks( + ctrl, client, mockTime, taskEngine, credentialsManager, imageManager, _, _ := mocks( t, ctx, &defaultConfig) defer ctrl.Finish() @@ -1746,7 +1814,7 @@ func TestTaskUseExecutionRolePullPrivateRegistryImage(t *testing.T) { func TestTaskUseExecutionRolePullPrivateRegistryImageNoASMResource(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, mockTime, taskEngine, _, _, _ := mocks( + ctrl, _, mockTime, taskEngine, _, _, _, _ := mocks( t, ctx, &defaultConfig) defer ctrl.Finish() @@ -1778,13 +1846,14 @@ func TestTaskUseExecutionRolePullPrivateRegistryImageNoASMResource(t *testing.T) func TestNewTaskTransitionOnRestart(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockTime.EXPECT().Now().AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() client.EXPECT().Version(gomock.Any(), gomock.Any()).MaxTimes(1) client.EXPECT().ContainerEvents(gomock.Any()).MaxTimes(1) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := taskEngine.Init(ctx) assert.NoError(t, err) @@ -1822,11 +1891,12 @@ func TestTaskWaitForHostResourceOnRestart(t *testing.T) { conf.ContainerMetadataEnabled = config.BooleanDefaultFalse{Value: config.ExplicitlyDisabled} ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, privateTaskEngine, _, imageManager, _ := mocks(t, ctx, conf) + ctrl, client, _, privateTaskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, conf) defer ctrl.Finish() client.EXPECT().Version(gomock.Any(), gomock.Any()).MaxTimes(1) client.EXPECT().ContainerEvents(gomock.Any()).MaxTimes(1) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := privateTaskEngine.Init(ctx) assert.NoError(t, err) @@ -1857,7 +1927,6 @@ func TestTaskWaitForHostResourceOnRestart(t *testing.T) { DockerID: containerID, }).Times(3) imageManager.EXPECT().RecordContainerReference(gomock.Any()).Times(3) - // start the two tasks taskEngine.synchronizeState() @@ -1881,7 +1950,7 @@ func TestTaskWaitForHostResourceOnRestart(t *testing.T) { func TestPullStartedStoppedAtWasSetCorrectly(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() testTask := &apitask.Task{ @@ -1928,7 +1997,7 @@ func TestPullStartedStoppedAtWasSetCorrectly(t *testing.T) { func TestPullStoppedAtWasSetCorrectlyWhenPullFail(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() testTask := &apitask.Task{ @@ -1976,42 +2045,106 @@ func TestPullStoppedAtWasSetCorrectlyWhenPullFail(t *testing.T) { } func TestSynchronizeContainerStatus(t *testing.T) { - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - ctrl, client, _, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) - defer ctrl.Finish() - - dockerID := "1234" - dockerContainer := &apicontainer.DockerContainer{ - DockerID: dockerID, - DockerName: "c1", - Container: &apicontainer.Container{}, - } - labels := map[string]string{ + testContainerName := "c1" + testDockerID := "1234" + testServiceConnectContainerName := "service-connect" + testLabels := map[string]string{ "name": "metadata", } - volumes := []types.MountPoint{ + testVolumes := []types.MountPoint{ { Name: "volume", Source: "/src/vol", Destination: "/vol", }, } - created := time.Now() - gomock.InOrder( - client.EXPECT().DescribeContainer(gomock.Any(), dockerID).Return(apicontainerstatus.ContainerRunning, - dockerapi.DockerContainerMetadata{ - Labels: labels, - DockerID: dockerID, - CreatedAt: created, - Volumes: volumes, - }), - imageManager.EXPECT().RecordContainerReference(dockerContainer.Container), - ) - taskEngine.(*DockerTaskEngine).synchronizeContainerStatus(dockerContainer, nil) - assert.Equal(t, created, dockerContainer.Container.GetCreatedAt()) - assert.Equal(t, labels, dockerContainer.Container.GetLabels()) - assert.Equal(t, volumes, dockerContainer.Container.GetVolumes()) + testAppContainer := &apicontainer.Container{ + Name: testContainerName, + Type: apicontainer.ContainerNormal, + } + testCases := []struct { + name string + serviceConnectEnabled bool + addPauseContainer bool + pauseContainerName string + pauseContainerPortBindings []apicontainer.PortBinding + networkMode string + }{ + { + name: "Service connect bridge mode with matched pause container", + serviceConnectEnabled: true, + addPauseContainer: true, + pauseContainerName: fmt.Sprintf("%s-%s", apitask.NetworkPauseContainerName, testContainerName), + pauseContainerPortBindings: []apicontainer.PortBinding{ + { + ContainerPort: 8080, + }, + }, + networkMode: networkModeBridge, + }, + { + name: "Default task", + serviceConnectEnabled: false, + addPauseContainer: false, + networkMode: networkModeAWSVPC, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + ctrl, client, _, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) + defer ctrl.Finish() + + testTask := &apitask.Task{ + Containers: []*apicontainer.Container{testAppContainer}, + NetworkMode: tc.networkMode, + } + + dockerContainer := &apicontainer.DockerContainer{ + DockerID: testDockerID, + DockerName: testContainerName, + Container: testAppContainer, + } + testCreated := time.Now() + gomock.InOrder( + client.EXPECT().DescribeContainer(gomock.Any(), testDockerID).Return(apicontainerstatus.ContainerRunning, + dockerapi.DockerContainerMetadata{ + Labels: testLabels, + DockerID: testDockerID, + CreatedAt: testCreated, + Volumes: testVolumes, + }), + imageManager.EXPECT().RecordContainerReference(dockerContainer.Container), + ) + + if tc.serviceConnectEnabled { + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: "service-connect", + } + scContainer := &apicontainer.Container{ + Name: testServiceConnectContainerName, + } + testTask.Containers = append(testTask.Containers, scContainer) + } + pauseContainer := &apicontainer.Container{} + if tc.addPauseContainer { + pauseContainer.Name = tc.pauseContainerName + pauseContainer.Type = apicontainer.ContainerCNIPause + pauseContainer.SetKnownPortBindings(tc.pauseContainerPortBindings) + testTask.Containers = append(testTask.Containers, pauseContainer) + } + taskEngine.(*DockerTaskEngine).synchronizeContainerStatus(dockerContainer, testTask) + assert.Equal(t, testCreated, dockerContainer.Container.GetCreatedAt()) + assert.Equal(t, testLabels, dockerContainer.Container.GetLabels()) + assert.Equal(t, testVolumes, dockerContainer.Container.GetVolumes()) + + if tc.serviceConnectEnabled && tc.addPauseContainer { + assert.Equal(t, tc.pauseContainerPortBindings, dockerContainer.Container.GetKnownPortBindings()) + } + }) + } } // TestHandleDockerHealthEvent tests the docker health event will only cause the @@ -2019,7 +2152,7 @@ func TestSynchronizeContainerStatus(t *testing.T) { func TestHandleDockerHealthEvent(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() state := taskEngine.(*DockerTaskEngine).State() @@ -2097,7 +2230,7 @@ func TestContainerMetadataUpdatedOnRestart(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() dockerContainer := &apicontainer.DockerContainer{ DockerID: dockerID, @@ -2168,7 +2301,7 @@ func TestContainerMetadataUpdatedOnRestart(t *testing.T) { func TestContainerProgressParallize(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, testTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, testTime, taskEngine, _, imageManager, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() stateChangeEvents := taskEngine.StateChangeEvents() @@ -2256,6 +2389,7 @@ func TestContainerProgressParallize(t *testing.T) { eventStream <- event }() }) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() taskEngine.Init(ctx) taskEngine.AddTask(testTask) @@ -2310,12 +2444,13 @@ func TestContainerProgressParallize(t *testing.T) { func TestSynchronizeResource(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockTime.EXPECT().Now().AnyTimes() client.EXPECT().Version(gomock.Any(), gomock.Any()).MaxTimes(1) client.EXPECT().ContainerEvents(gomock.Any()).MaxTimes(1) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := taskEngine.Init(ctx) assert.NoError(t, err) @@ -2348,13 +2483,14 @@ func TestSynchronizeResource(t *testing.T) { func TestSynchronizeENIAttachment(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockTime.EXPECT().Now().AnyTimes() mockTime.EXPECT().After(gomock.Any()).AnyTimes() client.EXPECT().Version(gomock.Any(), gomock.Any()).MaxTimes(1) client.EXPECT().ContainerEvents(gomock.Any()).MaxTimes(1) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := taskEngine.Init(ctx) assert.NoError(t, err) @@ -2383,13 +2519,14 @@ func TestSynchronizeENIAttachment(t *testing.T) { func TestSynchronizeENIAttachmentRemoveData(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, serviceConnectManager := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() dataClient, cleanup := newTestDataClient(t) defer cleanup() client.EXPECT().ContainerEvents(gomock.Any()).MaxTimes(1) + serviceConnectManager.EXPECT().GetAppnetContainerTarballDir().AnyTimes() err := taskEngine.Init(ctx) assert.NoError(t, err) @@ -2520,7 +2657,7 @@ func TestTaskSecretsEnvironmentVariables(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, credentialsManager, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() // sample test @@ -2679,9 +2816,10 @@ func TestCreateContainerAddFirelensLogDriverConfig(t *testing.T) { rawHostConfig, err := json.Marshal(&rawHostConfigInput) require.NoError(t, err) return &apitask.Task{ - Arn: taskARN, - Version: taskVersion, - Family: taskFamily, + Arn: taskARN, + Version: taskVersion, + Family: taskFamily, + NetworkMode: networkMode, Containers: []*apicontainer.Container{ { Name: taskName, @@ -2721,9 +2859,10 @@ func TestCreateContainerAddFirelensLogDriverConfig(t *testing.T) { rawHostConfig, err := json.Marshal(&rawHostConfigInput) require.NoError(t, err) return &apitask.Task{ - Arn: taskARN, - Version: taskVersion, - Family: taskFamily, + Arn: taskARN, + Version: taskVersion, + Family: taskFamily, + NetworkMode: networkMode, ENIs: []*apieni.ENI{ { IPV4Addresses: []*apieni.ENIIPV4Address{ @@ -2813,7 +2952,7 @@ func TestCreateContainerAddFirelensLogDriverConfig(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).AnyTimes() @@ -2854,7 +2993,7 @@ func TestCreateFirelensContainerSetFluentdUID(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() client.EXPECT().APIVersion().Return(defaultDockerClientAPIVersion, nil).AnyTimes() @@ -3006,7 +3145,7 @@ func TestStartFirelensContainerRetryForContainerIP(t *testing.T) { } ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() taskEngine.(*DockerTaskEngine).state.AddTask(testTask) taskEngine.(*DockerTaskEngine).state.AddContainer(&apicontainer.DockerContainer{ @@ -3034,7 +3173,7 @@ func TestStartExecAgent(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() nowTime := time.Now() - ctrl, client, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) dockerTaskEngine := taskEngine.(*DockerTaskEngine) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) dockerTaskEngine.execCmdMgr = execCmdMgr @@ -3148,7 +3287,7 @@ func TestStartExecAgent(t *testing.T) { func TestMonitorExecAgentRunning(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) dockerTaskEngine := taskEngine.(*DockerTaskEngine) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) dockerTaskEngine.execCmdMgr = execCmdMgr @@ -3256,7 +3395,7 @@ func TestMonitorExecAgentRunning(t *testing.T) { func TestMonitorExecAgentProcesses(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) nowTime := time.Now() dockerTaskEngine := taskEngine.(*DockerTaskEngine) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) @@ -3351,7 +3490,7 @@ func TestMonitorExecAgentProcesses(t *testing.T) { func TestMonitorExecAgentProcessExecDisabled(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) dockerTaskEngine := taskEngine.(*DockerTaskEngine) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) dockerTaskEngine.execCmdMgr = execCmdMgr @@ -3394,7 +3533,7 @@ func TestMonitorExecAgentProcessExecDisabled(t *testing.T) { func TestMonitorExecAgentsMultipleContainers(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) dockerTaskEngine := taskEngine.(*DockerTaskEngine) execCmdMgr := mock_execcmdagent.NewMockManager(ctrl) dockerTaskEngine.execCmdMgr = execCmdMgr @@ -3468,7 +3607,7 @@ func TestMonitorExecAgentsMultipleContainers(t *testing.T) { func TestPeriodicExecAgentsMonitoring(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() execAgentPID := "1234" testTask := &apitask.Task{ @@ -3525,7 +3664,7 @@ func TestCreateContainerWithExecAgent(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, _, engine, _, _, _ := mocks(t, ctx, &config.Config{}) + ctrl, client, _, engine, _, _, _, _ := mocks(t, ctx, &config.Config{}) defer ctrl.Finish() taskEngine, _ := engine.(*DockerTaskEngine) stateChangeEvents := engine.StateChangeEvents() diff --git a/agent/engine/docker_task_engine_unsupported.go b/agent/engine/docker_task_engine_unsupported.go index cebf298524d..a58505a11c7 100644 --- a/agent/engine/docker_task_engine_unsupported.go +++ b/agent/engine/docker_task_engine_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,6 +17,7 @@ package engine import ( + "context" "time" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" @@ -38,3 +40,18 @@ func (engine *DockerTaskEngine) updateTaskENIDependencies(task *apitask.Task) { func (engine *DockerTaskEngine) invokePluginsForContainer(task *apitask.Task, container *apicontainer.Container) error { return nil } + +// watchAppNetImage is a file watcher, if there is any change/update to AppNet image +// we reload the image and restart the relay instance task with updated AppNet image. +func (engine *DockerTaskEngine) watchAppNetImage(ctx context.Context) { +} + +// reloadAppNetImage reloads the new AppNet image for service connect +func (engine *DockerTaskEngine) reloadAppNetImage() error { + return nil +} + +// restartInstanceTask stop the running internal relay task and starts a new one +// with updated AppNet image +func (engine *DockerTaskEngine) restartInstanceTask() { +} diff --git a/agent/engine/docker_task_engine_windows.go b/agent/engine/docker_task_engine_windows.go index a5f80f9a91b..f0dcc56679d 100644 --- a/agent/engine/docker_task_engine_windows.go +++ b/agent/engine/docker_task_engine_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,13 +17,16 @@ package engine import ( + "context" + "strings" "time" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" "github.com/aws/amazon-ecs-agent/agent/logger" "github.com/aws/amazon-ecs-agent/agent/logger/field" + dockercontainer "github.com/docker/docker/api/types/container" - apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" - apitask "github.com/aws/amazon-ecs-agent/agent/api/task" "github.com/pkg/errors" ) @@ -46,7 +50,7 @@ func (engine *DockerTaskEngine) invokePluginsForContainer(task *apitask.Task, co return errors.Wrapf(err, "error occurred while inspecting container %v", container.Name) } - cniConfig, err := engine.buildCNIConfigFromTaskContainer(task, containerInspectOutput, false) + cniConfig, err := engine.buildCNIConfigFromTaskContainerAwsvpc(task, containerInspectOutput, false) if err != nil { return errors.Wrap(err, "unable to build cni configuration") } @@ -64,3 +68,33 @@ func (engine *DockerTaskEngine) invokePluginsForContainer(task *apitask.Task, co return nil } + +func (engine *DockerTaskEngine) watchAppNetImage(ctx context.Context) { +} + +func (engine *DockerTaskEngine) reloadAppNetImage() error { + return nil +} + +func (engine *DockerTaskEngine) restartInstanceTask() { +} + +// updateCredentialSpecMapping is used to map the credentialspec local file location to docker security opts +func (engine *DockerTaskEngine) updateCredentialSpecMapping(taskID string, containerName string, desiredCredSpecInjection string, hostConfig *dockercontainer.HostConfig) { + // Inject containers' hostConfig.SecurityOpt with the credentialspec resource + logger.Info("Injecting container with credentialspec resource", logger.Fields{ + field.TaskID: taskID, + field.Container: containerName, + "credentialSpec": desiredCredSpecInjection, + }) + + if len(hostConfig.SecurityOpt) == 0 { + hostConfig.SecurityOpt = []string{desiredCredSpecInjection} + } else { + for idx, opt := range hostConfig.SecurityOpt { + if strings.HasPrefix(opt, "credentialspec:") { + hostConfig.SecurityOpt[idx] = desiredCredSpecInjection + } + } + } +} diff --git a/agent/engine/docker_task_engine_windows_test.go b/agent/engine/docker_task_engine_windows_test.go index e11e7e67cdf..d32be52fac8 100644 --- a/agent/engine/docker_task_engine_windows_test.go +++ b/agent/engine/docker_task_engine_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -37,7 +38,6 @@ import ( mock_ssm_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" "github.com/aws/amazon-ecs-agent/agent/taskresource" "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" - "github.com/aws/aws-sdk-go/aws" "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" @@ -85,7 +85,7 @@ func TestDeleteTask(t *testing.T) { func TestCredentialSpecResourceTaskFile(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, credentialsManager, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() // metadata required for createContainer workflow validation @@ -133,16 +133,14 @@ func TestCredentialSpecResourceTaskFile(t *testing.T) { ssmClientCreator := mock_ssm_factory.NewMockSSMClientCreator(ctrl) s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) - credentialSpecReq := []string{credentialspecFile} - credentialSpecRes, cerr := credentialspec.NewCredentialSpecResource( testTask.Arn, defaultConfig.AWSRegion, - credentialSpecReq, credentialsID, credentialsManager, ssmClientCreator, - s3ClientCreator) + s3ClientCreator, + nil) assert.NoError(t, cerr) credSpecdata := map[string]string{ @@ -166,7 +164,7 @@ func TestCredentialSpecResourceTaskFile(t *testing.T) { func TestCredentialSpecResourceTaskFileErr(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, credentialsManager, _, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, credentialsManager, _, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() // metadata required for createContainer workflow validation @@ -214,16 +212,14 @@ func TestCredentialSpecResourceTaskFileErr(t *testing.T) { ssmClientCreator := mock_ssm_factory.NewMockSSMClientCreator(ctrl) s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) - credentialSpecReq := []string{credentialspecFile} - credentialSpecRes, cerr := credentialspec.NewCredentialSpecResource( testTask.Arn, defaultConfig.AWSRegion, - credentialSpecReq, credentialsID, credentialsManager, ssmClientCreator, - s3ClientCreator) + s3ClientCreator, + nil) assert.NoError(t, cerr) credSpecdata := map[string]string{ @@ -242,11 +238,12 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { config := defaultConfig ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, _, _, taskEngine, _, _, _ := mocks(t, ctx, &config) + ctrl, _, _, taskEngine, _, _, _, _ := mocks(t, ctx, &config) defer ctrl.Finish() testTask := testdata.LoadTask("sleep5") testTask.AddTaskENI(mockENI) + testTask.NetworkMode = apitask.AWSVPCNetworkMode testTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -268,7 +265,7 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { }, } - cniConfig, err := taskEngine.(*DockerTaskEngine).buildCNIConfigFromTaskContainer(testTask, containerInspectOutput, true) + cniConfig, err := taskEngine.(*DockerTaskEngine).buildCNIConfigFromTaskContainerAwsvpc(testTask, containerInspectOutput, true) assert.NoError(t, err) assert.Equal(t, containerID, cniConfig.ContainerID) assert.Equal(t, strconv.Itoa(containerPid), cniConfig.ContainerPID) @@ -286,7 +283,7 @@ func TestBuildCNIConfigFromTaskContainer(t *testing.T) { func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, client, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, client, mockTime, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() mockCNIClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -326,6 +323,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { client.EXPECT().CreateContainer(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do( func(ctx interface{}, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, containerName string, z time.Duration) { sleepTask.AddTaskENI(mockENI) + sleepTask.NetworkMode = apitask.AWSVPCNetworkMode sleepTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, ProxyIngressPort: proxyIngressPort, @@ -444,7 +442,7 @@ func TestTaskWithSteadyStateResourcesProvisioned(t *testing.T) { func TestPauseContainerHappyPath(t *testing.T) { ctx, cancel := context.WithCancel(context.TODO()) defer cancel() - ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _ := mocks(t, ctx, &defaultConfig) + ctrl, dockerClient, mockTime, taskEngine, _, imageManager, _, _ := mocks(t, ctx, &defaultConfig) defer ctrl.Finish() cniClient := mock_ecscni.NewMockCNIClient(ctrl) @@ -459,6 +457,7 @@ func TestPauseContainerHappyPath(t *testing.T) { // Add eni information to the task so the task can add dependency of pause container sleepTask.AddTaskENI(mockENI) + sleepTask.NetworkMode = apitask.AWSVPCNetworkMode sleepTask.SetAppMesh(&appmesh.AppMesh{ IgnoredUID: ignoredUID, diff --git a/agent/engine/dockerstate/docker_task_engine_state.go b/agent/engine/dockerstate/docker_task_engine_state.go index 32817de8d89..728036598b5 100644 --- a/agent/engine/dockerstate/docker_task_engine_state.go +++ b/agent/engine/dockerstate/docker_task_engine_state.go @@ -30,6 +30,9 @@ import ( type TaskEngineState interface { // AllTasks returns all of the tasks AllTasks() []*apitask.Task + // AllExternalTasks returns all tasks with IsInternal==false (i.e. customer-initiated tasks). + // Currently, ServiceConnect AppNet Relay task is the only internal task. + AllExternalTasks() []*apitask.Task // AllENIAttachments returns all of the eni attachments AllENIAttachments() []*apieni.ENIAttachment // AllImageStates returns all of the image.ImageStates @@ -152,13 +155,32 @@ func (state *DockerTaskEngineState) AllTasks() []*apitask.Task { } func (state *DockerTaskEngineState) allTasksUnsafe() []*apitask.Task { + return state.getFilteredTasksUnsafe(false) +} + +// AllExternalTasks returns all tasks with IsInternal==false (i.e. all customer-initiated tasks) +func (state *DockerTaskEngineState) AllExternalTasks() []*apitask.Task { + state.lock.RLock() + defer state.lock.RUnlock() + + return state.allExternalTasksUnsafe() +} + +func (state *DockerTaskEngineState) allExternalTasksUnsafe() []*apitask.Task { + return state.getFilteredTasksUnsafe(true) +} + +func (state *DockerTaskEngineState) getFilteredTasksUnsafe(excludeInternal bool) []*apitask.Task { ret := make([]*apitask.Task, len(state.tasks)) ndx := 0 for _, task := range state.tasks { + if excludeInternal && task.IsInternal { + continue + } ret[ndx] = task ndx++ } - return ret + return ret[:ndx] } // AllImageStates returns all of the image.ImageStates diff --git a/agent/engine/dockerstate/dockerstate_test.go b/agent/engine/dockerstate/dockerstate_test.go index fc5c94d5be8..7fe6a5ba7b8 100644 --- a/agent/engine/dockerstate/dockerstate_test.go +++ b/agent/engine/dockerstate/dockerstate_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/dockerstate/json_test.go b/agent/engine/dockerstate/json_test.go index 7a1f26ee358..c584c35787a 100644 --- a/agent/engine/dockerstate/json_test.go +++ b/agent/engine/dockerstate/json_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/dockerstate/mocks/dockerstate_mocks.go b/agent/engine/dockerstate/mocks/dockerstate_mocks.go index c690c2d7c6b..1a1a497c689 100644 --- a/agent/engine/dockerstate/mocks/dockerstate_mocks.go +++ b/agent/engine/dockerstate/mocks/dockerstate_mocks.go @@ -137,6 +137,20 @@ func (mr *MockTaskEngineStateMockRecorder) AllENIAttachments() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllENIAttachments", reflect.TypeOf((*MockTaskEngineState)(nil).AllENIAttachments)) } +// AllExternalTasks mocks base method +func (m *MockTaskEngineState) AllExternalTasks() []*task.Task { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AllExternalTasks") + ret0, _ := ret[0].([]*task.Task) + return ret0 +} + +// AllExternalTasks indicates an expected call of AllExternalTasks +func (mr *MockTaskEngineStateMockRecorder) AllExternalTasks() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllExternalTasks", reflect.TypeOf((*MockTaskEngineState)(nil).AllExternalTasks)) +} + // AllImageStates mocks base method func (m *MockTaskEngineState) AllImageStates() []*image.ImageState { m.ctrl.T.Helper() diff --git a/agent/engine/dockerstate/testutils/json_test.go b/agent/engine/dockerstate/testutils/json_test.go index 6170683aaea..c05710f91fe 100644 --- a/agent/engine/dockerstate/testutils/json_test.go +++ b/agent/engine/dockerstate/testutils/json_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/engine_integ_test.go b/agent/engine/engine_integ_test.go index b6d543a2648..09d4b3840c5 100644 --- a/agent/engine/engine_integ_test.go +++ b/agent/engine/engine_integ_test.go @@ -1,4 +1,5 @@ //go:build integration +// +build integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/engine_sudo_linux_integ_test.go b/agent/engine/engine_sudo_linux_integ_test.go index e439b783cf5..c627fc44d6a 100644 --- a/agent/engine/engine_sudo_linux_integ_test.go +++ b/agent/engine/engine_sudo_linux_integ_test.go @@ -1,4 +1,5 @@ //go:build linux && sudo +// +build linux,sudo // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -42,6 +43,7 @@ import ( dockercontainer "github.com/docker/docker/api/types/container" sdkClient "github.com/docker/docker/client" "github.com/pborman/uuid" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -60,6 +62,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + engineserviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" "github.com/aws/amazon-ecs-agent/agent/taskresource" cgroup "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control" @@ -375,8 +378,8 @@ func createFirelensTask(t *testing.T) *apitask.Task { } func waitCloudwatchLogs(client *cloudwatchlogs.CloudWatchLogs, params *cloudwatchlogs.GetLogEventsInput) (*cloudwatchlogs.GetLogEventsOutput, error) { - // The test could fail for timing issue, so retry for 30 seconds to make this test more stable - for i := 0; i < 30; i++ { + // The test could fail for timing issue, so retry for 60 seconds to make this test more stable + for i := 0; i < 60; i++ { resp, err := client.GetLogEvents(params) if err != nil { awsError, ok := err.(awserr.Error) @@ -584,7 +587,7 @@ func setupEngineForExecCommandAgent(t *testing.T, hostBinDir string) (TaskEngine taskEngine := NewDockerTaskEngine(cfg, dockerClient, credentialsManager, eventstream.NewEventStream("ENGINEINTEGTEST", context.Background()), imageManager, state, metadataManager, - nil, execCmdMgr) + nil, execCmdMgr, engineserviceconnect.NewManager()) taskEngine.monitorExecAgentsInterval = time.Second taskEngine.MustInit(context.TODO()) return taskEngine, func() { @@ -755,3 +758,108 @@ func verifyTaskRunningStateChange(t *testing.T, taskEngine TaskEngine) { assert.Equal(t, event.(api.TaskStateChange).Status, apitaskstatus.TaskRunning, "Expected task to be RUNNING") } + +func TestGMSATaskFile(t *testing.T) { + t.Setenv("ECS_GMSA_SUPPORTED", "True") + t.Setenv("ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION", "True") + t.Setenv("ZZZ_SKIP_CREDENTIALS_FETCHER_INVOCATION_CHECK_NOT_SUPPORTED_IN_PRODUCTION", "True") + + cfg := defaultTestConfigIntegTest() + cfg.TaskCPUMemLimit.Value = config.ExplicitlyDisabled + cfg.TaskCleanupWaitDuration = 3 * time.Second + cfg.GMSACapable = true + cfg.AWSRegion = "us-west-2" + + taskEngine, done, _ := setupGMSALinux(cfg, nil, t) + defer done() + + stateChangeEvents := taskEngine.StateChangeEvents() + + // Setup test gmsa file + credentialSpecDataDir := "/tmp" + testFileName := "test-gmsa.json" + testCredSpecFilePath := filepath.Join(credentialSpecDataDir, testFileName) + _, err := os.Create(testCredSpecFilePath) + require.NoError(t, err) + + // add local credentialspec file + testCredSpecData := []byte(`{ + "CmsPlugins": [ + "ActiveDirectory" + ], + "DomainJoinConfig": { + "Sid": "S-1-5-21-975084816-3050680612-2826754290", + "MachineAccountName": "gmsa-acct-test", + "Guid": "92a07e28-bd9f-4bf3-b1f7-0894815a5257", + "DnsTreeName": "gmsa.test.com", + "DnsName": "gmsa.test.com", + "NetBiosName": "gmsa" + }, + "ActiveDirectoryConfig": { + "GroupManagedServiceAccounts": [ + { + "Name": "gmsa-acct-test", + "Scope": "gmsa.test.com" + } + ] + } +}`) + + err = ioutil.WriteFile(testCredSpecFilePath, testCredSpecData, 0755) + require.NoError(t, err) + + testContainer := createTestContainer() + testContainer.Name = "testGMSATaskFile" + + hostConfig := "{\"SecurityOpt\": [\"credentialspec:file:///tmp/test-gmsa.json\"]}" + testContainer.DockerConfig.HostConfig = &hostConfig + + testTask := &apitask.Task{ + Arn: "testGMSAFileTaskARN", + Family: "family", + Version: "1", + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + Containers: []*apicontainer.Container{testContainer}, + } + testTask.Containers[0].TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + testTask.ResourcesMapUnsafe = make(map[string][]taskresource.TaskResource) + testTask.Containers[0].Command = getLongRunningCommand() + + go taskEngine.AddTask(testTask) + + verifyTaskIsRunning(stateChangeEvents, testTask) + + client, _ := sdkClient.NewClientWithOpts(sdkClient.WithHost(endpoint), sdkClient.WithVersion(sdkclientfactory.GetDefaultVersion().String())) + containerMap, _ := taskEngine.(*DockerTaskEngine).state.ContainerMapByArn(testTask.Arn) + cid := containerMap[testTask.Containers[0].Name].DockerID + + expectedBind := "/tmp/tgt:/var/credentials-fetcher/krbdir:ro" + err = verifyContainerBindMount(client, cid, expectedBind) + assert.NoError(t, err) + + // Kill the existing container now + err = client.ContainerKill(context.TODO(), cid, "SIGKILL") + assert.NoError(t, err, "Could not kill container") + + verifyTaskIsStopped(stateChangeEvents, testTask) + + // Cleanup the test file + err = os.RemoveAll(testCredSpecFilePath) + assert.NoError(t, err) + +} + +func verifyContainerBindMount(client *sdkClient.Client, id, expectedBind string) error { + dockerContainer, err := client.ContainerInspect(context.TODO(), id) + if err != nil { + return err + } + + for _, opt := range dockerContainer.HostConfig.Binds { + if opt == expectedBind { + return nil + } + } + + return errors.New("unable to validate the bind mount") +} diff --git a/agent/engine/engine_unix_integ_test.go b/agent/engine/engine_unix_integ_test.go index 9aa7a25ec09..061936c4dd8 100644 --- a/agent/engine/engine_unix_integ_test.go +++ b/agent/engine/engine_unix_integ_test.go @@ -1,4 +1,5 @@ //go:build !windows && integration +// +build !windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -403,7 +404,7 @@ func TestDynamicPortForward(t *testing.T) { } var bindingForcontainerPortOne uint16 for _, binding := range validPortBindings { - if binding.ContainerPort == port { + if port == binding.ContainerPort { bindingForcontainerPortOne = binding.HostPort } } @@ -459,7 +460,7 @@ func TestMultipleDynamicPortForward(t *testing.T) { var bindingForcontainerPortOne_1 uint16 var bindingForcontainerPortOne_2 uint16 for _, binding := range validPortBindings { - if binding.ContainerPort == port { + if port == binding.ContainerPort { if bindingForcontainerPortOne_1 == 0 { bindingForcontainerPortOne_1 = binding.HostPort } else { diff --git a/agent/engine/engine_windows_integ_test.go b/agent/engine/engine_windows_integ_test.go index 59bae72ac6f..b5ac2eb489d 100644 --- a/agent/engine/engine_windows_integ_test.go +++ b/agent/engine/engine_windows_integ_test.go @@ -1,4 +1,5 @@ //go:build windows && integration +// +build windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -28,12 +29,6 @@ import ( "testing" "time" - "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" - - "github.com/cihub/seelog" - - "github.com/docker/docker/api/types" - "github.com/aws/amazon-ecs-agent/agent/api" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" @@ -46,15 +41,20 @@ import ( "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory" "github.com/aws/amazon-ecs-agent/agent/ec2" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" "github.com/aws/amazon-ecs-agent/agent/engine/execcmd" + engineserviceconnect "github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect" "github.com/aws/amazon-ecs-agent/agent/eventstream" s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" "github.com/aws/amazon-ecs-agent/agent/taskresource" taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/aws-sdk-go/aws" + "github.com/cihub/seelog" + "github.com/docker/docker/api/types" sdkClient "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -488,14 +488,14 @@ func setupGMSA(cfg *config.Config, state dockerstate.TaskEngineState, t *testing resourceFields := &taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmfactory.NewSSMClientCreator(), + S3ClientCreator: s3factory.NewS3ClientCreator(), }, - DockerClient: dockerClient, - S3ClientCreator: s3factory.NewS3ClientCreator(), + DockerClient: dockerClient, } taskEngine := NewDockerTaskEngine(cfg, dockerClient, credentialsManager, eventstream.NewEventStream("ENGINEINTEGTEST", context.Background()), imageManager, state, metadataManager, - resourceFields, execcmd.NewManager()) + resourceFields, execcmd.NewManager(), engineserviceconnect.NewManager()) taskEngine.MustInit(context.TODO()) return taskEngine, func() { taskEngine.Shutdown() @@ -737,7 +737,7 @@ func setupEngineForExecCommandAgent(t *testing.T, hostBinDir string) (TaskEngine taskEngine := NewDockerTaskEngine(cfg, dockerClient, credentialsManager, eventstream.NewEventStream("ENGINEINTEGTEST", context.Background()), imageManager, state, metadataManager, - nil, execCmdMgr) + nil, execCmdMgr, engineserviceconnect.NewManager()) taskEngine.monitorExecAgentsInterval = time.Second taskEngine.MustInit(context.TODO()) return taskEngine, func() { @@ -838,7 +838,10 @@ func verifyMockExecCommandAgentStatus(t *testing.T, client *sdkClient.Client, co require.NotEqual(t, -1, pidPos, "PID title not found in the container top response") for _, proc := range top.Processes { matched, _ := regexp.MatchString(execCmdAgentProcessRegex, proc[cmdPos]) - if matched { + // Process we are checking to be stopped might still be running. + // expectedPid matches the pid of the process in that case, so wait if that's + // the case. + if matched && (checkIsRunning || expectedPid != proc[pidPos]) { res <- proc[pidPos] return } @@ -847,7 +850,7 @@ func verifyMockExecCommandAgentStatus(t *testing.T, client *sdkClient.Client, co select { case <-ctx.Done(): return - case <-time.After(time.Second * 4): + case <-time.After(time.Second * 1): } } }() @@ -888,7 +891,4 @@ func killMockExecCommandAgent(t *testing.T, client *sdkClient.Client, containerI Detach: true, }) require.NoError(t, err) - - // Windows docker exec takes longer than Linux - time.Sleep(4 * time.Second) } diff --git a/agent/engine/execcmd/manager_init_task_linux.go b/agent/engine/execcmd/manager_init_task_linux.go index 6398bf9bb1a..05af1582b1e 100644 --- a/agent/engine/execcmd/manager_init_task_linux.go +++ b/agent/engine/execcmd/manager_init_task_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/execcmd/manager_init_task_linux_test.go b/agent/engine/execcmd/manager_init_task_linux_test.go index cb242d995b2..564e58bada2 100644 --- a/agent/engine/execcmd/manager_init_task_linux_test.go +++ b/agent/engine/execcmd/manager_init_task_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/execcmd/manager_start_linux.go b/agent/engine/execcmd/manager_start_linux.go index b98634c856d..774c3435bc7 100644 --- a/agent/engine/execcmd/manager_start_linux.go +++ b/agent/engine/execcmd/manager_start_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/execcmd/manager_start_linux_test.go b/agent/engine/execcmd/manager_start_linux_test.go index 2613f9ab2cd..061617a61c4 100644 --- a/agent/engine/execcmd/manager_start_linux_test.go +++ b/agent/engine/execcmd/manager_start_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/execcmd/manager_unsupported.go b/agent/engine/execcmd/manager_unsupported.go index 4a8124c3b2b..3fed93585c4 100644 --- a/agent/engine/execcmd/manager_unsupported.go +++ b/agent/engine/execcmd/manager_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/mocks/engine_mocks.go b/agent/engine/mocks/engine_mocks.go index 2de73cc6299..976288e2998 100644 --- a/agent/engine/mocks/engine_mocks.go +++ b/agent/engine/mocks/engine_mocks.go @@ -266,6 +266,18 @@ func (mr *MockImageManagerMockRecorder) AddAllImageStates(arg0 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAllImageStates", reflect.TypeOf((*MockImageManager)(nil).AddAllImageStates), arg0) } +// AddImageToCleanUpExclusionList mocks base method +func (m *MockImageManager) AddImageToCleanUpExclusionList(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddImageToCleanUpExclusionList", arg0) +} + +// AddImageToCleanUpExclusionList indicates an expected call of AddImageToCleanUpExclusionList +func (mr *MockImageManagerMockRecorder) AddImageToCleanUpExclusionList(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddImageToCleanUpExclusionList", reflect.TypeOf((*MockImageManager)(nil).AddImageToCleanUpExclusionList), arg0) +} + // GetImageStateFromImageName mocks base method func (m *MockImageManager) GetImageStateFromImageName(arg0 string) (*image.ImageState, bool) { m.ctrl.T.Helper() diff --git a/agent/engine/ordering_integ_test.go b/agent/engine/ordering_integ_test.go index 25e787e3c85..31162998126 100644 --- a/agent/engine/ordering_integ_test.go +++ b/agent/engine/ordering_integ_test.go @@ -1,4 +1,5 @@ //go:build integration +// +build integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/ordering_integ_unix_test.go b/agent/engine/ordering_integ_unix_test.go index 2f8d0a2b3cf..1878e4bdb54 100644 --- a/agent/engine/ordering_integ_unix_test.go +++ b/agent/engine/ordering_integ_unix_test.go @@ -1,4 +1,5 @@ //go:build integration && !windows +// +build integration,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/ordering_integ_windows_test.go b/agent/engine/ordering_integ_windows_test.go index 89a1515cfa2..b411c377177 100644 --- a/agent/engine/ordering_integ_windows_test.go +++ b/agent/engine/ordering_integ_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && integration +// +build windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/serviceconnect/generate_mocks.go b/agent/engine/serviceconnect/generate_mocks.go new file mode 100644 index 00000000000..b75d10b9626 --- /dev/null +++ b/agent/engine/serviceconnect/generate_mocks.go @@ -0,0 +1,16 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +//go:generate mockgen -destination=mock/manager.go -copyright_file=../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect Manager diff --git a/agent/engine/serviceconnect/manager.go b/agent/engine/serviceconnect/manager.go new file mode 100644 index 00000000000..ce79e60f875 --- /dev/null +++ b/agent/engine/serviceconnect/manager.go @@ -0,0 +1,36 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "github.com/aws/amazon-ecs-agent/agent/api" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" + dockercontainer "github.com/docker/docker/api/types/container" +) + +type Manager interface { + loader.Loader + + GetLoadedImageName() string + AugmentTaskContainer(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error + CreateInstanceTask(config *config.Config) (*apitask.Task, error) + AugmentInstanceContainer(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error + SetECSClient(client api.ECSClient, containerInstanceARN string) + GetLoadedAppnetVersion() (string, error) + GetCapabilitiesForAppnetInterfaceVersion(appnetVersion string) ([]string, error) + GetAppnetContainerTarballDir() string +} diff --git a/agent/engine/serviceconnect/manager_linux.go b/agent/engine/serviceconnect/manager_linux.go new file mode 100644 index 00000000000..933e5e4ea4c --- /dev/null +++ b/agent/engine/serviceconnect/manager_linux.go @@ -0,0 +1,453 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + "time" + + "github.com/pborman/uuid" + + "github.com/aws/aws-sdk-go/aws" + + "github.com/aws/amazon-ecs-agent/agent/api" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" + apiserviceconnect "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/taskresource" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" + "github.com/docker/docker/api/types" + dockercontainer "github.com/docker/docker/api/types/container" +) + +const ( + defaultRelayPathContainer = "/var/run/ecs/relay/" + defaultRelayPathHost = "/var/run/ecs/service_connect/relay/" + defaultRelayFileName = "envoy_xds.sock" + defaultEndpointENV = "APPMESH_XDS_ENDPOINT" + defaultStatusPathContainer = "/var/run/ecs/" + // Expected to have task.GetID() appended to form actual host path + defaultStatusPathHostRoot = "/var/run/ecs/service_connect/" + defaultStatusFileName = "appnet_admin.sock" + defaultStatusENV = "APPNET_AGENT_ADMIN_UDS_PATH" + + // logging + defaultLogPathHostRoot = "/var/log/ecs/service_connect/" + defaultLogPathContainer = "/var/log/" + defaultECSAgentLogPathForSC = "/%s/service_connect/" // %s will be substituted with ECS Agent container log path + defaultAppnetEnvoyLogDestinationENV = "APPNET_ENVOY_LOG_DESTINATION" + + relayEnableENV = "APPNET_ENABLE_RELAY_MODE_FOR_XDS" + relayEnableOn = "1" + upstreamENV = "APPNET_RELAY_LISTENER_UDS_PATH" + regionENV = "AWS_REGION" + agentAuthENV = "ENVOY_ENABLE_IAM_AUTH_FOR_XDS" + agentAuthOff = "0" + agentModeENV = "APPNET_AGENT_ADMIN_MODE" + agentModeValue = "uds" + envoyModeENV = "ENVOY_ADMIN_MODE" + envoyModeValue = "uds" + + containerInstanceArnENV = "ECS_CONTAINER_INSTANCE_ARN" + + unixRequestPrefix = "unix://" + httpRequestPrefix = "http://localhost" + defaultAdminStatsRequest = httpRequestPrefix + "/stats/prometheus?usedonly&filter=metrics_extension&delta" + defaultAdminDrainRequest = httpRequestPrefix + "/drain_listeners?inboundonly" + + defaultAgentContainerImageName = "ecs-service-connect-agent" + defaultAgentContainerTagFormat = "interface-%s" + defaultAgentContainerTarballPathFormat = "/managed-agents/serviceconnect/ecs-service-connect-agent.interface-%s.tar" + + ecsAgentLogFileENV = "ECS_LOGFILE" + defaultECSAgentLogPathContainer = "/log" +) + +type manager struct { + // Path to where relayFileName exists which Envoy in the container will connect to + relayPathContainer string + // Path to where relayFileName exists on Host + relayPathHost string + // Filename without Path which Relay will create and Envoy in the container will connect to + relayFileName string + // Environment variable to set on Container with contents of relayPathContainer/relayFileName + endpointENV string + // Path to where statusFileName exists which Envoy in the container will create for status endpoint + statusPathContainer string + // PathRoot to be appended with TaskID statusPathHostRoot/task.GetID() where statusFileName exists on Host + statusPathHostRoot string + // Filename without Path which Envoy in container will create for status endpoint + statusFileName string + // Environment variable to set on Container with contents of statusPathContainer/statusFileName + statusENV string + // Path to where AppNet log file will be written to inside container + logPathContainer string + // Path to where AppNet log file will be written to on host + logPathHostRoot string + // Path to create logging dir for AppNet, from ECS Agent point of view (b/c "/log" for ECS Agent is "/var/log/ecs" on host) + logPathECSAgentRoot string + + // Http path + params to make a statistics request of AppNetAgent + adminStatsRequest string + // Http path + params to make a drain request of AppNetAgent + adminDrainRequest string + + agentContainerImageName string + agentContainerTag string + appnetInterfaceVersion string + + ecsClient api.ECSClient + containerInstanceARN string +} + +func NewManager() Manager { + return &manager{ + relayPathContainer: defaultRelayPathContainer, + relayPathHost: defaultRelayPathHost, + relayFileName: defaultRelayFileName, + endpointENV: defaultEndpointENV, + statusPathContainer: defaultStatusPathContainer, + statusPathHostRoot: defaultStatusPathHostRoot, + statusFileName: defaultStatusFileName, + statusENV: defaultStatusENV, + adminStatsRequest: defaultAdminStatsRequest, + adminDrainRequest: defaultAdminDrainRequest, + logPathContainer: defaultLogPathContainer, + logPathHostRoot: defaultLogPathHostRoot, + logPathECSAgentRoot: fmt.Sprintf(defaultECSAgentLogPathForSC, getECSAgentLogPathContainer()), + + agentContainerImageName: defaultAgentContainerImageName, + } +} + +func (m *manager) SetECSClient(client api.ECSClient, containerInstanceARN string) { + m.ecsClient = client + m.containerInstanceARN = containerInstanceARN +} + +func (m *manager) augmentAgentContainer(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error { + if task.IsNetworkModeBridge() { + err := m.initServiceConnectContainerMapping(task, container, hostConfig) + if err != nil { + return err + } + } + adminPath, err := m.initAgentDirectoryMounts(task.GetID(), container, hostConfig) + if err != nil { + return err + } + m.initAgentEnvironment(container) + + // Setup runtime configuration + var config apiserviceconnect.RuntimeConfig + config.AdminSocketPath = adminPath + config.StatsRequest = m.adminStatsRequest + config.DrainRequest = m.adminDrainRequest + + task.PopulateServiceConnectRuntimeConfig(config) + container.Image = m.GetLoadedImageName() + return nil +} + +func getBindMountMapping(hostDir, containerDir string) string { + return hostDir + ":" + containerDir +} + +var mkdirAllAndChown = defaultMkdirAllAndChown + +func defaultMkdirAllAndChown(path string, perm fs.FileMode, uid, gid int) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + err = os.MkdirAll(path, perm) + } + if err != nil { + return fmt.Errorf("failed to mkdir %s: %+v", path, err) + } + // AppNet Agent container is going to run as non-root user $AppNetUID. + // Change directory owner to $AppNetUID so that it has full permission (to create socket file and bind to it etc.) + if err = os.Chown(path, uid, gid); err != nil { + return fmt.Errorf("failed to chown %s: %+v", path, err) + } + return nil +} + +func (m *manager) initAgentDirectoryMounts(taskId string, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) (string, error) { + statusPathHost := filepath.Join(m.statusPathHostRoot, taskId) + + // Create host directories if they don't exist + for _, path := range []string{statusPathHost, m.relayPathHost} { + err := mkdirAllAndChown(path, 0700, apiserviceconnect.AppNetUID, os.Getegid()) + if err != nil { + return "", err + } + } + + hostConfig.Binds = append(hostConfig.Binds, getBindMountMapping(statusPathHost, m.statusPathContainer)) + hostConfig.Binds = append(hostConfig.Binds, getBindMountMapping(m.relayPathHost, m.relayPathContainer)) + + // create logging directory and bind mount, if customer has not configured a logging driver + if container.GetLogDriver() == "" { + logPathHost := filepath.Join(m.logPathHostRoot, taskId) + logPathECSAgent := filepath.Join(m.logPathECSAgentRoot, taskId) + err := mkdirAllAndChown(logPathECSAgent, 0700, apiserviceconnect.AppNetUID, os.Getegid()) + if err != nil { + return "", err + } + hostConfig.Binds = append(hostConfig.Binds, getBindMountMapping(logPathHost, m.logPathContainer)) + } + + return filepath.Join(statusPathHost, m.statusFileName), nil +} + +func (m *manager) initAgentEnvironment(container *apicontainer.Container) { + scEnv := map[string]string{ + m.endpointENV: unixRequestPrefix + filepath.Join(m.relayPathContainer, m.relayFileName), + m.statusENV: filepath.Join(m.statusPathContainer, m.statusFileName), + agentModeENV: agentModeValue, + agentAuthENV: agentAuthOff, + containerInstanceArnENV: m.containerInstanceARN, + } + if container.GetLogDriver() == "" { + scEnv[defaultAppnetEnvoyLogDestinationENV] = m.logPathContainer + } + + container.MergeEnvironmentVariables(scEnv) +} + +func (m *manager) initRelayEnvironment(config *config.Config, container *apicontainer.Container) { + endpoint := fmt.Sprintf("https://ecs-sc.%s.api.aws", config.AWSRegion) + if m.ecsClient != nil { + discoveredEndpoint, err := m.ecsClient.DiscoverServiceConnectEndpoint(m.containerInstanceARN) + if err != nil { + logger.Error("Failed to retrieve service connect endpoint from DiscoverPollEndpoint, failing back to default", logger.Fields{ + field.Error: err, + field.ManagedAgent: "service-connect", + "endpoint": endpoint, + }) + } else { + endpoint = discoveredEndpoint + } + } + scEnv := map[string]string{ + m.statusENV: filepath.Join(m.statusPathContainer, m.statusFileName), + upstreamENV: filepath.Join(m.relayPathContainer, m.relayFileName), + regionENV: config.AWSRegion, + envoyModeENV: envoyModeValue, + agentModeENV: agentModeValue, + relayEnableENV: relayEnableOn, + m.endpointENV: endpoint, + defaultAppnetEnvoyLogDestinationENV: m.logPathContainer, + } + + container.MergeEnvironmentVariables(scEnv) +} + +func (m *manager) initServiceConnectContainerMapping(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error { + // TODO [SC] - Move the function here + return task.PopulateServiceConnectContainerMappingEnvVar() +} + +// DNSConfigToDockerExtraHostsFormat converts a []DNSConfigEntry slice to a list of ExtraHost entries that Docker will +// recognize. +func DNSConfigToDockerExtraHostsFormat(dnsConfigs []apiserviceconnect.DNSConfigEntry) []string { + var hosts []string + for _, dnsConf := range dnsConfigs { + if len(dnsConf.Address) > 0 { + hosts = append(hosts, + fmt.Sprintf("%s:%s", dnsConf.HostName, dnsConf.Address)) + } + } + return hosts +} + +func (m *manager) AugmentTaskContainer(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error { + var err error + // Add SC VIPs to pause container's known hosts + if container.Type == apicontainer.ContainerCNIPause { + hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, + DNSConfigToDockerExtraHostsFormat(task.ServiceConnectConfig.DNSConfig)...) + } + if container == task.GetServiceConnectContainer() { + m.augmentAgentContainer(task, container, hostConfig) + } + return err +} + +func (m *manager) CreateInstanceTask(cfg *config.Config) (*apitask.Task, error) { + imageName := m.GetLoadedImageName() + containerRunning := apicontainerstatus.ContainerRunning + dockerHostConfig := dockercontainer.HostConfig{ + NetworkMode: apitask.HostNetworkMode, + // do not restart relay if it's stopped manually. + // the default value of 0 for MaximumRetryCount means that we will not enforce a maximum count + RestartPolicy: dockercontainer.RestartPolicy{ + Name: "on-failure", + MaximumRetryCount: 0, + }, + } + rawHostConfig, err := json.Marshal(&dockerHostConfig) + if err != nil { + return nil, err + } + // Configure AppNet relay container health check. + // For AppNet Agent container, the health check configuration is part of task payload, + // however for relay we need to create it ourselves. + healthConfig := dockercontainer.HealthConfig{ + Test: []string{"CMD-SHELL", "/health_check.sh"}, + Interval: 5 * time.Second, + Timeout: 2 * time.Second, + Retries: 3, + } + rawHealthConfig, err := json.Marshal(&healthConfig) + if err != nil { + return nil, err + } + // The raw host config needs to be created this way - if we marshal the entire config object + // directly, and the object only contains healthcheck, all other fields will be written as empty/nil + // in the result string. This will override the configurations that comes with the container image + // (CMD for example) + rawConfig := fmt.Sprintf("{\"Healthcheck\":%s}", string(rawHealthConfig)) + + // Create an internal task for AppNet Relay container + task := &apitask.Task{ + Arn: fmt.Sprintf("%s-%s", "arn:::::/service-connect-relay", uuid.NewUUID()), + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + Containers: []*apicontainer.Container{{ + Name: "instance-service-connect-relay", + Image: imageName, + ContainerArn: "arn:::::/instance-service-connect-relay", + Type: apicontainer.ContainerServiceConnectRelay, + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + Essential: true, + SteadyStateStatusUnsafe: &containerRunning, + DockerConfig: apicontainer.DockerConfig{ + Config: aws.String(rawConfig), + HostConfig: aws.String(string(rawHostConfig)), + }, + HealthCheckType: "DOCKER", + }}, + LaunchType: "EC2", + NetworkMode: apitask.HostNetworkMode, + ResourcesMapUnsafe: make(map[string][]taskresource.TaskResource), + IsInternal: true, + } + m.initRelayEnvironment(cfg, task.Containers[0]) + + return task, nil +} + +func (m *manager) AugmentInstanceContainer(task *apitask.Task, container *apicontainer.Container, hostConfig *dockercontainer.HostConfig) error { + adminPath, err := m.initAgentDirectoryMounts("relay", container, hostConfig) + if err != nil { + return err + } + + // Setup runtime configuration + var config apiserviceconnect.RuntimeConfig + config.AdminSocketPath = adminPath + config.StatsRequest = m.adminStatsRequest + config.DrainRequest = m.adminDrainRequest + + task.PopulateServiceConnectRuntimeConfig(config) + return nil +} + +func (agent *manager) setLoadedAppnetVerion(appnetInterfaceVersion string) { + agent.appnetInterfaceVersion = appnetInterfaceVersion +} + +// LoadImage helps load the AppNetAgent container image for the agent latest supported +// AppNet interface version by looking for the AppNet agent tar name from supported list +// of AppNet versions from highest to lowest version when loading AppNet image +func (agent *manager) LoadImage(ctx context.Context, _ *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + var loadErr error + for _, supportedAppnetInterfaceVersion := range getSupportedAppnetInterfaceVersions() { + agentContainerTarballPath := fmt.Sprintf(defaultAgentContainerTarballPathFormat, supportedAppnetInterfaceVersion) + if _, err := os.Stat(agentContainerTarballPath); err != nil { + logger.Warn(fmt.Sprintf("AppNet agent container tarball unavailable: %s", agentContainerTarballPath), logger.Fields{ + field.Error: err, + }) + continue + } + logger.Debug(fmt.Sprintf("Loading Appnet agent container tarball: %s", agentContainerTarballPath)) + if loadErr = loader.LoadFromFile(ctx, agentContainerTarballPath, dockerClient); loadErr != nil { + logger.Warn(fmt.Sprintf("Unable to load Appnet agent container tarball: %s", agentContainerTarballPath), + logger.Fields{ + field.Error: loadErr, + }) + continue + } + agent.setLoadedAppnetVerion(supportedAppnetInterfaceVersion) + imageName := agent.GetLoadedImageName() + logger.Info(fmt.Sprintf("Successfully loaded Appnet agent container tarball: %s", agentContainerTarballPath), + logger.Fields{ + field.Image: imageName, + }) + return loader.GetContainerImage(imageName, dockerClient) + } + return nil, loadErr +} + +func (agent *manager) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return loader.IsImageLoaded(agent.GetLoadedImageName(), dockerClient) +} + +func (agent *manager) GetLoadedImageName() string { + agent.agentContainerTag = fmt.Sprintf(defaultAgentContainerTagFormat, agent.appnetInterfaceVersion) + return fmt.Sprintf("%s:%s", agent.agentContainerImageName, agent.agentContainerTag) +} + +func (agent *manager) GetLoadedAppnetVersion() (string, error) { + return agent.appnetInterfaceVersion, nil +} + +// getECSAgentLogPathContainer returns the directory path for ECS_LOGFILE env value if exists, otherwise returns "/log" +func getECSAgentLogPathContainer() string { + ecsLogFilePath := os.Getenv(ecsAgentLogFileENV) + if ecsLogFilePath == "" { + return defaultECSAgentLogPathContainer + } + return path.Dir(ecsLogFilePath) +} + +// GetCapabilitiesForAppnetInterfaceVersion returns service connect capabilities +// supported by ECS Agent to register for a selected AppNet version. +// Suppose we decide to register ecs.service-connect.v2 capability for new AppNet version (ex: 1.24.0.0), +// now if ecs.service-connect.*v1* is continuously being supported by 1.24.0.0, +// we will then register multiple capabilities. +// +// { +// "v1": ["ecs.capability.service-connect-v1"], +// "v2": ["ecs.capability.service-connect-v2", "ecs.capability.service-connect-v2"] +// } +func (agent *manager) GetCapabilitiesForAppnetInterfaceVersion(appnetVersion string) ([]string, error) { + return supportedAppnetInterfaceVerToCapability[appnetVersion], nil +} + +// GetAppnetContainerTarballDir returns Appnet agent tarball path's directory +func (agent *manager) GetAppnetContainerTarballDir() string { + return filepath.Dir(defaultAgentContainerTarballPathFormat) +} diff --git a/agent/engine/serviceconnect/manager_linux_prvileged_test.go b/agent/engine/serviceconnect/manager_linux_prvileged_test.go new file mode 100644 index 00000000000..235fa81a16d --- /dev/null +++ b/agent/engine/serviceconnect/manager_linux_prvileged_test.go @@ -0,0 +1,10 @@ +//go:build linux && sudo_unit +// +build linux,sudo_unit + +package serviceconnect + +import "testing" + +func TestAgentContainerModificationsForServiceConnect_Privileged(t *testing.T) { + testAgentContainerModificationsForServiceConnect(t, true) +} diff --git a/agent/engine/serviceconnect/manager_linux_test.go b/agent/engine/serviceconnect/manager_linux_test.go new file mode 100644 index 00000000000..dea6b71477a --- /dev/null +++ b/agent/engine/serviceconnect/manager_linux_test.go @@ -0,0 +1,184 @@ +//go:build linux && unit +// +build linux,unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "io/fs" + "os" + "testing" + + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + dockercontainer "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/assert" +) + +func TestDNSConfigToDockerExtraHostsFormat(t *testing.T) { + tt := []struct { + dnsConfigs []serviceconnect.DNSConfigEntry + expectedRestult []string + }{ + { + dnsConfigs: []serviceconnect.DNSConfigEntry{ + { + HostName: "my.test.host", + Address: "169.254.1.1", + }, + { + HostName: "my.test.host2", + Address: "ff06::c3", + }, + }, + expectedRestult: []string{ + "my.test.host:169.254.1.1", + "my.test.host2:ff06::c3", + }, + }, + { + dnsConfigs: nil, + expectedRestult: nil, + }, + } + + for _, tc := range tt { + res := DNSConfigToDockerExtraHostsFormat(tc.dnsConfigs) + assert.Equal(t, tc.expectedRestult, res, "Wrong docker host config ") + } +} + +func TestPauseContainerModificationsForServiceConnect(t *testing.T) { + scTask, pauseContainer, serviceConnectContainer := getAWSVPCTask(t) + + expectedPauseExtraHosts := []string{ + "host1.my.corp:169.254.1.1", + "host1.my.corp:ff06::c4", + } + + type testCase struct { + name string + container *apicontainer.Container + expectedExtraHosts []string + needsImage bool + } + testcases := []testCase{ + { + name: "Pause container has extra hosts", + container: pauseContainer, + expectedExtraHosts: expectedPauseExtraHosts, + }, + } + // Add test cases for other containers expecting no modifications + for _, container := range scTask.Containers { + if container != pauseContainer { + testcases = append(testcases, testCase{name: container.Name, container: container, needsImage: container == serviceConnectContainer}) + } + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + origMkdir := mkdirAllAndChown + if tc.needsImage { + mkdirAllAndChown = func(path string, perm fs.FileMode, uid, gid int) error { + return nil + } + } + + hostConfig := &dockercontainer.HostConfig{} + scManager := &manager{ + agentContainerImageName: "container_image", + agentContainerTag: "tag", + } + err := scManager.AugmentTaskContainer(scTask, tc.container, hostConfig) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tc.expectedExtraHosts, hostConfig.ExtraHosts) + mkdirAllAndChown = origMkdir + }) + } +} + +func TestAgentContainerModificationsForServiceConnect_NonPrivileged(t *testing.T) { + testAgentContainerModificationsForServiceConnect(t, false) +} + +func TestGetECSAgentLogPathContainer(t *testing.T) { + oldVal := os.Getenv(ecsAgentLogFileENV) + defer func() { + if oldVal != "" { + os.Setenv(ecsAgentLogFileENV, oldVal) + } + }() + + type testCase struct { + envVal string + expected string + } + testcases := []testCase{ + { + envVal: "/log/ecs-agent.log", + expected: "/log", + }, + { + envVal: "/some/path/to/log/ecs-agent.log", + expected: "/some/path/to/log", + }, + { + envVal: "", + expected: "/log", + }, + } + for _, tc := range testcases { + t.Run("", func(t *testing.T) { + if tc.envVal == "" { + os.Unsetenv(ecsAgentLogFileENV) + } else { + os.Setenv(ecsAgentLogFileENV, tc.envVal) + } + actualPath := getECSAgentLogPathContainer() + assert.Equal(t, tc.expected, actualPath) + + }) + } +} + +func TestGetSupportedAppnetInterfaceVerToCapabilities(t *testing.T) { + testCases := []struct { + name string + appNetAgentVersion string + expectedCapabilities []string + }{ + { + name: "test supported service connect capabilities for AppNet agent version v1", + appNetAgentVersion: "", + expectedCapabilities: nil, + }, + { + name: "test supported service connect capabilities for AppNet agent version v1", + appNetAgentVersion: "v1", + expectedCapabilities: []string{"ecs.capability.service-connect-v1"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scManager := &manager{} + scCapabilities, err := scManager.GetCapabilitiesForAppnetInterfaceVersion(tc.appNetAgentVersion) + assert.NoError(t, err) + assert.Equal(t, tc.expectedCapabilities, scCapabilities) + }) + } +} diff --git a/agent/engine/serviceconnect/manager_linux_test_common.go b/agent/engine/serviceconnect/manager_linux_test_common.go new file mode 100644 index 00000000000..7c650faa8d5 --- /dev/null +++ b/agent/engine/serviceconnect/manager_linux_test_common.go @@ -0,0 +1,217 @@ +//go:build linux && (unit || sudo_unit) +// +build linux +// +build unit sudo_unit + +package serviceconnect + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + "github.com/aws/amazon-ecs-agent/agent/config" + + apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/engine/testdata" + "github.com/aws/aws-sdk-go/aws" + + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + dockercontainer "github.com/docker/docker/api/types/container" + "github.com/stretchr/testify/assert" +) + +const ( + ipv4 = "10.0.0.1" + gatewayIPv4 = "10.0.0.2/20" + mac = "1.2.3.4" + ipv6 = "f0:234:23" +) + +var ( + cfg config.Config + mockENI = &apieni.ENI{ + ID: "eni-id", + IPV4Addresses: []*apieni.ENIIPV4Address{ + { + Primary: true, + Address: ipv4, + }, + }, + MacAddress: mac, + IPV6Addresses: []*apieni.ENIIPV6Address{ + { + Address: ipv6, + }, + }, + SubnetGatewayIPV4Address: gatewayIPv4, + } +) + +func mockMkdirAllAndChown(path string, perm fs.FileMode, uid, gid int) error { + return nil +} + +func getAWSVPCTask(t *testing.T) (*apitask.Task, *apicontainer.Container, *apicontainer.Container) { + sleepTask := testdata.LoadTask("sleep5TwoContainers") + + sleepTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: "service-connect", + DNSConfig: []serviceconnect.DNSConfigEntry{ + { + HostName: "host1.my.corp", + Address: "169.254.1.1", + }, + { + HostName: "host1.my.corp", + Address: "ff06::c4", + }, + }, + } + dockerConfig := dockercontainer.Config{ + Healthcheck: &dockercontainer.HealthConfig{ + Test: []string{"echo", "ok"}, + Interval: time.Millisecond, + Timeout: time.Second, + Retries: 1, + }, + } + + pauseContainer := apicontainer.NewContainerWithSteadyState(apicontainerstatus.ContainerResourcesProvisioned) + pauseContainer.TransitionDependenciesMap = make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet) + pauseContainer.Name = apitask.NetworkPauseContainerName + pauseContainer.Image = fmt.Sprintf("%s:%s", cfg.PauseContainerImageName, cfg.PauseContainerTag) + pauseContainer.Essential = true + pauseContainer.Type = apicontainer.ContainerCNIPause + + rawConfig, err := json.Marshal(&dockerConfig) + if err != nil { + t.Fatal(err) + } + serviceConnectContainer := &apicontainer.Container{ + Name: sleepTask.ServiceConnectConfig.ContainerName, + HealthCheckType: apicontainer.DockerHealthCheckType, + DockerConfig: apicontainer.DockerConfig{ + Config: aws.String(string(rawConfig)), + }, + TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet), + } + sleepTask.Containers = append(sleepTask.Containers, serviceConnectContainer) + + // Add eni information to the task so the task can add dependency of pause container + sleepTask.AddTaskENI(mockENI) + return sleepTask, pauseContainer, serviceConnectContainer +} + +func testAgentContainerModificationsForServiceConnect(t *testing.T, privilegedMode bool) { + backupMkdirAllAndChown := mkdirAllAndChown + tempDir := t.TempDir() + if !privilegedMode { + mkdirAllAndChown = mockMkdirAllAndChown + } + defer func() { + mkdirAllAndChown = backupMkdirAllAndChown + os.RemoveAll(tempDir) + }() + scTask, _, serviceConnectContainer := getAWSVPCTask(t) + + expectedImage := "container:interface-v1" + + expectedBinds := []string{ + fmt.Sprintf("%s/status/%s:%s", tempDir, scTask.GetID(), "/some/other/run"), + fmt.Sprintf("%s/relay:%s", tempDir, "/not/var/run"), + fmt.Sprintf("%s/log/%s:%s", tempDir, scTask.GetID(), "/some/other/log"), + } + expectedENVs := map[string]string{ + "ReLaYgOeShErE": "unix:///not/var/run/relay_file_of_holiness", + "StAtUsGoEsHeRe": "/some/other/run/status_file_of_holiness", + "APPNET_AGENT_ADMIN_MODE": "uds", + "ENVOY_ENABLE_IAM_AUTH_FOR_XDS": "0", + "ECS_CONTAINER_INSTANCE_ARN": "fake_container_instance", + "APPNET_ENVOY_LOG_DESTINATION": "/some/other/log", + } + + type testCase struct { + name string + container *apicontainer.Container + expectedENV map[string]string + expectedBinds []string + expectedBindDirPerm string + expectedBindDirOwner uint32 + } + testcases := []testCase{ + { + name: "Service connect container has extra binds/ENV", + container: serviceConnectContainer, + expectedENV: expectedENVs, + expectedBinds: expectedBinds, + expectedBindDirPerm: fs.FileMode(0700).String(), + expectedBindDirOwner: serviceconnect.AppNetUID, + }, + } + // Add test cases for other containers expecting no modifications + for _, container := range scTask.Containers { + if container != serviceConnectContainer { + testcases = append(testcases, testCase{name: container.Name, container: container, expectedENV: map[string]string{}}) + } + } + scManager := &manager{ + relayPathContainer: "/not/var/run", + relayPathHost: filepath.Join(tempDir, "relay"), + relayFileName: "relay_file_of_holiness", + endpointENV: "ReLaYgOeShErE", + statusPathContainer: "/some/other/run", + statusPathHostRoot: filepath.Join(tempDir, "status"), + statusFileName: "status_file_of_holiness", + statusENV: "StAtUsGoEsHeRe", + adminStatsRequest: "/give?stats", + adminDrainRequest: "/do?drain", + + agentContainerImageName: "container", + appnetInterfaceVersion: "v1", + + containerInstanceARN: "fake_container_instance", + logPathContainer: "/some/other/log", + logPathHostRoot: filepath.Join(tempDir, "log"), + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + hostConfig := &dockercontainer.HostConfig{} + err := scManager.AugmentTaskContainer(scTask, tc.container, hostConfig) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, tc.expectedBinds, hostConfig.Binds) + assert.Equal(t, tc.expectedENV, tc.container.Environment) + if privilegedMode { + for _, bind := range hostConfig.Binds { + hostDir := strings.Split(bind, ":")[0] + dirStat, err := os.Stat(hostDir) + assert.NoError(t, err) + assert.Equal(t, tc.expectedBindDirPerm, dirStat.Mode().Perm().String(), + fmt.Sprintf("directory %s should have mode %s", hostDir, tc.expectedBindDirPerm)) + assert.Equal(t, tc.expectedBindDirOwner, dirStat.Sys().(*syscall.Stat_t).Uid) + } + } + }) + } + + assert.Equal(t, expectedImage, serviceConnectContainer.Image) + assert.Equal(t, fmt.Sprintf("%s/status/%s/%s", tempDir, scTask.GetID(), "status_file_of_holiness"), scTask.ServiceConnectConfig.RuntimeConfig.AdminSocketPath) + assert.Equal(t, "/give?stats", scTask.ServiceConnectConfig.RuntimeConfig.StatsRequest) + assert.Equal(t, "/do?drain", scTask.ServiceConnectConfig.RuntimeConfig.DrainRequest) + + config := scTask.GetServiceConnectRuntimeConfig() + assert.Equal(t, fmt.Sprintf("%s/status/%s/%s", tempDir, scTask.GetID(), "status_file_of_holiness"), config.AdminSocketPath) + assert.Equal(t, "/give?stats", config.StatsRequest) + assert.Equal(t, "/do?drain", config.DrainRequest) +} diff --git a/agent/engine/serviceconnect/manager_other.go b/agent/engine/serviceconnect/manager_other.go new file mode 100644 index 00000000000..be0b7f118a2 --- /dev/null +++ b/agent/engine/serviceconnect/manager_other.go @@ -0,0 +1,85 @@ +//go:build !linux +// +build !linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "context" + "fmt" + "runtime" + + "github.com/aws/amazon-ecs-agent/agent/api" + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" + + "github.com/docker/docker/api/types" + dockercontainer "github.com/docker/docker/api/types/container" +) + +type manager struct { +} + +func NewManager() Manager { + return &manager{} +} + +func (m *manager) AugmentTaskContainer(*apitask.Task, *apicontainer.Container, *dockercontainer.HostConfig) error { + return fmt.Errorf("ServiceConnect is only supported on linux") +} +func (m *manager) CreateInstanceTask(config *config.Config) (*apitask.Task, error) { + return nil, fmt.Errorf("ServiceConnect is only supported on linux") +} +func (m *manager) AugmentInstanceContainer(*apitask.Task, *apicontainer.Container, *dockercontainer.HostConfig) error { + return fmt.Errorf("ServiceConnect is only supported on linux") +} + +func (*manager) LoadImage(ctx context.Context, _ *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + return nil, loader.NewUnsupportedPlatformError(fmt.Errorf( + "appnetAgent container load: unsupported platform: %s/%s", + runtime.GOOS, runtime.GOARCH)) +} + +func (*manager) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return false, loader.NewUnsupportedPlatformError(fmt.Errorf( + "appnetAgent container isloaded: unsupported platform: %s/%s", + runtime.GOOS, runtime.GOARCH)) +} + +func (m *manager) SetECSClient(api.ECSClient, string) { +} + +func (*manager) GetLoadedImageName() string { + return "" +} + +func (*manager) GetLoadedAppnetVersion() (string, error) { + return "", loader.NewUnsupportedPlatformError(fmt.Errorf( + "appnetAgent container get loaded appnet version: unsupported platform: %s/%s", + runtime.GOOS, runtime.GOARCH)) +} + +func (*manager) GetCapabilitiesForAppnetInterfaceVersion(string) ([]string, error) { + return make([]string, 0), loader.NewUnsupportedPlatformError(fmt.Errorf( + "appnetAgent container get capabilities for appnet version: unsupported platform: %s/%s", + runtime.GOOS, runtime.GOARCH)) +} + +func (*manager) GetAppnetContainerTarballDir() string { + return "" +} diff --git a/agent/engine/serviceconnect/mock/manager.go b/agent/engine/serviceconnect/mock/manager.go new file mode 100644 index 00000000000..fa840fa3e9f --- /dev/null +++ b/agent/engine/serviceconnect/mock/manager.go @@ -0,0 +1,199 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/aws/amazon-ecs-agent/agent/engine/serviceconnect (interfaces: Manager) + +// Package mock_serviceconnect is a generated GoMock package. +package mock_serviceconnect + +import ( + context "context" + reflect "reflect" + + api "github.com/aws/amazon-ecs-agent/agent/api" + container "github.com/aws/amazon-ecs-agent/agent/api/container" + task "github.com/aws/amazon-ecs-agent/agent/api/task" + config "github.com/aws/amazon-ecs-agent/agent/config" + dockerapi "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + types "github.com/docker/docker/api/types" + container0 "github.com/docker/docker/api/types/container" + gomock "github.com/golang/mock/gomock" +) + +// MockManager is a mock of Manager interface +type MockManager struct { + ctrl *gomock.Controller + recorder *MockManagerMockRecorder +} + +// MockManagerMockRecorder is the mock recorder for MockManager +type MockManagerMockRecorder struct { + mock *MockManager +} + +// NewMockManager creates a new mock instance +func NewMockManager(ctrl *gomock.Controller) *MockManager { + mock := &MockManager{ctrl: ctrl} + mock.recorder = &MockManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder +} + +// AugmentInstanceContainer mocks base method +func (m *MockManager) AugmentInstanceContainer(arg0 *task.Task, arg1 *container.Container, arg2 *container0.HostConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AugmentInstanceContainer", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// AugmentInstanceContainer indicates an expected call of AugmentInstanceContainer +func (mr *MockManagerMockRecorder) AugmentInstanceContainer(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AugmentInstanceContainer", reflect.TypeOf((*MockManager)(nil).AugmentInstanceContainer), arg0, arg1, arg2) +} + +// AugmentTaskContainer mocks base method +func (m *MockManager) AugmentTaskContainer(arg0 *task.Task, arg1 *container.Container, arg2 *container0.HostConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AugmentTaskContainer", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// AugmentTaskContainer indicates an expected call of AugmentTaskContainer +func (mr *MockManagerMockRecorder) AugmentTaskContainer(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AugmentTaskContainer", reflect.TypeOf((*MockManager)(nil).AugmentTaskContainer), arg0, arg1, arg2) +} + +// CreateInstanceTask mocks base method +func (m *MockManager) CreateInstanceTask(arg0 *config.Config) (*task.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateInstanceTask", arg0) + ret0, _ := ret[0].(*task.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateInstanceTask indicates an expected call of CreateInstanceTask +func (mr *MockManagerMockRecorder) CreateInstanceTask(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInstanceTask", reflect.TypeOf((*MockManager)(nil).CreateInstanceTask), arg0) +} + +// GetAppnetContainerTarballDir mocks base method +func (m *MockManager) GetAppnetContainerTarballDir() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAppnetContainerTarballDir") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetAppnetContainerTarballDir indicates an expected call of GetAppnetContainerTarballDir +func (mr *MockManagerMockRecorder) GetAppnetContainerTarballDir() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppnetContainerTarballDir", reflect.TypeOf((*MockManager)(nil).GetAppnetContainerTarballDir)) +} + +// GetCapabilitiesForAppnetInterfaceVersion mocks base method +func (m *MockManager) GetCapabilitiesForAppnetInterfaceVersion(arg0 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCapabilitiesForAppnetInterfaceVersion", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCapabilitiesForAppnetInterfaceVersion indicates an expected call of GetCapabilitiesForAppnetInterfaceVersion +func (mr *MockManagerMockRecorder) GetCapabilitiesForAppnetInterfaceVersion(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCapabilitiesForAppnetInterfaceVersion", reflect.TypeOf((*MockManager)(nil).GetCapabilitiesForAppnetInterfaceVersion), arg0) +} + +// GetLoadedAppnetVersion mocks base method +func (m *MockManager) GetLoadedAppnetVersion() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadedAppnetVersion") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLoadedAppnetVersion indicates an expected call of GetLoadedAppnetVersion +func (mr *MockManagerMockRecorder) GetLoadedAppnetVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadedAppnetVersion", reflect.TypeOf((*MockManager)(nil).GetLoadedAppnetVersion)) +} + +// GetLoadedImageName mocks base method +func (m *MockManager) GetLoadedImageName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadedImageName") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetLoadedImageName indicates an expected call of GetLoadedImageName +func (mr *MockManagerMockRecorder) GetLoadedImageName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadedImageName", reflect.TypeOf((*MockManager)(nil).GetLoadedImageName)) +} + +// IsLoaded mocks base method +func (m *MockManager) IsLoaded(arg0 dockerapi.DockerClient) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsLoaded", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsLoaded indicates an expected call of IsLoaded +func (mr *MockManagerMockRecorder) IsLoaded(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLoaded", reflect.TypeOf((*MockManager)(nil).IsLoaded), arg0) +} + +// LoadImage mocks base method +func (m *MockManager) LoadImage(arg0 context.Context, arg1 *config.Config, arg2 dockerapi.DockerClient) (*types.ImageInspect, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadImage", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.ImageInspect) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadImage indicates an expected call of LoadImage +func (mr *MockManagerMockRecorder) LoadImage(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadImage", reflect.TypeOf((*MockManager)(nil).LoadImage), arg0, arg1, arg2) +} + +// SetECSClient mocks base method +func (m *MockManager) SetECSClient(arg0 api.ECSClient, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetECSClient", arg0, arg1) +} + +// SetECSClient indicates an expected call of SetECSClient +func (mr *MockManagerMockRecorder) SetECSClient(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetECSClient", reflect.TypeOf((*MockManager)(nil).SetECSClient), arg0, arg1) +} diff --git a/agent/engine/serviceconnect/version_linux.go b/agent/engine/serviceconnect/version_linux.go new file mode 100644 index 00000000000..b57d8b7f4fb --- /dev/null +++ b/agent/engine/serviceconnect/version_linux.go @@ -0,0 +1,64 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "sort" + "strconv" + "strings" + + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" +) + +const ( + appnetInterfaceV1 = "v1" + serviceConnectCapabilityV1 = "ecs.capability.service-connect-v1" +) + +var ( + supportedAppnetInterfaceVerToCapability = map[string][]string{ + appnetInterfaceV1: { + serviceConnectCapabilityV1, + }, + } +) + +// getSupportedAppnetInterfaceVersions returns the all the supported AppNet interface versions +// by ECS Agent from highest to lowest version +func getSupportedAppnetInterfaceVersions() []string { + supportedAppnetInterfaceVersions := make([]string, 0, len(supportedAppnetInterfaceVerToCapability)) + for supportedAppnetInterfaceVersion := range supportedAppnetInterfaceVerToCapability { + supportedAppnetInterfaceVersions = append(supportedAppnetInterfaceVersions, supportedAppnetInterfaceVersion) + } + sort.Slice( + supportedAppnetInterfaceVersions, + func(i, j int) bool { + return getVersionNumber(supportedAppnetInterfaceVersions[i]) > getVersionNumber(supportedAppnetInterfaceVersions[j]) + }, + ) + return supportedAppnetInterfaceVersions +} + +// getVersionNumber returns a version sort key in numeric order. +func getVersionNumber(version string) int { + versionNumber := strings.TrimPrefix(version, "v") + num, err := strconv.Atoi(versionNumber) + if err != nil { + logger.Error("Error parsing appnet interface version number as int:", logger.Fields{ + field.Error: err, + }) + } + return num +} diff --git a/agent/engine/serviceconnect/version_linux_test.go b/agent/engine/serviceconnect/version_linux_test.go new file mode 100644 index 00000000000..45aa5791d17 --- /dev/null +++ b/agent/engine/serviceconnect/version_linux_test.go @@ -0,0 +1,61 @@ +//go:build linux && unit +// +build linux,unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package serviceconnect + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +const appNetInterfaceVersionPattern = "^v(\\d*)$" + +func TestSupportedAppnetInterfaceVersion(t *testing.T) { + allowedVersionPattern, err := regexp.Compile(appNetInterfaceVersionPattern) + for supportedAppnetInterfaceVersion := range supportedAppnetInterfaceVerToCapability { + require.NoError(t, err, "Error while compiling regex pattern") + require.True(t, allowedVersionPattern.MatchString(supportedAppnetInterfaceVersion), + "Appnet interface version does not match the expected pattern, ex:`v1`") + } +} +func TestGetSupportedAppnetInterfaceVersion(t *testing.T) { + tempSupportedAppnetInterfaceVerToCapability := supportedAppnetInterfaceVerToCapability + defer func() { + supportedAppnetInterfaceVerToCapability = tempSupportedAppnetInterfaceVerToCapability + }() + + supportedAppnetInterfaceVerToCapability = map[string][]string{ + "v1": { + "ecs.capability.service-connect-v1", + }, + "v2": { + "ecs.capability.service-connect-v1", + "ecs.capability.service-connect-v2", + }, + "v11": { + "ecs.capability.service-connect-v10", + "ecs.capability.service-connect-v11", + }, + "v10": { + "ecs.capability.service-connect-v10", + }, + } + + expectedSupportedAppnetInterfaceVersions := []string{"v11", "v10", "v2", "v1"} + require.Equal(t, expectedSupportedAppnetInterfaceVersions, getSupportedAppnetInterfaceVersions()) +} diff --git a/agent/engine/task_manager.go b/agent/engine/task_manager.go index a0ea9274b96..277db312a92 100644 --- a/agent/engine/task_manager.go +++ b/agent/engine/task_manager.go @@ -87,7 +87,7 @@ type containerTransition struct { nextState apicontainerstatus.ContainerStatus actionRequired bool blockedOn *apicontainer.DependsOn - reason error + reason dependencygraph.DependencyError } // resourceTransition defines the struct for a resource to transition. @@ -115,10 +115,11 @@ type resourceTransition struct { // block and it is expected that the managedTask listen to those channels // almost constantly. // The general operation should be: -// 1) Listen to the channels -// 2) On an event, update the status of the task and containers (known/desired) -// 3) Figure out if any action needs to be done. If so, do it -// 4) GOTO 1 +// 1. Listen to the channels +// 2. On an event, update the status of the task and containers (known/desired) +// 3. Figure out if any action needs to be done. If so, do it +// 4. GOTO 1 +// // Item '3' obviously might lead to some duration where you are not listening // to the channels. However, this can be solved by kicking off '3' as a // goroutine and then only communicating the result back via the channels @@ -240,6 +241,7 @@ func (mtask *managedTask) overseeTask() { field.TaskID: mtask.GetID(), }) mtask.engine.checkTearDownPauseContainer(mtask.Task) + // TODO [SC]: We need to also tear down pause containets in bridge mode for SC-enabled tasks mtask.cleanupCredentials() if mtask.StopSequenceNumber != 0 { logger.Debug("Marking done for this sequence", logger.Fields{ @@ -753,7 +755,7 @@ func (mtask *managedTask) releaseIPInIPAM() { field.TaskID: mtask.GetID(), }) - cfg, err := mtask.BuildCNIConfig(true, &ecscni.Config{ + cfg, err := mtask.BuildCNIConfigAwsvpc(true, &ecscni.Config{ MinSupportedCNIVersion: config.DefaultMinSupportedCNIVersion, }) if err != nil { @@ -1088,6 +1090,9 @@ func (mtask *managedTask) startContainerTransitions(transitionFunc containerTran for _, cont := range mtask.Containers { transition := mtask.containerNextState(cont) if transition.reason != nil { + if transition.reason.IsTerminal() { + mtask.handleTerminalDependencyError(cont, transition.reason) + } // container can't be transitioned reasons = append(reasons, transition.reason) if transition.blockedOn != nil { @@ -1128,6 +1133,31 @@ func (mtask *managedTask) startContainerTransitions(transitionFunc containerTran return anyCanTransition, blocked, transitions, reasons } +func (mtask *managedTask) handleTerminalDependencyError(container *apicontainer.Container, error dependencygraph.DependencyError) { + logger.Error("Terminal error detected during transition; marking container as stopped", logger.Fields{ + field.Container: container.Name, + field.Error: error.Error(), + }) + container.SetDesiredStatus(apicontainerstatus.ContainerStopped) + exitCode := 143 + container.SetKnownExitCode(&exitCode) + // Change container status to STOPPED with exit code 143. This exit code is what docker reports when + // a container receives SIGTERM. In this case it's technically not true that we send SIGTERM because the + // container didn't even start, but we have to report an error and 143 seems the most appropriate. + go func(cont *apicontainer.Container) { + mtask.dockerMessages <- dockerContainerChange{ + container: cont, + event: dockerapi.DockerContainerChangeEvent{ + Status: apicontainerstatus.ContainerStopped, + DockerContainerMetadata: dockerapi.DockerContainerMetadata{ + Error: dockerapi.CannotStartContainerError{FromError: error}, + ExitCode: &exitCode, + }, + }, + } + }(container) +} + // startResourceTransitions steps through each resource in the task and calls // the passed transition function when a transition should occur func (mtask *managedTask) startResourceTransitions(transitionFunc resourceTransitionFunc) (bool, map[string]string) { @@ -1370,10 +1400,6 @@ func (mtask *managedTask) resourceNextState(resource taskresource.TaskResource) } func (mtask *managedTask) handleContainersUnableToTransitionState() { - logger.Critical("Task in a bad state; it's not steady state but no containers want to transition", logger.Fields{ - field.TaskID: mtask.GetID(), - }) - if mtask.GetDesiredStatus().Terminal() { // Ack, really bad. We want it to stop but the containers don't think // that's possible. let's just break out and hope for the best! @@ -1384,10 +1410,23 @@ func (mtask *managedTask) handleContainersUnableToTransitionState() { mtask.emitTaskEvent(mtask.Task, taskUnableToTransitionToStoppedReason) // TODO we should probably panic here } else { - logger.Critical("Moving task to stopped due to bad state", logger.Fields{ - field.TaskID: mtask.GetID(), - }) - mtask.handleDesiredStatusChange(apitaskstatus.TaskStopped, 0) + // If we end up here, it means containers are not able to transition anymore; maybe because of dependencies that + // are unable to start. Therefore, if there are essential containers that haven't started yet, we need to + // stop the task since they are not going to start. + stopTask := false + for _, c := range mtask.Containers { + if c.IsEssential() && !c.IsKnownSteadyState() { + stopTask = true + break + } + } + + if stopTask { + logger.Critical("Task in a bad state; it's not steady state but no containers want to transition", logger.Fields{ + field.TaskID: mtask.GetID(), + }) + mtask.handleDesiredStatusChange(apitaskstatus.TaskStopped, 0) + } } } diff --git a/agent/engine/task_manager_data_test.go b/agent/engine/task_manager_data_test.go index a60ebaa64e6..f6726d7b3f2 100644 --- a/agent/engine/task_manager_data_test.go +++ b/agent/engine/task_manager_data_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/engine/task_manager_test.go b/agent/engine/task_manager_test.go index 1ab79ddfcc2..521a0c604fa 100644 --- a/agent/engine/task_manager_test.go +++ b/agent/engine/task_manager_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -23,6 +24,8 @@ import ( "testing" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/aws/amazon-ecs-agent/agent/taskresource" mock_taskresource "github.com/aws/amazon-ecs-agent/agent/taskresource/mocks" @@ -240,7 +243,7 @@ func TestContainerNextState(t *testing.T) { containerDesiredStatus apicontainerstatus.ContainerStatus expectedContainerStatus apicontainerstatus.ContainerStatus expectedTransitionActionable bool - reason error + reason dependencygraph.DependencyError }{ // NONE -> RUNNING transition is allowed and actionable, when desired is Running // The expected next status is Pulled @@ -728,6 +731,86 @@ func TestStartContainerTransitionsWhenForwardTransitionIsNotPossible(t *testing. assert.Empty(t, transitions) } +func TestStartContainerTransitionsWithTerminalError(t *testing.T) { + firstContainerName := "container1" + firstContainer := &apicontainer.Container{ + KnownStatusUnsafe: apicontainerstatus.ContainerStopped, + DesiredStatusUnsafe: apicontainerstatus.ContainerStopped, + KnownExitCodeUnsafe: aws.Int(1), // This simulated the container has stopped unsuccessfully + Name: firstContainerName, + } + secondContainerName := "container2" + secondContainer := &apicontainer.Container{ + KnownStatusUnsafe: apicontainerstatus.ContainerCreated, + DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, + Name: secondContainerName, + DependsOnUnsafe: []apicontainer.DependsOn{ + { + ContainerName: firstContainerName, + Condition: "SUCCESS", // This means this condition can never be fulfilled since container1 has exited with non-zero code + }, + }, + } + thirdContainerName := "container3" + thirdContainer := &apicontainer.Container{ + KnownStatusUnsafe: apicontainerstatus.ContainerCreated, + DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, + Name: thirdContainerName, + DependsOnUnsafe: []apicontainer.DependsOn{ + { + ContainerName: secondContainerName, + Condition: "SUCCESS", // This means this condition can never be fulfilled since container2 has exited with non-zero code + }, + }, + } + dockerMessagesChan := make(chan dockerContainerChange) + task := &managedTask{ + Task: &apitask.Task{ + Containers: []*apicontainer.Container{ + firstContainer, + secondContainer, + thirdContainer, + }, + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + }, + engine: &DockerTaskEngine{}, + dockerMessages: dockerMessagesChan, + } + + canTransition, _, transitions, errors := task.startContainerTransitions( + func(cont *apicontainer.Container, nextStatus apicontainerstatus.ContainerStatus) { + t.Error("Transition function should not be called when no transitions are possible") + }) + assert.False(t, canTransition) + assert.Empty(t, transitions) + assert.Equal(t, 3, len(errors)) // first error is just indicating container1 is at desired status, following errors should be terminal + assert.False(t, errors[0].(dependencygraph.DependencyError).IsTerminal(), "Error should NOT be terminal") + assert.True(t, errors[1].(dependencygraph.DependencyError).IsTerminal(), "Error should be terminal") + assert.True(t, errors[2].(dependencygraph.DependencyError).IsTerminal(), "Error should be terminal") + + stoppedMessages := make(map[string]dockerContainerChange) + // verify we are sending STOPPED message + for i := 0; i < 2; i++ { + select { + case msg := <-dockerMessagesChan: + stoppedMessages[msg.container.Name] = msg + case <-time.After(time.Second): + t.Fatal("Timed out waiting for docker messages") + break + } + } + + assert.Equal(t, secondContainer, stoppedMessages[secondContainerName].container) + assert.Equal(t, apicontainerstatus.ContainerStopped, stoppedMessages[secondContainerName].event.Status) + assert.Error(t, stoppedMessages[secondContainerName].event.DockerContainerMetadata.Error) + assert.Equal(t, 143, *stoppedMessages[secondContainerName].event.DockerContainerMetadata.ExitCode) + + assert.Equal(t, thirdContainer, stoppedMessages[thirdContainerName].container) + assert.Equal(t, apicontainerstatus.ContainerStopped, stoppedMessages[thirdContainerName].event.Status) + assert.Error(t, stoppedMessages[thirdContainerName].event.DockerContainerMetadata.Error) + assert.Equal(t, 143, *stoppedMessages[thirdContainerName].event.DockerContainerMetadata.ExitCode) +} + func TestStartContainerTransitionsInvokesHandleContainerChange(t *testing.T) { eventStreamName := "TESTTASKENGINE" @@ -908,28 +991,48 @@ func TestOnContainersUnableToTransitionStateForDesiredStoppedTask(t *testing.T) } func TestOnContainersUnableToTransitionStateForDesiredRunningTask(t *testing.T) { - firstContainerName := "container1" - firstContainer := &apicontainer.Container{ - KnownStatusUnsafe: apicontainerstatus.ContainerCreated, - DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, - Name: firstContainerName, - } - task := &managedTask{ - Task: &apitask.Task{ - Containers: []*apicontainer.Container{ - firstContainer, - }, - DesiredStatusUnsafe: apitaskstatus.TaskRunning, + for _, tc := range []struct { + knownStatus apicontainerstatus.ContainerStatus + expectedContainerDesiredStatus apicontainerstatus.ContainerStatus + expectedTaskDesiredStatus apitaskstatus.TaskStatus + }{ + { + knownStatus: apicontainerstatus.ContainerCreated, + expectedContainerDesiredStatus: apicontainerstatus.ContainerStopped, + expectedTaskDesiredStatus: apitaskstatus.TaskStopped, }, - engine: &DockerTaskEngine{ - dataClient: data.NewNoopClient(), + { + knownStatus: apicontainerstatus.ContainerRunning, + expectedContainerDesiredStatus: apicontainerstatus.ContainerRunning, + expectedTaskDesiredStatus: apitaskstatus.TaskRunning, }, - ctx: context.TODO(), - } + } { + t.Run(fmt.Sprintf("Essential container with knownStatus=%s", tc.knownStatus.String()), func(t *testing.T) { + firstContainerName := "container1" + firstContainer := &apicontainer.Container{ + KnownStatusUnsafe: tc.knownStatus, + DesiredStatusUnsafe: apicontainerstatus.ContainerRunning, + Name: firstContainerName, + Essential: true, // setting this to true since at least one container in the task must be essential. + } + task := &managedTask{ + Task: &apitask.Task{ + Containers: []*apicontainer.Container{ + firstContainer, + }, + DesiredStatusUnsafe: apitaskstatus.TaskRunning, + }, + engine: &DockerTaskEngine{ + dataClient: data.NewNoopClient(), + }, + ctx: context.TODO(), + } - task.handleContainersUnableToTransitionState() - assert.Equal(t, task.GetDesiredStatus(), apitaskstatus.TaskStopped) - assert.Equal(t, task.Containers[0].GetDesiredStatus(), apicontainerstatus.ContainerStopped) + task.handleContainersUnableToTransitionState() + assert.Equal(t, tc.expectedTaskDesiredStatus, task.GetDesiredStatus()) + assert.Equal(t, tc.expectedContainerDesiredStatus, task.Containers[0].GetDesiredStatus()) + }) + } } // TODO: Test progressContainers workflow diff --git a/agent/engine/task_manager_unix_test.go b/agent/engine/task_manager_unix_test.go index abb0fb62f16..06903ef3d96 100644 --- a/agent/engine/task_manager_unix_test.go +++ b/agent/engine/task_manager_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -286,7 +287,7 @@ func TestStartResourceTransitionsEmpty(t *testing.T) { } } -//TestEFSNextStateWithTransitionDependencies verifies the dependencies are resolved correctly for task resource +// TestEFSNextStateWithTransitionDependencies verifies the dependencies are resolved correctly for task resource func TestEFSVolumeNextStateWithTransitionDependencies(t *testing.T) { testCases := []struct { name string diff --git a/agent/engine/testdata/load.go b/agent/engine/testdata/load.go index 2be16326070..6ef0a75cf0d 100644 --- a/agent/engine/testdata/load.go +++ b/agent/engine/testdata/load.go @@ -5,8 +5,10 @@ import ( "io/ioutil" "path/filepath" "runtime" + "strings" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/golang/mock/gomock" ) func LoadTask(name string) *apitask.Task { @@ -21,3 +23,26 @@ func LoadTask(name string) *apitask.Task { } return t } + +type dockerNameSubstr struct { + values []string +} + +func (m dockerNameSubstr) Matches(arg interface{}) bool { + sarg := arg.(string) + for _, s := range m.values { + if !strings.Contains(sarg, s) { + return false + } + } + return true +} + +// Not used here, but satisfies the Matcher interface. +func (m dockerNameSubstr) String() string { + return strings.Join(m.values, ", ") +} + +func DockerNameSubstr(values ...string) gomock.Matcher { + return dockerNameSubstr{values: values} +} diff --git a/agent/engine/testdata/test_tasks/sleep5PortMappings.json b/agent/engine/testdata/test_tasks/sleep5PortMappings.json new file mode 100644 index 00000000000..6d9c042e93a --- /dev/null +++ b/agent/engine/testdata/test_tasks/sleep5PortMappings.json @@ -0,0 +1,42 @@ +{ + "Arn":"arn:aws:ecs:us-west-2:123456789012:task/12345678-90ab-cdef-1234-56780abcdef1", + "Family":"sleep5", + "Version":"2", + "Containers": + [ + { + "Name":"sleep5", + "Image":"busybox", + "Command":["sleep","5"], + "Cpu":10, + "Memory":10, + "Links":null, + "volumesFrom":[], + "mountPoints":[], + "portMappings":[{ + "containerPort": 8080, + "hostPort": 0, + "protocol": "tcp" + }], + "Essential":true, + "EntryPoint":null, + "environment":{}, + "overrides":{"command":null}, + "desiredStatus":"NONE", + "KnownStatus":"NONE", + "RunDependencies":null, + "IsInternal":false, + "AppliedStatus":"NONE", + "ApplyingError":null, + "SentStatus":"NONE", + "KnownExitCode":null, + "KnownPortBindingsUnsafe":null, + "StatusLock":{} + } + ], + "volumes":[], + "DesiredStatus":"RUNNING", + "KnownStatus":"NONE", + "KnownTime":"0001-01-01T00:00:00Z", + "SentStatus":"NONE" +} diff --git a/agent/eni/iphelperwrapper/iphelper_windows.go b/agent/eni/iphelperwrapper/iphelper_windows.go index 1c77ff19154..6bf45de2096 100644 --- a/agent/eni/iphelperwrapper/iphelper_windows.go +++ b/agent/eni/iphelperwrapper/iphelper_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/iphelperwrapper/iphelper_windows_test.go b/agent/eni/iphelperwrapper/iphelper_windows_test.go index d0628291db5..adbd8aa42a5 100644 --- a/agent/eni/iphelperwrapper/iphelper_windows_test.go +++ b/agent/eni/iphelperwrapper/iphelper_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/iphelperwrapper/mibIpInterfaceRow_windows.go b/agent/eni/iphelperwrapper/mibIpInterfaceRow_windows.go index c27c5beedd1..1112094b416 100644 --- a/agent/eni/iphelperwrapper/mibIpInterfaceRow_windows.go +++ b/agent/eni/iphelperwrapper/mibIpInterfaceRow_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/netlinkwrapper/netlink_linux.go b/agent/eni/netlinkwrapper/netlink_linux.go index 54f14be1baf..74713472d98 100644 --- a/agent/eni/netlinkwrapper/netlink_linux.go +++ b/agent/eni/netlinkwrapper/netlink_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/networkutils/mocks/utils_windows.go b/agent/eni/networkutils/mocks/utils_windows.go new file mode 100644 index 00000000000..19149ccf44f --- /dev/null +++ b/agent/eni/networkutils/mocks/utils_windows.go @@ -0,0 +1,127 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: ./utils_windows.go + +// Package mock_networkutils is a generated GoMock package. +package mock_networkutils + +import ( + context "context" + net "net" + reflect "reflect" + time "time" + + networkutils "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" + gomock "github.com/golang/mock/gomock" +) + +// MockNetworkUtils is a mock of NetworkUtils interface +type MockNetworkUtils struct { + ctrl *gomock.Controller + recorder *MockNetworkUtilsMockRecorder +} + +// MockNetworkUtilsMockRecorder is the mock recorder for MockNetworkUtils +type MockNetworkUtilsMockRecorder struct { + mock *MockNetworkUtils +} + +// NewMockNetworkUtils creates a new mock instance +func NewMockNetworkUtils(ctrl *gomock.Controller) *MockNetworkUtils { + mock := &MockNetworkUtils{ctrl: ctrl} + mock.recorder = &MockNetworkUtilsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNetworkUtils) EXPECT() *MockNetworkUtilsMockRecorder { + return m.recorder +} + +// GetInterfaceMACByIndex mocks base method +func (m *MockNetworkUtils) GetInterfaceMACByIndex(arg0 int, arg1 context.Context, arg2 time.Duration) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInterfaceMACByIndex", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInterfaceMACByIndex indicates an expected call of GetInterfaceMACByIndex +func (mr *MockNetworkUtilsMockRecorder) GetInterfaceMACByIndex(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInterfaceMACByIndex", reflect.TypeOf((*MockNetworkUtils)(nil).GetInterfaceMACByIndex), arg0, arg1, arg2) +} + +// GetAllNetworkInterfaces mocks base method +func (m *MockNetworkUtils) GetAllNetworkInterfaces() ([]net.Interface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllNetworkInterfaces") + ret0, _ := ret[0].([]net.Interface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllNetworkInterfaces indicates an expected call of GetAllNetworkInterfaces +func (mr *MockNetworkUtilsMockRecorder) GetAllNetworkInterfaces() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllNetworkInterfaces", reflect.TypeOf((*MockNetworkUtils)(nil).GetAllNetworkInterfaces)) +} + +// GetDNSServerAddressList mocks base method +func (m *MockNetworkUtils) GetDNSServerAddressList(macAddress string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDNSServerAddressList", macAddress) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDNSServerAddressList indicates an expected call of GetDNSServerAddressList +func (mr *MockNetworkUtilsMockRecorder) GetDNSServerAddressList(macAddress interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSServerAddressList", reflect.TypeOf((*MockNetworkUtils)(nil).GetDNSServerAddressList), macAddress) +} + +// ConvertInterfaceAliasToLUID mocks base method +func (m *MockNetworkUtils) ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConvertInterfaceAliasToLUID", interfaceAlias) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConvertInterfaceAliasToLUID indicates an expected call of ConvertInterfaceAliasToLUID +func (mr *MockNetworkUtilsMockRecorder) ConvertInterfaceAliasToLUID(interfaceAlias interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConvertInterfaceAliasToLUID", reflect.TypeOf((*MockNetworkUtils)(nil).ConvertInterfaceAliasToLUID), interfaceAlias) +} + +// GetMIBIfEntryFromLUID mocks base method +func (m *MockNetworkUtils) GetMIBIfEntryFromLUID(ifaceLUID uint64) (*networkutils.MibIfRow2, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMIBIfEntryFromLUID", ifaceLUID) + ret0, _ := ret[0].(*networkutils.MibIfRow2) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMIBIfEntryFromLUID indicates an expected call of GetMIBIfEntryFromLUID +func (mr *MockNetworkUtilsMockRecorder) GetMIBIfEntryFromLUID(ifaceLUID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMIBIfEntryFromLUID", reflect.TypeOf((*MockNetworkUtils)(nil).GetMIBIfEntryFromLUID), ifaceLUID) +} diff --git a/agent/eni/networkutils/types_windows.go b/agent/eni/networkutils/types_windows.go new file mode 100644 index 00000000000..551ab47b2fd --- /dev/null +++ b/agent/eni/networkutils/types_windows.go @@ -0,0 +1,70 @@ +//go:build windows +// +build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package networkutils + +import "golang.org/x/sys/windows" + +const ( + ifMaxStringSize = 256 + ifMaxPhysAddressLength = 32 +) + +// MibIfRow2 structure stores information about a particular interface. +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_if_row2 +type MibIfRow2 struct { + InterfaceLUID uint64 + interfaceIndex uint32 + interfaceGUID windows.GUID + alias [ifMaxStringSize + 1]uint16 + description [ifMaxStringSize + 1]uint16 + physicalAddressLength uint32 + physicalAddress [ifMaxPhysAddressLength]byte + permanentPhysicalAddress [ifMaxPhysAddressLength]byte + mtu uint32 + ifType uint32 + tunnelType uint32 + mediaType uint32 + physicalMediumType uint32 + accessType uint32 + directionType uint32 + interfaceAndOperStatusFlags uint8 + operStatus uint32 + adminStatus uint32 + mediaConnectState uint32 + networkGUID windows.GUID + connectionType uint32 + transmitLinkSpeed uint64 + receiveLinkSpeed uint64 + InOctets uint64 + InUcastPkts uint64 + InNUcastPkts uint64 + InDiscards uint64 + InErrors uint64 + inUnknownProtos uint64 + inUcastOctets uint64 + inMulticastOctets uint64 + inBroadcastOctets uint64 + OutOctets uint64 + OutUcastPkts uint64 + OutNUcastPkts uint64 + OutDiscards uint64 + OutErrors uint64 + outUcastOctets uint64 + outMulticastOctets uint64 + outBroadcastOctets uint64 + outQLen uint64 +} diff --git a/agent/eni/networkutils/utils_linux.go b/agent/eni/networkutils/utils_linux.go index 9a04d2c56dd..10a7a21262f 100644 --- a/agent/eni/networkutils/utils_linux.go +++ b/agent/eni/networkutils/utils_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/networkutils/utils_linux_test.go b/agent/eni/networkutils/utils_linux_test.go index f94e12426d5..39e5f771c4b 100644 --- a/agent/eni/networkutils/utils_linux_test.go +++ b/agent/eni/networkutils/utils_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/networkutils/utils_windows.go b/agent/eni/networkutils/utils_windows.go index 402e4a39916..398d91962ed 100644 --- a/agent/eni/networkutils/utils_windows.go +++ b/agent/eni/networkutils/utils_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -24,22 +25,32 @@ import ( "time" "unsafe" - "golang.org/x/sys/windows" - apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" "github.com/aws/amazon-ecs-agent/agent/eni/netwrapper" "github.com/aws/amazon-ecs-agent/agent/utils/retry" + "github.com/cihub/seelog" "github.com/pkg/errors" + "golang.org/x/sys/windows" ) +//go:generate mockgen -destination=mocks/$GOFILE -copyright_file=../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/eni/networkutils NetworkUtils + // NetworkUtils is the interface used for accessing network related functionality on Windows. // The methods declared in this package may or may not add any additional logic over the actual networking api calls. type NetworkUtils interface { + // GetInterfaceMACByIndex returns the MAC address of the device with given interface index. + // We will retry with the given timeout and context before erroring out. GetInterfaceMACByIndex(int, context.Context, time.Duration) (string, error) + // GetAllNetworkInterfaces returns all the network interfaces in the host namespace. GetAllNetworkInterfaces() ([]net.Interface, error) + // GetDNSServerAddressList returns the DNS Server list associated to the interface with + // the given MAC address. GetDNSServerAddressList(macAddress string) ([]string, error) - SetNetWrapper(netWrapper netwrapper.NetWrapper) + // ConvertInterfaceAliasToLUID converts an interface alias to it's LUID. + ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) + // GetMIBIfEntryFromLUID returns the MIB_IF_ROW2 for the interface with the given LUID. + GetMIBIfEntryFromLUID(ifaceLUID uint64) (*MibIfRow2, error) } type networkUtils struct { @@ -52,14 +63,25 @@ type networkUtils struct { ctx context.Context // A wrapper over Golang's net package netWrapper netwrapper.NetWrapper + // funcConvertInterfaceAliasToLuid is the system call to ConvertInterfaceAliasToLuid Win32 API. + funcConvertInterfaceAliasToLuid func(a ...uintptr) (r1 uintptr, r2 uintptr, lastErr error) + // funcGetIfEntry2Ex is the system call to GetIfEntry2Ex Win32 API. + funcGetIfEntry2Ex func(a ...uintptr) (r1 uintptr, r2 uintptr, lastErr error) } var funcGetAdapterAddresses = getAdapterAddresses // New creates a new network utils. func New() NetworkUtils { + // We would be using GetIfTable2Ex, GetIfEntry2Ex, and FreeMibTable Win32 APIs from IP Helper. + moduleIPHelper := windows.NewLazySystemDLL("iphlpapi.dll") + procConvertInterfaceAliasToLuid := moduleIPHelper.NewProc("ConvertInterfaceAliasToLuid") + procGetIfEntry2Ex := moduleIPHelper.NewProc("GetIfEntry2Ex") + return &networkUtils{ - netWrapper: netwrapper.New(), + netWrapper: netwrapper.New(), + funcConvertInterfaceAliasToLuid: procConvertInterfaceAliasToLuid.Call, + funcGetIfEntry2Ex: procGetIfEntry2Ex.Call, } } @@ -117,11 +139,6 @@ func (utils *networkUtils) GetAllNetworkInterfaces() ([]net.Interface, error) { return utils.netWrapper.GetAllNetworkInterfaces() } -// SetNetWrapper is used to inject netWrapper instance. This will be handy while testing to inject mocks. -func (utils *networkUtils) SetNetWrapper(netWrapper netwrapper.NetWrapper) { - utils.netWrapper = netWrapper -} - // GetDNSServerAddressList returns the DNS server addresses of the queried interface. func (utils *networkUtils) GetDNSServerAddressList(macAddress string) ([]string, error) { addresses, err := funcGetAdapterAddresses() @@ -140,13 +157,46 @@ func (utils *networkUtils) GetDNSServerAddressList(macAddress string) ([]string, dnsServerAddressList := make([]string, 0) for firstDnsNode != nil { - dnsServerAddressList = append(dnsServerAddressList, utils.parseSocketAddress(firstDnsNode.Address)) + dnsServerAddressList = append(dnsServerAddressList, firstDnsNode.Address.IP().String()) firstDnsNode = firstDnsNode.Next } return dnsServerAddressList, nil } +// ConvertInterfaceAliasToLUID returns the LUID of the interface with given interface alias. +// Internally, it would invoke ConvertInterfaceAliasToLuid Win32 API to perform the conversion. +func (utils *networkUtils) ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) { + var luid uint64 + alias := windows.StringToUTF16Ptr(interfaceAlias) + + // ConvertInterfaceAliasToLuid function converts alias into LUID. + // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfacealiastoluid + retVal, _, _ := utils.funcConvertInterfaceAliasToLuid(uintptr(unsafe.Pointer(alias)), uintptr(unsafe.Pointer(&luid))) + if retVal != 0 { + return 0, errors.Errorf("error occured while calling ConvertInterfaceAliasToLuid: %s", syscall.Errno(retVal)) + } + + return luid, nil +} + +// GetMIBIfEntryFromLUID returns the MIB_IF_ROW2 object for the interface with given LUID. +// Internally, this would invoke GetIfEntry2Ex Win32 API to retrieve the specific row. +func (utils *networkUtils) GetMIBIfEntryFromLUID(ifaceLUID uint64) (*MibIfRow2, error) { + row := &MibIfRow2{ + InterfaceLUID: ifaceLUID, + } + + // GetIfEntry2Ex function retrieves the MIB-II interface for the given interface index.. + // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex + retVal, _, _ := utils.funcGetIfEntry2Ex(uintptr(0), uintptr(unsafe.Pointer(row))) + if retVal != 0 { + return nil, errors.Errorf("error occured while calling GetIfEntry2Ex: %s", syscall.Errno(retVal)) + } + + return row, nil +} + // parseMACAddress parses the physical address of windows.IpAdapterAddresses into net.HardwareAddr. func (utils *networkUtils) parseMACAddress(adapterAddress *windows.IpAdapterAddresses) net.HardwareAddr { hardwareAddr := make(net.HardwareAddr, adapterAddress.PhysicalAddressLength) @@ -157,18 +207,6 @@ func (utils *networkUtils) parseMACAddress(adapterAddress *windows.IpAdapterAddr return hardwareAddr } -// parseSocketAddress parses the SocketAddress into its string representation. -// This method needs to be deprecated in favour of IP() method of SocketAdress introduced in Go 1.13+. -// The method details have been taken from https://github.com/golang/sys/blob/release-branch.go1.13/windows/types_windows.go -func (utils *networkUtils) parseSocketAddress(addr windows.SocketAddress) string { - var ipAddr string - if uintptr(addr.SockaddrLength) >= unsafe.Sizeof(syscall.RawSockaddrInet4{}) && addr.Sockaddr.Addr.Family == syscall.AF_INET { - ip := net.IP((*syscall.RawSockaddrInet4)(unsafe.Pointer(addr.Sockaddr)).Addr[:]) - ipAddr = ip.String() - } - return ipAddr -} - // getAdapterAddresses returns a list of IP adapter and address // structures. The structure contains an IP adapter and flattened // multiple IP addresses including unicast, anycast and multicast diff --git a/agent/eni/networkutils/utils_windows_test.go b/agent/eni/networkutils/utils_windows_test.go index 352cccc7a49..5629a6e93ae 100644 --- a/agent/eni/networkutils/utils_windows_test.go +++ b/agent/eni/networkutils/utils_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -21,6 +22,7 @@ import ( "net" "syscall" "testing" + "unsafe" "golang.org/x/sys/windows" @@ -31,9 +33,13 @@ import ( ) const ( - interfaceIndex = 9 - macAddress = "02:22:ea:8c:81:dc" - validDnsServer = "10.0.0.2" + deviceName = "Ethernet 2" + ifaceLUID uint64 = 1689399649632256 + RxBytes uint64 = 1000 + TxBytes uint64 = 5000 + interfaceIndex = 9 + macAddress = "02:22:ea:8c:81:dc" + validDnsServer = "10.0.0.2" ) // This is a success test. We receive the appropriate MAC address corresponding to the interface index. @@ -43,8 +49,7 @@ func TestGetInterfaceMACByIndex(t *testing.T) { ctx := context.TODO() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} hardwareAddr, err := net.ParseMAC(macAddress) mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex).Return( @@ -67,8 +72,7 @@ func TestGetInterfaceMACByIndexEmptyAddress(t *testing.T) { ctx := context.TODO() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex).Return( &net.Interface{ @@ -90,8 +94,7 @@ func TestGetInterfaceMACByIndexRetries(t *testing.T) { ctx := context.TODO() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} hardwareAddr, err := net.ParseMAC(macAddress) emptyaddr := make([]byte, 0) @@ -122,8 +125,7 @@ func TestGetInterfaceMACByIndexContextTimeout(t *testing.T) { ctx := context.TODO() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex).Return( &net.Interface{ @@ -145,8 +147,7 @@ func TestGetInterfaceMACByIndexWithGolangNetError(t *testing.T) { ctx := context.TODO() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex).Return( nil, errors.New("unable to retrieve interface")) @@ -163,8 +164,7 @@ func TestGetAllNetworkInterfaces(t *testing.T) { defer mockCtrl.Finish() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} expectedIface := make([]net.Interface, 1) @@ -190,8 +190,7 @@ func TestGetAllNetworkInterfacesError(t *testing.T) { defer mockCtrl.Finish() mocknetwrapper := mock_netwrapper.NewMockNetWrapper(mockCtrl) - netUtils := New() - netUtils.SetNetWrapper(mocknetwrapper) + netUtils := &networkUtils{netWrapper: mocknetwrapper} mocknetwrapper.EXPECT().GetAllNetworkInterfaces().Return( nil, errors.New("error occurred while fetching interfaces"), @@ -236,3 +235,76 @@ func TestGetDNSServerAddressList(t *testing.T) { assert.Len(t, dnsServerList, 1) assert.EqualValues(t, dnsServerList[0], validDnsServer) } + +// TestGetMIBIfEntryFromLUID tests the GetMIBIfEntryFromLUID method. +func TestGetMIBIfEntryFromLUID(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcGetIfEntry2Ex = func(a ...uintptr) (uintptr, uintptr, error) { + row := (*MibIfRow2)(unsafe.Pointer(a[1])) + row.interfaceIndex = interfaceIndex + row.OutOctets = TxBytes + row.InOctets = RxBytes + return uintptr(0), uintptr(0), nil + } + + ifRow, err := netUtils.GetMIBIfEntryFromLUID(ifaceLUID) + assert.NoError(t, err) + assert.Equal(t, uint32(interfaceIndex), ifRow.interfaceIndex) + assert.Equal(t, TxBytes, ifRow.OutOctets) + assert.Equal(t, RxBytes, ifRow.InOctets) +} + +// TestGetMIBIfEntryFromLUIDError tests the GetMIBIfEntryFromLUID method in error case. +func TestGetMIBIfEntryFromLUIDError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcGetIfEntry2Ex = func(a ...uintptr) (uintptr, uintptr, error) { + // Return an error code. + return uintptr(1), uintptr(0), nil + } + + _, err := netUtils.GetMIBIfEntryFromLUID(ifaceLUID) + assert.Error(t, err) +} + +// TestConvertInterfaceAliasToLUID tests ConvertInterfaceAliasToLUID method. +func TestConvertInterfaceAliasToLUID(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcConvertInterfaceAliasToLuid = func(a ...uintptr) (uintptr, uintptr, error) { + luid := (*uint64)(unsafe.Pointer(a[1])) + *luid = ifaceLUID + return uintptr(0), uintptr(0), nil + } + + luid, err := netUtils.ConvertInterfaceAliasToLUID(deviceName) + assert.NoError(t, err) + assert.Equal(t, ifaceLUID, luid) +} + +// TestConvertInterfaceAliasToLUID tests ConvertInterfaceAliasToLUID method in case of error. +func TestConvertInterfaceAliasToLUIDError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcConvertInterfaceAliasToLuid = func(a ...uintptr) (uintptr, uintptr, error) { + return uintptr(1), uintptr(0), nil + } + + _, err := netUtils.ConvertInterfaceAliasToLUID(deviceName) + assert.Error(t, err) +} diff --git a/agent/eni/pause/generate_mocks.go b/agent/eni/pause/generate_mocks.go deleted file mode 100644 index 3cb9d064488..00000000000 --- a/agent/eni/pause/generate_mocks.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package pause - -//go:generate mockgen -destination=mocks/load_mocks.go -copyright_file=../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/eni/pause Loader diff --git a/agent/eni/pause/load.go b/agent/eni/pause/load.go index a20d2e9ea22..52e0d4c0a3c 100644 --- a/agent/eni/pause/load.go +++ b/agent/eni/pause/load.go @@ -14,56 +14,12 @@ package pause import ( - "context" - "fmt" - - "github.com/aws/amazon-ecs-agent/agent/config" - "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" - log "github.com/cihub/seelog" - "github.com/docker/docker/api/types" - "github.com/pkg/errors" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" ) -// Loader defines an interface for loading the pause container image. This is mostly -// to facilitate mocking and testing of the LoadImage method -type Loader interface { - LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) - IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) -} - -type loader struct{} +type pauseLoader struct{} // New creates a new pause image loader -func New() Loader { - return &loader{} -} - -// This function uses the DockerClient to inspect the image with the given name and tag. -func getPauseContainerImage(name string, tag string, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - imageName := fmt.Sprintf("%s:%s", name, tag) - log.Debugf("Inspecting pause container image: %s", imageName) - - image, err := dockerClient.InspectImage(imageName) - if err != nil { - return nil, errors.Wrapf(err, - "pause container load: failed to inspect image: %s", imageName) - } - - return image, nil -} - -// Common function for linux and windows to check if the container pause image has been loaded -func isImageLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - image, err := getPauseContainerImage( - config.DefaultPauseContainerImageName, config.DefaultPauseContainerTag, dockerClient) - - if err != nil { - return false, err - } - - if image == nil || image.ID == "" { - return false, nil - } - - return true, nil +func New() loader.Loader { + return &pauseLoader{} } diff --git a/agent/eni/pause/load_test.go b/agent/eni/pause/load_test.go deleted file mode 100644 index 32a88e0ccf0..00000000000 --- a/agent/eni/pause/load_test.go +++ /dev/null @@ -1,148 +0,0 @@ -//go:build unit - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package pause - -import ( - "context" - "errors" - "testing" - - "github.com/aws/amazon-ecs-agent/agent/config" - "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" - mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks" - mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks" - - "github.com/docker/docker/api/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -const ( - pauseName = "pause" - pauseTag = "tag" -) - -var defaultConfig = config.DefaultConfig() - -func TestGetPauseContainerImageInspectImageError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return( - types.ImageInspect{}, nil, errors.New("error")) - - _, err = getPauseContainerImage(pauseName, pauseTag, client) - assert.Error(t, err) -} - -func TestGetPauseContainerHappyPath(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), pauseName+":"+pauseTag).Return(types.ImageInspect{}, nil, nil) - - _, err = getPauseContainerImage(pauseName, pauseTag, client) - assert.NoError(t, err) -} - -func TestIsImageLoadedHappyPath(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{ID: "test123"}, nil, nil) - - isLoaded, err := isImageLoaded(client) - assert.NoError(t, err) - assert.True(t, isLoaded) -} - -func TestIsImageLoadedNotLoaded(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{}, nil, nil) - - isLoaded, err := isImageLoaded(client) - assert.NoError(t, err) - assert.False(t, isLoaded) -} - -func TestIsImageLoadedError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return( - types.ImageInspect{}, nil, errors.New("error")) - - isLoaded, err := isImageLoaded(client) - assert.Error(t, err) - assert.False(t, isLoaded) -} diff --git a/agent/eni/pause/pause_linux.go b/agent/eni/pause/pause_linux.go index c983c24a9f9..492ecdd3e93 100644 --- a/agent/eni/pause/pause_linux.go +++ b/agent/eni/pause/pause_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,49 +18,25 @@ package pause import ( "context" - "os" "github.com/aws/amazon-ecs-agent/agent/config" - "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" - log "github.com/cihub/seelog" "github.com/docker/docker/api/types" - "github.com/pkg/errors" ) // LoadImage helps load the pause container image for the agent -func (*loader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - log.Debugf("Loading pause container tarball: %s", cfg.PauseContainerTarballPath) - if err := loadFromFile(ctx, cfg.PauseContainerTarballPath, dockerClient); err != nil { +func (*pauseLoader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + logger.Debug("Loading pause container tarball:", logger.Fields{ + field.Image: cfg.PauseContainerTarballPath, + }) + if err := loader.LoadFromFile(ctx, cfg.PauseContainerTarballPath, dockerClient); err != nil { return nil, err } - return getPauseContainerImage( - config.DefaultPauseContainerImageName, config.DefaultPauseContainerTag, dockerClient) -} - -func (*loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - return isImageLoaded(dockerClient) -} - -var open = os.Open - -func loadFromFile(ctx context.Context, path string, dockerClient dockerapi.DockerClient) error { - pauseContainerReader, err := open(path) - if err != nil { - if err.Error() == noSuchFile { - return NewNoSuchFileError(errors.Wrapf(err, - "pause container load: failed to read pause container image: %s", path)) - } - return errors.Wrapf(err, - "pause container load: failed to read pause container image: %s", path) - } - if err := dockerClient.LoadImage(ctx, pauseContainerReader, dockerclient.LoadImageTimeout); err != nil { - return errors.Wrapf(err, - "pause container load: failed to load pause container image: %s", path) - } - - return nil - + return loader.GetContainerImage( + config.DefaultPauseContainerImageName+":"+config.DefaultPauseContainerTag, dockerClient) } diff --git a/agent/eni/pause/pause_linux_test.go b/agent/eni/pause/pause_linux_test.go deleted file mode 100644 index 6cb7725176a..00000000000 --- a/agent/eni/pause/pause_linux_test.go +++ /dev/null @@ -1,121 +0,0 @@ -//go:build linux && unit - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package pause - -import ( - "context" - "errors" - "os" - "testing" - - "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" - mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks" - mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks" - - "github.com/docker/docker/api/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -const ( - pauseTarballPath = "/path/to/pause.tar" -) - -func mockOpen() func() { - open = func(name string) (*os.File, error) { - return nil, nil - } - return func() { - open = os.Open - } -} - -// TestLoadFromFileWithReaderError tests loadFromFile with reader error -func TestLoadFromFileWithReaderError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - - open = func(name string) (*os.File, error) { - return nil, errors.New("Dummy Reader Error") - } - defer func() { - open = os.Open - }() - - err = loadFromFile(ctx, pauseTarballPath, client) - assert.Error(t, err) -} - -// TestLoadFromFileHappyPath tests loadFromFile against happy path -func TestLoadFromFileHappyPath(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{}, nil) - defer mockOpen()() - - err = loadFromFile(ctx, pauseTarballPath, client) - assert.NoError(t, err) -} - -// TestLoadFromFileDockerLoadImageError tests loadFromFile against error -// from Docker clients LoadImage -func TestLoadFromFileDockerLoadImageError(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - // Docker SDK tests - mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) - mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) - sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) - sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) - assert.NoError(t, err) - mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{}, - errors.New("Dummy Load Image Error")) - - defer mockOpen()() - - err = loadFromFile(ctx, pauseTarballPath, client) - assert.Error(t, err) -} diff --git a/agent/eni/pause/pause_supported.go b/agent/eni/pause/pause_supported.go new file mode 100644 index 00000000000..d42ad2527c7 --- /dev/null +++ b/agent/eni/pause/pause_supported.go @@ -0,0 +1,29 @@ +//go:build linux || windows +// +build linux windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package pause + +import ( + "github.com/aws/amazon-ecs-agent/agent/utils/loader" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" +) + +// This method is used to inspect the presence of the pause image. If the image has not been loaded then we return false. +func (*pauseLoader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return loader.IsImageLoaded(config.DefaultPauseContainerImageName+":"+config.DefaultPauseContainerTag, dockerClient) +} diff --git a/agent/eni/pause/pause_unsupported.go b/agent/eni/pause/pause_unsupported.go index bc5eafd7bf2..4f68b877313 100644 --- a/agent/eni/pause/pause_unsupported.go +++ b/agent/eni/pause/pause_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,23 +18,24 @@ package pause import ( "context" + "fmt" "runtime" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/utils/loader" "github.com/docker/docker/api/types" - "github.com/pkg/errors" ) // LoadImage returns UnsupportedPlatformError on the unsupported platform -func (*loader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - return nil, NewUnsupportedPlatformError(errors.Errorf( +func (*pauseLoader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + return nil, loader.NewUnsupportedPlatformError(fmt.Errorf( "pause container load: unsupported platform: %s/%s", runtime.GOOS, runtime.GOARCH)) } -func (*loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - return false, NewUnsupportedPlatformError(errors.Errorf( +func (*pauseLoader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { + return false, loader.NewUnsupportedPlatformError(fmt.Errorf( "pause container isloaded: unsupported platform: %s/%s", runtime.GOOS, runtime.GOARCH)) } diff --git a/agent/eni/pause/pause_windows.go b/agent/eni/pause/pause_windows.go index 9f640857e8d..f62790926fb 100644 --- a/agent/eni/pause/pause_windows.go +++ b/agent/eni/pause/pause_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,20 +18,15 @@ package pause import ( "context" + "fmt" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/docker/docker/api/types" - "github.com/pkg/errors" ) // In Linux, we use a tar archive to load the pause image. Whereas in Windows, we will cache the image during AMI build. // Therefore, this functionality is not supported in Windows. -func (*loader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { - return nil, errors.New("this functionality is not supported on this platform.") -} - -// This method is used to inspect the presence of the pause image. If the image has not been loaded then we return false. -func (*loader) IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) { - return isImageLoaded(dockerClient) +func (*pauseLoader) LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + return nil, fmt.Errorf("this functionality is not supported on this platform.") } diff --git a/agent/eni/udevwrapper/udev_linux.go b/agent/eni/udevwrapper/udev_linux.go index 32cf01f4105..9d696c9fddc 100644 --- a/agent/eni/udevwrapper/udev_linux.go +++ b/agent/eni/udevwrapper/udev_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/udevwrapper/udev_unsupported.go b/agent/eni/udevwrapper/udev_unsupported.go index 09c70ab73ea..452edf7a639 100644 --- a/agent/eni/udevwrapper/udev_unsupported.go +++ b/agent/eni/udevwrapper/udev_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux +// +build !linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/watcher/watcher_linux.go b/agent/eni/watcher/watcher_linux.go index b5a1124f926..f055fec79d5 100644 --- a/agent/eni/watcher/watcher_linux.go +++ b/agent/eni/watcher/watcher_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/watcher/watcher_linux_test.go b/agent/eni/watcher/watcher_linux_test.go index ce17f13b8af..469555b4f55 100644 --- a/agent/eni/watcher/watcher_linux_test.go +++ b/agent/eni/watcher/watcher_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/watcher/watcher_test.go b/agent/eni/watcher/watcher_test.go index 9fda7494e30..9bd90c170bb 100644 --- a/agent/eni/watcher/watcher_test.go +++ b/agent/eni/watcher/watcher_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/watcher/watcher_unsupported.go b/agent/eni/watcher/watcher_unsupported.go index 222653c87ab..7adae3c5979 100644 --- a/agent/eni/watcher/watcher_unsupported.go +++ b/agent/eni/watcher/watcher_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/watcher/watcher_windows.go b/agent/eni/watcher/watcher_windows.go index 3a712955761..50ec7baa75e 100644 --- a/agent/eni/watcher/watcher_windows.go +++ b/agent/eni/watcher/watcher_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -58,8 +59,6 @@ func newWatcher(ctx context.Context, state dockerstate.TaskEngineState, stateChangeEvents chan<- statechange.Event) (*ENIWatcher, error) { - derivedContext, cancel := context.WithCancel(ctx) - eniMonitor := iphelperwrapper.NewMonitor() notificationChannel := make(chan int) err := eniMonitor.Start(notificationChannel) @@ -68,6 +67,7 @@ func newWatcher(ctx context.Context, } log.Info("windows eni watcher has been initialized") + derivedContext, cancel := context.WithCancel(ctx) return &ENIWatcher{ ctx: derivedContext, cancel: cancel, @@ -171,9 +171,3 @@ func (eniWatcher *ENIWatcher) getAllInterfaces() (state map[string]int, err erro } return state, nil } - -// SetNetworkUtils is used for injecting NetworkUtils instance in eniWatcher -// This will be handy while testing to inject mock objects -func (eniWatcher *ENIWatcher) SetNetworkUtils(utils networkutils.NetworkUtils) { - eniWatcher.netutils = utils -} diff --git a/agent/eni/watcher/watcher_windows_test.go b/agent/eni/watcher/watcher_windows_test.go index 97d5c772a0f..af2eb1ac52d 100644 --- a/agent/eni/watcher/watcher_windows_test.go +++ b/agent/eni/watcher/watcher_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,7 +18,6 @@ package watcher import ( "context" - "net" "sync" "testing" @@ -29,9 +29,9 @@ import ( mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" "github.com/aws/amazon-ecs-agent/agent/eni/iphelperwrapper" mock_iphelperwrapper "github.com/aws/amazon-ecs-agent/agent/eni/iphelperwrapper/mocks" - "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" - mock_gonetwrapper "github.com/aws/amazon-ecs-agent/agent/eni/netwrapper/mocks" + mock_networkutils "github.com/aws/amazon-ecs-agent/agent/eni/networkutils/mocks" "github.com/aws/amazon-ecs-agent/agent/statechange" + "github.com/golang/mock/gomock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -81,7 +81,6 @@ func constructInterfaceList() []net.Interface { // newTestWatcher is used to create a watcher for testing purpose func newTestWatcher(ctx context.Context, primaryMAC string, state dockerstate.TaskEngineState, stateChangeEvents chan<- statechange.Event, eniMonitor iphelperwrapper.InterfaceMonitor) (*ENIWatcher, error) { - derivedContext, cancel := context.WithCancel(ctx) notificationChannel := make(chan int) err := eniMonitor.Start(notificationChannel) @@ -89,6 +88,7 @@ func newTestWatcher(ctx context.Context, primaryMAC string, state dockerstate.Ta return nil, errors.Wrapf(err, "error occurred while instantiating watcher") } + derivedContext, cancel := context.WithCancel(ctx) return &ENIWatcher{ ctx: derivedContext, cancel: cancel, @@ -97,7 +97,6 @@ func newTestWatcher(ctx context.Context, primaryMAC string, state dockerstate.Ta primaryMAC: primaryMAC, interfaceMonitor: eniMonitor, notifications: notificationChannel, - netutils: networkutils.New(), }, nil } @@ -163,13 +162,11 @@ func TestReconcileOnce(t *testing.T) { }, true) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) mockiphelper.EXPECT().Start(gomock.Any()).Return(nil) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) - mocknetwrapper.EXPECT().GetAllNetworkInterfaces().Return(constructInterfaceList(), nil) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) + mockNetworkUtils.EXPECT().GetAllNetworkInterfaces().Return(constructInterfaceList(), nil) watcher, _ := newTestWatcher(ctx, primaryMAC, mockStateManager, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils waitForEvents.Add(2) @@ -203,13 +200,11 @@ func TestReconcileOnceNetUtilsError(t *testing.T) { mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) mockiphelper.EXPECT().Start(gomock.Any()).Return(nil) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) - mocknetwrapper.EXPECT().GetAllNetworkInterfaces().Return(nil, errors.New("Error while retrieving interfaces")) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) + mockNetworkUtils.EXPECT().GetAllNetworkInterfaces().Return(nil, errors.New("Error while retrieving interfaces")) watcher, _ := newTestWatcher(ctx, primaryMAC, nil, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils err := watcher.Init() assert.Error(t, err) @@ -226,13 +221,11 @@ func TestReconcileOnceEmptyInterfaceList(t *testing.T) { mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) mockiphelper.EXPECT().Start(gomock.Any()).Return(nil) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) - mocknetwrapper.EXPECT().GetAllNetworkInterfaces().Return(make([]net.Interface, 0), nil) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) + mockNetworkUtils.EXPECT().GetAllNetworkInterfaces().Return(make([]net.Interface, 0), nil) watcher, _ := newTestWatcher(ctx, primaryMAC, nil, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils err := watcher.Init() assert.NoError(t, err) @@ -251,17 +244,12 @@ func TestEventHandlerSuccess(t *testing.T) { eventChannel := make(chan statechange.Event) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) mockStateManager := mock_dockerstate.NewMockTaskEngineState(mockCtrl) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) - mac1, _ := net.ParseMAC(macAddress1) gomock.InOrder( mockiphelper.EXPECT().Start(gomock.Any()).Return(nil), - mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex1).Return(&net.Interface{ - Index: interfaceIndex1, - Name: interfacename1, - HardwareAddr: mac1, - }, nil), + mockNetworkUtils.EXPECT().GetInterfaceMACByIndex(interfaceIndex1, gomock.Any(), sendENIStateChangeRetryTimeout).Return(macAddress1, nil), mockStateManager.EXPECT().ENIByMac(macAddress1). Return(&apieni.ENIAttachment{ MACAddress: macAddress1, @@ -270,9 +258,7 @@ func TestEventHandlerSuccess(t *testing.T) { ) watcher, _ := newTestWatcher(ctx, primaryMAC, mockStateManager, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils go watcher.eventHandler() watcher.notifications <- interfaceIndex1 @@ -309,17 +295,16 @@ func TestEventHandlerGetInterfaceByMACError(t *testing.T) { eventChannel := make(chan statechange.Event) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) gomock.InOrder( mockiphelper.EXPECT().Start(gomock.Any()).Return(nil), - mocknetwrapper.EXPECT().FindInterfaceByIndex(gomock.Any()).Return(nil, errors.New("Error while retrieving details")), + mockNetworkUtils.EXPECT().GetInterfaceMACByIndex(interfaceIndex1, gomock.Any(), sendENIStateChangeRetryTimeout).Return( + "", errors.New("Error while retrieving details")), ) watcher, _ := newTestWatcher(ctx, primaryMAC, nil, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils go watcher.eventHandler() watcher.notifications <- interfaceIndex1 @@ -347,17 +332,12 @@ func TestEventHandlerENIStatusAlreadySent(t *testing.T) { eventChannel := make(chan statechange.Event) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) mockStateManager := mock_dockerstate.NewMockTaskEngineState(mockCtrl) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) - mac1, _ := net.ParseMAC(macAddress1) gomock.InOrder( mockiphelper.EXPECT().Start(gomock.Any()).Return(nil), - mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex1).Return(&net.Interface{ - Index: interfaceIndex1, - Name: interfacename1, - HardwareAddr: mac1, - }, nil), + mockNetworkUtils.EXPECT().GetInterfaceMACByIndex(interfaceIndex1, gomock.Any(), sendENIStateChangeRetryTimeout).Return(macAddress1, nil), mockStateManager.EXPECT().ENIByMac(macAddress1). Return(&apieni.ENIAttachment{ MACAddress: macAddress1, @@ -366,9 +346,7 @@ func TestEventHandlerENIStatusAlreadySent(t *testing.T) { ) watcher, _ := newTestWatcher(ctx, primaryMAC, mockStateManager, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils go watcher.eventHandler() watcher.notifications <- interfaceIndex1 @@ -396,25 +374,18 @@ func TestEventHandlerUnmanagedENI(t *testing.T) { eventChannel := make(chan statechange.Event) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) mockStateManager := mock_dockerstate.NewMockTaskEngineState(mockCtrl) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) - mac1, _ := net.ParseMAC(macAddress1) gomock.InOrder( mockiphelper.EXPECT().Start(gomock.Any()).Return(nil), - mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex1).Return(&net.Interface{ - Index: interfaceIndex1, - Name: interfacename1, - HardwareAddr: mac1, - }, nil), + mockNetworkUtils.EXPECT().GetInterfaceMACByIndex(interfaceIndex1, gomock.Any(), sendENIStateChangeRetryTimeout).Return(macAddress1, nil), mockStateManager.EXPECT().ENIByMac(macAddress1). Return(nil, false).AnyTimes(), ) watcher, _ := newTestWatcher(ctx, primaryMAC, mockStateManager, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils go watcher.eventHandler() watcher.notifications <- interfaceIndex1 @@ -442,17 +413,12 @@ func TestEventHandlerExpiredENI(t *testing.T) { eventChannel := make(chan statechange.Event) mockiphelper := mock_iphelperwrapper.NewMockInterfaceMonitor(mockCtrl) - mocknetwrapper := mock_gonetwrapper.NewMockNetWrapper(mockCtrl) mockStateManager := mock_dockerstate.NewMockTaskEngineState(mockCtrl) + mockNetworkUtils := mock_networkutils.NewMockNetworkUtils(mockCtrl) - mac1, _ := net.ParseMAC(macAddress1) gomock.InOrder( mockiphelper.EXPECT().Start(gomock.Any()).Return(nil), - mocknetwrapper.EXPECT().FindInterfaceByIndex(interfaceIndex1).Return(&net.Interface{ - Index: interfaceIndex1, - Name: interfacename1, - HardwareAddr: mac1, - }, nil), + mockNetworkUtils.EXPECT().GetInterfaceMACByIndex(interfaceIndex1, gomock.Any(), sendENIStateChangeRetryTimeout).Return(macAddress1, nil), mockStateManager.EXPECT().ENIByMac(macAddress1). Return(&apieni.ENIAttachment{ MACAddress: macAddress1, @@ -462,9 +428,7 @@ func TestEventHandlerExpiredENI(t *testing.T) { ) watcher, _ := newTestWatcher(ctx, primaryMAC, mockStateManager, eventChannel, mockiphelper) - netutils := networkutils.New() - netutils.SetNetWrapper(mocknetwrapper) - watcher.SetNetworkUtils(netutils) + watcher.netutils = mockNetworkUtils go watcher.eventHandler() watcher.notifications <- interfaceIndex1 diff --git a/agent/eventhandler/attachment_handler_test.go b/agent/eventhandler/attachment_handler_test.go index 3ddadb7f8f5..3a5303e0cf4 100644 --- a/agent/eventhandler/attachment_handler_test.go +++ b/agent/eventhandler/attachment_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eventhandler/handler_test.go b/agent/eventhandler/handler_test.go index 1698b3724f1..1d85fa99e0e 100644 --- a/agent/eventhandler/handler_test.go +++ b/agent/eventhandler/handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eventhandler/task_handler_test.go b/agent/eventhandler/task_handler_test.go index 4c9ded191f4..ba1277e0d83 100644 --- a/agent/eventhandler/task_handler_test.go +++ b/agent/eventhandler/task_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eventhandler/task_handler_types_test.go b/agent/eventhandler/task_handler_types_test.go index e745249ea43..b9bad21c6bb 100644 --- a/agent/eventhandler/task_handler_types_test.go +++ b/agent/eventhandler/task_handler_types_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eventstream/eventstream_test.go b/agent/eventstream/eventstream_test.go index 032e10f39d1..1b9e5768ae3 100644 --- a/agent/eventstream/eventstream_test.go +++ b/agent/eventstream/eventstream_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/fsx/fsx_test.go b/agent/fsx/fsx_test.go index c298744337b..e7e9349bcc8 100644 --- a/agent/fsx/fsx_test.go +++ b/agent/fsx/fsx_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/go.mod b/agent/go.mod index 327aabe84f5..473fbc18684 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -7,10 +7,10 @@ require ( github.com/aws/aws-sdk-go v1.36.0 github.com/awslabs/go-config-generator-for-fluentd-and-fluentbit v0.0.0-20190829210224-55d4fd2e6f35 github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 - github.com/containerd/cgroups v1.0.3 - github.com/containerd/containerd v1.4.12 // indirect + github.com/containerd/cgroups v1.0.4-0.20220221221032-e710ed6ebb1a + github.com/containerd/containerd v1.4.13 // indirect github.com/containerd/continuity v0.0.0-20181023183536-c220ac4f01b8 // indirect - github.com/containernetworking/cni v0.7.1 + github.com/containernetworking/cni v0.8.1 github.com/containernetworking/plugins v0.8.6 github.com/deniswernert/udev v0.0.0-20140626150257-82fe5be8ca5f github.com/didip/tollbooth v3.0.2+incompatible @@ -18,8 +18,9 @@ require ( github.com/docker/docker v0.0.0-20200531234253-77e06fda0c94 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 - github.com/golang/mock v1.1.1 - github.com/google/go-cmp v0.5.6 // indirect + github.com/fsnotify/fsnotify v1.5.4 + github.com/godbus/dbus/v5 v5.0.6 // indirect + github.com/golang/mock v1.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 @@ -27,21 +28,23 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/opencontainers/runtime-spec v1.0.2 + github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pborman/uuid v0.0.0-20150603214016-ca53cad383ca github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v0.9.4 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 + github.com/prometheus/common v0.4.1 + github.com/prometheus/procfs v0.6.0 // indirect github.com/stretchr/testify v1.7.0 - github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf + github.com/vishvananda/netlink v1.1.0 go.etcd.io/bbolt v1.3.6 - golang.org/x/net v0.0.0-20210525063256-abc453219eb5 - golang.org/x/sys v0.0.0-20210510120138-977fb7262007 - golang.org/x/text v0.3.6 // indirect - golang.org/x/time v0.0.0-20170927054726-6dc17368e09b // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e + golang.org/x/sys v0.0.0-20220624220833-87e55d714810 golang.org/x/tools v0.1.5 - google.golang.org/grpc v1.38.0 // indirect + google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5 // indirect + google.golang.org/grpc v1.48.0 + google.golang.org/protobuf v1.28.1 gotest.tools v2.2.0+incompatible // indirect ) diff --git a/agent/go.sum b/agent/go.sum index dc2051bfad0..f8eba1b0ec4 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -1,14 +1,75 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY= github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go v1.36.0 h1:CscTrS+szX5iu34zk2bZrChnGO/GMtUYgMK1Xzs2hYo= github.com/aws/aws-sdk-go v1.36.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/awslabs/go-config-generator-for-fluentd-and-fluentbit v0.0.0-20190829210224-55d4fd2e6f35 h1:nwDvkjUJ6L2g508W1+Wla1VxP5qiNc+a7vDAIHTqXu4= @@ -18,19 +79,34 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= +github.com/cilium/ebpf v0.4.0 h1:QlHdikaxALkqWasW8hAC1mfR0jdmvbfaBdBPFmRSglA= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= -github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= -github.com/containerd/containerd v1.4.12 h1:V+SHzYmhng/iju6M5nFrpTTusrhidoxKTwdwLw+u4c4= -github.com/containerd/containerd v1.4.12/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/cgroups v1.0.4-0.20220221221032-e710ed6ebb1a h1:e956Q1zTD3IIKTQPWb92C20PvU3S2ohP9jWqqD0JJWE= +github.com/containerd/cgroups v1.0.4-0.20220221221032-e710ed6ebb1a/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/containerd v1.4.13 h1:Z0CbagVdn9VN4K6htOCY/jApSw8YKP+RdLZ5dkXF8PM= +github.com/containerd/containerd v1.4.13/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20181023183536-c220ac4f01b8 h1:lJeDcldQnYskl7krc3lTppg8NKomoQkmQg1AzOXtQbA= github.com/containerd/continuity v0.0.0-20181023183536-c220ac4f01b8/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containernetworking/cni v0.7.1 h1:fE3r16wpSEyaqY4Z4oFrLMmIGfBYIKpPrHK31EJ9FzE= github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1 h1:7zpDnQ3T3s4ucOuJ/ZCLrYBxzkg0AELFfII3Epo9TmI= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/containernetworking/plugins v0.8.6 h1:npZTLiMa4CRn6m5P9+1Dz4O1j0UeFbm8VYN6dlsw568= github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= @@ -60,41 +136,107 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129 h1:tT8iWCYw4uOem71yYA3htfH+LNopJvcqZQshm56G5L4= github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 h1:S4qyfL2sEm5Budr4KVMyEniCy+PbS55651I/a+Kn/NQ= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -102,8 +244,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -121,8 +265,9 @@ github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2i github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v0.0.0-20150603214016-ca53cad383ca h1:dKRMHfduZ/ZqOHuYGk/0kkTIUbnyorkAfzLOp6Ts8pU= @@ -140,13 +285,17 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -156,55 +305,308 @@ github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936 h1:J9gO8RJCAFlln github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3 h1:f4/ZD59VsBOaJmWeI2yqtHvJhmRRPzi73C88ZtfhAIk= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a h1:+HHJiFUXVOIS9mr1ThqkQD1N8vpFCfCShqADBM12KTc= golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 h1:Wdn4Yb8d5VrsO3jWgaeSZss09x1VLVBMePDh4VW/xSQ= golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU= golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20171114152239-bd4635fd2559 h1:jng59vod++FU+Px/VHK2MyNYEuWUAH/ihe8jPUu7AJU= golang.org/x/tools v0.0.0-20171114152239-bd4635fd2559/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5 h1:ou3VRVAif8UJqz3l1r4Isoz7rrUWHWDHBonShMNYoQs= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/agent/gogenerate/awssdk.go b/agent/gogenerate/awssdk.go index fee08b8d51f..fbd8706aed7 100644 --- a/agent/gogenerate/awssdk.go +++ b/agent/gogenerate/awssdk.go @@ -1,4 +1,5 @@ //go:build codegen +// +build codegen // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/gpu/generate_mocks.go b/agent/gpu/generate_mocks.go index 26dadfa72b5..f609d8ff1a5 100644 --- a/agent/gpu/generate_mocks.go +++ b/agent/gpu/generate_mocks.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/gpu/nvidia_gpu_manager_unix.go b/agent/gpu/nvidia_gpu_manager_unix.go index 9c5b385e31c..c18d6d20140 100644 --- a/agent/gpu/nvidia_gpu_manager_unix.go +++ b/agent/gpu/nvidia_gpu_manager_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/gpu/nvidia_gpu_manager_unix_test.go b/agent/gpu/nvidia_gpu_manager_unix_test.go index 70b674a8e3e..ededb252214 100644 --- a/agent/gpu/nvidia_gpu_manager_unix_test.go +++ b/agent/gpu/nvidia_gpu_manager_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/agentapi/taskprotection/v1/handlers/generate_mocks.go b/agent/handlers/agentapi/taskprotection/v1/handlers/generate_mocks.go new file mode 100644 index 00000000000..cf06d6fafc2 --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/handlers/generate_mocks.go @@ -0,0 +1,3 @@ +package handlers + +//go:generate mockgen -destination=handlers_mocks.go -package=handlers -copyright_file=../../../../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/handlers TaskProtectionClientFactoryInterface diff --git a/agent/handlers/agentapi/taskprotection/v1/handlers/handlers.go b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers.go new file mode 100644 index 00000000000..db2911a0ac3 --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers.go @@ -0,0 +1,360 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package handlers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/aws/amazon-ecs-agent/agent/api" + "github.com/aws/amazon-ecs-agent/agent/api/ecsclient" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/credentials" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" + "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" + "github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/types" + "github.com/aws/amazon-ecs-agent/agent/handlers/utils" + v3 "github.com/aws/amazon-ecs-agent/agent/handlers/v3" + "github.com/aws/amazon-ecs-agent/agent/httpclient" + "github.com/aws/amazon-ecs-agent/agent/logger" + loggerfield "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awscreds "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" +) + +const ( + ExpectedProtectionResponseLength = 1 + + // timeout for ECS SDK calls + // must be lower than server write timeout + ecsCallTimeout = 4 * time.Second + ecsCallTimedOutError = "Timed out calling ECS Task Protection API" +) + +// TaskProtectionPath Returns endpoint path for UpdateTaskProtection API +func TaskProtectionPath() string { + return fmt.Sprintf( + "/api/%s/task-protection/v1/state", + utils.ConstructMuxVar(v3.V3EndpointIDMuxName, utils.AnythingButSlashRegEx)) +} + +// TaskProtectionRequest is the Task protection request received from customers pending validation +type TaskProtectionRequest struct { + ProtectionEnabled *bool + ExpiresInMinutes *int64 +} + +// TaskProtectionClientFactory implements TaskProtectionClientFactoryInterface +type TaskProtectionClientFactory struct { + Region string + Endpoint string + AcceptInsecureCert bool +} + +// UpdateTaskProtectionHandler returns an HTTP request handler function for +// UpdateTaskProtection API +func UpdateTaskProtectionHandler(state dockerstate.TaskEngineState, credentialsManager credentials.Manager, + factory TaskProtectionClientFactoryInterface, cluster string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + updateTaskProtectionRequestType := "api/UpdateTaskProtection/v1" + + var request TaskProtectionRequest + jsonDecoder := json.NewDecoder(r.Body) + jsonDecoder.DisallowUnknownFields() + if err := jsonDecoder.Decode(&request); err != nil { + logger.Error("UpdateTaskProtection: failed to decode request", logger.Fields{ + loggerfield.Error: err, + }) + writeJSONResponse(w, http.StatusBadRequest, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr("", ecs.ErrCodeInvalidParameterException, + "UpdateTaskProtection: failed to decode request"), nil), + updateTaskProtectionRequestType) + return + } + + task, statusCode, errorCode, err := getTaskFromRequest(state, r) + if err != nil { + writeJSONResponse(w, statusCode, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr("", errorCode, err.Error()), nil), + updateTaskProtectionRequestType) + return + } + + if request.ProtectionEnabled == nil { + writeJSONResponse(w, http.StatusBadRequest, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeInvalidParameterException, + "Invalid request: does not contain 'ProtectionEnabled' field"), nil), + updateTaskProtectionRequestType) + return + } + + taskProtection := types.NewTaskProtection(*request.ProtectionEnabled, request.ExpiresInMinutes) + + logger.Info("UpdateTaskProtection endpoint was called", logger.Fields{ + loggerfield.Cluster: cluster, + loggerfield.TaskARN: task.Arn, + loggerfield.TaskProtection: taskProtection, + }) + + taskRoleCredential, ok := credentialsManager.GetTaskCredentials(task.GetCredentialsID()) + if !ok { + err = fmt.Errorf("Invalid Request: no task IAM role credentials available for task") + logger.Error(err.Error(), logger.Fields{ + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusForbidden, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeAccessDeniedException, err.Error()), nil), + updateTaskProtectionRequestType) + return + } + ecsClient := factory.newTaskProtectionClient(taskRoleCredential) + + ctx, cancel := context.WithTimeout(r.Context(), ecsCallTimeout) + defer cancel() + response, err := ecsClient.UpdateTaskProtectionWithContext(ctx, &ecs.UpdateTaskProtectionInput{ + Cluster: aws.String(cluster), + ExpiresInMinutes: taskProtection.GetExpiresInMinutes(), + ProtectionEnabled: aws.Bool(taskProtection.GetProtectionEnabled()), + Tasks: aws.StringSlice([]string{task.Arn}), + }) + + if err != nil { + errorCode, errorMsg, statusCode, reqId := getErrorCodeAndStatusCode(err) + var requestIdString = "" + if reqId != nil { + requestIdString = *reqId + } + logger.Error("Got an exception when calling UpdateTaskProtection.", logger.Fields{ + loggerfield.Error: err, + "ErrorCode": errorCode, + "ExceptionMessage": errorMsg, + "StatusCode": statusCode, + "RequestId": requestIdString, + }) + writeJSONResponse(w, statusCode, types.NewTaskProtectionResponseError(types.NewErrorResponsePtr(task.Arn, errorCode, errorMsg), reqId), + updateTaskProtectionRequestType) + return + } + + logger.Debug("updateTaskProtection response:", logger.Fields{ + loggerfield.TaskProtection: response.ProtectedTasks, + loggerfield.Reason: response.Failures, + }) + + // there are no exceptions but there are failures when setting protection in scheduler + if len(response.Failures) > 0 { + if len(response.Failures) > ExpectedProtectionResponseLength { + err := fmt.Errorf("expect at most %v failure in response, get %v", ExpectedProtectionResponseLength, len(response.Failures)) + logger.Error("Unexpected number of failures", logger.Fields{ + loggerfield.Error: err, + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusInternalServerError, types.NewTaskProtectionResponseError( + types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeServerException, "Unexpected error occurred"), nil), + updateTaskProtectionRequestType) + return + } + writeJSONResponse(w, http.StatusOK, types.NewTaskProtectionResponseFailure(response.Failures[0]), updateTaskProtectionRequestType) + return + } + if len(response.ProtectedTasks) > ExpectedProtectionResponseLength { + err := fmt.Errorf("expect %v protectedTask in response when no failure, get %v", ExpectedProtectionResponseLength, len(response.ProtectedTasks)) + logger.Error("Unexpected number of protections", logger.Fields{ + loggerfield.Error: err, + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusInternalServerError, types.NewTaskProtectionResponseError( + types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeServerException, "Unexpected error occurred"), nil), + updateTaskProtectionRequestType) + return + } + writeJSONResponse(w, http.StatusOK, types.NewTaskProtectionResponseProtection(response.ProtectedTasks[0]), updateTaskProtectionRequestType) + } +} + +// GetTaskProtectionHandler returns a handler function for GetTaskProtection API +func GetTaskProtectionHandler(state dockerstate.TaskEngineState, credentialsManager credentials.Manager, + factory TaskProtectionClientFactoryInterface, cluster string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + getTaskProtectionRequestType := "api/GetTaskProtection/v1" + + task, statusCode, errorCode, err := getTaskFromRequest(state, r) + if err != nil { + writeJSONResponse(w, statusCode, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr("", errorCode, err.Error()), nil), + getTaskProtectionRequestType) + return + } + + logger.Info("GetTaskProtection endpoint was called", logger.Fields{ + loggerfield.Cluster: cluster, + loggerfield.TaskARN: task.Arn, + }) + + taskRoleCredential, ok := credentialsManager.GetTaskCredentials(task.GetCredentialsID()) + if !ok { + err = fmt.Errorf("Invalid Request: no task IAM role credentials available for task") + logger.Error(err.Error(), logger.Fields{ + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusForbidden, + types.NewTaskProtectionResponseError(types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeAccessDeniedException, err.Error()), nil), + getTaskProtectionRequestType) + return + } + + ecsClient := factory.newTaskProtectionClient(taskRoleCredential) + + ctx, cancel := context.WithTimeout(r.Context(), ecsCallTimeout) + defer cancel() + response, err := ecsClient.GetTaskProtectionWithContext(ctx, &ecs.GetTaskProtectionInput{ + Cluster: aws.String(cluster), + Tasks: aws.StringSlice([]string{task.Arn}), + }) + + if err != nil { + errorCode, errorMsg, statusCode, reqId := getErrorCodeAndStatusCode(err) + var requestIdString = "" + if reqId != nil { + requestIdString = *reqId + } + logger.Error("Got an exception when calling GetTaskProtection.", logger.Fields{ + loggerfield.Error: err, + "ErrorCode": errorCode, + "ExceptionMessage": errorMsg, + "StatusCode": statusCode, + "RequestId": requestIdString, + }) + writeJSONResponse(w, statusCode, types.NewTaskProtectionResponseError(types.NewErrorResponsePtr(task.Arn, errorCode, errorMsg), reqId), + getTaskProtectionRequestType) + return + } + + logger.Debug("getTaskProtection response:", logger.Fields{ + loggerfield.TaskProtection: response.ProtectedTasks, + loggerfield.Reason: response.Failures, + }) + + // there are no exceptions but there are failures when getting protection in scheduler + if len(response.Failures) > 0 { + if len(response.Failures) > ExpectedProtectionResponseLength { + err := fmt.Errorf("expect at most %v failure in response, get %v", ExpectedProtectionResponseLength, len(response.Failures)) + logger.Error("Unexpected number of failures", logger.Fields{ + loggerfield.Error: err, + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusInternalServerError, types.NewTaskProtectionResponseError( + types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeServerException, "Unexpected error occurred"), nil), + getTaskProtectionRequestType) + return + } + writeJSONResponse(w, http.StatusOK, types.NewTaskProtectionResponseFailure(response.Failures[0]), getTaskProtectionRequestType) + return + } + + if len(response.ProtectedTasks) > ExpectedProtectionResponseLength { + err := fmt.Errorf("expect %v protectedTask in response when no failure, get %v", ExpectedProtectionResponseLength, len(response.ProtectedTasks)) + logger.Error("Unexpected number of protections", logger.Fields{ + loggerfield.Error: err, + loggerfield.TaskARN: task.Arn, + }) + writeJSONResponse(w, http.StatusInternalServerError, types.NewTaskProtectionResponseError( + types.NewErrorResponsePtr(task.Arn, ecs.ErrCodeServerException, "Unexpected error occurred"), nil), + getTaskProtectionRequestType) + return + } + writeJSONResponse(w, http.StatusOK, types.NewTaskProtectionResponseProtection(response.ProtectedTasks[0]), getTaskProtectionRequestType) + } +} + +// Helper function for retrieving credential from credentials manager and create ecs client +func (factory TaskProtectionClientFactory) newTaskProtectionClient(taskRoleCredential credentials.TaskIAMRoleCredentials) api.ECSTaskProtectionSDK { + taskCredential := taskRoleCredential.GetIAMRoleCredentials() + cfg := aws.NewConfig(). + WithCredentials(awscreds.NewStaticCredentials(taskCredential.AccessKeyID, + taskCredential.SecretAccessKey, + taskCredential.SessionToken)). + WithRegion(factory.Region). + WithHTTPClient(httpclient.New(ecsclient.RoundtripTimeout, factory.AcceptInsecureCert)). + WithEndpoint(factory.Endpoint) + + ecsClient := ecs.New(session.Must(session.NewSession()), cfg) + return ecsClient +} + +// Helper function to parse error to get ErrorCode, ExceptionMessage, HttpStatusCode, RequestID. +// RequestID will be empty if the request is not able to reach AWS +func getErrorCodeAndStatusCode(err error) (string, string, int, *string) { + msg := err.Error() + // The error is a Generic AWS Error with Code, Message, and original error (if any) + if awsErr, ok := err.(awserr.Error); ok { + // The error is an AWS service error occurred + msg = awsErr.Message() + if reqErr, ok := err.(awserr.RequestFailure); ok { + reqId := reqErr.RequestID() + return awsErr.Code(), msg, reqErr.StatusCode(), &reqId + } else if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode { + return aerr.Code(), ecsCallTimedOutError, http.StatusGatewayTimeout, nil + } else { + logger.Error(fmt.Sprintf("got an exception that does not implement RequestFailure interface but is an aws error. This should not happen, return statusCode 500 for whatever errorCode. Original err: %v.", err)) + return awsErr.Code(), msg, http.StatusInternalServerError, nil + } + } else { + logger.Error(fmt.Sprintf("non aws error received: %v", err)) + return ecs.ErrCodeServerException, msg, http.StatusInternalServerError, nil + } +} + +// Helper function for finding task for the request +func getTaskFromRequest(state dockerstate.TaskEngineState, r *http.Request) (*apitask.Task, int, string, error) { + taskARN, err := v3.GetTaskARNByRequest(r, state) + if err != nil { + logger.Error("Failed to find task ARN for task protection request", logger.Fields{ + loggerfield.Error: err, + }) + return nil, http.StatusNotFound, ecs.ErrCodeResourceNotFoundException, errors.New("Invalid request: no task was found") + } + + task, found := state.TaskByArn(taskARN) + if !found { + logger.Critical("No task was found for taskARN for task protection request", logger.Fields{ + loggerfield.TaskARN: taskARN, + }) + return nil, http.StatusInternalServerError, ecs.ErrCodeServerException, errors.New("Failed to find a task for the request") + } + + return task, http.StatusOK, "", nil +} + +// Writes the provided response to the ResponseWriter and handles any errors +func writeJSONResponse(w http.ResponseWriter, statusCode int, response types.TaskProtectionResponse, requestType string) { + bytes, err := json.Marshal(response) + if err != nil { + logger.Error("Agent API Task Protection V1: failed to marshal response as JSON", logger.Fields{ + "response": response, + loggerfield.Error: err, + }) + utils.WriteJSONToResponse(w, http.StatusInternalServerError, []byte(`{}`), + requestType) + } else { + utils.WriteJSONToResponse(w, statusCode, bytes, requestType) + } +} diff --git a/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_mocks.go b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_mocks.go new file mode 100644 index 00000000000..791525ce21c --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_mocks.go @@ -0,0 +1,64 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/handlers (interfaces: TaskProtectionClientFactoryInterface) + +// Package handlers is a generated GoMock package. +package handlers + +import ( + reflect "reflect" + + api "github.com/aws/amazon-ecs-agent/agent/api" + credentials "github.com/aws/amazon-ecs-agent/agent/credentials" + gomock "github.com/golang/mock/gomock" +) + +// MockTaskProtectionClientFactoryInterface is a mock of TaskProtectionClientFactoryInterface interface +type MockTaskProtectionClientFactoryInterface struct { + ctrl *gomock.Controller + recorder *MockTaskProtectionClientFactoryInterfaceMockRecorder +} + +// MockTaskProtectionClientFactoryInterfaceMockRecorder is the mock recorder for MockTaskProtectionClientFactoryInterface +type MockTaskProtectionClientFactoryInterfaceMockRecorder struct { + mock *MockTaskProtectionClientFactoryInterface +} + +// NewMockTaskProtectionClientFactoryInterface creates a new mock instance +func NewMockTaskProtectionClientFactoryInterface(ctrl *gomock.Controller) *MockTaskProtectionClientFactoryInterface { + mock := &MockTaskProtectionClientFactoryInterface{ctrl: ctrl} + mock.recorder = &MockTaskProtectionClientFactoryInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTaskProtectionClientFactoryInterface) EXPECT() *MockTaskProtectionClientFactoryInterfaceMockRecorder { + return m.recorder +} + +// newTaskProtectionClient mocks base method +func (m *MockTaskProtectionClientFactoryInterface) newTaskProtectionClient(arg0 credentials.TaskIAMRoleCredentials) api.ECSTaskProtectionSDK { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "newTaskProtectionClient", arg0) + ret0, _ := ret[0].(api.ECSTaskProtectionSDK) + return ret0 +} + +// newTaskProtectionClient indicates an expected call of newTaskProtectionClient +func (mr *MockTaskProtectionClientFactoryInterfaceMockRecorder) newTaskProtectionClient(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "newTaskProtectionClient", reflect.TypeOf((*MockTaskProtectionClientFactoryInterface)(nil).newTaskProtectionClient), arg0) +} diff --git a/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_test.go b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_test.go new file mode 100644 index 00000000000..32a8a087e7b --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/handlers/handlers_test.go @@ -0,0 +1,662 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/aws/amazon-ecs-agent/agent/api" + mock_api "github.com/aws/amazon-ecs-agent/agent/api/mocks" + "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/credentials" + mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" + "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" + mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" + "github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/types" + v3 "github.com/aws/amazon-ecs-agent/agent/handlers/v3" + "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +const ( + testAccessKey = "accessKey" + testSecretKey = "secretKey" + testSessionToken = "sessionToken" + testCluster = "cluster" + testRegion = "region" + testECSEndpoint = "endpoint" + testTaskCredentialsId = "taskCredentialsId" + testV3EndpointId = "endpointId" + testTaskArn = "taskArn" + testServiceName = "serviceName" + testAcceptInsecureCert = false + protectionEnabledFieldName = "ProtectionEnabled" + expiresInMinutesFieldName = "ExpiresInMinutes" + testExpiresInMinutes = 5 + testProtectionEnabled = true + testRequestID = "requestID" + testFailureReason = "failureReason" +) + +// Tests the path for UpdateTaskProtection API +func TestTaskProtectionPath(t *testing.T) { + assert.Equal(t, "/api/{v3EndpointIDMuxName:[^/]*}/task-protection/v1/state", TaskProtectionPath()) +} + +// TestGetECSClientHappyCase tests newTaskProtectionClient uses credential in credentials manager and +// returns an ECS client with correct status code and error +func TestGetECSClientHappyCase(t *testing.T) { + + testIAMRoleCredentials := credentials.TaskIAMRoleCredentials{ + IAMRoleCredentials: credentials.IAMRoleCredentials{ + AccessKeyID: testAccessKey, + SecretAccessKey: testSecretKey, + SessionToken: testSessionToken, + }, + } + + factory := TaskProtectionClientFactory{ + Region: testRegion, Endpoint: testECSEndpoint, AcceptInsecureCert: testAcceptInsecureCert, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ret := factory.newTaskProtectionClient(testIAMRoleCredentials) + _, ok := ret.(api.ECSTaskProtectionSDK) + + // Assert response + assert.True(t, ok) +} + +func getRequestWithUnknownFields(t *testing.T) map[string]interface{} { + request := TaskProtectionRequest{ProtectionEnabled: utils.BoolPtr(false)} + requestJSON, err := json.Marshal(request) + assert.NoError(t, err) + + var rawRequest map[string]interface{} + err = json.Unmarshal(requestJSON, &rawRequest) + assert.NoError(t, err) + rawRequest["UnknownField"] = 5 + return rawRequest +} + +// Helper function for running tests for UpdateTaskProtection handler +func testUpdateTaskProtectionHandler(t *testing.T, state dockerstate.TaskEngineState, + v3EndpointID string, credentialsManager credentials.Manager, factory TaskProtectionClientFactoryInterface, + request interface{}, expectedResponse interface{}, expectedResponseCode int) { + // Prepare request + requestBytes, err := json.Marshal(request) + assert.NoError(t, err) + bodyReader := bytes.NewReader(requestBytes) + req, err := http.NewRequest("PUT", "", bodyReader) + assert.NoError(t, err) + req = mux.SetURLVars(req, map[string]string{v3.V3EndpointIDMuxName: v3EndpointID}) + + // Call handler + rr := httptest.NewRecorder() + handler := http.HandlerFunc(UpdateTaskProtectionHandler(state, credentialsManager, factory, testCluster)) + handler.ServeHTTP(rr, req) + + expectedResponseJSON, err := json.Marshal(expectedResponse) + assert.NoError(t, err, "Expected response must be JSON encodable") + + // Assert response + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + assert.Equal(t, expectedResponseCode, rr.Code) + responseBody, err := io.ReadAll(rr.Body) + assert.NoError(t, err, "Failed to read response body") + assert.Equal(t, string(expectedResponseJSON), string(responseBody)) +} + +func generateRequestIdPtr() *string { + requestIdString := testRequestID + return &requestIdString +} + +// TestUpdateTaskProtectionHandler_InputValidationsDecodeError tests UpdateTaskProtection handler's +// behavior with different invalid inputs with decode error +func TestUpdateTaskProtectionHandler_InputValidationsDecodeError(t *testing.T) { + testCases := []struct { + name string + request interface{} + expectedError *types.ErrorResponse + }{ + { + name: "InvalidTypes", + request: &map[string]interface{}{ + protectionEnabledFieldName: true, + expiresInMinutesFieldName: "badType", + }, + expectedError: &types.ErrorResponse{Code: ecs.ErrCodeInvalidParameterException, Message: "UpdateTaskProtection: failed to decode request"}, + }, + { + name: "UnknownFieldsInRequest", + request: getRequestWithUnknownFields(t), + expectedError: &types.ErrorResponse{Code: ecs.ErrCodeInvalidParameterException, Message: "UpdateTaskProtection: failed to decode request"}, + }, + { + name: "InvalidJSONRequest", + request: "", + expectedError: &types.ErrorResponse{Code: ecs.ErrCodeInvalidParameterException, Message: "UpdateTaskProtection: failed to decode request"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + expectedResponse := types.TaskProtectionResponse{Error: tc.expectedError} + testUpdateTaskProtectionHandler(t, mock_dockerstate.NewMockTaskEngineState(ctrl), + testV3EndpointId, nil, nil, tc.request, expectedResponse, http.StatusBadRequest) + }) + } +} + +// TestUpdateTaskProtectionHandlerTaskARNNotFound tests UpdateTaskProtection handler's +// behavior when task ARN was not found for the request. +func TestUpdateTaskProtectionHandlerTaskARNNotFound(t *testing.T) { + request := TaskProtectionRequest{ProtectionEnabled: utils.BoolPtr(false)} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return("", false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Code: ecs.ErrCodeResourceNotFoundException, + Message: "Invalid request: no task was found", + }, + } + testUpdateTaskProtectionHandler(t, mockState, testV3EndpointId, nil, nil, request, + expectedResponse, http.StatusNotFound) +} + +// TestUpdateTaskProtectionHandlerTaskNotFound tests UpdateTaskProtection handler's +// behavior when task ARN was not found for the request. +func TestUpdateTaskProtectionHandlerTaskNotFound(t *testing.T) { + request := TaskProtectionRequest{ProtectionEnabled: utils.BoolPtr(false)} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(nil, false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Code: ecs.ErrCodeServerException, + Message: "Failed to find a task for the request", + }, + } + + testUpdateTaskProtectionHandler(t, mockState, testV3EndpointId, nil, nil, request, + expectedResponse, http.StatusInternalServerError) +} + +// TestUpdateTaskProtectionHandler_EmptyRequest tests UpdateTaskProtection handler's behavior with empty inputs +func TestUpdateTaskProtectionHandler_EmptyRequest(t *testing.T) { + expectedError := &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeInvalidParameterException, Message: "Invalid request: does not contain 'ProtectionEnabled' field"} + testTask := task.Task{ + Arn: testTaskArn, + ServiceName: testServiceName, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(&testTask, true) + expectedResponse := types.TaskProtectionResponse{Error: expectedError} + testUpdateTaskProtectionHandler(t, mockState, testV3EndpointId, nil, nil, + nil, expectedResponse, http.StatusBadRequest) +} + +// TestUpdateTaskProtectionHandlerTaskRoleCredentialsNotFound tests UpdateTaskProtection handler's +// behavior when task IAM role credential is not found for the request. +func TestUpdateTaskProtectionHandlerTaskRoleCredentialsNotFound(t *testing.T) { + request := TaskProtectionRequest{ + ProtectionEnabled: utils.BoolPtr(true), + } + + testTask := task.Task{ + Arn: testTaskArn, + ServiceName: testServiceName, + } + testTask.SetCredentialsID(testTaskCredentialsId) + + factory := TaskProtectionClientFactory{ + Region: testRegion, Endpoint: testECSEndpoint, AcceptInsecureCert: testAcceptInsecureCert, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockManager := mock_credentials.NewMockManager(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(&testTask, true) + mockManager.EXPECT().GetTaskCredentials(gomock.Eq(testTaskCredentialsId)).Return(credentials.TaskIAMRoleCredentials{}, false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Arn: testTaskArn, + Code: ecs.ErrCodeAccessDeniedException, + Message: "Invalid Request: no task IAM role credentials available for task", + }, + } + + testUpdateTaskProtectionHandler(t, mockState, testV3EndpointId, mockManager, factory, request, + expectedResponse, http.StatusForbidden) +} + +// TestUpdateTaskProtectionHandler_PostCall tests UpdateTaskProtection handler's +// behavior when request successfully reached ECS and get response +func TestUpdateTaskProtectionHandler_PostCall(t *testing.T) { + testCases := []struct { + name string + ecsError error + ecsResponse *ecs.UpdateTaskProtectionOutput + expectedProtection *ecs.ProtectedTask + expectedFailure *ecs.Failure + expectedError *types.ErrorResponse + expectedRequestId *string + expectedStatusCode int + time time.Time + }{ + { + name: "RequestFailure_ServerException", + ecsError: awserr.NewRequestFailure(awserr.New(ecs.ErrCodeServerException, "error message", nil), http.StatusInternalServerError, testRequestID), + ecsResponse: &ecs.UpdateTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeServerException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + expectedRequestId: generateRequestIdPtr(), + }, + { + name: "RequestFailure_OtherExceptions", + ecsError: awserr.NewRequestFailure(awserr.New(ecs.ErrCodeAccessDeniedException, "error message", nil), http.StatusBadRequest, testRequestID), + ecsResponse: &ecs.UpdateTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeAccessDeniedException, Message: "error message"}, + expectedStatusCode: http.StatusBadRequest, + expectedRequestId: generateRequestIdPtr(), + }, + { + name: "NonRequestFailureAwsError", + ecsError: awserr.New(ecs.ErrCodeInvalidParameterException, "error message", nil), + ecsResponse: &ecs.UpdateTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeInvalidParameterException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Agent timeout", + ecsError: awserr.New(request.CanceledErrorCode, "request cancelled", nil), + ecsResponse: &ecs.UpdateTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{ + Arn: testTaskArn, + Code: request.CanceledErrorCode, + Message: ecsCallTimedOutError, + }, + expectedStatusCode: http.StatusGatewayTimeout, + }, + { + name: "NonAwsError", + ecsError: fmt.Errorf("error message"), + ecsResponse: &ecs.UpdateTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeServerException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Failure", + ecsError: nil, + ecsResponse: &ecs.UpdateTaskProtectionOutput{ + Failures: []*ecs.Failure{{ + Arn: aws.String(testTaskArn), + Reason: aws.String(testFailureReason), + }}, + ProtectedTasks: []*ecs.ProtectedTask{}, + }, + expectedFailure: &ecs.Failure{ + Arn: aws.String(testTaskArn), + Reason: aws.String(testFailureReason), + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "SuccessProtected", + ecsError: nil, + ecsResponse: &ecs.UpdateTaskProtectionOutput{ + Failures: []*ecs.Failure{}, + ProtectedTasks: []*ecs.ProtectedTask{{ + ProtectionEnabled: aws.Bool(true), + ExpirationDate: aws.Time(time.UnixMilli(0)), + TaskArn: aws.String(testTaskArn), + }}, + }, + expectedProtection: &ecs.ProtectedTask{ + ProtectionEnabled: aws.Bool(true), + ExpirationDate: aws.Time(time.UnixMilli(0)), + TaskArn: aws.String(testTaskArn), + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "SuccessNotProtected", + ecsError: nil, + ecsResponse: &ecs.UpdateTaskProtectionOutput{ + Failures: []*ecs.Failure{}, + ProtectedTasks: []*ecs.ProtectedTask{{ + ProtectionEnabled: aws.Bool(false), + ExpirationDate: nil, + TaskArn: aws.String(testTaskArn), + }}, + }, + expectedProtection: &ecs.ProtectedTask{ + ProtectionEnabled: aws.Bool(false), + ExpirationDate: nil, + TaskArn: aws.String(testTaskArn), + }, + expectedStatusCode: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + request := TaskProtectionRequest{ + ProtectionEnabled: utils.BoolPtr(testProtectionEnabled), + ExpiresInMinutes: utils.Int64Ptr(testExpiresInMinutes), + } + + testTask := task.Task{ + Arn: testTaskArn, + ServiceName: testServiceName, + } + testTask.SetCredentialsID(testTaskCredentialsId) + + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockManager := mock_credentials.NewMockManager(ctrl) + mockFactory := NewMockTaskProtectionClientFactoryInterface(ctrl) + mockECSClient := mock_api.NewMockECSTaskProtectionSDK(ctrl) + + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(&testTask, true) + mockManager.EXPECT().GetTaskCredentials(gomock.Eq(testTaskCredentialsId)).Return(credentials.TaskIAMRoleCredentials{}, true) + mockFactory.EXPECT().newTaskProtectionClient(gomock.Eq(credentials.TaskIAMRoleCredentials{})).Return(mockECSClient) + mockECSClient.EXPECT(). + UpdateTaskProtectionWithContext(gomock.Any(), gomock.Any()). + Return(tc.ecsResponse, tc.ecsError) + + expectedResponse := types.TaskProtectionResponse{ + Protection: tc.expectedProtection, + Failure: tc.expectedFailure, + Error: tc.expectedError, + RequestID: tc.expectedRequestId, + } + + testUpdateTaskProtectionHandler(t, mockState, testV3EndpointId, mockManager, mockFactory, request, expectedResponse, tc.expectedStatusCode) + }) + } +} + +func testGetTaskProtectionHandler(t *testing.T, state dockerstate.TaskEngineState, + v3EndpointID string, credentialsManager credentials.Manager, factory TaskProtectionClientFactoryInterface, expectedResponse interface{}, expectedResponseCode int) { + // Prepare request + bodyReader := bytes.NewReader([]byte{}) + req, err := http.NewRequest("GET", "", bodyReader) + assert.NoError(t, err) + req = mux.SetURLVars(req, map[string]string{v3.V3EndpointIDMuxName: v3EndpointID}) + + // Call handler + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetTaskProtectionHandler(state, credentialsManager, factory, testCluster)) + handler.ServeHTTP(rr, req) + + expectedResponseJSON, err := json.Marshal(expectedResponse) + assert.NoError(t, err, "Expected response must be JSON encodable") + + // Assert response + assert.Equal(t, expectedResponseCode, rr.Code) + responseBody, err := io.ReadAll(rr.Body) + assert.NoError(t, err, "Failed to read response body") + assert.Equal(t, string(expectedResponseJSON), string(responseBody)) +} + +// TestGetTaskProtectionHandlerTaskARNNotFound tests GetTaskProtection handler's +// behavior when task ARN was not found for the request. +func TestGetTaskProtectionHandlerTaskARNNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return("", false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Code: ecs.ErrCodeResourceNotFoundException, + Message: "Invalid request: no task was found", + }, + } + testGetTaskProtectionHandler(t, mockState, testV3EndpointId, nil, nil, + expectedResponse, http.StatusNotFound) +} + +// TestGetTaskProtectionHandlerTaskNotFound tests GetTaskProtection handler's +// behavior when task ARN was not found for the request. +func TestGetTaskProtectionHandlerTaskNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(nil, false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Code: ecs.ErrCodeServerException, + Message: "Failed to find a task for the request", + }, + } + + testGetTaskProtectionHandler(t, mockState, testV3EndpointId, nil, nil, + expectedResponse, http.StatusInternalServerError) +} + +// TestGetTaskProtectionHandlerTaskRoleCredentialsNotFound tests GetTaskProtection handler's +// behavior when task IAM role credential is not found for the request. +func TestGetTaskProtectionHandlerTaskRoleCredentialsNotFound(t *testing.T) { + testTask := task.Task{ + Arn: testTaskArn, + ServiceName: testServiceName, + } + testTask.SetCredentialsID(testTaskCredentialsId) + + factory := TaskProtectionClientFactory{ + Region: testRegion, Endpoint: testECSEndpoint, AcceptInsecureCert: testAcceptInsecureCert, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockManager := mock_credentials.NewMockManager(ctrl) + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(&testTask, true) + mockManager.EXPECT().GetTaskCredentials(gomock.Eq(testTaskCredentialsId)).Return(credentials.TaskIAMRoleCredentials{}, false) + + expectedResponse := types.TaskProtectionResponse{ + Error: &types.ErrorResponse{ + Arn: testTaskArn, + Code: ecs.ErrCodeAccessDeniedException, + Message: "Invalid Request: no task IAM role credentials available for task", + }, + } + + testGetTaskProtectionHandler(t, mockState, testV3EndpointId, mockManager, factory, + expectedResponse, http.StatusForbidden) +} + +// TestGetTaskProtectionHandler_PostCall tests GetTaskProtection handler's +// behavior when request successfully reached ECS and get response +func TestGetTaskProtectionHandler_PostCall(t *testing.T) { + testCases := []struct { + name string + ecsError error + ecsResponse *ecs.GetTaskProtectionOutput + expectedProtection *ecs.ProtectedTask + expectedFailure *ecs.Failure + expectedError *types.ErrorResponse + expectedRequestId *string + expectedStatusCode int + time time.Time + }{ + { + name: "RequestFailure_ServerException", + ecsError: awserr.NewRequestFailure(awserr.New(ecs.ErrCodeServerException, "error message", nil), http.StatusInternalServerError, testRequestID), + ecsResponse: &ecs.GetTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeServerException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + expectedRequestId: generateRequestIdPtr(), + }, + { + name: "RequestFailure_OtherExceptions", + ecsError: awserr.NewRequestFailure(awserr.New(ecs.ErrCodeAccessDeniedException, "error message", nil), http.StatusBadRequest, testRequestID), + ecsResponse: &ecs.GetTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeAccessDeniedException, Message: "error message"}, + expectedStatusCode: http.StatusBadRequest, + expectedRequestId: generateRequestIdPtr(), + }, + { + name: "NonRequestFailureAwsError", + ecsError: awserr.New(ecs.ErrCodeInvalidParameterException, "error message", nil), + ecsResponse: &ecs.GetTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeInvalidParameterException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Agent timeout", + ecsError: awserr.New(request.CanceledErrorCode, "request cancelled", nil), + ecsResponse: &ecs.GetTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{ + Arn: testTaskArn, + Code: request.CanceledErrorCode, + Message: ecsCallTimedOutError, + }, + expectedStatusCode: http.StatusGatewayTimeout, + }, + { + name: "NonAwsError", + ecsError: fmt.Errorf("error message"), + ecsResponse: &ecs.GetTaskProtectionOutput{}, + expectedError: &types.ErrorResponse{Arn: testTaskArn, Code: ecs.ErrCodeServerException, Message: "error message"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Failure", + ecsError: nil, + ecsResponse: &ecs.GetTaskProtectionOutput{ + Failures: []*ecs.Failure{{ + Arn: aws.String(testTaskArn), + Reason: aws.String(testFailureReason), + }}, + ProtectedTasks: []*ecs.ProtectedTask{}, + }, + expectedFailure: &ecs.Failure{ + Arn: aws.String(testTaskArn), + Reason: aws.String(testFailureReason), + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "SuccessProtected", + ecsError: nil, + ecsResponse: &ecs.GetTaskProtectionOutput{ + Failures: []*ecs.Failure{}, + ProtectedTasks: []*ecs.ProtectedTask{{ + ProtectionEnabled: aws.Bool(true), + ExpirationDate: aws.Time(time.UnixMilli(0)), + TaskArn: aws.String(testTaskArn), + }}, + }, + expectedProtection: &ecs.ProtectedTask{ + ProtectionEnabled: aws.Bool(true), + ExpirationDate: aws.Time(time.UnixMilli(0)), + TaskArn: aws.String(testTaskArn), + }, + expectedStatusCode: http.StatusOK, + }, + { + name: "SuccessNotProtected", + ecsError: nil, + ecsResponse: &ecs.GetTaskProtectionOutput{ + Failures: []*ecs.Failure{}, + ProtectedTasks: []*ecs.ProtectedTask{{ + ProtectionEnabled: aws.Bool(false), + ExpirationDate: nil, + TaskArn: aws.String(testTaskArn), + }}, + }, + expectedProtection: &ecs.ProtectedTask{ + ProtectionEnabled: aws.Bool(false), + ExpirationDate: nil, + TaskArn: aws.String(testTaskArn), + }, + expectedStatusCode: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testTask := task.Task{ + Arn: testTaskArn, + ServiceName: testServiceName, + } + testTask.SetCredentialsID(testTaskCredentialsId) + + mockState := mock_dockerstate.NewMockTaskEngineState(ctrl) + mockManager := mock_credentials.NewMockManager(ctrl) + mockFactory := NewMockTaskProtectionClientFactoryInterface(ctrl) + mockECSClient := mock_api.NewMockECSTaskProtectionSDK(ctrl) + + mockState.EXPECT().TaskARNByV3EndpointID(gomock.Eq(testV3EndpointId)).Return(testTaskArn, true) + mockState.EXPECT().TaskByArn(gomock.Eq(testTaskArn)).Return(&testTask, true) + mockManager.EXPECT().GetTaskCredentials(gomock.Eq(testTaskCredentialsId)).Return(credentials.TaskIAMRoleCredentials{}, true) + mockFactory.EXPECT().newTaskProtectionClient(gomock.Eq(credentials.TaskIAMRoleCredentials{})).Return(mockECSClient) + mockECSClient.EXPECT(). + GetTaskProtectionWithContext(gomock.Any(), gomock.Any()). + Return(tc.ecsResponse, tc.ecsError) + + expectedResponse := types.TaskProtectionResponse{ + Protection: tc.expectedProtection, + Failure: tc.expectedFailure, + Error: tc.expectedError, + RequestID: tc.expectedRequestId, + } + + testGetTaskProtectionHandler(t, mockState, testV3EndpointId, mockManager, mockFactory, expectedResponse, tc.expectedStatusCode) + }) + } +} diff --git a/agent/handlers/agentapi/taskprotection/v1/handlers/interface.go b/agent/handlers/agentapi/taskprotection/v1/handlers/interface.go new file mode 100644 index 00000000000..067e06a95ab --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/handlers/interface.go @@ -0,0 +1,10 @@ +package handlers + +import ( + "github.com/aws/amazon-ecs-agent/agent/api" + "github.com/aws/amazon-ecs-agent/agent/credentials" +) + +type TaskProtectionClientFactoryInterface interface { + newTaskProtectionClient(taskRoleCredential credentials.TaskIAMRoleCredentials) api.ECSTaskProtectionSDK +} diff --git a/agent/handlers/agentapi/taskprotection/v1/types/types.go b/agent/handlers/agentapi/taskprotection/v1/types/types.go new file mode 100644 index 00000000000..5434e7cd73f --- /dev/null +++ b/agent/handlers/agentapi/taskprotection/v1/types/types.go @@ -0,0 +1,107 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package types + +import ( + "encoding/json" + "fmt" + + "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" +) + +// taskProtection is type of Protection for a Task +type taskProtection struct { + protectionEnabled bool + expiresInMinutes *int64 +} + +// MarshalJSON is custom JSON marshal function to marshal unexported fields for logging purposes +func (taskProtection *taskProtection) MarshalJSON() ([]byte, error) { + jsonBytes, err := json.Marshal(struct { + ProtectionEnabled bool + ExpiresInMinutes *int64 + }{ + ProtectionEnabled: taskProtection.protectionEnabled, + ExpiresInMinutes: taskProtection.expiresInMinutes, + }) + + if err != nil { + return nil, err + } + + return jsonBytes, nil +} + +// NewTaskProtection creates a taskProtection +func NewTaskProtection(protectionEnabled bool, expiresInMinutes *int64) *taskProtection { + return &taskProtection{ + protectionEnabled: protectionEnabled, + expiresInMinutes: expiresInMinutes, + } +} + +func (taskProtection *taskProtection) GetProtectionEnabled() bool { + return taskProtection.protectionEnabled +} + +func (taskProtection *taskProtection) GetExpiresInMinutes() *int64 { + return taskProtection.expiresInMinutes +} + +func (taskProtection *taskProtection) String() string { + jsonBytes, err := taskProtection.MarshalJSON() + if err != nil { + return fmt.Sprintf("failed to get string representation of taskProtection type: %v", err) + } + return string(jsonBytes) +} + +// TaskProtectionResponse is response type for all Update/GetTaskProtection requests +type TaskProtectionResponse struct { + RequestID *string `json:"requestID,omitempty"` + Protection *ecs.ProtectedTask `json:"protection,omitempty"` + Failure *ecs.Failure `json:"failure,omitempty"` + Error *ErrorResponse `json:"error,omitempty"` +} + +// NewTaskProtectionResponseProtection creates a TaskProtectionResponse when it is a successful response (has protection) +func NewTaskProtectionResponseProtection(protection *ecs.ProtectedTask) TaskProtectionResponse { + return TaskProtectionResponse{Protection: protection} +} + +// NewTaskProtectionResponseFailure creates a TaskProtectionResponse when there is a failed response with failure +func NewTaskProtectionResponseFailure(failure *ecs.Failure) TaskProtectionResponse { + return TaskProtectionResponse{Failure: failure} +} + +// NewTaskProtectionResponseError creates a TaskProtectionResponse when there is an error response with optional requestID +func NewTaskProtectionResponseError(error *ErrorResponse, requestID *string) TaskProtectionResponse { + return TaskProtectionResponse{RequestID: requestID, Error: error} +} + +// ErrorResponse is the type for all Update/GetTaskProtection request errors +type ErrorResponse struct { + Arn string `json:"Arn,omitempty"` + Code string + Message string +} + +// NewErrorResponsePtr creates a *ErrorResponse for Agent input validations failures and exceptions +func NewErrorResponsePtr(arn string, code string, message string) *ErrorResponse { + return &ErrorResponse{ + Arn: arn, + Code: code, + Message: message, + } +} diff --git a/agent/handlers/introspection_server_setup_test.go b/agent/handlers/introspection_server_setup_test.go index d8a90bbf5b9..e6d0b4c3dee 100644 --- a/agent/handlers/introspection_server_setup_test.go +++ b/agent/handlers/introspection_server_setup_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/task_server_setup.go b/agent/handlers/task_server_setup.go index f4bba9953c0..4aacc619f00 100644 --- a/agent/handlers/task_server_setup.go +++ b/agent/handlers/task_server_setup.go @@ -23,6 +23,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" + agentAPITaskProtectionV1 "github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/handlers" handlersutils "github.com/aws/amazon-ecs-agent/agent/handlers/utils" v1 "github.com/aws/amazon-ecs-agent/agent/handlers/v1" v2 "github.com/aws/amazon-ecs-agent/agent/handlers/v2" @@ -51,11 +52,15 @@ func taskServerSetup(credentialsManager credentials.Manager, state dockerstate.TaskEngineState, ecsClient api.ECSClient, cluster string, + region string, statsEngine stats.Engine, steadyStateRate int, burstRate int, availabilityZone string, - containerInstanceArn string) *http.Server { + vpcID string, + containerInstanceArn string, + apiEndpoint string, + acceptInsecureCert bool) *http.Server { muxRouter := mux.NewRouter() // Set this to false so that for request like "//v3//metadata/task" @@ -69,7 +74,9 @@ func taskServerSetup(credentialsManager credentials.Manager, v3HandlersSetup(muxRouter, state, ecsClient, statsEngine, cluster, availabilityZone, containerInstanceArn) - v4HandlersSetup(muxRouter, state, ecsClient, statsEngine, cluster, availabilityZone, containerInstanceArn) + v4HandlersSetup(muxRouter, state, ecsClient, statsEngine, cluster, availabilityZone, vpcID, containerInstanceArn) + + agentAPIV1HandlersSetup(muxRouter, state, credentialsManager, cluster, region, apiEndpoint, acceptInsecureCert) limiter := tollbooth.NewLimiter(int64(steadyStateRate), nil) limiter.SetOnLimitReached(handlersutils.LimitReachedHandler(auditLogger)) @@ -141,10 +148,11 @@ func v4HandlersSetup(muxRouter *mux.Router, statsEngine stats.Engine, cluster string, availabilityZone string, + vpcID string, containerInstanceArn string) { muxRouter.HandleFunc(v4.ContainerMetadataPath, v4.ContainerMetadataHandler(state)) - muxRouter.HandleFunc(v4.TaskMetadataPath, v4.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, false)) - muxRouter.HandleFunc(v4.TaskWithTagsMetadataPath, v4.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, containerInstanceArn, true)) + muxRouter.HandleFunc(v4.TaskMetadataPath, v4.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, vpcID, containerInstanceArn, false)) + muxRouter.HandleFunc(v4.TaskWithTagsMetadataPath, v4.TaskMetadataHandler(state, ecsClient, cluster, availabilityZone, vpcID, containerInstanceArn, true)) muxRouter.HandleFunc(v4.ContainerStatsPath, v4.ContainerStatsHandler(state, statsEngine)) muxRouter.HandleFunc(v4.TaskStatsPath, v4.TaskStatsHandler(state, statsEngine)) muxRouter.HandleFunc(v4.ContainerAssociationsPath, v4.ContainerAssociationsHandler(state)) @@ -152,7 +160,24 @@ func v4HandlersSetup(muxRouter *mux.Router, muxRouter.HandleFunc(v4.ContainerAssociationPath, v4.ContainerAssociationHandler(state)) } -// ServeTaskHTTPEndpoint serves task/container metadata, task/container stats, and IAM Role Credentials +// agentAPIV1HandlersSetup adds handlers for Agent API V1 +func agentAPIV1HandlersSetup(muxRouter *mux.Router, state dockerstate.TaskEngineState, credentialsManager credentials.Manager, cluster string, region string, endpoint string, acceptInsecureCert bool) { + factory := agentAPITaskProtectionV1.TaskProtectionClientFactory{ + Region: region, Endpoint: endpoint, AcceptInsecureCert: acceptInsecureCert, + } + muxRouter. + HandleFunc( + agentAPITaskProtectionV1.TaskProtectionPath(), + agentAPITaskProtectionV1.UpdateTaskProtectionHandler(state, credentialsManager, factory, cluster)). + Methods("PUT") + muxRouter. + HandleFunc( + agentAPITaskProtectionV1.TaskProtectionPath(), + agentAPITaskProtectionV1.GetTaskProtectionHandler(state, credentialsManager, factory, cluster)). + Methods("GET") +} + +// ServeTaskHTTPEndpoint serves task/container metadata, task/container stats, IAM Role Credentials, and Agent APIs // for tasks being managed by the agent. func ServeTaskHTTPEndpoint( ctx context.Context, @@ -162,7 +187,8 @@ func ServeTaskHTTPEndpoint( containerInstanceArn string, cfg *config.Config, statsEngine stats.Engine, - availabilityZone string) { + availabilityZone string, + vpcID string) { // Create and initialize the audit log logger, err := seelog.LoggerFromConfigAsString(audit.AuditLoggerConfig(cfg)) if err != nil { @@ -173,8 +199,9 @@ func ServeTaskHTTPEndpoint( auditLogger := audit.NewAuditLog(containerInstanceArn, cfg, logger) - server := taskServerSetup(credentialsManager, auditLogger, state, ecsClient, cfg.Cluster, statsEngine, - cfg.TaskMetadataSteadyStateRate, cfg.TaskMetadataBurstRate, availabilityZone, containerInstanceArn) + server := taskServerSetup(credentialsManager, auditLogger, state, ecsClient, cfg.Cluster, cfg.AWSRegion, statsEngine, + cfg.TaskMetadataSteadyStateRate, cfg.TaskMetadataBurstRate, availabilityZone, vpcID, containerInstanceArn, cfg.APIEndpoint, + cfg.AcceptInsecureCert) go func() { <-ctx.Done() diff --git a/agent/handlers/task_server_setup_test.go b/agent/handlers/task_server_setup_test.go index 46ae12d87d4..dfe59ffcc74 100644 --- a/agent/handlers/task_server_setup_test.go +++ b/agent/handlers/task_server_setup_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -19,6 +20,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/http/httptest" @@ -38,6 +40,7 @@ import ( mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" "github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs" mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" + task_protection_v1 "github.com/aws/amazon-ecs-agent/agent/handlers/agentapi/taskprotection/v1/handlers" "github.com/aws/amazon-ecs-agent/agent/handlers/utils" v1 "github.com/aws/amazon-ecs-agent/agent/handlers/v1" v2 "github.com/aws/amazon-ecs-agent/agent/handlers/v2" @@ -46,6 +49,7 @@ import ( mock_audit "github.com/aws/amazon-ecs-agent/agent/logger/audit/mocks" "github.com/aws/amazon-ecs-agent/agent/stats" mock_stats "github.com/aws/amazon-ecs-agent/agent/stats/mock" + agentutils "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/aws-sdk-go/aws" "github.com/docker/docker/api/types" "github.com/golang/mock/gomock" @@ -85,6 +89,7 @@ const ( v4BasePath = "/v4/" v3EndpointID = "v3eid" availabilityzone = "us-west-2b" + vpcID = "test-vpc-id" containerInstanceArn = "containerInstanceArn-test" associationType = "elastic-inference" associationName = "dev1" @@ -97,6 +102,9 @@ const ( macAddress = "06:96:9a:ce:a6:ce" privateDNSName = "ip-172-31-47-69.us-west-2.compute.internal" subnetGatewayIpv4Address = "172.31.32.1/20" + region = "us-west-2" + endpoint = "ecsEndpoint" + acceptInsecureCert = true ) var ( @@ -126,6 +134,7 @@ var ( Version: version, DesiredStatusUnsafe: apitaskstatus.TaskRunning, KnownStatusUnsafe: apitaskstatus.TaskRunning, + NetworkMode: apitask.AWSVPCNetworkMode, ENIs: []*apieni.ENI{ { IPV4Addresses: []*apieni.ENIIPV4Address{ @@ -152,6 +161,7 @@ var ( Version: version, DesiredStatusUnsafe: apitaskstatus.TaskRunning, KnownStatusUnsafe: apitaskstatus.TaskStatusNone, + NetworkMode: apitask.AWSVPCNetworkMode, ENIs: []*apieni.ENI{ { IPV4Addresses: []*apieni.ENIIPV4Address{ @@ -446,6 +456,7 @@ var ( LaunchType: "EC2", }, Containers: []v4.ContainerResponse{expectedV4ContainerResponse}, + VPCID: vpcID, } expectedV4PulledTaskResponse = v4.TaskResponse{ TaskResponse: &v2.TaskResponse{ @@ -467,6 +478,7 @@ var ( LaunchType: "EC2", }, Containers: []v4.ContainerResponse{expectedV4ContainerResponse, expectedV4PulledContainerResponse}, + VPCID: vpcID, } expectedV4BridgeContainerResponse = v4.ContainerResponse{ ContainerResponse: &expectedBridgeContainerResponse, @@ -504,6 +516,7 @@ var ( LaunchType: "EC2", }, Containers: []v4.ContainerResponse{expectedV4BridgeContainerResponse}, + VPCID: vpcID, } ) @@ -650,8 +663,9 @@ func testErrorResponsesFromServer(t *testing.T, path string, expectedErrorMessag credentialsManager := mock_credentials.NewMockManager(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", nil, config.DefaultTaskMetadataSteadyStateRate, - config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", "", nil, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, "", true) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", path, nil) @@ -685,8 +699,9 @@ func getResponseForCredentialsRequest(t *testing.T, expectedStatus int, credentialsManager := mock_credentials.NewMockManager(ctrl) auditLog := mock_audit.NewMockAuditLogger(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", nil, config.DefaultTaskMetadataSteadyStateRate, - config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentialsManager, auditLog, nil, ecsClient, "", "", nil, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, "", true) recorder := httptest.NewRecorder() creds, ok := getCredentials() @@ -753,8 +768,9 @@ func TestV2TaskMetadata(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(task, true), state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", tc.path, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -838,8 +854,9 @@ func TestV2TaskWithTagsMetadata(t *testing.T) { }, }, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v2BaseMetadataWithTagsPath, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -869,8 +886,9 @@ func TestV2ContainerMetadata(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByID(containerID).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v2BaseMetadataPath+"/"+containerID, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -899,8 +917,9 @@ func TestV2ContainerStats(t *testing.T) { state.EXPECT().GetTaskByIPAddress(remoteIP).Return(taskARN, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v2BaseStatsPath+"/"+containerID, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -948,8 +967,9 @@ func TestV2TaskStats(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerMap, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", tc.path, nil) req.RemoteAddr = remoteIP + ":" + remotePort @@ -982,8 +1002,9 @@ func TestV3TaskMetadata(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToDockerContainer, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1012,8 +1033,9 @@ func TestV3BridgeTaskMetadata(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1041,8 +1063,9 @@ func TestV3BridgeContainerMetadata(t *testing.T) { state.EXPECT().TaskByID(containerID).Return(bridgeTask, true), state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID, nil) server.Handler.ServeHTTP(recorder, req) @@ -1112,8 +1135,9 @@ func TestV3TaskMetadataWithTags(t *testing.T) { }, nil), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/taskWithTags", nil) server.Handler.ServeHTTP(recorder, req) @@ -1140,8 +1164,9 @@ func TestV3ContainerMetadata(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByID(containerID).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID, nil) server.Handler.ServeHTTP(recorder, req) @@ -1177,8 +1202,9 @@ func TestV3TaskStats(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerMap, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/task/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -1210,8 +1236,9 @@ func TestV3ContainerStats(t *testing.T) { state.EXPECT().DockerIDByV3EndpointID(v3EndpointID).Return(containerID, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -1239,8 +1266,9 @@ func TestV3ContainerAssociations(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/associations/"+associationType, nil) server.Handler.ServeHTTP(recorder, req) @@ -1267,8 +1295,9 @@ func TestV3ContainerAssociation(t *testing.T) { state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v3BasePath+v3EndpointID+"/associations/"+associationType+"/"+associationName, nil) server.Handler.ServeHTTP(recorder, req) @@ -1295,8 +1324,9 @@ func TestV4TaskMetadata(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(task, true).AnyTimes(), state.EXPECT().PulledContainerMapByArn(taskARN).Return(nil, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1328,8 +1358,9 @@ func TestV4TaskMetadataWithPulledContainers(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(pulledTask, true).AnyTimes(), state.EXPECT().PulledContainerMapByArn(taskARN).Return(pulledContainerNameToDockerContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1358,8 +1389,9 @@ func TestV4ContainerMetadata(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByID(containerID).Return(task, true).Times(2), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "us-west-2b", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID, nil) server.Handler.ServeHTTP(recorder, req) @@ -1437,8 +1469,9 @@ func TestV4TaskMetadataWithTags(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(task, true).AnyTimes(), state.EXPECT().PulledContainerMapByArn(taskARN).Return(nil, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/taskWithTags", nil) server.Handler.ServeHTTP(recorder, req) @@ -1466,14 +1499,15 @@ func TestV4BridgeTaskMetadata(t *testing.T) { gomock.InOrder( state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), - state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToBridgeContainer, true), state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), + state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToBridgeContainer, true), state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), state.EXPECT().PulledContainerMapByArn(taskARN).Return(nil, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1501,14 +1535,15 @@ func TestV4BridgeTaskMetadataAllowMissingContainerNetwork(t *testing.T) { gomock.InOrder( state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), - state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToBridgeContainer, true), state.EXPECT().TaskByArn(taskARN).Return(bridgeTask, true), + state.EXPECT().ContainerMapByArn(taskARN).Return(containerNameToBridgeContainer, true), state.EXPECT().ContainerByID(containerID).Return(nil, false), state.EXPECT().PulledContainerMapByArn(taskARN).Return(nil, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, availabilityzone, vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/task", nil) server.Handler.ServeHTTP(recorder, req) @@ -1536,8 +1571,9 @@ func TestV4BridgeContainerMetadata(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(bridgeContainer, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID, nil) server.Handler.ServeHTTP(recorder, req) @@ -1575,8 +1611,9 @@ func TestV4TaskStats(t *testing.T) { state.EXPECT().ContainerMapByArn(taskARN).Return(containerMap, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/task/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -1608,8 +1645,9 @@ func TestV4ContainerStats(t *testing.T) { state.EXPECT().DockerIDByV3EndpointID(v3EndpointID).Return(containerID, true), statsEngine.EXPECT().ContainerDockerStats(taskARN, containerID).Return(dockerStats, &stats.NetworkStatsPerSec{}, nil), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/stats", nil) server.Handler.ServeHTTP(recorder, req) @@ -1637,8 +1675,9 @@ func TestV4ContainerAssociations(t *testing.T) { state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/associations/"+associationType, nil) server.Handler.ServeHTTP(recorder, req) @@ -1665,7 +1704,9 @@ func TestV4ContainerAssociation(t *testing.T) { state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) recorder := httptest.NewRecorder() req, _ := http.NewRequest("GET", v4BasePath+v3EndpointID+"/associations/"+associationType+"/"+associationName, nil) server.Handler.ServeHTTP(recorder, req) @@ -1689,8 +1730,9 @@ func TestTaskHTTPEndpoint301Redirect(t *testing.T) { statsEngine := mock_stats.NewMockEngine(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) for testPath, expectedPath := range testPathsMap { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { @@ -1730,8 +1772,9 @@ func TestTaskHTTPEndpointErrorCode404(t *testing.T) { statsEngine := mock_stats.NewMockEngine(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) for _, testPath := range testPaths { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { @@ -1770,8 +1813,9 @@ func TestTaskHTTPEndpointErrorCode400(t *testing.T) { statsEngine := mock_stats.NewMockEngine(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) for _, testPath := range testPaths { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { @@ -1812,8 +1856,9 @@ func TestTaskHTTPEndpointErrorCode500(t *testing.T) { statsEngine := mock_stats.NewMockEngine(ctrl) ecsClient := mock_api.NewMockECSClient(ctrl) - server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, statsEngine, - config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", containerInstanceArn) + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) for _, testPath := range testPaths { t.Run(fmt.Sprintf("Test path: %s", testPath), func(t *testing.T) { @@ -1831,3 +1876,55 @@ func TestTaskHTTPEndpointErrorCode500(t *testing.T) { }) } } + +// Helper function for testing Agent API Task Protection v1 handlers +func testAgentAPITaskProtectionV1Handler(t *testing.T, requestBody interface{}, method string) { + // Prepare dependency mocks + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + state := mock_dockerstate.NewMockTaskEngineState(ctrl) + auditLog := mock_audit.NewMockAuditLogger(ctrl) + statsEngine := mock_stats.NewMockEngine(ctrl) + ecsClient := mock_api.NewMockECSClient(ctrl) + + gomock.InOrder( + state.EXPECT().TaskARNByV3EndpointID(v3EndpointID).Return(taskARN, true), + state.EXPECT().TaskByArn(taskARN).Return(task, true), + ) + + // Set up the server + server := taskServerSetup(credentials.NewManager(), auditLog, state, ecsClient, clusterName, region, statsEngine, + config.DefaultTaskMetadataSteadyStateRate, config.DefaultTaskMetadataBurstRate, "", vpcID, + containerInstanceArn, endpoint, acceptInsecureCert) + + // Prepare the request + var requestReader io.Reader = nil + if requestBody != nil { + requestBodyJSON, err := json.Marshal(requestBody) + assert.NoError(t, err) + requestReader = bytes.NewReader(requestBodyJSON) + } + + // Send request and record response + recorder := httptest.NewRecorder() + req, _ := http.NewRequest(method, fmt.Sprintf("/api/%s/task-protection/v1/state", v3EndpointID), + requestReader) + server.Handler.ServeHTTP(recorder, req) + + // assert that there is response + assert.NotNil(t, recorder.Body) +} + +// Tests that Agent API v1 GetTaskProtection handler is registered correctly +func TestAgentAPIV1GetTaskProtectionHandler(t *testing.T) { + testAgentAPITaskProtectionV1Handler(t, nil, "GET") +} + +// Tests that Agent API v1 UpdateTaskProtection handler is registered correctly +func TestAgentAPIV1UpdateTaskProtectionHandler(t *testing.T) { + requestBody := task_protection_v1.TaskProtectionRequest{ + ProtectionEnabled: agentutils.BoolPtr(false), + } + testAgentAPITaskProtectionV1Handler(t, requestBody, "PUT") +} diff --git a/agent/handlers/utils/helpers_test.go b/agent/handlers/utils/helpers_test.go index 1b691a8297b..7be7c9c5d0d 100644 --- a/agent/handlers/utils/helpers_test.go +++ b/agent/handlers/utils/helpers_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/v1/license_handler_test.go b/agent/handlers/v1/license_handler_test.go index 51c7b94e373..13812bd9cb0 100644 --- a/agent/handlers/v1/license_handler_test.go +++ b/agent/handlers/v1/license_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/v1/response.go b/agent/handlers/v1/response.go index 069cf400159..2883e817fb1 100644 --- a/agent/handlers/v1/response.go +++ b/agent/handlers/v1/response.go @@ -175,7 +175,7 @@ func NewVolumesResponse(dockerContainer *apicontainer.DockerContainer) []VolumeR // NewTasksResponse creates TasksResponse for all the tasks. func NewTasksResponse(state dockerstate.TaskEngineState) *TasksResponse { - allTasks := state.AllTasks() + allTasks := state.AllExternalTasks() taskResponses := make([]*TaskResponse, len(allTasks)) for ndx, task := range allTasks { containerMap, _ := state.ContainerMapByArn(task.Arn) diff --git a/agent/handlers/v1/response_test.go b/agent/handlers/v1/response_test.go index 34bfff87431..e18301aeecc 100644 --- a/agent/handlers/v1/response_test.go +++ b/agent/handlers/v1/response_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -23,6 +24,7 @@ import ( apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" ) diff --git a/agent/handlers/v2/response_test.go b/agent/handlers/v2/response_test.go index 798e51356f5..19d89f2c573 100644 --- a/agent/handlers/v2/response_test.go +++ b/agent/handlers/v2/response_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/v2/stats_response_test.go b/agent/handlers/v2/stats_response_test.go index b3ca475b097..b1d2635dbc8 100644 --- a/agent/handlers/v2/stats_response_test.go +++ b/agent/handlers/v2/stats_response_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/v3/response_test.go b/agent/handlers/v3/response_test.go index 3039a57aff5..38d5b27845b 100644 --- a/agent/handlers/v3/response_test.go +++ b/agent/handlers/v3/response_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/handlers/v4/response.go b/agent/handlers/v4/response.go index 88a261a5f21..6c5398f9884 100644 --- a/agent/handlers/v4/response.go +++ b/agent/handlers/v4/response.go @@ -29,7 +29,9 @@ import ( // with the v2 task response object. type TaskResponse struct { *v2.TaskResponse - Containers []ContainerResponse `json:"Containers,omitempty"` + Containers []ContainerResponse `json:"Containers,omitempty"` + VPCID string `json:"VPCID,omitempty"` + ServiceName string `json:"ServiceName,omitempty"` } // ContainerResponse is the v4 Container response. It augments the v4 Network response @@ -82,7 +84,9 @@ func NewTaskResponse( ecsClient api.ECSClient, cluster string, az string, + vpcID string, containerInstanceARN string, + serviceName string, propagateTags bool, ) (*TaskResponse, error) { // Construct the v2 response first. @@ -109,6 +113,8 @@ func NewTaskResponse( return &TaskResponse{ TaskResponse: v2Resp, Containers: containers, + VPCID: vpcID, + ServiceName: serviceName, }, nil } diff --git a/agent/handlers/v4/response_test.go b/agent/handlers/v4/response_test.go index e99b52435df..0423d910423 100644 --- a/agent/handlers/v4/response_test.go +++ b/agent/handlers/v4/response_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -27,6 +28,7 @@ import ( apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" mock_dockerstate "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate/mocks" + "github.com/docker/docker/api/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -38,6 +40,7 @@ const ( cluster = "default" family = "sleep" version = "1" + serviceName = "someService" containerID = "cid" containerName = "sleepy" imageName = "busybox" @@ -53,6 +56,7 @@ const ( volSource = "/var/lib/volume1" volDestination = "/volume" availabilityZone = "us-west-2b" + vpcID = "test-vpc-id" containerInstanceArn = "containerInstance-test" ) @@ -67,6 +71,7 @@ func TestNewTaskContainerResponses(t *testing.T) { Arn: taskARN, Family: family, Version: version, + ServiceName: serviceName, DesiredStatusUnsafe: apitaskstatus.TaskRunning, KnownStatusUnsafe: apitaskstatus.TaskRunning, ENIs: []*apieni.ENI{ @@ -133,7 +138,8 @@ func TestNewTaskContainerResponses(t *testing.T) { state.EXPECT().TaskByArn(taskARN).Return(task, true), ) - taskResponse, err := NewTaskResponse(taskARN, state, ecsClient, cluster, availabilityZone, containerInstanceArn, false) + taskResponse, err := NewTaskResponse(taskARN, state, ecsClient, cluster, + availabilityZone, vpcID, containerInstanceArn, task.ServiceName, false) require.NoError(t, err) _, err = json.Marshal(taskResponse) require.NoError(t, err) @@ -142,6 +148,7 @@ func TestNewTaskContainerResponses(t *testing.T) { assert.Equal(t, eniIPv6Address, taskResponse.Containers[0].Networks[0].IPv6Addresses[0]) assert.Equal(t, ipv6SubnetCIDRBlock, taskResponse.Containers[0].Networks[0].IPv6SubnetCIDRBlock) assert.Equal(t, subnetGatewayIPV4Address, taskResponse.Containers[0].Networks[0].SubnetGatewayIPV4Address) + assert.Equal(t, serviceName, taskResponse.ServiceName) gomock.InOrder( state.EXPECT().ContainerByID(containerID).Return(dockerContainer, true), diff --git a/agent/handlers/v4/task_metadata_handler.go b/agent/handlers/v4/task_metadata_handler.go index a315d8ac361..332a36cc0a1 100644 --- a/agent/handlers/v4/task_metadata_handler.go +++ b/agent/handlers/v4/task_metadata_handler.go @@ -33,7 +33,7 @@ var TaskMetadataPath = "/v4/" + utils.ConstructMuxVar(v3.V3EndpointIDMuxName, ut var TaskWithTagsMetadataPath = "/v4/" + utils.ConstructMuxVar(v3.V3EndpointIDMuxName, utils.AnythingButSlashRegEx) + "/taskWithTags" // TaskMetadataHandler returns the handler method for handling task metadata requests. -func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSClient, cluster, az, containerInstanceArn string, propagateTags bool) func(http.ResponseWriter, *http.Request) { +func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSClient, cluster, az, vpcID, containerInstanceArn string, propagateTags bool) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var taskArn, err = v3.GetTaskARNByRequest(r, state) if err != nil { @@ -45,9 +45,12 @@ func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSCli return } + task, _ := state.TaskByArn(taskArn) + seelog.Infof("V4 taskMetadata handler: Writing response for task '%s'", taskArn) - taskResponse, err := NewTaskResponse(taskArn, state, ecsClient, cluster, az, containerInstanceArn, propagateTags) + taskResponse, err := NewTaskResponse(taskArn, state, ecsClient, cluster, + az, vpcID, containerInstanceArn, task.ServiceName, propagateTags) if err != nil { errResponseJson, err := json.Marshal("Unable to generate metadata for v4 task: '" + taskArn + "'") if e := utils.WriteResponseIfMarshalError(w, err); e != nil { @@ -56,8 +59,6 @@ func TaskMetadataHandler(state dockerstate.TaskEngineState, ecsClient api.ECSCli utils.WriteJSONToResponse(w, http.StatusInternalServerError, errResponseJson, utils.RequestTypeTaskMetadata) return } - - task, _ := state.TaskByArn(taskArn) // for non-awsvpc task mode if !task.IsNetworkModeAWSVPC() { // fill in non-awsvpc network details for container responses here diff --git a/agent/handlers/v4/task_stats_handler.go b/agent/handlers/v4/task_stats_handler.go index 6536bf9e5db..257eae71f8c 100644 --- a/agent/handlers/v4/task_stats_handler.go +++ b/agent/handlers/v4/task_stats_handler.go @@ -38,7 +38,6 @@ func TaskStatsHandler(state dockerstate.TaskEngineState, statsEngine stats.Engin utils.WriteJSONToResponse(w, http.StatusBadRequest, errResponseJSON, utils.RequestTypeTaskStats) return } - seelog.Infof("V4 tasks stats handler: writing response for task '%s'", taskArn) WriteV4TaskStatsResponse(w, taskArn, state, statsEngine) } } @@ -64,6 +63,5 @@ func WriteV4TaskStatsResponse(w http.ResponseWriter, if e := utils.WriteResponseIfMarshalError(w, err); e != nil { return } - seelog.Infof("V4 Stats response json is %v", responseJSON) utils.WriteJSONToResponse(w, http.StatusOK, responseJSON, utils.RequestTypeTaskStats) } diff --git a/agent/httpclient/httpclient_test.go b/agent/httpclient/httpclient_test.go index 362722da25c..e8967718c28 100644 --- a/agent/httpclient/httpclient_test.go +++ b/agent/httpclient/httpclient_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/audit/audit_log_test.go b/agent/logger/audit/audit_log_test.go index f2a6b53045d..c88ea4ee643 100644 --- a/agent/logger/audit/audit_log_test.go +++ b/agent/logger/audit/audit_log_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/buffer_pool_test.go b/agent/logger/buffer_pool_test.go index 9cca77e8e66..9eeb97971b2 100644 --- a/agent/logger/buffer_pool_test.go +++ b/agent/logger/buffer_pool_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/eventlog_windows.go b/agent/logger/eventlog_windows.go index f920da0c26f..f72539c1271 100644 --- a/agent/logger/eventlog_windows.go +++ b/agent/logger/eventlog_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/field/constants.go b/agent/logger/field/constants.go index 18b79d38a99..fbfd713ffe9 100644 --- a/agent/logger/field/constants.go +++ b/agent/logger/field/constants.go @@ -14,25 +14,29 @@ package field const ( - TaskID = "task" - TaskARN = "taskARN" - Container = "container" - DockerId = "dockerId" - ManagedAgent = "managedAgent" - KnownStatus = "knownStatus" - KnownSent = "knownSent" - DesiredStatus = "desiredStatus" - SentStatus = "sentStatus" - FailedStatus = "failedStatus" - Sequence = "seqnum" - Reason = "reason" - Status = "status" - RuntimeID = "runtimeID" - Elapsed = "elapsed" - Resource = "resource" - Error = "error" - Event = "event" - Image = "image" - Volume = "volume" - Time = "time" + TaskID = "task" + TaskARN = "taskARN" + Container = "container" + DockerId = "dockerId" + ManagedAgent = "managedAgent" + KnownStatus = "knownStatus" + KnownSent = "knownSent" + DesiredStatus = "desiredStatus" + SentStatus = "sentStatus" + FailedStatus = "failedStatus" + Sequence = "seqnum" + Reason = "reason" + Status = "status" + RuntimeID = "runtimeID" + Elapsed = "elapsed" + Resource = "resource" + Error = "error" + Event = "event" + Image = "image" + Volume = "volume" + Time = "time" + NetworkMode = "networkMode" + Cluster = "cluster" + ServiceName = "ServiceName" + TaskProtection = "TaskProtection" ) diff --git a/agent/logger/format_test.go b/agent/logger/format_test.go index 80e43d58d3a..91445da5f1b 100644 --- a/agent/logger/format_test.go +++ b/agent/logger/format_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/global_test.go b/agent/logger/global_test.go index 032a9691afe..05435114080 100644 --- a/agent/logger/global_test.go +++ b/agent/logger/global_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/log_init_test.go b/agent/logger/log_init_test.go index 0148f1dbd09..496c1b07757 100644 --- a/agent/logger/log_init_test.go +++ b/agent/logger/log_init_test.go @@ -1,4 +1,5 @@ //go:build unit || integration +// +build unit integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/log_test.go b/agent/logger/log_test.go index 61e6747a1f6..8d25cd434ea 100644 --- a/agent/logger/log_test.go +++ b/agent/logger/log_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/log_unix_test.go b/agent/logger/log_unix_test.go index 64be35cff4a..0b063d1837b 100644 --- a/agent/logger/log_unix_test.go +++ b/agent/logger/log_unix_test.go @@ -1,4 +1,5 @@ //go:build !windows && unit +// +build !windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/log_windows_test.go b/agent/logger/log_windows_test.go index 9f2fe12ce78..c763d595ac8 100644 --- a/agent/logger/log_windows_test.go +++ b/agent/logger/log_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/platform_unix.go b/agent/logger/platform_unix.go index 8b8ef812649..85cb4385906 100644 --- a/agent/logger/platform_unix.go +++ b/agent/logger/platform_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/logger/structured_logger_test.go b/agent/logger/structured_logger_test.go index d51c2a76178..94e1bd8dd3c 100644 --- a/agent/logger/structured_logger_test.go +++ b/agent/logger/structured_logger_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/metrics/generic_metrics_client.go b/agent/metrics/generic_metrics_client.go index 7aab5a633b8..d206f34b08c 100644 --- a/agent/metrics/generic_metrics_client.go +++ b/agent/metrics/generic_metrics_client.go @@ -31,10 +31,11 @@ const ( ) // A GenericMetricsClient records 3 metrics: -// 1) A Prometheus summary vector representing call durations for different API calls -// 2) A durations guage vector that updates the last recorded duration for the API call -// allowing for a time series view in the Prometheus browser -// 3) A counter vector that increments call counts for each API call +// 1. A Prometheus summary vector representing call durations for different API calls +// 2. A durations guage vector that updates the last recorded duration for the API call +// allowing for a time series view in the Prometheus browser +// 3. A counter vector that increments call counts for each API call +// // The outstandingCalls map allows Fired CallStarts to be matched with Fired CallEnds type GenericMetrics struct { durationVec *prometheus.SummaryVec diff --git a/agent/metrics/metrics_test.go b/agent/metrics/metrics_test.go index 6f454da97f2..f70eb8cb1ce 100644 --- a/agent/metrics/metrics_test.go +++ b/agent/metrics/metrics_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -161,7 +162,7 @@ func TestMetricCollection(t *testing.T) { // A type for storing a Tree-based map. We map the MetricName to a map of metrics // under that name. This second map indexes by MetricLabelName+MetricLabelValue to // a slice MetricType and MetricValue. -//MetricName:metricLabelName+metricLabelValue:[metricType, metricValue] +// MetricName:metricLabelName+metricLabelValue:[metricType, metricValue] type metricMap map[string]map[string][]interface{} // In order to verify the MetricFamily with the expected metric values, we do a simple diff --git a/agent/s3/factory/factory.go b/agent/s3/factory/factory.go index b6ae683860c..3c7f42670c9 100644 --- a/agent/s3/factory/factory.go +++ b/agent/s3/factory/factory.go @@ -33,35 +33,48 @@ const ( ) type S3ClientCreator interface { - NewS3ClientForBucket(bucket, region string, creds credentials.IAMRoleCredentials) (s3client.S3Client, error) + NewS3ManagerClient(bucket, region string, creds credentials.IAMRoleCredentials) (s3client.S3ManagerClient, error) + NewS3Client(region string, creds credentials.IAMRoleCredentials) s3client.S3Client } +// NewS3ClientCreator provide 2 implementations +// NewS3ManagerClient implements methods from aws-sdk-go/service/s3manager. +// NewS3Client implements methods from aws-sdk-go/service/s3. func NewS3ClientCreator() S3ClientCreator { return &s3ClientCreator{} } type s3ClientCreator struct{} -// NewS3Client returns a new S3 client based on the region of the bucket. -func (*s3ClientCreator) NewS3ClientForBucket(bucket, region string, - creds credentials.IAMRoleCredentials) (s3client.S3Client, error) { +// NewS3ManagerClient returns a new S3 client based on the region of the bucket. +func (*s3ClientCreator) NewS3ManagerClient(bucket, region string, + creds credentials.IAMRoleCredentials) (s3client.S3ManagerClient, error) { cfg := aws.NewConfig(). WithHTTPClient(httpclient.New(roundtripTimeout, false)). WithCredentials( awscreds.NewStaticCredentials(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken)).WithRegion(region) sess := session.Must(session.NewSession(cfg)) - svc := s3.New(sess) bucketRegion, err := getRegionFromBucket(svc, bucket) if err != nil { return nil, err } - sessWithRegion := session.Must(session.NewSession(cfg.WithRegion(bucketRegion))) return s3manager.NewDownloaderWithClient(s3.New(sessWithRegion)), nil } +// NewS3Client returns a new S3 client to support s3 operations which are not provided by s3manager. +func (*s3ClientCreator) NewS3Client(region string, + creds credentials.IAMRoleCredentials) s3client.S3Client { + cfg := aws.NewConfig(). + WithHTTPClient(httpclient.New(roundtripTimeout, false)). + WithCredentials( + awscreds.NewStaticCredentials(creds.AccessKeyID, creds.SecretAccessKey, + creds.SessionToken)).WithRegion(region) + sess := session.Must(session.NewSession(cfg)) + return s3.New(sess) +} func getRegionFromBucket(svc *s3.S3, bucket string) (string, error) { input := &s3.GetBucketLocationInput{ Bucket: aws.String(bucket), @@ -73,6 +86,5 @@ func getRegionFromBucket(svc *s3.S3, bucket string) (string, error) { if result.LocationConstraint == nil { // GetBucketLocation returns nil for bucket in us-east-1. return bucketLocationDefault, nil } - return aws.StringValue(result.LocationConstraint), nil } diff --git a/agent/s3/factory/mocks/factory_mocks.go b/agent/s3/factory/mocks/factory_mocks.go index 1b675987b2d..d19603715a8 100644 --- a/agent/s3/factory/mocks/factory_mocks.go +++ b/agent/s3/factory/mocks/factory_mocks.go @@ -49,17 +49,31 @@ func (m *MockS3ClientCreator) EXPECT() *MockS3ClientCreatorMockRecorder { return m.recorder } -// NewS3ClientForBucket mocks base method -func (m *MockS3ClientCreator) NewS3ClientForBucket(arg0, arg1 string, arg2 credentials.IAMRoleCredentials) (s3.S3Client, error) { +// NewS3Client mocks base method +func (m *MockS3ClientCreator) NewS3Client(arg0 string, arg1 credentials.IAMRoleCredentials) s3.S3Client { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewS3ClientForBucket", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "NewS3Client", arg0, arg1) ret0, _ := ret[0].(s3.S3Client) + return ret0 +} + +// NewS3Client indicates an expected call of NewS3Client +func (mr *MockS3ClientCreatorMockRecorder) NewS3Client(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewS3Client", reflect.TypeOf((*MockS3ClientCreator)(nil).NewS3Client), arg0, arg1) +} + +// NewS3ManagerClient mocks base method +func (m *MockS3ClientCreator) NewS3ManagerClient(arg0, arg1 string, arg2 credentials.IAMRoleCredentials) (s3.S3ManagerClient, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewS3ManagerClient", arg0, arg1, arg2) + ret0, _ := ret[0].(s3.S3ManagerClient) ret1, _ := ret[1].(error) return ret0, ret1 } -// NewS3ClientForBucket indicates an expected call of NewS3ClientForBucket -func (mr *MockS3ClientCreatorMockRecorder) NewS3ClientForBucket(arg0, arg1, arg2 interface{}) *gomock.Call { +// NewS3ManagerClient indicates an expected call of NewS3ManagerClient +func (mr *MockS3ClientCreatorMockRecorder) NewS3ManagerClient(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewS3ClientForBucket", reflect.TypeOf((*MockS3ClientCreator)(nil).NewS3ClientForBucket), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewS3ManagerClient", reflect.TypeOf((*MockS3ClientCreator)(nil).NewS3ManagerClient), arg0, arg1, arg2) } diff --git a/agent/s3/generate_mocks.go b/agent/s3/generate_mocks.go index ba2fccdbe03..1a540728623 100644 --- a/agent/s3/generate_mocks.go +++ b/agent/s3/generate_mocks.go @@ -13,4 +13,5 @@ package s3 +//go:generate mockgen -destination=mocks/s3manager/s3_mocks.go -copyright_file=../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/s3 S3ManagerClient //go:generate mockgen -destination=mocks/s3_mocks.go -copyright_file=../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/s3 S3Client diff --git a/agent/s3/interface.go b/agent/s3/interface.go index f68807e79b6..16eab56df2a 100644 --- a/agent/s3/interface.go +++ b/agent/s3/interface.go @@ -21,7 +21,14 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" ) -// S3Client interface wraps the S3 API. -type S3Client interface { +// S3ManagerClient interface wraps the S3Manager APIs. +// Any method that belongs aws-sdk-go/service/s3manager goes here. +type S3ManagerClient interface { DownloadWithContext(ctx aws.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*s3manager.Downloader)) (n int64, err error) } + +// S3Client interface wraps the generic S3 APIs. +// Any method that belongs to aws-sdk-go/service/s3 goes here. +type S3Client interface { + GetObject(*s3.GetObjectInput) (*s3.GetObjectOutput, error) +} diff --git a/agent/s3/mocks/s3_mocks.go b/agent/s3/mocks/s3_mocks.go index 97f6de8c006..6bab27370e6 100644 --- a/agent/s3/mocks/s3_mocks.go +++ b/agent/s3/mocks/s3_mocks.go @@ -19,12 +19,9 @@ package mock_s3 import ( - context "context" - io "io" reflect "reflect" s3 "github.com/aws/aws-sdk-go/service/s3" - s3manager "github.com/aws/aws-sdk-go/service/s3/s3manager" gomock "github.com/golang/mock/gomock" ) @@ -51,22 +48,17 @@ func (m *MockS3Client) EXPECT() *MockS3ClientMockRecorder { return m.recorder } -// DownloadWithContext mocks base method -func (m *MockS3Client) DownloadWithContext(arg0 context.Context, arg1 io.WriterAt, arg2 *s3.GetObjectInput, arg3 ...func(*s3manager.Downloader)) (int64, error) { +// GetObject mocks base method +func (m *MockS3Client) GetObject(arg0 *s3.GetObjectInput) (*s3.GetObjectOutput, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2} - for _, a := range arg3 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "DownloadWithContext", varargs...) - ret0, _ := ret[0].(int64) + ret := m.ctrl.Call(m, "GetObject", arg0) + ret0, _ := ret[0].(*s3.GetObjectOutput) ret1, _ := ret[1].(error) return ret0, ret1 } -// DownloadWithContext indicates an expected call of DownloadWithContext -func (mr *MockS3ClientMockRecorder) DownloadWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { +// GetObject indicates an expected call of GetObject +func (mr *MockS3ClientMockRecorder) GetObject(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadWithContext", reflect.TypeOf((*MockS3Client)(nil).DownloadWithContext), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockS3Client)(nil).GetObject), arg0) } diff --git a/agent/s3/mocks/s3manager/s3_mocks.go b/agent/s3/mocks/s3manager/s3_mocks.go new file mode 100644 index 00000000000..3ca9b4cd8f6 --- /dev/null +++ b/agent/s3/mocks/s3manager/s3_mocks.go @@ -0,0 +1,72 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/aws/amazon-ecs-agent/agent/s3 (interfaces: S3ManagerClient) + +// Package mock_s3 is a generated GoMock package. +package mock_s3 + +import ( + context "context" + io "io" + reflect "reflect" + + s3 "github.com/aws/aws-sdk-go/service/s3" + s3manager "github.com/aws/aws-sdk-go/service/s3/s3manager" + gomock "github.com/golang/mock/gomock" +) + +// MockS3ManagerClient is a mock of S3ManagerClient interface +type MockS3ManagerClient struct { + ctrl *gomock.Controller + recorder *MockS3ManagerClientMockRecorder +} + +// MockS3ManagerClientMockRecorder is the mock recorder for MockS3ManagerClient +type MockS3ManagerClientMockRecorder struct { + mock *MockS3ManagerClient +} + +// NewMockS3ManagerClient creates a new mock instance +func NewMockS3ManagerClient(ctrl *gomock.Controller) *MockS3ManagerClient { + mock := &MockS3ManagerClient{ctrl: ctrl} + mock.recorder = &MockS3ManagerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockS3ManagerClient) EXPECT() *MockS3ManagerClientMockRecorder { + return m.recorder +} + +// DownloadWithContext mocks base method +func (m *MockS3ManagerClient) DownloadWithContext(arg0 context.Context, arg1 io.WriterAt, arg2 *s3.GetObjectInput, arg3 ...func(*s3manager.Downloader)) (int64, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DownloadWithContext", varargs...) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DownloadWithContext indicates an expected call of DownloadWithContext +func (mr *MockS3ManagerClientMockRecorder) DownloadWithContext(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadWithContext", reflect.TypeOf((*MockS3ManagerClient)(nil).DownloadWithContext), varargs...) +} diff --git a/agent/s3/s3.go b/agent/s3/s3.go index 71c86b2579f..edd58639b3e 100644 --- a/agent/s3/s3.go +++ b/agent/s3/s3.go @@ -29,7 +29,7 @@ const ( ) // DownloadFile downloads a file from s3 and writes it with the writer. -func DownloadFile(bucket, key string, timeout time.Duration, w io.WriterAt, client S3Client) error { +func DownloadFile(bucket, key string, timeout time.Duration, w io.WriterAt, client S3ManagerClient) error { input := &s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), @@ -50,3 +50,24 @@ func ParseS3ARN(s3ARN string) (bucket string, key string, err error) { } return match[2], match[3], nil } + +func GetObject(bucket string, key string, client S3Client) (string, error) { + requestInput := &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(key), + } + + result, err := client.GetObject(requestInput) + if err != nil { + return "", err + } + + defer result.Body.Close() + resultBody, err := io.ReadAll(result.Body) + if err != nil { + return "", err + } + credSpecData := string(resultBody) + + return credSpecData, nil +} diff --git a/agent/s3/s3_test.go b/agent/s3/s3_test.go index 636bd8cc146..6186255c788 100644 --- a/agent/s3/s3_test.go +++ b/agent/s3/s3_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -18,6 +19,7 @@ package s3 import ( "errors" "io" + "strings" "testing" "time" @@ -27,6 +29,7 @@ import ( "github.com/stretchr/testify/assert" mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks" + mock_s3manager "github.com/aws/amazon-ecs-agent/agent/s3/mocks/s3manager" mock_oswrapper "github.com/aws/amazon-ecs-agent/agent/utils/oswrapper/mocks" ) @@ -41,15 +44,15 @@ func TestDownloadFile(t *testing.T) { defer ctrl.Finish() mockFile := mock_oswrapper.NewMockFile() - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3ManagerClient := mock_s3manager.NewMockS3ManagerClient(ctrl) - mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Do(func(ctx aws.Context, + mockS3ManagerClient.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Do(func(ctx aws.Context, w io.WriterAt, input *s3sdk.GetObjectInput) { assert.Equal(t, testBucket, aws.StringValue(input.Bucket)) assert.Equal(t, testKey, aws.StringValue(input.Key)) }) - err := DownloadFile(testBucket, testKey, testTimeout, mockFile, mockS3Client) + err := DownloadFile(testBucket, testKey, testTimeout, mockFile, mockS3ManagerClient) assert.NoError(t, err) } @@ -57,12 +60,12 @@ func TestDownloadFileError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3ManagerClient := mock_s3manager.NewMockS3ManagerClient(ctrl) mockFile := mock_oswrapper.NewMockFile() - mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), errors.New("test error")) + mockS3ManagerClient.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), errors.New("test error")) - err := DownloadFile(testBucket, testKey, testTimeout, mockFile, mockS3Client) + err := DownloadFile(testBucket, testKey, testTimeout, mockFile, mockS3ManagerClient) assert.Error(t, err) } @@ -77,3 +80,31 @@ func TestParseS3ARNInvalid(t *testing.T) { _, _, err := ParseS3ARN("arn:aws:xxx:::xxx") assert.Error(t, err) } + +func TestGetObject(t *testing.T) { + expectedValue := "testdata" + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGetObjectResponse := &s3sdk.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader(expectedValue)), + } + mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client.EXPECT().GetObject(gomock.Any()).Return(mockGetObjectResponse, nil) + + actualValue, err := GetObject(testBucket, testKey, mockS3Client) + assert.NoError(t, err) + assert.Equal(t, actualValue, expectedValue) +} + +func TestGetObjectErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGetObjectResponse := &s3sdk.GetObjectOutput{} + mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client.EXPECT().GetObject(gomock.Any()).Return(mockGetObjectResponse, errors.New("test error")) + + _, err := GetObject(testBucket, testKey, mockS3Client) + assert.Error(t, err) +} diff --git a/agent/sighandlers/debug_handler.go b/agent/sighandlers/debug_handler.go index cdb15a47704..790e61b11a9 100644 --- a/agent/sighandlers/debug_handler.go +++ b/agent/sighandlers/debug_handler.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/sighandlers/debug_handler_windows.go b/agent/sighandlers/debug_handler_windows.go index 27f383055ca..2c414848ad1 100644 --- a/agent/sighandlers/debug_handler_windows.go +++ b/agent/sighandlers/debug_handler_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/sighandlers/termination_handler.go b/agent/sighandlers/termination_handler.go index 9e6a7517665..f1e0cb599af 100644 --- a/agent/sighandlers/termination_handler.go +++ b/agent/sighandlers/termination_handler.go @@ -13,9 +13,12 @@ // Package sighandlers handle signals and behave appropriately. // SIGTERM: -// Flush state to disk and exit +// +// Flush state to disk and exit +// // SIGUSR1: -// Print a dump of goroutines to the logger and DON'T exit +// +// Print a dump of goroutines to the logger and DON'T exit package sighandlers import ( diff --git a/agent/sighandlers/termination_handler_test.go b/agent/sighandlers/termination_handler_test.go index 86369e272fb..31ed8781127 100644 --- a/agent/sighandlers/termination_handler_test.go +++ b/agent/sighandlers/termination_handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -45,7 +46,7 @@ func TestFinalSave(t *testing.T) { state := dockerstate.NewTaskEngineState() taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, - nil, nil, state, nil, nil, nil) + nil, nil, state, nil, nil, nil, nil) task := &apitask.Task{ Arn: taskARN, diff --git a/agent/ssm/factory/factory.go b/agent/ssm/factory/factory.go index 0e68cf1b3c1..02dd453c590 100644 --- a/agent/ssm/factory/factory.go +++ b/agent/ssm/factory/factory.go @@ -39,7 +39,7 @@ func NewSSMClientCreator() SSMClientCreator { type ssmClientCreator struct{} -//SSM Client will automatically retry 3 times when has throttling error +// SSM Client will automatically retry 3 times when has throttling error func (*ssmClientCreator) NewSSMClient(region string, creds credentials.IAMRoleCredentials) ssmclient.SSMClient { cfg := aws.NewConfig(). diff --git a/agent/ssm/ssm_test.go b/agent/ssm/ssm_test.go index 4028b8c5450..2699083beac 100644 --- a/agent/ssm/ssm_test.go +++ b/agent/ssm/ssm_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/dependencies/dependencies_windows.go b/agent/statemanager/dependencies/dependencies_windows.go index 7d920a24f01..709bcf0d021 100644 --- a/agent/statemanager/dependencies/dependencies_windows.go +++ b/agent/statemanager/dependencies/dependencies_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/helper_unix_test.go b/agent/statemanager/helper_unix_test.go index f141753b8ea..9c164e1f26f 100644 --- a/agent/statemanager/helper_unix_test.go +++ b/agent/statemanager/helper_unix_test.go @@ -1,4 +1,5 @@ //go:build !windows && unit +// +build !windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/helper_windows_test.go b/agent/statemanager/helper_windows_test.go index 4d4539c71f9..178cc6b03cc 100644 --- a/agent/statemanager/helper_windows_test.go +++ b/agent/statemanager/helper_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/state_manager_test.go b/agent/statemanager/state_manager_test.go index bec7cec51d7..41815dcf55d 100644 --- a/agent/statemanager/state_manager_test.go +++ b/agent/statemanager/state_manager_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -44,7 +45,7 @@ func TestLoadsV1DataCorrectly(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v1", "1")} taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), - nil, nil, nil) + nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 @@ -89,7 +90,7 @@ func TestLoadsV13DataCorrectly(t *testing.T) { defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v13", "1")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 @@ -137,7 +138,7 @@ func TestLoadsDataForContainerHealthCheckTask(t *testing.T) { defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v10", "container-health-check")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 @@ -179,7 +180,7 @@ func TestLoadsDataForPrivateRegistryTask(t *testing.T) { defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v14", "private-registry")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 @@ -225,7 +226,7 @@ func TestLoadsDataForSecretsTask(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v17", "secrets")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -263,7 +264,7 @@ func TestLoadsDataForAddingAvailabilityZoneInTask(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v18", "availabilityZone")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID, availabilityZone string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -294,7 +295,7 @@ func TestLoadsDataForASMSecretsTask(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v18", "secrets")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -333,7 +334,7 @@ func TestLoadsDataForContainerOrdering(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v20", "containerOrdering")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -368,7 +369,7 @@ func TestLoadsDataForPerContainerTimeouts(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v20", "perContainerTimeouts")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -403,7 +404,7 @@ func TestLoadsDataForContainerRuntimeID(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v23", "perContainerRuntimeID")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -434,7 +435,7 @@ func TestLoadsDataForContainerImageDigest(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v24", "perContainerImageDigest")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -465,7 +466,7 @@ func TestLoadsDataSeqTaskManifest(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v25", "seqNumTaskManifest")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber, seqNumTaskManifest int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -492,7 +493,7 @@ func TestLoadsDataForEnvFiles(t *testing.T) { require.Nil(t, err, "Failed to set up test") defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v28", "environmentFiles")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, diff --git a/agent/statemanager/state_manager_unix.go b/agent/statemanager/state_manager_unix.go index 7bd207fec03..ddf9ebfebd9 100644 --- a/agent/statemanager/state_manager_unix.go +++ b/agent/statemanager/state_manager_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/state_manager_unix_test.go b/agent/statemanager/state_manager_unix_test.go index 209422b4e87..9ce00684fbe 100644 --- a/agent/statemanager/state_manager_unix_test.go +++ b/agent/statemanager/state_manager_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -26,7 +27,9 @@ import ( "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" engine_testutils "github.com/aws/amazon-ecs-agent/agent/engine/testutils" "github.com/aws/amazon-ecs-agent/agent/statemanager" + "github.com/aws/amazon-ecs-agent/agent/taskresource/credentialspec" "github.com/aws/amazon-ecs-agent/agent/taskresource/firelens" + "github.com/aws/amazon-ecs-agent/agent/taskresource/status" resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/stretchr/testify/assert" @@ -47,7 +50,7 @@ func TestStateManager(t *testing.T) { // Now let's make some state to save containerInstanceArn := "" taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), - nil, nil, nil) + nil, nil, nil, nil) manager, err = statemanager.NewStateManager(cfg, statemanager.AddSaveable("TaskEngine", taskEngine), statemanager.AddSaveable("ContainerInstanceArn", &containerInstanceArn)) @@ -65,7 +68,7 @@ func TestStateManager(t *testing.T) { // Now make sure we can load that state sanely loadedTaskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), - nil, nil, nil) + nil, nil, nil, nil) var loadedContainerInstanceArn string manager, err = statemanager.NewStateManager(cfg, statemanager.AddSaveable("TaskEngine", &loadedTaskEngine), @@ -110,7 +113,7 @@ func TestLoadsDataForAWSVPCTask(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v11", tc.dir)} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string stateManager, err := statemanager.NewStateManager(cfg, @@ -149,7 +152,7 @@ func TestLoadsDataForAWSVPCTask(t *testing.T) { // verify that the state manager correctly loads gpu related fields in state file func TestLoadsDataForGPU(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v18", "gpu")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -194,7 +197,7 @@ func TestLoadsDataForGPU(t *testing.T) { func TestLoadsDataForFirelensTask(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v23", "firelens")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -242,7 +245,7 @@ func TestLoadsDataForFirelensTask(t *testing.T) { func TestLoadsDataForFirelensTaskWithExternalConfig(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v24", "firelens")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -296,7 +299,7 @@ func TestLoadsDataForFirelensTaskWithExternalConfig(t *testing.T) { func TestLoadsDataForEFSGATask(t *testing.T) { cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v27", "efs")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 stateManager, err := statemanager.NewStateManager(cfg, @@ -329,3 +332,54 @@ func TestLoadsDataForEFSGATask(t *testing.T) { assert.Equal(t, "tls,tlsport=20050,iam,awscredsuri=/v2/credentials/xxx,accesspoint=fsap-xxx,netns=/proc/123/ns/net", volumeResource.VolumeConfig.DriverOpts["o"]) assert.Equal(t, "efs", volumeResource.VolumeConfig.DriverOpts["type"]) } + +func TestLoadsDataForGMSATask(t *testing.T) { + cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v31", "gmsalinux")} + taskEngineState := dockerstate.NewTaskEngineState() + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, taskEngineState, nil, nil, nil, nil) + var containerInstanceArn, cluster, savedInstanceID string + var sequenceNumber int64 + + stateManager, err := statemanager.NewStateManager(cfg, + statemanager.AddSaveable("TaskEngine", taskEngine), + statemanager.AddSaveable("ContainerInstanceArn", &containerInstanceArn), + statemanager.AddSaveable("Cluster", &cluster), + statemanager.AddSaveable("EC2InstanceID", &savedInstanceID), + statemanager.AddSaveable("SeqNum", &sequenceNumber), + ) + + assert.NoError(t, err) + err = stateManager.Load() + assert.NoError(t, err) + assert.Equal(t, "gmsa-test", cluster) + assert.EqualValues(t, 0, sequenceNumber) + tasks, err := taskEngine.ListTasks() + assert.NoError(t, err) + assert.Equal(t, 1, len(tasks)) + task := tasks[0] + + assert.Equal(t, "arn:aws:ecs:ap-northeast-1:1234567890:task/b8e2bd3c-c82a-4b43-9bde-199ae05b49a5", task.Arn) + assert.Equal(t, "gmsa-test", task.Family) + assert.Equal(t, 1, len(task.Containers)) + container := task.Containers[0] + assert.Equal(t, "linux_sample_app", container.Name) + + resource, ok := task.GetCredentialSpecResource() + assert.True(t, ok) + assert.NotEmpty(t, resource) + + credSpecResource := resource[0].(*credentialspec.CredentialSpecResource) + + assert.Equal(t, status.ResourceCreated, credSpecResource.GetDesiredStatus()) + assert.Equal(t, status.ResourceCreated, credSpecResource.GetKnownStatus()) + + credSpecMap := credSpecResource.CredSpecMap + assert.NotEmpty(t, credSpecMap) + + testCredSpec := "credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + expectedKerberosTicketPath := "/var/credentials-fetcher/krbdir/123456/webapp01" + + actualKerberosTicketPath, err := credSpecResource.GetTargetMapping(testCredSpec) + assert.NoError(t, err) + assert.Equal(t, expectedKerberosTicketPath, actualKerberosTicketPath) +} diff --git a/agent/statemanager/state_manager_win_test.go b/agent/statemanager/state_manager_win_test.go index 68bf2a182e9..125e6a55ac8 100644 --- a/agent/statemanager/state_manager_win_test.go +++ b/agent/statemanager/state_manager_win_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -35,7 +36,7 @@ func TestLoadsDataForGMSATask(t *testing.T) { defer cleanup() cfg := &config.Config{DataDir: filepath.Join(".", "testdata", "v26", "gmsa")} - taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + taskEngine := engine.NewTaskEngine(&config.Config{}, nil, nil, nil, nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) var containerInstanceArn, cluster, savedInstanceID string var sequenceNumber int64 diff --git a/agent/statemanager/state_manager_windows.go b/agent/statemanager/state_manager_windows.go index cc79d16007f..09362c5fe25 100644 --- a/agent/statemanager/state_manager_windows.go +++ b/agent/statemanager/state_manager_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/state_manager_windows_test.go b/agent/statemanager/state_manager_windows_test.go index b347501f404..0d31c4b8f76 100644 --- a/agent/statemanager/state_manager_windows_test.go +++ b/agent/statemanager/state_manager_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/statemanager/testdata/v26/gmsa/ecs_agent_data.json b/agent/statemanager/testdata/v26/gmsa/ecs_agent_data.json index 0a5c02735b1..53c50c5cf7d 100644 --- a/agent/statemanager/testdata/v26/gmsa/ecs_agent_data.json +++ b/agent/statemanager/testdata/v26/gmsa/ecs_agent_data.json @@ -95,10 +95,10 @@ "desiredStatus": "CREATED", "knownStatus": "CREATED", "credentialSpecResources": [ - "credentialspec:file://WebApp01.json" + "credentialspec:file://WebApp01.json" ], "CredSpecMap": { - "credentialspec:file://WebApp01.json": "credentialspec=file://WebApp01.json" + "credentialspec:file://WebApp01.json": "credentialspec=file://WebApp01.json" }, "executionCredentialsID": "b1a6ede6-1a9f-4ab3-a02e-bd3e51b11244" } diff --git a/agent/statemanager/testdata/v31/gmsalinux/ecs_agent_data.json b/agent/statemanager/testdata/v31/gmsalinux/ecs_agent_data.json new file mode 100644 index 00000000000..bdd5afd800a --- /dev/null +++ b/agent/statemanager/testdata/v31/gmsalinux/ecs_agent_data.json @@ -0,0 +1,230 @@ +{ + "Data": { + "Cluster": "gmsa-test", + "ContainerInstanceArn": "arn:aws:ecs:ap-northeast-1:1234567890:container-instance/e3f3aa05-ab9a-43a4-a4b8-736fc113bdb8", + "EC2InstanceID": "i-021fda4c225662a98", + "TaskEngine": { + "Tasks": [ + { + "Arn": "arn:aws:ecs:ap-northeast-1:1234567890:task/b8e2bd3c-c82a-4b43-9bde-199ae05b49a5", + "Family": "gmsa-test", + "Version": "5", + "Containers": [ + { + "Name": "linux_sample_app", + "RuntimeID": "c7c4ce17a76d30f26f48743faa9331867ffc0ae53971929f100124836ded7be7", + "V3EndpointID": "9cd43f50-acff-42fe-829e-3ed4aa68f637", + "Image": "busybox", + "ImageID": "sha256:281fc8d130220f569fcdaf500cf8ebc614f5f7227188432f73bc9da19bdbae1b", + "ImageDigest": "", + "Command": [ + "sh", "-c", "sleep 5s; echo ab; echo bc; echo ac; exit 42" + ], + "Cpu": 512, + "GPUIDs": null, + "Memory": 768, + "Links": null, + "firelensConfiguration": null, + "volumesFrom": [], + "mountPoints": [], + "portMappings": [ + { + "ContainerPort": 80, + "HostPort": 8080, + "BindIp": "", + "Protocol": "tcp" + } + ], + "secrets": [], + "Essential": true, + "EntryPoint": [ + "powershell", + "-Command" + ], + "environment": { + "AWS_EXECUTION_ENV": "AWS_ECS_EC2", + "ECS_CONTAINER_METADATA_URI": "http://169.254.170.2/v3/9cd43f50-acff-42fe-829e-3ed4aa68f637" + }, + "overrides": { + "command": null + }, + "dockerConfig": { + "config": "{}", + "hostConfig": "{\"Binds\":[\"/var/credentials-fetcher/krbdir/123456/webap01:/var/credebntials-fetcher/krbdir\"],\"CapAdd\":[],\"CapDrop\":[]}", + "version": "1.17" + }, + "registryAuthentication": null, + "LogsAuthStrategy": "", + "StartTimeout": 0, + "StopTimeout": 0, + "desiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "TransitionDependencySet": { + "2": { + "ContainerDependencies": null, + "ResourceDependencies": [ + { + "Name": "credentialspec", + "RequiredStatus": 1 + } + ] + } + }, + "RunDependencies": null, + "IsInternal": "NORMAL", + "ApplyingError": null, + "SentStatus": "RUNNING", + "metadataFileUpdated": false, + "KnownExitCode": null, + "KnownPortBindings": [ + { + "ContainerPort": 80, + "HostPort": 8080, + "BindIp": "0.0.0.0", + "Protocol": "tcp" + } + ] + } + ], + "associations": [], + "resources": { + "credentialspec": [ + { + "taskARN": "arn:aws:ecs:ap-northeast-1:1234567890:task/b8e2bd3c-c82a-4b43-9bde-199ae05b49a5", + "createdAt": "0001-01-01T00:00:00Z", + "desiredStatus": "CREATED", + "knownStatus": "CREATED", + "credentialSpecResources": [ + "arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + ], + "CredSpecMap": { + "credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json": "/var/credentials-fetcher/krbdir/123456/webapp01" + }, + "executionCredentialsID": "b1a6ede6-1a9f-4ab3-a02e-bd3e51b11244", + "leaseID": "123456" + } + ] + }, + "volumes": [], + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "KnownTime": "2019-11-14T21:26:31.5329611Z", + "PullStartedAt": "2019-11-14T21:26:24.4911792Z", + "PullStoppedAt": "2019-11-14T21:26:27.0614396Z", + "ExecutionStoppedAt": "0001-01-01T00:00:00Z", + "SentStatus": "RUNNING", + "StartSequenceNumber": 3, + "StopSequenceNumber": 0, + "executionCredentialsID": "", + "ENI": null, + "AppMesh": null, + "PlatformFields": { + "cpuUnbounded": false, + "memoryUnbounded": false + } + } + ], + "IdToContainer": { + "c7c4ce17a76d30f26f48743faa9331867ffc0ae53971929f100124836ded7be7": { + "DockerId": "c7c4ce17a76d30f26f48743faa9331867ffc0ae53971929f100124836ded7be7", + "DockerName": "ecs-gmsa-test-5-linuxsampleapp-d28d85d3c994a7ccaf01", + "Container": { + "Name": "linux_sample_app", + "RuntimeID": "c7c4ce17a76d30f26f48743faa9331867ffc0ae53971929f100124836ded7be7", + "V3EndpointID": "9cd43f50-acff-42fe-829e-3ed4aa68f637", + "Image": "busybox", + "ImageID": "sha256:281fc8d130220f569fcdaf500cf8ebc614f5f7227188432f73bc9da19bdbae1b", + "ImageDigest": "", + "Command": [ + "sh", "-c", "sleep 5s; echo ab; echo bc; echo ac; exit 42" + ], + "Cpu": 512, + "GPUIDs": null, + "Memory": 768, + "Links": null, + "firelensConfiguration": null, + "volumesFrom": [], + "mountPoints": [], + "portMappings": [ + { + "ContainerPort": 80, + "HostPort": 8080, + "BindIp": "", + "Protocol": "tcp" + } + ], + "secrets": null, + "Essential": true, + "EntryPoint": [ + "powershell", + "-Command" + ], + "environment": { + "AWS_EXECUTION_ENV": "AWS_ECS_EC2", + "ECS_CONTAINER_METADATA_URI": "http://169.254.170.2/v3/9cd43f50-acff-42fe-829e-3ed4aa68f637" + }, + "overrides": { + "command": null + }, + "dockerConfig": { + "config": "{}", + "hostConfig": "{}", + "version": "1.17" + }, + "registryAuthentication": null, + "LogsAuthStrategy": "", + "StartTimeout": 0, + "StopTimeout": 0, + "desiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "TransitionDependencySet": { + "2": { + "ContainerDependencies": null, + "ResourceDependencies": [ + { + "Name": "credentialspec", + "RequiredStatus": 1 + } + ] + } + }, + "RunDependencies": null, + "IsInternal": "NORMAL", + "ApplyingError": null, + "SentStatus": "RUNNING", + "metadataFileUpdated": false, + "KnownExitCode": null, + "KnownPortBindings": [ + { + "ContainerPort": 80, + "HostPort": 8080, + "BindIp": "0.0.0.0", + "Protocol": "tcp" + } + ] + } + } + }, + "IdToTask": { + "c7c4ce17a76d30f26f48743faa9331867ffc0ae53971929f100124836ded7be7": "arn:aws:ecs:ap-northeast-1:1234567890:task/b8e2bd3c-c82a-4b43-9bde-199ae05b49a5" + }, + "ImageStates": [ + { + "Image": { + "ImageID": "sha256:281fc8d130220f569fcdaf500cf8ebc614f5f7227188432f73bc9da19bdbae1b", + "Names": [ + "httpd" + ], + "Size": 5005450904 + }, + "PulledAt": "2022-09-14T21:23:43.5555065Z", + "LastUsedAt": "2022-09-14T21:23:43.5555065Z", + "PullSucceeded": true + } + ], + "ENIAttachments": null, + "IPToTask": {} + } + }, + "Version": 31 +} \ No newline at end of file diff --git a/agent/stats/common_test.go b/agent/stats/common_test.go index e7598e83608..ccb8df05f9e 100644 --- a/agent/stats/common_test.go +++ b/agent/stats/common_test.go @@ -51,10 +51,11 @@ const ( // for the waiting after container cleanup before checking the state of the manager. waitForCleanupSleep = 10 * time.Millisecond - taskArn = "gremlin" - taskDefinitionFamily = "docker-gremlin" - taskDefinitionVersion = "1" - containerName = "gremlin-container" + taskArn = "gremlin" + taskDefinitionFamily = "docker-gremlin" + taskDefinitionVersion = "1" + containerName = "gremlin-container" + serviceConnectContainerName = "service-connect-container" ) var ( @@ -141,8 +142,8 @@ func (resolver *IntegContainerMetadataResolver) ResolveContainer(containerID str return container, nil } -func validateInstanceMetrics(t *testing.T, engine *DockerStatsEngine) { - metadata, taskMetrics, err := engine.GetInstanceMetrics() +func validateInstanceMetrics(t *testing.T, engine *DockerStatsEngine, includeServiceConnectStats bool) { + metadata, taskMetrics, err := engine.GetInstanceMetrics(includeServiceConnectStats) assert.NoError(t, err, "gettting instance metrics failed") assert.NoError(t, validateMetricsMetadata(metadata), "validating metadata failed") assert.Len(t, taskMetrics, 1, "incorrect number of tasks") @@ -151,6 +152,24 @@ func validateInstanceMetrics(t *testing.T, engine *DockerStatsEngine) { assert.Equal(t, aws.StringValue(taskMetric.TaskDefinitionFamily), taskDefinitionFamily, "unexpected task definition family") assert.Equal(t, aws.StringValue(taskMetric.TaskDefinitionVersion), taskDefinitionVersion, "unexpected task definition version") assert.NoError(t, validateContainerMetrics(taskMetric.ContainerMetrics, 1), "validating container metrics failed") + if includeServiceConnectStats { + assert.NoError(t, validateServiceConnectMetrics(taskMetric.ServiceConnectMetricsWrapper, 1), "validating service connect metrics failed") + } +} + +func validateInstanceMetricsWithDisabledMetrics(t *testing.T, engine *DockerStatsEngine, includeServiceConnectStats bool) { + metadata, taskMetrics, err := engine.GetInstanceMetrics(includeServiceConnectStats) + assert.NoError(t, err, "gettting instance metrics failed") + assert.NoError(t, validateMetricsMetadata(metadata), "validating metadata failed") + assert.Len(t, taskMetrics, 1, "incorrect number of tasks") + + taskMetric := taskMetrics[0] + assert.Equal(t, aws.StringValue(taskMetric.TaskDefinitionFamily), taskDefinitionFamily, "unexpected task definition family") + assert.Equal(t, aws.StringValue(taskMetric.TaskDefinitionVersion), taskDefinitionVersion, "unexpected task definition version") + assert.NoError(t, validateContainerMetrics(taskMetric.ContainerMetrics, 0), "validating container metrics failed") + if includeServiceConnectStats { + assert.NoError(t, validateServiceConnectMetrics(taskMetric.ServiceConnectMetricsWrapper, 1), "validating service connect metrics failed") + } } func validateContainerMetrics(containerMetrics []*ecstcs.ContainerMetric, expected int) error { @@ -177,8 +196,23 @@ func validateContainerMetrics(containerMetrics []*ecstcs.ContainerMetric, expect return nil } +func validateServiceConnectMetrics(serviceConnectMetrics []*ecstcs.GeneralMetricsWrapper, expected int) error { + if len(serviceConnectMetrics) != expected { + return fmt.Errorf("Mismatch in number of serviceConnectMetrics elements. Expected: %d, Got: %d", expected, len(serviceConnectMetrics)) + } + for _, serviceConnectMetric := range serviceConnectMetrics { + if *serviceConnectMetric.GeneralMetrics[0].MetricName == "" { + return fmt.Errorf("service Connect MetricName is empty") + } + if serviceConnectMetric.Dimensions == nil { + return fmt.Errorf("service Connect Metric DimensionSet is nil") + } + } + return nil +} + func validateIdleContainerMetrics(t *testing.T, engine *DockerStatsEngine) { - metadata, taskMetrics, err := engine.GetInstanceMetrics() + metadata, taskMetrics, err := engine.GetInstanceMetrics(false) assert.NoError(t, err, "getting instance metrics failed") assert.NoError(t, validateMetricsMetadata(metadata), "validating metadata failed") diff --git a/agent/stats/common_unix_test.go b/agent/stats/common_unix_test.go index fe488de2613..928364211d6 100644 --- a/agent/stats/common_unix_test.go +++ b/agent/stats/common_unix_test.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/common_windows_test.go b/agent/stats/common_windows_test.go index 349aec3fa61..e818904ad6e 100644 --- a/agent/stats/common_windows_test.go +++ b/agent/stats/common_windows_test.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/container_test.go b/agent/stats/container_test.go index 9adfb90e9ea..bba649b0f62 100644 --- a/agent/stats/container_test.go +++ b/agent/stats/container_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/engine.go b/agent/stats/engine.go index 7b5ce54dd9e..ace69b859f1 100644 --- a/agent/stats/engine.go +++ b/agent/stats/engine.go @@ -68,9 +68,12 @@ type DockerContainerMetadataResolver struct { // Engine defines methods to be implemented by the engine struct. It is // defined to make testing easier. type Engine interface { - GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) + GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) ContainerDockerStats(taskARN string, containerID string) (*types.StatsJSON, *NetworkStatsPerSec, error) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstcs.TaskHealth, error) + GetPublishServiceConnectTickerInterval() int32 + SetPublishServiceConnectTickerInterval(int32) + GetPublishMetricsTicker() *time.Ticker } // DockerStatsEngine is used to monitor docker container events and to report @@ -91,8 +94,11 @@ type DockerStatsEngine struct { // tasksToHealthCheckContainers map task arns to the containers that has health check enabled tasksToHealthCheckContainers map[string]map[string]*StatsContainer // tasksToDefinitions maps task arns to task definition name and family metadata objects. - tasksToDefinitions map[string]*taskDefinition - taskToTaskStats map[string]*StatsTask + tasksToDefinitions map[string]*taskDefinition + taskToTaskStats map[string]*StatsTask + taskToServiceConnectStats map[string]*ServiceConnectStats + publishServiceConnectTickerInterval int32 + publishMetricsTicker *time.Ticker } // ResolveTask resolves the api task object, given container id. @@ -137,14 +143,16 @@ func (resolver *DockerContainerMetadataResolver) ResolveContainer(dockerID strin // MustInit() must be called to initialize the fields of the new event listener. func NewDockerStatsEngine(cfg *config.Config, client dockerapi.DockerClient, containerChangeEventStream *eventstream.EventStream) *DockerStatsEngine { return &DockerStatsEngine{ - client: client, - resolver: nil, - config: cfg, - tasksToContainers: make(map[string]map[string]*StatsContainer), - tasksToHealthCheckContainers: make(map[string]map[string]*StatsContainer), - tasksToDefinitions: make(map[string]*taskDefinition), - taskToTaskStats: make(map[string]*StatsTask), - containerChangeEventStream: containerChangeEventStream, + client: client, + resolver: nil, + config: cfg, + tasksToContainers: make(map[string]map[string]*StatsContainer), + tasksToHealthCheckContainers: make(map[string]map[string]*StatsContainer), + tasksToDefinitions: make(map[string]*taskDefinition), + taskToTaskStats: make(map[string]*StatsTask), + taskToServiceConnectStats: make(map[string]*ServiceConnectStats), + containerChangeEventStream: containerChangeEventStream, + publishServiceConnectTickerInterval: 0, } } @@ -218,6 +226,7 @@ func (engine *DockerStatsEngine) MustInit(ctx context.Context, taskEngine ecseng logger.Info("Initializing stats engine") engine.cluster = cluster engine.containerInstanceArn = containerInstanceArn + engine.publishMetricsTicker = time.NewTicker(config.DefaultContainerMetricsPublishInterval) var err error engine.resolver, err = newDockerContainerMetadataResolver(taskEngine) @@ -259,6 +268,9 @@ func (engine *DockerStatsEngine) waitToStop() { logger.Debug("Event stream closed, stop listening to the event stream") engine.containerChangeEventStream.Unsubscribe(containerChangeHandler) engine.removeAll() + if engine.publishMetricsTicker != nil { + engine.publishMetricsTicker.Stop() + } } // removeAll stops the periodic usage data collection for all containers @@ -305,6 +317,18 @@ func (engine *DockerStatsEngine) addToStatsTaskMapUnsafe(task *apitask.Task, doc } } +func (engine *DockerStatsEngine) addTaskToServiceConnectStatsUnsafe(taskArn string) { + _, taskExists := engine.taskToServiceConnectStats[taskArn] + if !taskExists { + serviceConnectStats, err := newServiceConnectStats() + if err != nil { + seelog.Errorf("Error adding task %s to the service connect stats watchlist : %v", taskArn, err) + return + } + engine.taskToServiceConnectStats[taskArn] = serviceConnectStats + } +} + // addContainerUnsafe adds a container to the map of containers being watched. func (engine *DockerStatsEngine) addContainerUnsafe(dockerID string) (*StatsContainer, *StatsTask, error) { // Make sure that this container belongs to a task and that the task @@ -351,6 +375,10 @@ func (engine *DockerStatsEngine) addContainerUnsafe(dockerID string) (*StatsCont seelog.Debugf("Adding container to stats health check watch list, id: %s, task: %s", dockerID, task.Arn) } + if errResolveContainer == nil && task.GetServiceConnectContainer() == dockerContainer.Container { + engine.addTaskToServiceConnectStatsUnsafe(task.Arn) + } + if !watchStatsContainer { return nil, nil, nil } @@ -394,8 +422,7 @@ func (engine *DockerStatsEngine) addToStatsContainerMapUnsafe( } // GetInstanceMetrics gets all task metrics and instance metadata from stats engine. -func (engine *DockerStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { - var taskMetrics []*ecstcs.TaskMetric +func (engine *DockerStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { idle := engine.isIdle() metricsMetadata := &ecstcs.MetricsMetadata{ Cluster: aws.String(engine.cluster), @@ -404,6 +431,7 @@ func (engine *DockerStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, MessageId: aws.String(uuid.NewRandom().String()), } + var taskMetrics []*ecstcs.TaskMetric if idle { seelog.Debug("Instance is idle. No task metrics to report") fin := true @@ -414,16 +442,31 @@ func (engine *DockerStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, engine.lock.Lock() defer engine.lock.Unlock() - for taskArn := range engine.tasksToContainers { + if includeServiceConnectStats { + err := engine.getServiceConnectStats() + if err != nil { + seelog.Errorf("Error getting service connect metrics: %v", err) + } + } + + taskStatsToCollect := engine.getTaskStatsToCollect() + for taskArn := range taskStatsToCollect { + _, isServiceConnectTask := engine.taskToServiceConnectStats[taskArn] containerMetrics, err := engine.taskContainerMetricsUnsafe(taskArn) if err != nil { seelog.Debugf("Error getting container metrics for task: %s, err: %v", taskArn, err) - continue + // skip collecting service connect related metrics, if task is not service connect enabled + if !isServiceConnectTask { + continue + } } if len(containerMetrics) == 0 { seelog.Debugf("Empty containerMetrics for task, ignoring, task: %s", taskArn) - continue + // skip collecting service connect related metrics, if task is not service connect enabled + if !isServiceConnectTask { + continue + } } taskDef, exists := engine.tasksToDefinitions[taskArn] @@ -439,6 +482,15 @@ func (engine *DockerStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, TaskDefinitionVersion: &taskDef.version, ContainerMetrics: containerMetrics, } + + if includeServiceConnectStats { + if serviceConnectStats, ok := engine.taskToServiceConnectStats[taskArn]; ok { + if !serviceConnectStats.HasStatsBeenSent() { + taskMetric.ServiceConnectMetricsWrapper = serviceConnectStats.GetStats() + serviceConnectStats.SetStatsSent(true) + } + } + } taskMetrics = append(taskMetrics, taskMetric) } @@ -486,7 +538,7 @@ func (engine *DockerStatsEngine) isIdle() bool { engine.lock.RLock() defer engine.lock.RUnlock() - return len(engine.tasksToContainers) == 0 + return len(engine.tasksToContainers) == 0 && len(engine.taskToServiceConnectStats) == 0 } func (engine *DockerStatsEngine) containerHealthsToMonitor() bool { @@ -713,27 +765,33 @@ func (engine *DockerStatsEngine) taskContainerMetricsUnsafe(taskArn string) ([]* field.Error: err, }) } else { - // send network stats for default/bridge/nat/awsvpc network modes - if !task.IsNetworkModeAWSVPC() && container.containerMetadata.NetworkMode != hostNetworkMode && - container.containerMetadata.NetworkMode != noneNetworkMode { - networkStatsSet, err := container.statsQueue.GetNetworkStatsSet() - if err != nil { - // we log the error and still continue to publish cpu, memory stats - logger.Warn("Error getting network stats for container", logger.Fields{ - field.Container: dockerID, - field.Error: err, - }) - } else { - containerMetric.NetworkStatsSet = networkStatsSet - } - } else if task.IsNetworkModeAWSVPC() { - taskStatsMap, taskExistsInTaskStats := engine.taskToTaskStats[taskArn] - if !taskExistsInTaskStats { - return nil, fmt.Errorf("task not found") - } - if dockerContainer, err := engine.resolver.ResolveContainer(dockerID); err != nil { - seelog.Debugf("Could not map container ID to container, container: %s, err: %s", dockerID, err) - } else { + if dockerContainer, err := engine.resolver.ResolveContainer(dockerID); err != nil { + logger.Warn("Could not map container ID to container, container", logger.Fields{ + field.DockerId: dockerID, + field.Error: err, + }) + } else { + // send network stats for default/bridge/nat/awsvpc network modes + if task.IsNetworkModeBridge() { + if task.IsServiceConnectEnabled() && dockerContainer.Container.Type == apicontainer.ContainerCNIPause { + seelog.Debug("Skip adding network stats for pause container in Service Connect enabled task") + } else { + networkStatsSet, err := container.statsQueue.GetNetworkStatsSet() + if err != nil { + // we log the error and still continue to publish cpu, memory stats + logger.Warn("Error getting network stats for container", logger.Fields{ + field.Container: dockerID, + field.Error: err, + }) + } else { + containerMetric.NetworkStatsSet = networkStatsSet + } + } + } else if task.IsNetworkModeAWSVPC() { + taskStatsMap, taskExistsInTaskStats := engine.taskToTaskStats[taskArn] + if !taskExistsInTaskStats { + return nil, fmt.Errorf("task not found") + } // do not add network stats for pause container if dockerContainer.Container.Type != apicontainer.ContainerCNIPause { networkStats, err := taskStatsMap.StatsQueue.GetNetworkStatsSet() @@ -770,6 +828,11 @@ func (engine *DockerStatsEngine) doRemoveContainerUnsafe(container *StatsContain seelog.Debugf("Deleted task from tasks, arn: %s", taskArn) } + if _, ok := engine.taskToServiceConnectStats[taskArn]; ok { + delete(engine.taskToServiceConnectStats, taskArn) + seelog.Debugf("Deleted task from service connect stats watch list, arn: %s", taskArn) + } + // Remove the container from health container watch list if _, ok := engine.tasksToHealthCheckContainers[taskArn][dockerID]; !ok { return @@ -835,3 +898,62 @@ func (engine *DockerStatsEngine) ContainerDockerStats(taskARN string, containerI return containerStats, containerNetworkRateStats, nil } + +// getTaskStatsToCollect returns a map of taskArns for which task metrics needs to collected +func (engine *DockerStatsEngine) getTaskStatsToCollect() map[string]bool { + taskStatsToCollect := make(map[string]bool) + for taskArn := range engine.tasksToContainers { + if _, taskArnExists := taskStatsToCollect[taskArn]; !taskArnExists { + taskStatsToCollect[taskArn] = true + } + } + for taskArn := range engine.taskToServiceConnectStats { + if _, taskArnExists := taskStatsToCollect[taskArn]; !taskArnExists { + taskStatsToCollect[taskArn] = true + } + } + return taskStatsToCollect +} + +// getServiceConnectStats invokes the workflow to retrieve all service connect +// related metrics for all service connect enabled tasks +func (engine *DockerStatsEngine) getServiceConnectStats() error { + var wg sync.WaitGroup + + for taskArn := range engine.taskToServiceConnectStats { + wg.Add(1) + task, err := engine.resolver.ResolveTaskByARN(taskArn) + if err != nil { + return errors.Errorf("stats engine: task '%s' not found", taskArn) + } + // TODO [SC]: Check if task is service-connect enabled + serviceConnectStats, ok := engine.taskToServiceConnectStats[taskArn] + if !ok { + return errors.Errorf("task '%s' is not registered to collect service connect metrics", taskArn) + } + go func() { + serviceConnectStats.retrieveServiceConnectStats(task) + wg.Done() + }() + } + wg.Wait() + return nil +} + +func (engine *DockerStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + engine.lock.RLock() + defer engine.lock.RUnlock() + + return engine.publishServiceConnectTickerInterval +} + +func (engine *DockerStatsEngine) SetPublishServiceConnectTickerInterval(publishServiceConnectTickerInterval int32) { + engine.lock.Lock() + defer engine.lock.Unlock() + + engine.publishServiceConnectTickerInterval = publishServiceConnectTickerInterval +} + +func (engine *DockerStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return engine.publishMetricsTicker +} diff --git a/agent/stats/engine_integ_test.go b/agent/stats/engine_integ_test.go index fa344c9bcbc..9790aa9eaef 100644 --- a/agent/stats/engine_integ_test.go +++ b/agent/stats/engine_integ_test.go @@ -1,4 +1,5 @@ //go:build integration +// +build integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -40,7 +41,10 @@ func init() { dockerClient, _ = dockerapi.NewDockerGoClient(sdkClientFactory, &cfg, ctx) } -func createRunningTask() *apitask.Task { +func createRunningTask(networkMode string) *apitask.Task { + if networkMode == "default" { + networkMode = "bridge" + } return &apitask.Task{ Arn: taskArn, DesiredStatusUnsafe: apitaskstatus.TaskRunning, @@ -52,6 +56,7 @@ func createRunningTask() *apitask.Task { Name: containerName, }, }, + NetworkMode: networkMode, } } @@ -78,8 +83,8 @@ func TestStatsEngineWithExistingContainersWithoutHealth(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithExistingContainersWithoutHealth") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) - testTask := createRunningTask() + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask("default") // Populate Tasks and Container map in the engine. dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine) dockerTaskEngine.State().AddTask(testTask) @@ -99,7 +104,7 @@ func TestStatsEngineWithExistingContainersWithoutHealth(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - validateInstanceMetrics(t, engine) + validateInstanceMetrics(t, engine, false) validateEmptyTaskHealthMetrics(t, engine) err = client.ContainerStop(ctx, container.ID, &timeout) @@ -137,8 +142,8 @@ func TestStatsEngineWithNewContainersWithoutHealth(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithNewContainers") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) - testTask := createRunningTask() + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask("default") // Populate Tasks and Container map in the engine. dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine) dockerTaskEngine.State().AddTask(testTask) @@ -171,7 +176,7 @@ func TestStatsEngineWithNewContainersWithoutHealth(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - validateInstanceMetrics(t, engine) + validateInstanceMetrics(t, engine, false) validateEmptyTaskHealthMetrics(t, engine) err = client.ContainerStop(ctx, container.ID, &timeout) @@ -215,8 +220,8 @@ func TestStatsEngineWithExistingContainers(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithExistingContainers") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) - testTask := createRunningTask() + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask("bridge") // enable container health check for this container testTask.Containers[0].HealthCheckType = "docker" // Populate Tasks and Container map in the engine. @@ -240,7 +245,7 @@ func TestStatsEngineWithExistingContainers(t *testing.T) { time.Sleep(checkPointSleep) // Verify the metrics of the container - validateInstanceMetrics(t, engine) + validateInstanceMetrics(t, engine, false) // Verify the health metrics of container validateTaskHealthMetrics(t, engine) @@ -282,9 +287,9 @@ func TestStatsEngineWithNewContainers(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithNewContainers") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) - testTask := createRunningTask() + testTask := createRunningTask("bridge") // enable health check of the container testTask.Containers[0].HealthCheckType = "docker" // Populate Tasks and Container map in the engine. @@ -317,7 +322,7 @@ func TestStatsEngineWithNewContainers(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - validateInstanceMetrics(t, engine) + validateInstanceMetrics(t, engine, false) // Verify the health metrics of container validateTaskHealthMetrics(t, engine) @@ -364,9 +369,9 @@ func TestStatsEngineWithNewContainersWithPolling(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithNewContainers") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) - testTask := createRunningTask() + testTask := createRunningTask("bridge") // enable health check of the container testTask.Containers[0].HealthCheckType = "docker" // Populate Tasks and Container map in the engine. @@ -399,7 +404,7 @@ func TestStatsEngineWithNewContainersWithPolling(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(10 * time.Second) - validateInstanceMetrics(t, engine) + validateInstanceMetrics(t, engine, false) // Verify the health metrics of container validateTaskHealthMetrics(t, engine) @@ -429,7 +434,7 @@ func TestStatsEngineWithNewContainersWithPolling(t *testing.T) { func TestStatsEngineWithDockerTaskEngine(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithDockerTaskEngine") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) container, err := createHealthContainer(client) require.NoError(t, err, "creating container failed") ctx, cancel := context.WithCancel(context.TODO()) @@ -439,7 +444,7 @@ func TestStatsEngineWithDockerTaskEngine(t *testing.T) { unmappedContainer, err := createHealthContainer(client) require.NoError(t, err, "creating container failed") defer client.ContainerRemove(ctx, unmappedContainer.ID, types.ContainerRemoveOptions{Force: true}) - testTask := createRunningTask() + testTask := createRunningTask("bridge") // enable the health check of the container testTask.Containers[0].HealthCheckType = "docker" // Populate Tasks and Container map in the engine. @@ -489,7 +494,7 @@ func TestStatsEngineWithDockerTaskEngine(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - validateInstanceMetrics(t, statsEngine) + validateInstanceMetrics(t, statsEngine, false) validateTaskHealthMetrics(t, statsEngine) err = client.ContainerStop(ctx, container.ID, &timeout) @@ -513,14 +518,14 @@ func TestStatsEngineWithDockerTaskEngine(t *testing.T) { func TestStatsEngineWithDockerTaskEngineMissingRemoveEvent(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithDockerTaskEngineMissingRemoveEvent") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) ctx, cancel := context.WithCancel(context.TODO()) defer cancel() container, err := createHealthContainer(client) require.NoError(t, err, "creating container failed") defer client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true}) - testTask := createRunningTask() + testTask := createRunningTask("") // enable container health check of this container testTask.Containers[0].HealthCheckType = "docker" testTask.Containers[0].KnownStatusUnsafe = apicontainerstatus.ContainerStopped @@ -567,7 +572,7 @@ func TestStatsEngineWithDockerTaskEngineMissingRemoveEvent(t *testing.T) { time.Sleep(checkPointSleep) // Simulate tcs client invoking GetInstanceMetrics. - _, _, err = statsEngine.GetInstanceMetrics() + _, _, err = statsEngine.GetInstanceMetrics(false) assert.Error(t, err, "expect error 'no task metrics tp report' when getting instance metrics") // Should not contain any metrics after cleanup. @@ -576,10 +581,10 @@ func TestStatsEngineWithDockerTaskEngineMissingRemoveEvent(t *testing.T) { } func TestStatsEngineWithNetworkStatsDefaultMode(t *testing.T) { - testNetworkModeStats(t, "default", false) + testNetworkModeStatsInteg(t, "default", false) } -func testNetworkModeStats(t *testing.T, networkMode string, statsEmpty bool) { +func testNetworkModeStatsInteg(t *testing.T, networkMode string, statsEmpty bool) { // Create a new docker stats engine engine := NewDockerStatsEngine(&cfg, dockerClient, eventStream("TestStatsEngineWithNetworkStats")) ctx, cancel := context.WithCancel(context.TODO()) @@ -602,8 +607,8 @@ func testNetworkModeStats(t *testing.T, networkMode string, statsEmpty bool) { containerChangeEventStream := eventStream("TestStatsEngineWithNetworkStats") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) - testTask := createRunningTask() + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask(networkMode) // Populate Tasks and Container map in the engine. dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine) @@ -632,7 +637,7 @@ func testNetworkModeStats(t *testing.T, networkMode string, statsEmpty bool) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - _, taskMetrics, err := engine.GetInstanceMetrics() + _, taskMetrics, err := engine.GetInstanceMetrics(false) assert.NoError(t, err, "getting instance metrics failed") taskMetric := taskMetrics[0] for _, containerMetric := range taskMetric.ContainerMetrics { @@ -683,8 +688,8 @@ func TestStorageStats(t *testing.T) { containerChangeEventStream := eventStream("TestStatsEngineWithStorageStats") taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, - nil, dockerstate.NewTaskEngineState(), nil, nil, nil) - testTask := createRunningTask() + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask("bridge") // Populate Tasks and Container map in the engine. dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine) @@ -712,7 +717,7 @@ func TestStorageStats(t *testing.T) { // Wait for the stats collection go routine to start. time.Sleep(checkPointSleep) - _, taskMetrics, err := engine.GetInstanceMetrics() + _, taskMetrics, err := engine.GetInstanceMetrics(false) assert.NoError(t, err, "getting instance metrics failed") taskMetric := taskMetrics[0] for _, containerMetric := range taskMetric.ContainerMetrics { diff --git a/agent/stats/engine_test.go b/agent/stats/engine_test.go index 00a03076eaf..e46d827c20c 100644 --- a/agent/stats/engine_test.go +++ b/agent/stats/engine_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -24,6 +25,7 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/config" @@ -38,16 +40,22 @@ import ( "github.com/stretchr/testify/require" ) +const ( + DefaultNetworkMode = "default" + BridgeNetworkMode = "bridge" + SCContainerName = "service-connect" +) + func TestStatsEngineAddRemoveContainers(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() resolver := mock_resolver.NewMockContainerMetadataResolver(ctrl) mockDockerClient := mock_dockerapi.NewMockDockerClient(ctrl) - t1 := &apitask.Task{Arn: "t1", Family: "f1"} + networkMode := "bridge" + t1 := &apitask.Task{Arn: "t1", Family: "f1", NetworkMode: networkMode} t2 := &apitask.Task{Arn: "t2", Family: "f2"} t3 := &apitask.Task{Arn: "t3"} name := "testContainer" - networkMode := "bridge" resolver.EXPECT().ResolveTask("c1").AnyTimes().Return(t1, nil) resolver.EXPECT().ResolveTask("c2").AnyTimes().Return(t1, nil) resolver.EXPECT().ResolveTask("c3").AnyTimes().Return(t2, nil) @@ -115,7 +123,37 @@ func TestStatsEngineAddRemoveContainers(t *testing.T) { t.Errorf("Error validating container metrics: %v", err) } - metadata, taskMetrics, err := engine.GetInstanceMetrics() + metadata, taskMetrics, err := engine.GetInstanceMetrics(false) + if err != nil { + t.Errorf("Error gettting instance metrics: %v", err) + } + + err = validateMetricsMetadata(metadata) + require.NoError(t, err) + require.Len(t, taskMetrics, 1, "Incorrect number of tasks.") + err = validateContainerMetrics(taskMetrics[0].ContainerMetrics, 2) + require.NoError(t, err) + require.Equal(t, "t1", *taskMetrics[0].TaskArn) + + for _, statsContainer := range containers { + assert.Equal(t, name, statsContainer.containerMetadata.Name) + assert.Equal(t, networkMode, statsContainer.containerMetadata.NetworkMode) + for _, fakeContainerStats := range createFakeContainerStats() { + statsContainer.statsQueue.add(fakeContainerStats) + } + } + + // Ensure task shows up in metrics. + containerMetrics, err = engine.taskContainerMetricsUnsafe("t1") + if err != nil { + t.Errorf("Error getting container metrics: %v", err) + } + err = validateContainerMetrics(containerMetrics, 2) + if err != nil { + t.Errorf("Error validating container metrics: %v", err) + } + + metadata, taskMetrics, err = engine.GetInstanceMetrics(true) if err != nil { t.Errorf("Error gettting instance metrics: %v", err) } @@ -152,7 +190,7 @@ func TestStatsEngineAddRemoveContainers(t *testing.T) { t.Error("Container c3 not found in engine") } - _, _, err = engine.GetInstanceMetrics() + _, _, err = engine.GetInstanceMetrics(false) if err == nil { t.Error("Expected non-empty error for empty stats.") } @@ -174,7 +212,7 @@ func TestStatsEngineMetadataInStatsSets(t *testing.T) { defer mockCtrl.Finish() resolver := mock_resolver.NewMockContainerMetadataResolver(mockCtrl) mockDockerClient := mock_dockerapi.NewMockDockerClient(mockCtrl) - t1 := &apitask.Task{Arn: "t1", Family: "f1"} + t1 := &apitask.Task{Arn: "t1", Family: "f1", NetworkMode: "bridge"} resolver.EXPECT().ResolveTask("c1").AnyTimes().Return(t1, nil) resolver.EXPECT().ResolveContainer(gomock.Any()).AnyTimes().Return(&apicontainer.DockerContainer{ Container: &apicontainer.Container{ @@ -206,7 +244,7 @@ func TestStatsEngineMetadataInStatsSets(t *testing.T) { statsContainer.statsQueue.setLastStat(dockerStats[i]) } } - metadata, taskMetrics, err := engine.GetInstanceMetrics() + metadata, taskMetrics, err := engine.GetInstanceMetrics(false) if err != nil { t.Errorf("Error gettting instance metrics: %v", err) } @@ -445,23 +483,27 @@ func TestSynchronizeOnRestart(t *testing.T) { func TestTaskNetworkStatsSet(t *testing.T) { var networkModes = []struct { - ENIs []*apieni.ENI - NetworkMode string - StatsEmpty bool + ENIs []*apieni.ENI + NetworkMode string + ServiceConnectEnabled bool + StatsEmpty bool }{ - {nil, "default", false}, + {nil, DefaultNetworkMode, false, false}, + {nil, DefaultNetworkMode, true, true}, } for _, tc := range networkModes { - testNetworkModeStats(t, tc.NetworkMode, tc.ENIs, tc.StatsEmpty) + testNetworkModeStats(t, tc.NetworkMode, tc.ENIs, tc.ServiceConnectEnabled, tc.StatsEmpty) } } -func testNetworkModeStats(t *testing.T, netMode string, enis []*apieni.ENI, emptyStats bool) { +func testNetworkModeStats(t *testing.T, netMode string, enis []*apieni.ENI, serviceConnectEnabled, emptyStats bool) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() resolver := mock_resolver.NewMockContainerMetadataResolver(mockCtrl) mockDockerClient := mock_dockerapi.NewMockDockerClient(mockCtrl) - + if netMode == DefaultNetworkMode { + netMode = BridgeNetworkMode + } testContainer := &apicontainer.DockerContainer{ Container: &apicontainer.Container{ Name: "test", @@ -469,17 +511,23 @@ func testNetworkModeStats(t *testing.T, netMode string, enis []*apieni.ENI, empt Type: apicontainer.ContainerCNIPause, }, } - t1 := &apitask.Task{ Arn: "t1", Family: "f1", ENIs: enis, KnownStatusUnsafe: apitaskstatus.TaskRunning, + NetworkMode: netMode, Containers: []*apicontainer.Container{ {Name: "test"}, {Name: "test1"}, + {Name: SCContainerName}, }, } + if serviceConnectEnabled { + t1.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: SCContainerName, + } + } resolver.EXPECT().ResolveTask("c1").AnyTimes().Return(t1, nil) resolver.EXPECT().ResolveTaskByARN(gomock.Any()).Return(t1, nil).AnyTimes() @@ -513,7 +561,7 @@ func testNetworkModeStats(t *testing.T, netMode string, enis []*apieni.ENI, empt statsContainer.statsQueue.setLastStat(dockerStats[i]) } } - _, taskMetrics, err := engine.GetInstanceMetrics() + _, taskMetrics, err := engine.GetInstanceMetrics(false) assert.NoError(t, err) assert.Len(t, taskMetrics, 1) for _, containerMetric := range taskMetrics[0].ContainerMetrics { diff --git a/agent/stats/engine_unix_integ_test.go b/agent/stats/engine_unix_integ_test.go index 578e571979c..502fdde87f0 100644 --- a/agent/stats/engine_unix_integ_test.go +++ b/agent/stats/engine_unix_integ_test.go @@ -1,4 +1,5 @@ //go:build !windows && integration +// +build !windows,integration // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,7 +17,40 @@ package stats import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "path/filepath" "testing" + "time" + + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + ecsengine "github.com/aws/amazon-ecs-agent/agent/engine" + "github.com/aws/amazon-ecs-agent/agent/engine/dockerstate" + "github.com/docker/docker/api/types" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testStatsRestPath = "/get/stats" + testStatsRestURL = "http://testhost" + testStatsRestPath +) + +const ( + stats = `# TYPE MetricFamily3 histogram + MetricFamily3{dimensionX="value1", dimensionY="value2", le="0.5"} 1 + MetricFamily3{dimensionX="value1", dimensionY="value2", le="1"} 2 + MetricFamily3{dimensionX="value1", dimensionY="value2", le="5"} 3 + ` ) func TestStatsEngineWithNetworkStatsDifferentModes(t *testing.T) { @@ -29,6 +63,127 @@ func TestStatsEngineWithNetworkStatsDifferentModes(t *testing.T) { {"none", true}, } for _, tc := range networkModes { - testNetworkModeStats(t, tc.NetworkMode, tc.StatsEmpty) + testNetworkModeStatsInteg(t, tc.NetworkMode, tc.StatsEmpty) + } +} + +func TestStatsEngineWithServiceConnectMetrics(t *testing.T) { + testcases := []struct { + name string + shouldDisableMetrics bool + }{ + { + name: "Test Stats engine for Service Connect task with metrics enabled", + }, + { + name: "Test Stats engine for Service Connect task with metrics disabled", + shouldDisableMetrics: true, + }, + } + testUDSPath := filepath.Join(t.TempDir(), "test_stats_metrics.sock") + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + testConfig := cfg + if tc.shouldDisableMetrics { + testConfig.DisableMetrics = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} + } + + // Create a new docker stats engine + engine := NewDockerStatsEngine(&testConfig, dockerClient, eventStream("TestStatsEngineWithServiceConnectMetrics")) + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + // Assign ContainerStop timeout to addressable variable + timeout := defaultDockerTimeoutSeconds + + // Create a container to get the container id. + container, err := createGremlin(client, "default") + require.NoError(t, err, "creating container failed") + defer client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true}) + + engine.cluster = defaultCluster + engine.containerInstanceArn = defaultContainerInstance + + err = client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}) + require.NoError(t, err, "starting container failed") + defer client.ContainerStop(ctx, container.ID, &timeout) + + containerChangeEventStream := eventStream("TestStatsEngineWithServiceConnectMetrics") + taskEngine := ecsengine.NewTaskEngine(&config.Config{}, nil, nil, containerChangeEventStream, + nil, dockerstate.NewTaskEngineState(), nil, nil, nil, nil) + testTask := createRunningTask("bridge") + testTask.ServiceConnectConfig = &serviceconnect.Config{ + ContainerName: serviceConnectContainerName, + RuntimeConfig: serviceconnect.RuntimeConfig{ + AdminSocketPath: testUDSPath, + StatsRequest: testStatsRestURL, + }, + } + // Populate Tasks and Container map in the engine. + dockerTaskEngine := taskEngine.(*ecsengine.DockerTaskEngine) + dockerTaskEngine.State().AddTask(testTask) + dockerTaskEngine.State().AddContainer( + &apicontainer.DockerContainer{ + DockerID: container.ID, + DockerName: "gremlin", + Container: testTask.Containers[0], + }, + testTask) + + // Simulate container start prior to listener initialization. + time.Sleep(checkPointSleep) + err = engine.MustInit(ctx, taskEngine, defaultCluster, defaultContainerInstance) + require.NoError(t, err, "initializing stats engine failed") + serviceConnectStats, err := newServiceConnectStats() + require.NoError(t, err, "expected no error") + engine.taskToServiceConnectStats[taskArn] = serviceConnectStats + assert.Equal(t, 1, len(engine.taskToServiceConnectStats)) + + defer engine.containerChangeEventStream.Unsubscribe(containerChangeHandler) + + // simulate appnet server providing service connect metrics + r := mux.NewRouter() + r.HandleFunc(testStatsRestPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%v", stats) + })) + ts := httptest.NewUnstartedServer(r) + l, err := net.Listen("unix", testUDSPath) + require.NoError(t, err) + + ts.Listener.Close() + ts.Listener = l + ts.Start() + defer ts.Close() + + // Wait for the stats collection go routine to start. + time.Sleep(checkPointSleep) + if tc.shouldDisableMetrics { + validateInstanceMetricsWithDisabledMetrics(t, engine, true) + } else { + validateInstanceMetrics(t, engine, true) + } + scStats := engine.taskToServiceConnectStats[taskArn] + require.True(t, scStats.sent, "expected service connect metrics sent flag to be set") + validateEmptyTaskHealthMetrics(t, engine) + + err = client.ContainerStop(ctx, container.ID, &timeout) + require.NoError(t, err, "stopping container failed") + + err = engine.containerChangeEventStream.WriteToEventStream(dockerapi.DockerContainerChangeEvent{ + Status: apicontainerstatus.ContainerStopped, + DockerContainerMetadata: dockerapi.DockerContainerMetadata{ + DockerID: container.ID, + }, + }) + assert.NoError(t, err, "failed to write to container change event stream") + + time.Sleep(waitForCleanupSleep) + + // Should not contain any metrics after cleanup. + if !tc.shouldDisableMetrics { + validateIdleContainerMetrics(t, engine) + } + validateEmptyTaskHealthMetrics(t, engine) + }) } } diff --git a/agent/stats/engine_unix_test.go b/agent/stats/engine_unix_test.go index 657c9999ec5..bb8448249d1 100644 --- a/agent/stats/engine_unix_test.go +++ b/agent/stats/engine_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -21,8 +22,10 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/config" mock_dockerapi "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi/mocks" mock_resolver "github.com/aws/amazon-ecs-agent/agent/stats/resolver/mock" "github.com/docker/docker/api/types" @@ -36,13 +39,13 @@ func TestLinuxTaskNetworkStatsSet(t *testing.T) { NetworkMode string StatsEmpty bool }{ - {[]*apieni.ENI{{ID: "ec2Id"}}, "", true}, + {[]*apieni.ENI{{ID: "ec2Id"}}, "awsvpc", true}, {nil, "host", true}, {nil, "bridge", false}, {nil, "none", true}, } for _, tc := range networkModes { - testNetworkModeStats(t, tc.NetworkMode, tc.ENIs, tc.StatsEmpty) + testNetworkModeStats(t, tc.NetworkMode, tc.ENIs, false, tc.StatsEmpty) } } @@ -69,6 +72,7 @@ func TestNetworkModeStatsAWSVPCMode(t *testing.T) { Arn: "t1", Family: "f1", ENIs: []*apieni.ENI{{ID: "ec2Id"}}, + NetworkMode: apitask.AWSVPCNetworkMode, KnownStatusUnsafe: apitaskstatus.TaskRunning, Containers: []*apicontainer.Container{ {Name: "test"}, @@ -113,7 +117,7 @@ func TestNetworkModeStatsAWSVPCMode(t *testing.T) { taskContainers.StatsQueue.add(containerStats[i]) } } - _, taskMetrics, err := engine.GetInstanceMetrics() + _, taskMetrics, err := engine.GetInstanceMetrics(false) assert.NoError(t, err) assert.Len(t, taskMetrics, 1) for _, containerMetric := range taskMetrics[0].ContainerMetrics { @@ -124,3 +128,41 @@ func TestNetworkModeStatsAWSVPCMode(t *testing.T) { } } } + +func TestServiceConnectWithDisabledMetrics(t *testing.T) { + disableMetricsConfig := cfg + disableMetricsConfig.DisableMetrics = config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled} + containerID := "containerID" + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + container := apicontainer.Container{ + Name: "service-connect", + HealthCheckType: "docker", + } + resolver := mock_resolver.NewMockContainerMetadataResolver(mockCtrl) + resolver.EXPECT().ResolveTask(containerID).Return(&apitask.Task{ + Arn: "t1", + KnownStatusUnsafe: apitaskstatus.TaskRunning, + Family: "f1", + ServiceConnectConfig: &serviceconnect.Config{ + ContainerName: "service-connect", + }, + Containers: []*apicontainer.Container{&container}, + }, nil) + resolver.EXPECT().ResolveContainer(containerID).Return(&apicontainer.DockerContainer{ + DockerID: containerID, + Container: &container, + }, nil).Times(2) + + engine := NewDockerStatsEngine(&disableMetricsConfig, nil, eventStream("TestServiceConnectWithDisabledMetrics")) + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + engine.ctx = ctx + engine.resolver = resolver + engine.addAndStartStatsContainer(containerID) + + assert.Len(t, engine.tasksToContainers, 0, "No containers should be tracked if metrics is disabled") + assert.Len(t, engine.tasksToHealthCheckContainers, 1) + assert.Len(t, engine.taskToServiceConnectStats, 1) +} diff --git a/agent/stats/mock/engine.go b/agent/stats/mock/engine.go index 3ffcb5b5c43..05e956d0baf 100644 --- a/agent/stats/mock/engine.go +++ b/agent/stats/mock/engine.go @@ -20,6 +20,7 @@ package mock_stats import ( reflect "reflect" + time "time" stats "github.com/aws/amazon-ecs-agent/agent/stats" ecstcs "github.com/aws/amazon-ecs-agent/agent/tcs/model/ecstcs" @@ -67,9 +68,9 @@ func (mr *MockEngineMockRecorder) ContainerDockerStats(arg0, arg1 interface{}) * } // GetInstanceMetrics mocks base method -func (m *MockEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (m *MockEngine) GetInstanceMetrics(arg0 bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetInstanceMetrics") + ret := m.ctrl.Call(m, "GetInstanceMetrics", arg0) ret0, _ := ret[0].(*ecstcs.MetricsMetadata) ret1, _ := ret[1].([]*ecstcs.TaskMetric) ret2, _ := ret[2].(error) @@ -77,9 +78,37 @@ func (m *MockEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.Ta } // GetInstanceMetrics indicates an expected call of GetInstanceMetrics -func (mr *MockEngineMockRecorder) GetInstanceMetrics() *gomock.Call { +func (mr *MockEngineMockRecorder) GetInstanceMetrics(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceMetrics", reflect.TypeOf((*MockEngine)(nil).GetInstanceMetrics)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceMetrics", reflect.TypeOf((*MockEngine)(nil).GetInstanceMetrics), arg0) +} + +// GetPublishMetricsTicker mocks base method +func (m *MockEngine) GetPublishMetricsTicker() *time.Ticker { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublishMetricsTicker") + ret0, _ := ret[0].(*time.Ticker) + return ret0 +} + +// GetPublishMetricsTicker indicates an expected call of GetPublishMetricsTicker +func (mr *MockEngineMockRecorder) GetPublishMetricsTicker() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishMetricsTicker", reflect.TypeOf((*MockEngine)(nil).GetPublishMetricsTicker)) +} + +// GetPublishServiceConnectTickerInterval mocks base method +func (m *MockEngine) GetPublishServiceConnectTickerInterval() int32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPublishServiceConnectTickerInterval") + ret0, _ := ret[0].(int32) + return ret0 +} + +// GetPublishServiceConnectTickerInterval indicates an expected call of GetPublishServiceConnectTickerInterval +func (mr *MockEngineMockRecorder) GetPublishServiceConnectTickerInterval() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublishServiceConnectTickerInterval", reflect.TypeOf((*MockEngine)(nil).GetPublishServiceConnectTickerInterval)) } // GetTaskHealthMetrics mocks base method @@ -97,3 +126,15 @@ func (mr *MockEngineMockRecorder) GetTaskHealthMetrics() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskHealthMetrics", reflect.TypeOf((*MockEngine)(nil).GetTaskHealthMetrics)) } + +// SetPublishServiceConnectTickerInterval mocks base method +func (m *MockEngine) SetPublishServiceConnectTickerInterval(arg0 int32) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetPublishServiceConnectTickerInterval", arg0) +} + +// SetPublishServiceConnectTickerInterval indicates an expected call of SetPublishServiceConnectTickerInterval +func (mr *MockEngineMockRecorder) SetPublishServiceConnectTickerInterval(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPublishServiceConnectTickerInterval", reflect.TypeOf((*MockEngine)(nil).SetPublishServiceConnectTickerInterval), arg0) +} diff --git a/agent/stats/queue_test.go b/agent/stats/queue_test.go index d727b7746ee..919c5a35032 100644 --- a/agent/stats/queue_test.go +++ b/agent/stats/queue_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/service_connect_linux.go b/agent/stats/service_connect_linux.go new file mode 100644 index 00000000000..e78fa35453e --- /dev/null +++ b/agent/stats/service_connect_linux.go @@ -0,0 +1,261 @@ +//go:build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package stats + +import ( + "math" + "sort" + "strings" + "sync" + + "github.com/aws/amazon-ecs-agent/agent/api" + "github.com/aws/amazon-ecs-agent/agent/api/appnet" + + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/aws/amazon-ecs-agent/agent/logger/field" + "github.com/aws/amazon-ecs-agent/agent/tcs/model/ecstcs" + prometheus "github.com/prometheus/client_model/go" +) + +type ServiceConnectStats struct { + stats []*ecstcs.GeneralMetricsWrapper + appnetClient api.AppnetClient + sent bool + lock sync.RWMutex +} + +const ( + ingress = "1" + egress = "2" +) + +var directionToMetricType = map[string]string{ + "ingress": ingress, + "egress": egress, +} + +func newServiceConnectStats() (*ServiceConnectStats, error) { + return &ServiceConnectStats{ + appnetClient: appnet.Client(), + }, nil +} + +// TODO [SC]: Add retries on failure to retrieve service connect stats +func (sc *ServiceConnectStats) retrieveServiceConnectStats(task *apitask.Task) { + stats, err := sc.appnetClient.GetStats(task.GetServiceConnectRuntimeConfig()) + if err != nil { + logger.Error("Error retrieving Service Connect stats for task", logger.Fields{ + field.TaskID: task.GetID(), + field.Error: err, + }) + return + } + + statsCollectedList, err := convertToTACSStats(stats, task.GetID()) + if err != nil { + logger.Error("Error parsing service-connect stats", logger.Fields{ + field.TaskID: task.GetID(), + field.Error: err, + }) + return + } + sc.resetStats() + sc.setStats(statsCollectedList) +} + +func convertToTACSStats(mf map[string]*prometheus.MetricFamily, taskId string) ([]*ecstcs.GeneralMetricsWrapper, error) { + statsMap := make(map[string]*ecstcs.GeneralMetricsWrapper) + + for _, v := range mf { + for _, metric := range v.Metric { + var metricValues []*float64 + var metricCounts []*int64 + metricCountForCountersAndGauges := int64(1) + // Get Metric values and counts + switch v.GetType() { + case prometheus.MetricType_COUNTER: + metricValues = append(metricValues, metric.Counter.Value) + // MetricCount for Counter will always be [1] + metricCounts = append(metricCounts, &metricCountForCountersAndGauges) + case prometheus.MetricType_GAUGE: + metricValues = append(metricValues, metric.Gauge.Value) + // MetricCount for Gauge will always be [1] + metricCounts = append(metricCounts, &metricCountForCountersAndGauges) + case prometheus.MetricType_HISTOGRAM: + for _, bucket := range metric.Histogram.Bucket { + // We do not want to add the metricValue if it is +Inf + if math.IsInf(bucket.GetUpperBound(), 0) { + continue + } + metricValues = append(metricValues, bucket.UpperBound) + + // Prometheus histogram CumulativeCount is type *uint64, TACS wants *int64. + var metricCount int64 + if bucket.GetCumulativeCount() <= uint64(math.MaxInt64) { + metricCount = int64(bucket.GetCumulativeCount()) + } else { + metricCount = math.MaxInt64 + logger.Warn("Service Connect histogram metric is emitting a value larger than max int64", logger.Fields{ + field.TaskID: taskId, + "metric": v.Type.String(), + "bucketCumulativeCount": bucket.GetCumulativeCount(), + }) + } + metricCounts = append(metricCounts, &metricCount) + } + + metricValues, metricCounts = convertHistogramMetricCounts(metricValues, metricCounts) + + // If all values are 0 in metricCount, then no need to send the metrics to TACS + if len(metricCounts) == 0 { + logger.Debug("There were no non-zero metricCount received for TargetResponseTime metric. Skipping this metric.", logger.Fields{ + field.TaskID: taskId, + }) + continue + } + + default: + logger.Warn("Service connect stats received invalid Metric type", logger.Fields{ + field.TaskID: taskId, + "metric": v.Type.String(), + }) + continue + } + + generalMetric := &ecstcs.GeneralMetric{} + generalMetric.MetricName = v.Name + generalMetric.MetricValues = metricValues + generalMetric.MetricCounts = metricCounts + + // Get metric dimensions + var dimensions []*ecstcs.Dimension + var metricType string + if metric.Label != nil { + for _, d := range metric.Label { + if *d.Name == "Direction" { + metricType = directionToMetricType[*d.Value] + continue + } + + dimension := &ecstcs.Dimension{ + Key: d.Name, + Value: d.Value, + } + dimensions = append(dimensions, dimension) + } + } + + dimensionAsString := sortAndConvertDimensionsintoStrings(dimensions) + + if generalMetricsWrapper, ok := statsMap[dimensionAsString]; !ok { + // Dimension does not exist in statsMap, add it to the statsMap + generalMetricsList := []*ecstcs.GeneralMetric{generalMetric} + generalMetricsWrapper = &ecstcs.GeneralMetricsWrapper{ + Dimensions: dimensions, + GeneralMetrics: generalMetricsList, + MetricType: &metricType, + } + statsMap[dimensionAsString] = generalMetricsWrapper + } else { + // Add this metric to the metric list for the already existing dimesion + generalMetricsWrapper.GeneralMetrics = append(generalMetricsWrapper.GeneralMetrics, generalMetric) + } + } + } + + statsCollectedList := []*ecstcs.GeneralMetricsWrapper{} + for _, gm := range statsMap { + statsCollectedList = append(statsCollectedList, gm) + } + + return statsCollectedList, nil +} + +func (sc *ServiceConnectStats) setStats(stats []*ecstcs.GeneralMetricsWrapper) { + sc.lock.Lock() + defer sc.lock.Unlock() + + sc.stats = stats +} + +func (sc *ServiceConnectStats) GetStats() []*ecstcs.GeneralMetricsWrapper { + sc.lock.RLock() + defer sc.lock.RUnlock() + + return sc.stats +} + +func (sc *ServiceConnectStats) SetStatsSent(sent bool) { + sc.lock.Lock() + defer sc.lock.Unlock() + + sc.sent = sent +} + +func (sc *ServiceConnectStats) HasStatsBeenSent() bool { + sc.lock.RLock() + defer sc.lock.RUnlock() + + return sc.sent +} + +func (sc *ServiceConnectStats) resetStats() { + sc.lock.Lock() + defer sc.lock.Unlock() + + sc.stats = nil + sc.sent = false +} + +// CloudWatch accepts the histogram buckets in a disjoint manner while the prometheus emits these values in a cumulative way. +// This method performs that conversion. We discard any metricCount that is 0 and also its corresponding metricValue. +func convertHistogramMetricCounts(metricValues []*float64, metricCounts []*int64) ([]*float64, []*int64) { + var mV []*float64 + var mC []*int64 + prevCount := int64(0) + for i := 0; i < len(metricCounts); i++ { + prevCount, *metricCounts[i] = *metricCounts[i], *metricCounts[i]-prevCount + if metricCounts[i] != nil && *metricCounts[i] != 0 { + mV = append(mV, metricValues[i]) + mC = append(mC, metricCounts[i]) + } + } + + return mV, mC +} + +// This method sorts the dimensions according to the keyName. +// This helps to check if a dimension set already has a set of general metrics associated with it. +// Sorting helps us to take order of the dimension list into consideration. +// It then converts dimensions into strings. This is because we cannot +// have a slice as keys in maps +func sortAndConvertDimensionsintoStrings(dimension []*ecstcs.Dimension) string { + sort.Slice(dimension, func(i, j int) bool { + return *dimension[i].Key < *dimension[j].Key + }) + + return convertDimensionsintoStrings(dimension) +} + +func convertDimensionsintoStrings(dimension []*ecstcs.Dimension) string { + var sb strings.Builder + for _, d := range dimension { + sb.WriteString(*d.Key) + sb.WriteString(*d.Value) + } + return sb.String() +} diff --git a/agent/stats/service_connect_linux_test.go b/agent/stats/service_connect_linux_test.go new file mode 100644 index 00000000000..1a78a00e741 --- /dev/null +++ b/agent/stats/service_connect_linux_test.go @@ -0,0 +1,168 @@ +//go:build linux && unit +// +build linux,unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package stats + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "sort" + "testing" + + "github.com/aws/amazon-ecs-agent/agent/api/appnet" + "github.com/aws/amazon-ecs-agent/agent/api/serviceconnect" + + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apieni "github.com/aws/amazon-ecs-agent/agent/api/eni" + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/tcs/model/ecstcs" + "github.com/aws/aws-sdk-go/aws" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func TestRetrieveServiceConnectMetrics(t *testing.T) { + t1 := &apitask.Task{ + Arn: "t1", + Family: "f1", + ENIs: []*apieni.ENI{{ID: "ec2Id"}}, + KnownStatusUnsafe: apitaskstatus.TaskRunning, + Containers: []*apicontainer.Container{ + {Name: "test"}, + }, + LocalIPAddressUnsafe: "127.0.0.1", + ServiceConnectConfig: &serviceconnect.Config{ + RuntimeConfig: serviceconnect.RuntimeConfig{ + AdminSocketPath: "/tmp/appnet_admin.sock", + StatsRequest: "http://myhost/get/them/stats", + }, + }, + } + + var tests = []struct { + stats string + expectedStats []*ecstcs.GeneralMetricsWrapper + }{ + { + stats: `# TYPE MetricFamily1 counter + MetricFamily1{DimensionA="value1", DimensionB="value2", Direction="ingress"} 1 + # TYPE MetricFamily2 counter + MetricFamily2{DimensionB="value2", DimensionA="value1", Direction="ingress"} 1 + `, + expectedStats: []*ecstcs.GeneralMetricsWrapper{ + { + MetricType: aws.String("1"), + Dimensions: []*ecstcs.Dimension{ + { + Key: aws.String("DimensionA"), + Value: aws.String("value1"), + }, { + Key: aws.String("DimensionB"), + Value: aws.String("value2"), + }}, + GeneralMetrics: []*ecstcs.GeneralMetric{ + { + MetricCounts: []*int64{aws.Int64(1)}, + MetricName: aws.String("MetricFamily1"), + MetricValues: []*float64{aws.Float64(1)}, + }, + { + MetricCounts: []*int64{aws.Int64(1)}, + MetricName: aws.String("MetricFamily2"), + MetricValues: []*float64{aws.Float64(1)}, + }, + }, + }, + }, + }, + { + stats: `# TYPE MetricFamily3 histogram + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="0.5"} 1 + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="1"} 1 + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="5"} 3 + `, + expectedStats: []*ecstcs.GeneralMetricsWrapper{ + { + MetricType: aws.String("2"), + Dimensions: []*ecstcs.Dimension{ + { + Key: aws.String("DimensionX"), + Value: aws.String("value1"), + }, { + Key: aws.String("DimensionY"), + Value: aws.String("value2"), + }}, + GeneralMetrics: []*ecstcs.GeneralMetric{ + { + MetricCounts: []*int64{aws.Int64(1), aws.Int64(2)}, + MetricName: aws.String("MetricFamily3"), + MetricValues: []*float64{aws.Float64(0.5), aws.Float64(5)}, + }, + }, + }, + }, + }, + { + stats: `# TYPE MetricFamily3 histogram + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="0.5"} 0 + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="1"} 0 + MetricFamily3{DimensionX="value1", DimensionY="value2", Direction="egress", le="5"} 0 + `, + expectedStats: []*ecstcs.GeneralMetricsWrapper{}, + }, + } + + for _, test := range tests { + func() { + // Set up a mock http sever on the statsUrlpath + mockUDSPath := "/tmp/appnet_admin.sock" + r := mux.NewRouter() + r.HandleFunc("/get/them/stats", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%v", test.stats) + })) + + ts := httptest.NewUnstartedServer(r) + defer ts.Close() + + l, err := net.Listen("unix", mockUDSPath) + assert.NoError(t, err) + + ts.Listener.Close() + ts.Listener = l + ts.Start() + + serviceConnectStats := &ServiceConnectStats{ + appnetClient: appnet.Client(), + } + serviceConnectStats.retrieveServiceConnectStats(t1) + + sortMetrics(serviceConnectStats.GetStats()) + sortMetrics(test.expectedStats) + assert.Equal(t, test.expectedStats, serviceConnectStats.GetStats()) + }() + } +} + +func sortMetrics(metricList []*ecstcs.GeneralMetricsWrapper) { + for _, metric := range metricList { + sort.Slice(metric.GeneralMetrics, func(i, j int) bool { + return *metric.GeneralMetrics[i].MetricName < *metric.GeneralMetrics[j].MetricName + }) + } +} diff --git a/agent/stats/service_connect_unspecified.go b/agent/stats/service_connect_unspecified.go new file mode 100644 index 00000000000..2c10a537b33 --- /dev/null +++ b/agent/stats/service_connect_unspecified.go @@ -0,0 +1,46 @@ +//go:build !linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package stats + +import ( + apitask "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/tcs/model/ecstcs" + "github.com/pkg/errors" +) + +type ServiceConnectStats struct { + stats []*ecstcs.GeneralMetricsWrapper + sent bool +} + +func newServiceConnectStats() (*ServiceConnectStats, error) { + return nil, errors.New("Unsupported platform") +} + +func (sc *ServiceConnectStats) retrieveServiceConnectStats(task *apitask.Task) { +} + +func (sc *ServiceConnectStats) GetStats() []*ecstcs.GeneralMetricsWrapper { + return nil +} + +func (sc *ServiceConnectStats) SetStatsSent(sent bool) { + sc.sent = false +} + +func (sc *ServiceConnectStats) HasStatsBeenSent() bool { + return false +} diff --git a/agent/stats/task_linux.go b/agent/stats/task_linux.go index b7374a6f4bd..7baa4a0fdda 100644 --- a/agent/stats/task_linux.go +++ b/agent/stats/task_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/task_linux_test.go b/agent/stats/task_linux_test.go index 05997589126..1ebedf665dd 100644 --- a/agent/stats/task_linux_test.go +++ b/agent/stats/task_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/task_unspecified.go b/agent/stats/task_unspecified.go index bdb6e442468..cd96cdeb47c 100644 --- a/agent/stats/task_unspecified.go +++ b/agent/stats/task_unspecified.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/task_windows.go b/agent/stats/task_windows.go index 165cab6ca02..6e9d0744a28 100644 --- a/agent/stats/task_windows.go +++ b/agent/stats/task_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,66 +18,43 @@ package stats import ( "context" - "os/exec" - "strconv" - "strings" "time" "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" "github.com/aws/amazon-ecs-agent/agent/stats/resolver" dockerstats "github.com/docker/docker/api/types" "github.com/pkg/errors" ) -const ( - receivedBroadcastPackets = "ReceivedBroadcastPackets" - receivedMulticastPackets = "ReceivedMulticastPackets" - receivedUnicastPackets = "ReceivedUnicastPackets" - sentBroadcastPackets = "SentBroadcastPackets" - sentMulticastPackets = "SentMulticastPackets" - sentUnicastPackets = "SentUnicastPackets" - receivedBytes = "ReceivedBytes" - receivedPacketErrors = "ReceivedPacketErrors" - receivedDiscardedPackets = "ReceivedDiscardedPackets" - sentBytes = "SentBytes" - outboundPacketErrors = "OutboundPacketErrors" - outboundDiscardedPackets = "OutboundDiscardedPackets" -) - -var ( - // Making it visible for unit testing - execCommand = exec.Command - // Fields to be extracted from the stats returned by cmdlet. - networkStatKeys = []string{ - receivedBroadcastPackets, - receivedMulticastPackets, - receivedUnicastPackets, - sentBroadcastPackets, - sentMulticastPackets, - sentUnicastPackets, - receivedBytes, - receivedPacketErrors, - receivedDiscardedPackets, - sentBytes, - outboundDiscardedPackets, - outboundPacketErrors, - } -) - type StatsTask struct { *statsTaskCommon + interfaceLUID []uint64 + netUtils networkutils.NetworkUtils } func newStatsTaskContainer(taskARN, taskId, containerPID string, numberOfContainers int, resolver resolver.ContainerMetadataResolver, publishInterval time.Duration, taskENIs task.TaskENIs) (*StatsTask, error) { - ctx, cancel := context.WithCancel(context.Background()) + + // Instantiate an instance of network utils. + // This interface would be used to invoke Windows networking APIs. + netUtils := networkutils.New() devices := make([]string, len(taskENIs)) + ifaceLUID := make([]uint64, len(taskENIs)) + // Find and store the device name along with the interface LUID. for index, device := range taskENIs { devices[index] = device.LinkName + + interfaceLUID, err := netUtils.ConvertInterfaceAliasToLUID(device.LinkName) + if err != nil { + return nil, errors.Wrapf(err, "failed to initialise stats task container") + } + ifaceLUID[index] = interfaceLUID } + ctx, cancel := context.WithCancel(context.Background()) return &StatsTask{ statsTaskCommon: &statsTaskCommon{ TaskMetadata: &TaskMetadata{ @@ -90,86 +68,53 @@ func newStatsTaskContainer(taskARN, taskId, containerPID string, numberOfContain Resolver: resolver, metricPublishInterval: publishInterval, }, + interfaceLUID: ifaceLUID, + netUtils: netUtils, }, nil } +// retrieveNetworkStatistics retrieves the network statistics for the task devices by querying +// the Windows networking APIs. func (taskStat *StatsTask) retrieveNetworkStatistics() (map[string]dockerstats.NetworkStats, error) { if len(taskStat.TaskMetadata.DeviceName) == 0 { return nil, errors.Errorf("unable to find any device name associated with the task %s", taskStat.TaskMetadata.TaskArn) } networkStats := make(map[string]dockerstats.NetworkStats, len(taskStat.TaskMetadata.DeviceName)) - for _, device := range taskStat.TaskMetadata.DeviceName { - networkAdaptorStatistics, err := taskStat.getNetworkAdaptorStatistics(device) + for index, device := range taskStat.TaskMetadata.DeviceName { + numberOfContainers := taskStat.TaskMetadata.NumberContainers + + // Query the MIB_IF_ROW2 for the given interface LUID which would contain network statistics. + ifaceLUID := taskStat.interfaceLUID[index] + ifRow, err := taskStat.netUtils.GetMIBIfEntryFromLUID(ifaceLUID) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to retrieve network stats") } + + // Parse the retrieved network statistics. + networkAdaptorStatistics := taskStat.parseNetworkStatsPerContainerFromIfRow(ifRow, numberOfContainers) networkStats[device] = *networkAdaptorStatistics } return networkStats, nil } -// getNetworkAdaptorStatistics returns the network statistics per container for the given network interface. -func (taskStat *StatsTask) getNetworkAdaptorStatistics(device string) (*dockerstats.NetworkStats, error) { - // Ref: https://docs.microsoft.com/en-us/powershell/module/netadapter/get-netadapterstatistics?view=windowsserver2019-ps - // The Get-NetAdapterStatistics cmdlet gets networking statistics from a network adapter. - // The statistics include broadcast, multicast, discards, and errors. - cmd := "Get-NetAdapterStatistics -Name \"" + device + "\" | Format-List -Property *" - out, err := execCommand("powershell", "-Command", cmd).CombinedOutput() - - if err != nil { - return nil, errors.Wrapf(err, "failed to run Get-NetAdapterStatistics for %s", device) - } - str := string(out) - - // Extract rawStats from the cmdlet output. - lines := strings.Split(str, "\n") - rawStats := make(map[string]string) - for _, line := range lines { - // populate all the network metrics in a map - kv := strings.Split(line, ":") - if len(kv) != 2 { - continue - } - key := strings.TrimSpace(kv[0]) - value := strings.TrimSpace(kv[1]) - rawStats[key] = value - } - - // Parse the required fields from the generated map. - parsedStats := make(map[string]uint64) - for _, key := range networkStatKeys { - value, err := taskStat.getMapValue(rawStats, key) - if err != nil { - return nil, err - } - parsedStats[key] = value - } - - numberOfContainers := uint64(taskStat.TaskMetadata.NumberContainers) - - return &dockerstats.NetworkStats{ - RxBytes: parsedStats[receivedBytes] / numberOfContainers, - RxPackets: (parsedStats[receivedBroadcastPackets] + parsedStats[receivedMulticastPackets] + parsedStats[receivedUnicastPackets]) / numberOfContainers, - RxErrors: parsedStats[receivedPacketErrors] / numberOfContainers, - RxDropped: parsedStats[receivedDiscardedPackets] / numberOfContainers, - TxBytes: parsedStats[sentBytes] / numberOfContainers, - TxPackets: (parsedStats[sentBroadcastPackets] + parsedStats[sentMulticastPackets] + parsedStats[sentUnicastPackets]) / numberOfContainers, - TxErrors: parsedStats[outboundPacketErrors] / numberOfContainers, - TxDropped: parsedStats[outboundDiscardedPackets] / numberOfContainers, - }, nil -} - -// getMapValue retrieves the value of the key from the given map. -func (taskStat *StatsTask) getMapValue(m map[string]string, key string) (uint64, error) { - v, ok := m[key] - if !ok { - return 0, errors.Errorf("failed to find key: %s in output", key) - } - val, err := strconv.ParseUint(v, 10, 64) - if err != nil { - return 0, errors.Errorf("failed to parse network stats for %s with value: %s", key, v) - } - return val, nil +// parseNetworkStatsPerContainerFromIfRow parses the network statistics from MibIfRow2 row into +// docker network stats. The stats are averaged over all the task containers. +func (taskStat *StatsTask) parseNetworkStatsPerContainerFromIfRow( + iface *networkutils.MibIfRow2, + numberOfContainers int, +) *dockerstats.NetworkStats { + + stats := &dockerstats.NetworkStats{} + stats.RxBytes = iface.InOctets / uint64(numberOfContainers) + stats.RxPackets = (iface.InNUcastPkts + iface.InUcastPkts) / uint64(numberOfContainers) + stats.RxErrors = iface.InErrors / uint64(numberOfContainers) + stats.RxDropped = iface.InDiscards / uint64(numberOfContainers) + stats.TxBytes = iface.OutOctets / uint64(numberOfContainers) + stats.TxPackets = (iface.OutNUcastPkts + iface.OutUcastPkts) / uint64(numberOfContainers) + stats.TxErrors = iface.OutErrors / uint64(numberOfContainers) + stats.TxDropped = iface.OutDiscards / uint64(numberOfContainers) + + return stats } diff --git a/agent/stats/task_windows_test.go b/agent/stats/task_windows_test.go index 874e930efdc..52867d52ece 100644 --- a/agent/stats/task_windows_test.go +++ b/agent/stats/task_windows_test.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,15 +18,14 @@ package stats import ( "context" - "fmt" - "os" - "os/exec" "testing" "time" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" + mock_networkutils "github.com/aws/amazon-ecs-agent/agent/eni/networkutils/mocks" mock_resolver "github.com/aws/amazon-ecs-agent/agent/stats/resolver/mock" dockerstats "github.com/docker/docker/api/types" @@ -34,33 +34,27 @@ import ( ) const ( - networkAdapterStatisticsResult = `ifAlias : Ethernet 3 -InterfaceAlias : Ethernet 3 -InterfaceDescription : Amazon Elastic Network Adapter #2 -Name : Ethernet 3 -Source : 2 -OutboundDiscardedPackets : 30 -OutboundPacketErrors : 20 -ReceivedBroadcastBytes : 247548 -ReceivedBroadcastPackets : 5894 -ReceivedBytes : 249578 -ReceivedDiscardedPackets : 10 -ReceivedMulticastBytes : 0 -ReceivedMulticastPackets : 0 -ReceivedPacketErrors : 4 -ReceivedUnicastBytes : 2030 -ReceivedUnicastPackets : 8 -SentBroadcastBytes : 2858 -SentBroadcastPackets : 26 -SentBytes : 256478 -SentMulticastBytes : 5995 -SentMulticastPackets : 65 -SentUnicastBytes : 247624 -SentUnicastPackets : 5895 -SupportedStatistics : 4163583` + deviceName = "Ethernet 3" + ifaceLUID uint64 = 1689399649632256 ) -var expectedNetworkStats = dockerstats.NetworkStats{ +// Result from GetIfEntry2Ex Win32 API call. +var ifRowResult = &networkutils.MibIfRow2{ + InterfaceLUID: ifaceLUID, + InOctets: 249578, + InUcastPkts: 8, + InNUcastPkts: 5894, + InErrors: 4, + InDiscards: 10, + OutOctets: 256478, + OutUcastPkts: 5895, + OutNUcastPkts: 91, + OutErrors: 20, + OutDiscards: 30, +} + +// Expected output from the stats collection module. +var expectedNetworkStats = &dockerstats.NetworkStats{ RxBytes: 249578, RxPackets: 5902, RxErrors: 4, @@ -73,31 +67,14 @@ var expectedNetworkStats = dockerstats.NetworkStats{ InstanceID: "", } -// Supporting methods for network stats test. -func fakeExecCommandForNetworkStats(command string, args ...string) *exec.Cmd { - cs := []string{"-test.run=TestNetworkStatsProcess", "--", command} - cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} - return cmd -} - -// TestNetworkStatsProcess is invoked from fakeExecCommand to return the network stats. -func TestNetworkStatsProcess(t *testing.T) { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { - return - } - fmt.Fprintf(os.Stdout, networkAdapterStatisticsResult) - os.Exit(0) -} - +// TestTaskStatsCollection tests the network statistics collection. func TestTaskStatsCollection(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() ctx, cancel := context.WithCancel(context.TODO()) resolver := mock_resolver.NewMockContainerMetadataResolver(ctrl) - execCommand = fakeExecCommandForNetworkStats + mockNetUtils := mock_networkutils.NewMockNetworkUtils(ctrl) containerPID := "23" taskId := "task1" @@ -108,7 +85,7 @@ func TestTaskStatsCollection(t *testing.T) { TaskMetadata: &TaskMetadata{ TaskArn: taskId, ContainerPID: containerPID, - DeviceName: []string{"Ethernet 3"}, + DeviceName: []string{deviceName}, NumberContainers: numberOfContainers, }, Ctx: ctx, @@ -116,6 +93,8 @@ func TestTaskStatsCollection(t *testing.T) { Resolver: resolver, metricPublishInterval: time.Second, }, + netUtils: mockNetUtils, + interfaceLUID: []uint64{ifaceLUID}, } testTask := &apitask.Task{ @@ -126,6 +105,7 @@ func TestTaskStatsCollection(t *testing.T) { KnownStatusUnsafe: apitaskstatus.TaskRunning, } resolver.EXPECT().ResolveTaskByARN(gomock.Any()).Return(testTask, nil).AnyTimes() + mockNetUtils.EXPECT().GetMIBIfEntryFromLUID(ifaceLUID).Return(ifRowResult, nil).AnyTimes() taskStats.StartStatsCollection() time.Sleep(checkPointSleep) diff --git a/agent/stats/utils_test.go b/agent/stats/utils_test.go index 3210c0eed8e..bdca0e169a4 100644 --- a/agent/stats/utils_test.go +++ b/agent/stats/utils_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/utils_unix.go b/agent/stats/utils_unix.go index c89bf5af6cf..b4f384c90a3 100644 --- a/agent/stats/utils_unix.go +++ b/agent/stats/utils_unix.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -18,6 +19,7 @@ package stats import ( "fmt" + "github.com/aws/amazon-ecs-agent/agent/config" "github.com/cihub/seelog" "github.com/docker/docker/api/types" ) @@ -25,7 +27,7 @@ import ( // dockerStatsToContainerStats returns a new object of the ContainerStats object from docker stats. func dockerStatsToContainerStats(dockerStats *types.StatsJSON) (*ContainerStats, error) { cpuUsage := dockerStats.CPUStats.CPUUsage.TotalUsage / numCores - memoryUsage := dockerStats.MemoryStats.Usage - dockerStats.MemoryStats.Stats["cache"] + memoryUsage := getMemUsage(dockerStats.MemoryStats) storageReadBytes, storageWriteBytes := getStorageStats(dockerStats) networkStats := getNetworkStats(dockerStats) return &ContainerStats{ @@ -38,10 +40,32 @@ func dockerStatsToContainerStats(dockerStats *types.StatsJSON) (*ContainerStats, }, nil } +func getMemUsage(mem types.MemoryStats) uint64 { + if config.CgroupV2 { + // for cgroupv2 systems, mem usage calculation uses the same method that the docker cli uses + // https://github.com/docker/cli/blob/e198123693b1aaa724041fff602c7d75c8fe4b57/cli/command/container/stats_helpers.go#L227-L249 + // see https://github.com/aws/amazon-ecs-agent/issues/3323 + if v, ok := mem.Stats["inactive_file"]; ok && v < mem.Usage { + return mem.Usage - v + } + } + if v, ok := mem.Stats["cache"]; ok && v < mem.Usage { + return mem.Usage - v + } + return mem.Usage +} + func validateDockerStats(dockerStats *types.StatsJSON) error { - // The length of PercpuUsage represents the number of cores in an instance. - if len(dockerStats.CPUStats.CPUUsage.PercpuUsage) == 0 || numCores == uint64(0) { - return fmt.Errorf("invalid container statistics reported, no cpu core usage reported") + if config.CgroupV2 { + // PercpuUsage is not available in cgroupv2 + if numCores == uint64(0) { + return fmt.Errorf("invalid number of cores returned from runtime.NumCPU, numCores=0") + } + } else { + // The length of PercpuUsage represents the number of cores in an instance. + if len(dockerStats.CPUStats.CPUUsage.PercpuUsage) == 0 || numCores == uint64(0) { + return fmt.Errorf("invalid container statistics reported, no cpu core usage reported") + } } return nil } diff --git a/agent/stats/utils_unix_test.go b/agent/stats/utils_unix_test.go index 84fa3a83a8e..9696e43a465 100644 --- a/agent/stats/utils_unix_test.go +++ b/agent/stats/utils_unix_test.go @@ -1,4 +1,5 @@ //go:build !windows && unit +// +build !windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -52,8 +53,9 @@ func TestDockerStatsToContainerStatsEmptyCpuUsageGeneratesError(t *testing.T) { jsonBytes, _ := ioutil.ReadFile(inputJsonFile) dockerStat := &types.StatsJSON{} json.Unmarshal([]byte(jsonBytes), dockerStat) - // empty the PercpuUsage array - dockerStat.CPUStats.CPUUsage.PercpuUsage = make([]uint64, 0) + prevNumCores := numCores + numCores = uint64(0) err := validateDockerStats(dockerStat) - assert.Error(t, err, "expected error converting container stats with empty PercpuUsage") + assert.Error(t, err, "expected error converting container stats with numCores=0") + numCores = prevNumCores } diff --git a/agent/stats/utils_windows.go b/agent/stats/utils_windows.go index b8b202ffa6f..9e0545720ea 100644 --- a/agent/stats/utils_windows.go +++ b/agent/stats/utils_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/stats/utils_windows_test.go b/agent/stats/utils_windows_test.go index 792c500e8e4..6d611bb2fe9 100644 --- a/agent/stats/utils_windows_test.go +++ b/agent/stats/utils_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/asmauth/asmauth_test.go b/agent/taskresource/asmauth/asmauth_test.go index 09c032e9a60..73f14fbae36 100644 --- a/agent/taskresource/asmauth/asmauth_test.go +++ b/agent/taskresource/asmauth/asmauth_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/asmauth/asmauthstatus_test.go b/agent/taskresource/asmauth/asmauthstatus_test.go index 54882cb7940..e35218bfcdf 100644 --- a/agent/taskresource/asmauth/asmauthstatus_test.go +++ b/agent/taskresource/asmauth/asmauthstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/asmsecret/asmsecret_test.go b/agent/taskresource/asmsecret/asmsecret_test.go index 59fbd0128ff..d1b093a53a6 100644 --- a/agent/taskresource/asmsecret/asmsecret_test.go +++ b/agent/taskresource/asmsecret/asmsecret_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/asmsecret/asmsecretstatus_test.go b/agent/taskresource/asmsecret/asmsecretstatus_test.go index dac469c84fe..b779aea9785 100644 --- a/agent/taskresource/asmsecret/asmsecretstatus_test.go +++ b/agent/taskresource/asmsecret/asmsecretstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/cgroup.go b/agent/taskresource/cgroup/cgroup.go index e4ebcdc78ee..f6dc2007c45 100644 --- a/agent/taskresource/cgroup/cgroup.go +++ b/agent/taskresource/cgroup/cgroup.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -28,6 +29,7 @@ import ( apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/taskresource" control "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control" resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" @@ -71,12 +73,14 @@ type CgroupResource struct { } // NewCgroupResource is used to return an object that implements the Resource interface -func NewCgroupResource(taskARN string, +func NewCgroupResource( + taskARN string, control control.Control, ioutil ioutilwrapper.IOUtil, cgroupRoot string, cgroupMountPath string, - resourceSpec specs.LinuxResources) *CgroupResource { + resourceSpec specs.LinuxResources, +) *CgroupResource { c := &CgroupResource{ taskARN: taskARN, control: control, @@ -256,7 +260,7 @@ func (cgroup *CgroupResource) GetCreatedAt() time.Time { func (cgroup *CgroupResource) Create() error { err := cgroup.setupTaskCgroup() if err != nil { - seelog.Criticalf("Cgroup resource [%s]: unable to setup cgroup root: %v", cgroup.taskARN, err) + // this error is already formatted in setupTaskCgroup function return err } return nil @@ -264,10 +268,9 @@ func (cgroup *CgroupResource) Create() error { func (cgroup *CgroupResource) setupTaskCgroup() error { cgroupRoot := cgroup.cgroupRoot - seelog.Debugf("Cgroup resource [%s]: setting up cgroup at: %s", cgroup.taskARN, cgroupRoot) if cgroup.control.Exists(cgroupRoot) { - seelog.Debugf("Cgroup resource [%s]: cgroup at %s already exists, skipping creation", cgroup.taskARN, cgroupRoot) + seelog.Debugf("Cgroup already exists, skipping creation taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2) return nil } @@ -276,16 +279,19 @@ func (cgroup *CgroupResource) setupTaskCgroup() error { Specs: &cgroup.resourceSpec, } - _, err := cgroup.control.Create(&cgroupSpec) + seelog.Infof("Creating task cgroup taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2) + err := cgroup.control.Create(&cgroupSpec) if err != nil { - return fmt.Errorf("cgroup resource [%s]: setup cgroup: unable to create cgroup at %s: %w", cgroup.taskARN, cgroupRoot, err) + return fmt.Errorf("cgroup resource: setup cgroup: unable to create cgroup taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err) } - // enabling cgroup memory hierarchy by doing 'echo 1 > memory.use_hierarchy' - memoryHierarchyPath := filepath.Join(cgroup.cgroupMountPath, memorySubsystem, cgroupRoot, memoryUseHierarchy) - err = cgroup.ioutil.WriteFile(memoryHierarchyPath, enableMemoryHierarchy, rootReadOnlyPermissions) - if err != nil { - return fmt.Errorf("cgroup resource [%s]: setup cgroup: unable to set use hierarchy flag: %w", cgroup.taskARN, err) + if !config.CgroupV2 { + // enabling cgroup memory hierarchy by doing 'echo 1 > memory.use_hierarchy' + memoryHierarchyPath := filepath.Join(cgroup.cgroupMountPath, memorySubsystem, cgroupRoot, memoryUseHierarchy) + err = cgroup.ioutil.WriteFile(memoryHierarchyPath, enableMemoryHierarchy, rootReadOnlyPermissions) + if err != nil { + return fmt.Errorf("cgroup resource: setup cgroup: unable to set use hierarchy flag taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err) + } } return nil diff --git a/agent/taskresource/cgroup/cgroup_test.go b/agent/taskresource/cgroup/cgroup_test.go index 7aa409a5e33..12f1fc48978 100644 --- a/agent/taskresource/cgroup/cgroup_test.go +++ b/agent/taskresource/cgroup/cgroup_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -21,8 +22,8 @@ import ( "testing" "time" + "github.com/aws/amazon-ecs-agent/agent/config" cgroup "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control" - mock_cgroups "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/factory/mock" "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/mock_control" resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" mock_ioutilwrapper "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper/mocks" @@ -42,6 +43,9 @@ const ( ) func TestCreateHappyPath(t *testing.T) { + if config.CgroupV2 { + t.Skip("Skipping TestCreateHappyPath for CgroupV2 as memory.use_hierarchy is not created when cgroupV2=true") + } ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -53,7 +57,7 @@ func TestCreateHappyPath(t *testing.T) { gomock.InOrder( mockControl.EXPECT().Exists(gomock.Any()).Return(false), - mockControl.EXPECT().Create(gomock.Any()).Return(nil, nil), + mockControl.EXPECT().Create(gomock.Any()).Return(nil), mockIO.EXPECT().WriteFile(cgroupMemoryPath, gomock.Any(), gomock.Any()).Return(nil), ) cgroupResource := NewCgroupResource("taskArn", mockControl, mockIO, cgroupRoot, cgroupMountPath, specs.LinuxResources{}) @@ -68,6 +72,9 @@ func TestCreateCgroupPathExists(t *testing.T) { mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } gomock.InOrder( mockControl.EXPECT().Exists(gomock.Any()).Return(true), @@ -83,13 +90,15 @@ func TestCreateCgroupError(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) - mockCgroup := mock_cgroups.NewMockCgroup(ctrl) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } gomock.InOrder( mockControl.EXPECT().Exists(gomock.Any()).Return(false), - mockControl.EXPECT().Create(gomock.Any()).Return(mockCgroup, errors.New("cgroup create error")), + mockControl.EXPECT().Create(gomock.Any()).Return(errors.New("cgroup create error")), ) cgroupResource := NewCgroupResource("taskArn", mockControl, mockIO, cgroupRoot, cgroupMountPath, specs.LinuxResources{}) @@ -102,6 +111,9 @@ func TestCleanupHappyPath(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } mockControl.EXPECT().Remove(cgroupRoot).Return(nil) @@ -115,6 +127,9 @@ func TestCleanupRemoveError(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } mockControl.EXPECT().Remove(gomock.Any()).Return(errors.New("cgroup remove error")) @@ -128,6 +143,9 @@ func TestCleanupCgroupDeletedError(t *testing.T) { mockControl := mock_control.NewMockControl(ctrl) cgroupRoot := fmt.Sprintf("/ecs/%s", taskID) + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", taskID) + } err := cgroups.ErrCgroupDeleted wrappedErr := fmt.Errorf("cgroup remove: unable to obtain controller: %w", err) @@ -145,6 +163,11 @@ func TestMarshal(t *testing.T) { "\"createdAt\":\"0001-01-01T00:00:00Z\",\"desiredStatus\":\"CREATED\",\"knownStatus\":\"NONE\",\"resourceSpec\":{}}" cgroupRoot := "/ecs/taskid" + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", "taskid") + cgroupStr = "{\"cgroupRoot\":\"ecstasks-taskid.slice\",\"cgroupMountPath\":\"/sys/fs/cgroup\"," + + "\"createdAt\":\"0001-01-01T00:00:00Z\",\"desiredStatus\":\"CREATED\",\"knownStatus\":\"NONE\",\"resourceSpec\":{}}" + } cgroupMountPath := "/sys/fs/cgroup" cgroup := NewCgroupResource("", cgroup.New(), nil, cgroupRoot, cgroupMountPath, specs.LinuxResources{}) @@ -161,6 +184,14 @@ func TestUnmarshal(t *testing.T) { cgroupMountPath := "/sys/fs/cgroup" bytes := []byte("{\"CgroupRoot\":\"/ecs/taskid\",\"CgroupMountPath\":\"/sys/fs/cgroup\"," + "\"CreatedAt\":\"0001-01-01T00:00:00Z\",\"DesiredStatus\":\"CREATED\",\"KnownStatus\":\"NONE\"}") + + if config.CgroupV2 { + cgroupRoot = fmt.Sprintf("ecstasks-%s.slice", "taskid") + bytes = []byte("{\"CgroupRoot\":\"ecstasks-taskid.slice\",\"CgroupMountPath\":\"/sys/fs/cgroup\"," + + "\"CreatedAt\":\"0001-01-01T00:00:00Z\",\"DesiredStatus\":\"CREATED\",\"KnownStatus\":\"NONE\"}") + + } + unmarshalledCgroup := &CgroupResource{} err := unmarshalledCgroup.UnmarshalJSON(bytes) assert.NoError(t, err) diff --git a/agent/taskresource/cgroup/cgroup_unsupported.go b/agent/taskresource/cgroup/cgroup_unsupported.go index 6208687b7b0..544130a606c 100644 --- a/agent/taskresource/cgroup/cgroup_unsupported.go +++ b/agent/taskresource/cgroup/cgroup_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux +// +build !linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/cgroupstatus.go b/agent/taskresource/cgroup/cgroupstatus.go index 156e7ac6788..6e6e0df8c50 100644 --- a/agent/taskresource/cgroup/cgroupstatus.go +++ b/agent/taskresource/cgroup/cgroupstatus.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/cgroupstatus_test.go b/agent/taskresource/cgroup/cgroupstatus_test.go index 482e4535104..065b3df2456 100644 --- a/agent/taskresource/cgroup/cgroupstatus_test.go +++ b/agent/taskresource/cgroup/cgroupstatus_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/control/cgroup_controller_linux.go b/agent/taskresource/cgroup/control/cgroup_controller_linux.go index 915363b1a6f..ff124f7a1b1 100644 --- a/agent/taskresource/cgroup/control/cgroup_controller_linux.go +++ b/agent/taskresource/cgroup/control/cgroup_controller_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -18,7 +19,9 @@ package control import ( "fmt" + "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/factory" + specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/cihub/seelog" "github.com/containerd/cgroups" @@ -32,6 +35,9 @@ type control struct { // New is used to obtain a new cgroup control object func New() Control { + if config.CgroupV2 { + return &controlv2{} + } return newControl(&factory.GlobalCgroupFactory{}) } @@ -43,27 +49,25 @@ func newControl(cgroupFact factory.CgroupFactory) Control { } // Create creates a new cgroup based off the spec post validation -func (c *control) Create(cgroupSpec *Spec) (cgroups.Cgroup, error) { +func (c *control) Create(cgroupSpec *Spec) error { // Validate incoming spec err := validateCgroupSpec(cgroupSpec) if err != nil { - return nil, fmt.Errorf("cgroup create: failed to validate spec: %w", err) + return fmt.Errorf("cgroup create: failed to validate spec: %w", err) } - // Create cgroup - seelog.Infof("Creating cgroup %s", cgroupSpec.Root) - controller, err := c.New(cgroups.V1, cgroups.StaticPath(cgroupSpec.Root), cgroupSpec.Specs) - + seelog.Debugf("Creating cgroup cgroupPath=%s", cgroupSpec.Root) + _, err = c.New(cgroups.V1, cgroups.StaticPath(cgroupSpec.Root), cgroupSpec.Specs) if err != nil { - return nil, fmt.Errorf("cgroup create: unable to create controller: %w", err) + return fmt.Errorf("cgroup create: unable to create controller: v1: %s", err) } - return controller, nil + return nil } // Remove is used to delete the cgroup func (c *control) Remove(cgroupPath string) error { - seelog.Debugf("Removing cgroup %s", cgroupPath) + seelog.Debugf("Removing cgroup cgroupPath=%s", cgroupPath) controller, err := c.Load(cgroups.V1, cgroups.StaticPath(cgroupPath)) if err != nil { @@ -81,7 +85,7 @@ func (c *control) Remove(cgroupPath string) error { // Exists is used to verify the existence of a cgroup func (c *control) Exists(cgroupPath string) bool { - seelog.Debugf("Checking existence of cgroup: %s", cgroupPath) + seelog.Debugf("Checking existence of cgroup cgroupPath=%s", cgroupPath) controller, err := c.Load(cgroups.V1, cgroups.StaticPath(cgroupPath)) if err != nil || controller == nil { @@ -91,6 +95,19 @@ func (c *control) Exists(cgroupPath string) bool { return true } +// Init is used to setup the cgroup root for ecs +func (c *control) Init() error { + seelog.Debugf("Creating root ecs cgroup cgroupPath=%s", config.DefaultTaskCgroupV1Prefix) + + // Build cgroup spec + cgroupSpec := &Spec{ + Root: config.DefaultTaskCgroupV1Prefix, + Specs: &specs.LinuxResources{}, + } + err := c.Create(cgroupSpec) + return err +} + // validateCgroupSpec checks the cgroup spec for valid path and specifications func validateCgroupSpec(cgroupSpec *Spec) error { if cgroupSpec == nil { diff --git a/agent/taskresource/cgroup/control/cgroup_controller_linux_test.go b/agent/taskresource/cgroup/control/cgroup_controller_linux_test.go index 027f5912e29..2bcbd83651d 100644 --- a/agent/taskresource/cgroup/control/cgroup_controller_linux_test.go +++ b/agent/taskresource/cgroup/control/cgroup_controller_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -44,8 +45,7 @@ func TestCreateHappyCase(t *testing.T) { control := newControl(mockCgroupFactory) - res, err := control.Create(&Spec{testCgroupRoot, testSpecs}) - assert.Equal(t, mockCgroup, res) + err := control.Create(&Spec{testCgroupRoot, testSpecs}) assert.NoError(t, err) } @@ -61,8 +61,7 @@ func TestCreateErrorCase(t *testing.T) { control := newControl(mockCgroupFactory) - res, err := control.Create(&Spec{testCgroupRoot, testSpecs}) - assert.Nil(t, res) + err := control.Create(&Spec{testCgroupRoot, testSpecs}) assert.Error(t, err) } @@ -87,9 +86,8 @@ func TestCreateWithBadSpecs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - control, err := cg.Create(tc.spec) + err := cg.Create(tc.spec) assert.Error(t, err, "Create should return an error") - assert.Nil(t, control, "Create call should not return a controller") }) } } @@ -180,3 +178,30 @@ func TestExistsErrorPathWithLoadError(t *testing.T) { assert.False(t, control.Exists(testCgroupRoot)) } + +func TestInitHappyCase(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCgroup := mock_cgroups.NewMockCgroup(ctrl) + mockCgroupFactory := mock_factory.NewMockCgroupFactory(ctrl) + + mockCgroupFactory.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockCgroup, nil) + + control := newControl(mockCgroupFactory) + + assert.NoError(t, control.Init()) +} + +func TestInitErrorCase(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCgroupFactory := mock_factory.NewMockCgroupFactory(ctrl) + + mockCgroupFactory.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("cgroup error")) + + control := newControl(mockCgroupFactory) + + assert.Error(t, control.Init()) +} diff --git a/agent/taskresource/cgroup/control/cgroupv2_controller_linux.go b/agent/taskresource/cgroup/control/cgroupv2_controller_linux.go new file mode 100644 index 00000000000..98809ef37d5 --- /dev/null +++ b/agent/taskresource/cgroup/control/cgroupv2_controller_linux.go @@ -0,0 +1,145 @@ +//go:build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package control + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/cihub/seelog" + cgroupsv2 "github.com/containerd/cgroups/v2" +) + +const ( + defaultCgroupv2Path = "/sys/fs/cgroup" + parentCgroupSlice = "/" + // This PID is only used when creating a slice for an existing systemd process. + // When creating a "general slice" that will be used as a parent slice for docker + // containers, then we use a dummy PID of -1. + // see https://github.com/containerd/cgroups/blob/1df78138f1e1e6ee593db155c6b369466f577651/v2/manager.go#L732-L735 + generalSlicePID int = -1 +) + +// controlv2 is used to implement the cgroup Control interface +type controlv2 struct{} + +// Create creates a new cgroup based off the spec post validation +func (c *controlv2) Create(cgroupSpec *Spec) error { + // Validate incoming spec + err := validateCgroupSpec(cgroupSpec) + if err != nil { + return fmt.Errorf("cgroupv2 create: failed to validate spec: %w", err) + } + + cgroupPath := cgroupSpec.Root + seelog.Infof("Creating cgroup cgroupv2root=%s parentSlice=%s cgroupPath=%s", defaultCgroupv2Path, parentCgroupSlice, cgroupPath) + + m, err := cgroupsv2.NewSystemd(parentCgroupSlice, cgroupPath, generalSlicePID, cgroupsv2.ToResources(cgroupSpec.Specs)) + if err != nil { + return fmt.Errorf("cgroupv2 create: unable to create v2 manager: %w", err) + } + + if err := initializeControllers(m); err != nil { + return fmt.Errorf("cgroupv2 create: unable initialize cgroup controllers: %w", err) + } + + return nil +} + +// Remove is used to delete the cgroup +func (c *controlv2) Remove(cgroupPath string) error { + seelog.Infof("Removing cgroup cgroupv2root=%s parentSlice=%s cgroupPath=%s", defaultCgroupv2Path, parentCgroupSlice, cgroupPath) + + m, err := cgroupsv2.LoadSystemd(parentCgroupSlice, cgroupPath) + if err != nil { + return fmt.Errorf("cgroupv2 remove: error loading systemd cgroup: %w", err) + } + err = m.DeleteSystemd() + if err != nil { + return fmt.Errorf("cgroupv2 remove: error deleting systemd cgroup: %w", err) + } + return nil +} + +// Exists is used to verify the existence of a cgroup +func (c *controlv2) Exists(cgroupPath string) bool { + fullCgroupPath := fullCgroupPath(cgroupPath) + seelog.Infof("Checking existence of cgroup cgroupv2root=%s parentSlice=%s cgroupPath=%s fullPath=%s", defaultCgroupv2Path, parentCgroupSlice, cgroupPath, fullCgroupPath) + + _, err := os.Stat(fullCgroupPath) + if os.IsNotExist(err) { + return false + } + if err != nil { + seelog.Errorf("error checking if cgroup exists err=%s", err) + return false + } + return true +} + +// Init is used to setup the cgroup root for ecs +func (c *controlv2) Init() error { + // Load the "root" cgroup and verify cpu and memory cgroup controllers are available. + m, err := cgroupsv2.LoadSystemd("", "") + if err != nil { + return fmt.Errorf("cgroupv2 init: unable to load root cgroup: %w", err) + } + + if err := initializeControllers(m); err != nil { + return err + } + + seelog.Infof("ECS task resource limits cgroupv2 functionality initialized") + return nil +} + +func initializeControllers(manager *cgroupsv2.Manager) error { + // enable cpu and memory cgroup controllers + err := manager.ToggleControllers([]string{"cpu", "memory"}, cgroupsv2.Enable) + if err != nil { + return fmt.Errorf("cgroupv2 init: error enabling cpu and memory controllers: %w", err) + } + + // verify that cpu and memory controllers are available + controllers, err := manager.Controllers() + if err != nil { + return fmt.Errorf("cgroupv2 init: unable to get cgroup controllers: %w", err) + } + if err := validateController("memory", controllers); err != nil { + return fmt.Errorf("cgroupv2 init: unable to validate cgroup controllers: %w", err) + } + if err := validateController("cpu", controllers); err != nil { + return fmt.Errorf("cgroupv2 init: unable to validate cgroup controllers: %w", err) + } + return nil +} + +func validateController(controller string, controllers []string) error { + for _, v := range controllers { + if controller == v { + return nil + } + } + return fmt.Errorf("unable to validate cgroup controllers, did not find %s controller in list of controllers=%v", controller, controllers) +} + +// fullCgroupPath returns the full path on disk to a task cgroup slice. +// example: /sys/fs/cgroup/ecstasks.slice/ecstasks-529630467358463ab6bbba4e73afe704.slice +func fullCgroupPath(cgroupPath string) string { + return filepath.Join(defaultCgroupv2Path, parentCgroupSlice, config.DefaultTaskCgroupV2Prefix+".slice", cgroupPath) +} diff --git a/agent/taskresource/cgroup/control/factory/factory_linux.go b/agent/taskresource/cgroup/control/factory/factory_linux.go index 6e9e3f895d3..6ce587e7ec5 100644 --- a/agent/taskresource/cgroup/control/factory/factory_linux.go +++ b/agent/taskresource/cgroup/control/factory/factory_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/control/factory/generate_mocks_linux.go b/agent/taskresource/cgroup/control/factory/generate_mocks_linux.go index 31143cc3e8b..e36c74e54f7 100644 --- a/agent/taskresource/cgroup/control/factory/generate_mocks_linux.go +++ b/agent/taskresource/cgroup/control/factory/generate_mocks_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/control/generate_mocks_linux.go b/agent/taskresource/cgroup/control/generate_mocks_linux.go index 38219dc0189..7b7a341acaa 100644 --- a/agent/taskresource/cgroup/control/generate_mocks_linux.go +++ b/agent/taskresource/cgroup/control/generate_mocks_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/cgroup/control/init_linux.go b/agent/taskresource/cgroup/control/init_linux.go deleted file mode 100644 index a29da1df472..00000000000 --- a/agent/taskresource/cgroup/control/init_linux.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build linux - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package control - -import ( - "github.com/aws/amazon-ecs-agent/agent/config" - "github.com/aws/amazon-ecs-agent/agent/logger" - - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -// Init is used to setup the cgroup root for ecs -func (c *control) Init() error { - logger.Info("Creating root ecs cgroup", logger.Fields{ - "cgroup": config.DefaultTaskCgroupPrefix, - }) - - // Build cgroup spec - cgroupSpec := &Spec{ - Root: config.DefaultTaskCgroupPrefix, - Specs: &specs.LinuxResources{}, - } - _, err := c.Create(cgroupSpec) - return err -} diff --git a/agent/taskresource/cgroup/control/init_linux_test.go b/agent/taskresource/cgroup/control/init_linux_test.go deleted file mode 100644 index fc218362e8a..00000000000 --- a/agent/taskresource/cgroup/control/init_linux_test.go +++ /dev/null @@ -1,54 +0,0 @@ -//go:build linux && unit - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package control - -import ( - "errors" - "testing" - - mock_cgroups "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/factory/mock" - "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/factory/mock_factory" - - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -func TestInitHappyCase(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockCgroup := mock_cgroups.NewMockCgroup(ctrl) - mockCgroupFactory := mock_factory.NewMockCgroupFactory(ctrl) - - mockCgroupFactory.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockCgroup, nil) - - control := newControl(mockCgroupFactory) - - assert.NoError(t, control.Init()) -} - -func TestInitErrorCase(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockCgroupFactory := mock_factory.NewMockCgroupFactory(ctrl) - - mockCgroupFactory.EXPECT().New(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("cgroup error")) - - control := newControl(mockCgroupFactory) - - assert.Error(t, control.Init()) -} diff --git a/agent/taskresource/cgroup/control/mock_control/mock_cgroup_control_linux.go b/agent/taskresource/cgroup/control/mock_control/mock_cgroup_control_linux.go index 690ac9c6d39..746a06a4a6c 100644 --- a/agent/taskresource/cgroup/control/mock_control/mock_cgroup_control_linux.go +++ b/agent/taskresource/cgroup/control/mock_control/mock_cgroup_control_linux.go @@ -22,7 +22,6 @@ import ( reflect "reflect" control "github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control" - cgroups "github.com/containerd/cgroups" gomock "github.com/golang/mock/gomock" ) @@ -50,12 +49,11 @@ func (m *MockControl) EXPECT() *MockControlMockRecorder { } // Create mocks base method -func (m *MockControl) Create(arg0 *control.Spec) (cgroups.Cgroup, error) { +func (m *MockControl) Create(arg0 *control.Spec) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0) - ret0, _ := ret[0].(cgroups.Cgroup) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret0, _ := ret[0].(error) + return ret0 } // Create indicates an expected call of Create diff --git a/agent/taskresource/cgroup/control/types_linux.go b/agent/taskresource/cgroup/control/types_linux.go index d6c0842609a..ef97f1d5dee 100644 --- a/agent/taskresource/cgroup/control/types_linux.go +++ b/agent/taskresource/cgroup/control/types_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,7 +17,6 @@ package control import ( - "github.com/containerd/cgroups" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -30,7 +30,7 @@ type Spec struct { } type Control interface { - Create(cgroupSpec *Spec) (cgroups.Cgroup, error) + Create(cgroupSpec *Spec) error Remove(cgroupPath string) error Exists(cgroupPath string) bool Init() error diff --git a/agent/taskresource/credentialspec/credentialspec.go b/agent/taskresource/credentialspec/credentialspec.go new file mode 100644 index 00000000000..bb811e01d89 --- /dev/null +++ b/agent/taskresource/credentialspec/credentialspec.go @@ -0,0 +1,362 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package credentialspec + +import ( + "encoding/json" + "sync" + "time" + + apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" + apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" + "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/credentials" + s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" + ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" + "github.com/aws/amazon-ecs-agent/agent/taskresource" + resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" + "github.com/cihub/seelog" + "github.com/pkg/errors" +) + +type CredentialSpecResourceCommon struct { + taskARN string + region string + executionCredentialsID string + credentialsManager credentials.Manager + createdAt time.Time + desiredStatusUnsafe resourcestatus.ResourceStatus + knownStatusUnsafe resourcestatus.ResourceStatus + // appliedStatus is the status that has been "applied" (e.g., we've called some + // operation such as 'Create' on the resource) but we don't yet know that the + // application was successful, which may then change the known status. This is + // used while progressing resource states in progressTask() of task manager + appliedStatus resourcestatus.ResourceStatus + resourceStatusToTransitionFunction map[resourcestatus.ResourceStatus]func() error + // terminalReason should be set for resource creation failures. This ensures + // the resource object carries some context for why provisioning failed. + terminalReason string + terminalReasonOnce sync.Once + // ssmClientCreator is a factory interface that creates new SSM clients. This is + // needed mostly for testing. + ssmClientCreator ssmfactory.SSMClientCreator + // s3ClientCreator is a factory interface that creates new S3 clients. This is + // needed mostly for testing. + s3ClientCreator s3factory.S3ClientCreator + // map to transform credentialspec values, key is an input credentialspec + // Examples: (windows) + // * key := credentialspec:file://credentialspec.json, value := credentialspec=file://credentialspec.json + // * key := credentialspec:s3ARN, value := credentialspec=file://CredentialSpecResourceLocation/s3_taskARN_fileName.json + // * key := credentialspec:ssmARN, value := credentialspec=file://CredentialSpecResourceLocation/ssm_taskARN_param.json + // (linux) + // * key := credentialspec:file://credentialspec.json, value := Path to kerberos tickets on the host machine + // * key := credentialspec:ssmARN, value := Path to kerberos tickets on the host machine + // * key := credentialspec:asmARN, value := Path to kerberos tickets on the host machine + CredSpecMap map[string]string + // The essential map of credentialspecs needed for the containers. It stores the map with the credentialSpecARN as + // the key container name as the value. + // Example item := arn:aws:ssm:us-east-1:XXXXXXXXXXXXX:parameter/x/y/c:container-sql + // This stores the map of a credential spec to corresponding container name + credentialSpecContainerMap map[string]string + // lock is used for fields that are accessed and updated concurrently + lock sync.RWMutex +} + +func (cs *CredentialSpecResource) Initialize(resourceFields *taskresource.ResourceFields, + _ status.TaskStatus, + _ status.TaskStatus) { + + cs.credentialsManager = resourceFields.CredentialsManager + cs.ssmClientCreator = resourceFields.SSMClientCreator + cs.s3ClientCreator = resourceFields.S3ClientCreator + cs.initStatusToTransition() +} + +func (cs *CredentialSpecResource) initStatusToTransition() { + resourceStatusToTransitionFunction := map[resourcestatus.ResourceStatus]func() error{ + resourcestatus.ResourceStatus(CredentialSpecCreated): cs.Create, + } + cs.resourceStatusToTransitionFunction = resourceStatusToTransitionFunction +} + +// GetTerminalReason returns an error string to propagate up through to task +// state change messages +func (cs *CredentialSpecResource) GetTerminalReason() string { + return cs.terminalReason +} + +func (cs *CredentialSpecResource) setTerminalReason(reason string) { + cs.terminalReasonOnce.Do(func() { + seelog.Debugf("credentialspec resource: setting terminal reason for credentialspec resource in task: [%s]", cs.taskARN) + cs.terminalReason = reason + }) +} + +// GetDesiredStatus safely returns the desired status of the task +func (cs *CredentialSpecResource) GetDesiredStatus() resourcestatus.ResourceStatus { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.desiredStatusUnsafe +} + +// SetDesiredStatus safely sets the desired status of the resource +func (cs *CredentialSpecResource) SetDesiredStatus(status resourcestatus.ResourceStatus) { + cs.lock.Lock() + defer cs.lock.Unlock() + + cs.desiredStatusUnsafe = status +} + +// DesiredTerminal returns true if the credentialspec's desired status is REMOVED +func (cs *CredentialSpecResource) DesiredTerminal() bool { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.desiredStatusUnsafe == resourcestatus.ResourceStatus(CredentialSpecRemoved) +} + +// KnownCreated returns true if the credentialspec's known status is CREATED +func (cs *CredentialSpecResource) KnownCreated() bool { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.knownStatusUnsafe == resourcestatus.ResourceStatus(CredentialSpecCreated) +} + +// TerminalStatus returns the last transition state of credentialspec +func (cs *CredentialSpecResource) TerminalStatus() resourcestatus.ResourceStatus { + return resourcestatus.ResourceStatus(CredentialSpecRemoved) +} + +// NextKnownState returns the state that the resource should +// progress to based on its `KnownState`. +func (cs *CredentialSpecResource) NextKnownState() resourcestatus.ResourceStatus { + return cs.GetKnownStatus() + 1 +} + +// ApplyTransition calls the function required to move to the specified status +func (cs *CredentialSpecResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error { + transitionFunc, ok := cs.resourceStatusToTransitionFunction[nextState] + if !ok { + err := errors.Errorf("resource [%s]: transition to %s impossible", cs.GetName(), + cs.StatusString(nextState)) + cs.setTerminalReason(err.Error()) + return err + } + + return transitionFunc() +} + +// SteadyState returns the transition state of the resource defined as "ready" +func (cs *CredentialSpecResource) SteadyState() resourcestatus.ResourceStatus { + return resourcestatus.ResourceStatus(CredentialSpecCreated) +} + +// SetKnownStatus safely sets the currently known status of the resource +func (cs *CredentialSpecResource) SetKnownStatus(status resourcestatus.ResourceStatus) { + cs.lock.Lock() + defer cs.lock.Unlock() + + cs.knownStatusUnsafe = status + cs.updateAppliedStatusUnsafe(status) +} + +// updateAppliedStatusUnsafe updates the resource transitioning status +func (cs *CredentialSpecResource) updateAppliedStatusUnsafe(knownStatus resourcestatus.ResourceStatus) { + if cs.appliedStatus == resourcestatus.ResourceStatus(CredentialSpecStatusNone) { + return + } + + // Check if the resource transition has already finished + if cs.appliedStatus <= knownStatus { + cs.appliedStatus = resourcestatus.ResourceStatus(CredentialSpecStatusNone) + } +} + +// SetAppliedStatus sets the applied status of resource and returns whether +// the resource is already in a transition +func (cs *CredentialSpecResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool { + cs.lock.Lock() + defer cs.lock.Unlock() + + if cs.appliedStatus != resourcestatus.ResourceStatus(CredentialSpecStatusNone) { + // return false to indicate the set operation failed + return false + } + + cs.appliedStatus = status + return true +} + +// GetKnownStatus safely returns the currently known status of the task +func (cs *CredentialSpecResource) GetKnownStatus() resourcestatus.ResourceStatus { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.knownStatusUnsafe +} + +// StatusString returns the string of the cgroup resource status +func (cs *CredentialSpecResource) StatusString(status resourcestatus.ResourceStatus) string { + return CredentialSpecStatus(status).String() +} + +// SetCreatedAt sets the timestamp for resource's creation time +func (cs *CredentialSpecResource) SetCreatedAt(createdAt time.Time) { + if createdAt.IsZero() { + return + } + cs.lock.Lock() + defer cs.lock.Unlock() + + cs.createdAt = createdAt +} + +// GetCreatedAt sets the timestamp for resource's creation time +func (cs *CredentialSpecResource) GetCreatedAt() time.Time { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.createdAt +} + +// getExecutionCredentialsID returns the execution role's credential ID +func (cs *CredentialSpecResource) getExecutionCredentialsID() string { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.executionCredentialsID +} + +// GetName safely returns the name of the resource +func (cs *CredentialSpecResource) GetName() string { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return ResourceName +} + +func (cs *CredentialSpecResource) GetTargetMapping(credSpecInput string) (string, error) { + cs.lock.RLock() + defer cs.lock.RUnlock() + + targetCredSpecMapping, ok := cs.CredSpecMap[credSpecInput] + if !ok { + return "", errors.New("unable to obtain credentialspec mapping") + } + + return targetCredSpecMapping, nil +} + +// CredentialSpecResourceJSON is the json representation of the credentialspec resource +type CredentialSpecResourceJSONCommon struct { + TaskARN string `json:"taskARN"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + DesiredStatus *CredentialSpecStatus `json:"desiredStatus"` + KnownStatus *CredentialSpecStatus `json:"knownStatus"` + CredentialSpecContainerMap map[string]string `json:"CredentialSpecContainerMap"` + CredSpecMap map[string]string `json:"CredSpecMap"` + ExecutionCredentialsID string `json:"executionCredentialsID"` +} + +// MarshalJSON serialises the CredentialSpecResourceJSON struct to JSON +func (cs *CredentialSpecResource) MarshalJSON() ([]byte, error) { + if cs == nil { + return nil, errors.New("credential specresource is nil") + } + createdAt := cs.GetCreatedAt() + + credentialSpecResourceJSON := CredentialSpecResourceJSON{ + CredentialSpecResourceJSONCommon: &CredentialSpecResourceJSONCommon{ + TaskARN: cs.taskARN, + CreatedAt: &createdAt, + DesiredStatus: func() *CredentialSpecStatus { + desiredState := cs.GetDesiredStatus() + s := CredentialSpecStatus(desiredState) + return &s + }(), + KnownStatus: func() *CredentialSpecStatus { + knownState := cs.GetKnownStatus() + s := CredentialSpecStatus(knownState) + return &s + }(), + CredentialSpecContainerMap: cs.credentialSpecContainerMap, + CredSpecMap: cs.getCredSpecMap(), + ExecutionCredentialsID: cs.getExecutionCredentialsID(), + }, + } + cs.MarshallPlatformSpecificFields(&credentialSpecResourceJSON) + return json.Marshal(credentialSpecResourceJSON) +} + +func (cs *CredentialSpecResource) getCredSpecMap() map[string]string { + cs.lock.RLock() + defer cs.lock.RUnlock() + + return cs.CredSpecMap +} + +// UnmarshalJSON deserialises the raw JSON to a CredentialSpecResourceJSON struct +func (cs *CredentialSpecResource) UnmarshalJSON(b []byte) error { + temp := CredentialSpecResourceJSON{ + CredentialSpecResourceJSONCommon: &CredentialSpecResourceJSONCommon{}, + } + + if err := json.Unmarshal(b, &temp); err != nil { + return err + } + + if cs.CredentialSpecResourceCommon == nil { + cs.CredentialSpecResourceCommon = &CredentialSpecResourceCommon{} + } + + if temp.DesiredStatus != nil { + cs.SetDesiredStatus(resourcestatus.ResourceStatus(*temp.DesiredStatus)) + } + if temp.KnownStatus != nil { + cs.SetKnownStatus(resourcestatus.ResourceStatus(*temp.KnownStatus)) + } + if temp.CreatedAt != nil && !temp.CreatedAt.IsZero() { + cs.SetCreatedAt(*temp.CreatedAt) + } + if temp.CredentialSpecContainerMap != nil { + cs.credentialSpecContainerMap = temp.CredentialSpecContainerMap + } + if temp.CredSpecMap != nil { + cs.CredSpecMap = temp.CredSpecMap + } + cs.taskARN = temp.TaskARN + cs.executionCredentialsID = temp.ExecutionCredentialsID + cs.UnmarshallPlatformSpecificFields(temp) + + return nil +} + +// GetAppliedStatus safely returns the currently applied status of the resource +func (cs *CredentialSpecResource) GetAppliedStatus() resourcestatus.ResourceStatus { + return resourcestatus.ResourceStatusNone +} + +func (cs *CredentialSpecResource) DependOnTaskNetwork() bool { + return false +} + +func (cs *CredentialSpecResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus, + dependent resourcestatus.ResourceStatus) { +} + +func (cs *CredentialSpecResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency { + return nil +} diff --git a/agent/taskresource/credentialspec/credentialspec_linux.go b/agent/taskresource/credentialspec/credentialspec_linux.go new file mode 100644 index 00000000000..801646918c0 --- /dev/null +++ b/agent/taskresource/credentialspec/credentialspec_linux.go @@ -0,0 +1,417 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package credentialspec + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/aws/amazon-ecs-agent/agent/s3" + "github.com/aws/amazon-ecs-agent/agent/ssm" + "github.com/aws/amazon-ecs-agent/agent/utils" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/cihub/seelog" + + "github.com/aws/amazon-ecs-agent/agent/credentials" + s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" + ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" + credentialsfetcherclient "github.com/aws/amazon-ecs-agent/agent/taskresource/grpcclient" + "github.com/pkg/errors" +) + +const ( + // envSkipCredentialsFetcherInvocation is an environment setting that can be used to skip + // credentials fetcher daemon invocation. This is useful for integration and + // functional-tests but should not be set for any non-test use-case. + envSkipCredentialsFetcherInvocation = "ZZZ_SKIP_CREDENTIALS_FETCHER_INVOCATION_CHECK_NOT_SUPPORTED_IN_PRODUCTION" +) + +// CredentialSpecResource is the abstraction for credentialspec resources +type CredentialSpecResource struct { + *CredentialSpecResourceCommon + // This stores the identifier associated with the kerberos tickets created for the task + leaseID string + // This stores credspec arn and the corresponding service account name, domain name + // * key := credentialspec:ssmARN, value := corresponding ServiceAccountInfo + // * key := credentialspec:asmARN, value := corresponding ServiceAccountInfo + ServiceAccountInfoMap map[string]ServiceAccountInfo + // This stores credspec contents associated to all the containers of the task + credentialsFetcherRequest []string +} + +// ServiceAccountInfo contains account info associated to a credentialspec +type ServiceAccountInfo struct { + serviceAccountName string + domainName string +} + +// CredentialSpec object schema +type CredentialSpecSchema struct { + CmsPlugins []string `json:"CmsPlugins"` + DomainJoinConfig struct { + Sid string `json:"Sid"` + MachineAccountName string `json:"MachineAccountName"` + GUID string `json:"Guid"` + DNSTreeName string `json:"DnsTreeName"` + DNSName string `json:"DnsName"` + NetBiosName string `json:"NetBiosName"` + } `json:"DomainJoinConfig"` + ActiveDirectoryConfig struct { + GroupManagedServiceAccounts []struct { + Name string `json:"Name"` + Scope string `json:"Scope"` + } `json:"GroupManagedServiceAccounts"` + } `json:"ActiveDirectoryConfig"` +} + +// NewCredentialSpecResource creates a new CredentialSpecResource object +func NewCredentialSpecResource(taskARN, region string, + executionCredentialsID string, + credentialsManager credentials.Manager, + ssmClientCreator ssmfactory.SSMClientCreator, + s3ClientCreator s3factory.S3ClientCreator, + credentialSpecContainerMap map[string]string) (*CredentialSpecResource, error) { + s := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + taskARN: taskARN, + region: region, + credentialsManager: credentialsManager, + executionCredentialsID: executionCredentialsID, + ssmClientCreator: ssmClientCreator, + s3ClientCreator: s3ClientCreator, + CredSpecMap: make(map[string]string), + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: make(map[string]ServiceAccountInfo), + } + s.initStatusToTransition() + return s, nil +} + +// Create is used to retrieve credentialspec resources for a given task +func (cs *CredentialSpecResource) Create() error { + var iamCredentials credentials.IAMRoleCredentials + + executionCredentials, ok := cs.credentialsManager.GetTaskCredentials(cs.getExecutionCredentialsID()) + if ok { + iamCredentials = executionCredentials.GetIAMRoleCredentials() + } + + var wg sync.WaitGroup + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + for credSpecStr := range cs.credentialSpecContainerMap { + credSpecSplit := strings.SplitAfterN(credSpecStr, "credentialspec:", 2) + if len(credSpecSplit) != 2 { + seelog.Errorf("Invalid credentialspec: %s", credSpecStr) + continue + } + + credSpecValue := credSpecSplit[1] + if strings.HasPrefix(credSpecValue, "file://") { + wg.Add(1) + go cs.handleCredentialspecFile(credSpecStr, &wg, errorEvents) + continue + } + + parsedARN, err := arn.Parse(credSpecValue) + if err != nil { + cs.setTerminalReason(err.Error()) + return err + } + parsedARNService := parsedARN.Service + switch parsedARNService { + case "s3": + wg.Add(1) + go cs.handleS3CredentialspecFile(credSpecStr, credSpecValue, iamCredentials, &wg, errorEvents) + case "ssm": + wg.Add(1) + go cs.handleSSMCredentialspecFile(credSpecStr, credSpecValue, iamCredentials, &wg, errorEvents) + default: + err = errors.New("unsupported credentialspec ARN, only s3/ssm ARNs are valid") + cs.setTerminalReason(err.Error()) + return err + } + } + wg.Wait() + close(errorEvents) + if len(errorEvents) > 0 { + var terminalReasons []string + for err := range errorEvents { + terminalReasons = append(terminalReasons, err.Error()) + } + + errorString := strings.Join(terminalReasons, ";") + cs.setTerminalReason(errorString) + return errors.New(errorString) + } + + seelog.Infof("credentials fetcher daemon request: %v", cs.credentialsFetcherRequest) + + // Check if skip credential fetcher invocation check override is present + skipSkipCredentialsFetcherInvocationCheck := utils.ParseBool(os.Getenv(envSkipCredentialsFetcherInvocation), false) + if skipSkipCredentialsFetcherInvocationCheck { + seelog.Info("Skipping credential fetcher invocation based on environment override") + testKrbFilePath := "/tmp/tgt" + os.Create(testKrbFilePath) + // assign temporary variable for test + cs.leaseID = "12345" + for k := range cs.ServiceAccountInfoMap { + cs.CredSpecMap[k] = testKrbFilePath + } + return nil + } + + err := cs.handleKerberosTicketCreation() + if err != nil { + cs.setTerminalReason(err.Error()) + return err + } + + return nil +} + +func (cs *CredentialSpecResource) handleKerberosTicketCreation() error { + // Create kerberos tickets for the gMSA service accounts on the host location /var/credentials-fetcher/krbdir + if len(cs.credentialsFetcherRequest) > 0 { + //set up server connection to communicate with credentials fetcher daemon + conn, err := credentialsfetcherclient.GetGrpcClientConnection() + seelog.Infof("grpc connection: %v", conn) + if err != nil { + seelog.Errorf("failed to connect with credentials fetcher daemon: %s", err) + return err + } + // make the grpc call to add kerberos lease api to create kerberos tickets for the gmsa account + response, err := credentialsfetcherclient.NewCredentialsFetcherClient(conn, time.Minute).AddKerberosLease(context.Background(), cs.credentialsFetcherRequest) + + if err != nil { + seelog.Errorf("failed to create kerberos tickets associated service account, error: %s", err) + cs.setTerminalReason(err.Error()) + return err + } + + cs.leaseID = response.LeaseID + seelog.Infof("credentials fetcher response leaseID: %v", cs.leaseID) + + //update the mapping of credspec ARN to the kerberos ticket location on the container instance + for _, kerberosTicketLocation := range response.KerberosTicketPaths { + for k, v := range cs.ServiceAccountInfoMap { + result := strings.Contains(strings.ToLower(kerberosTicketLocation), strings.ToLower(v.serviceAccountName)) + if result { + cs.CredSpecMap[k] = kerberosTicketLocation + break + } + } + } + } + return nil +} + +func (cs *CredentialSpecResource) handleCredentialspecFile(credentialSpec string, wg *sync.WaitGroup, errorEvents chan error) { + defer wg.Done() + + credSpecSplit := strings.SplitAfterN(credentialSpec, "credentialspec:", 2) + if len(credSpecSplit) != 2 { + seelog.Errorf("Invalid credentialspec: %s", credentialSpec) + err := errors.New("invalid credentialspec file specification") + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + credSpecFile := credSpecSplit[1] + + if !strings.HasPrefix(credSpecFile, "file://") { + err := errors.New("invalid credentialspec file specification") + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + fileName := strings.SplitAfterN(credSpecFile, "file://", 2) + data, err := os.ReadFile(fileName[1]) + if err != nil { + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + credSpecData := string(data) + + cs.updateCredSpecMapping(credentialSpec, credSpecData) +} + +func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentialSpec, credentialSpecS3ARN string, iamCredentials credentials.IAMRoleCredentials, wg *sync.WaitGroup, errorEvents chan error) { + defer wg.Done() + if iamCredentials == (credentials.IAMRoleCredentials{}) { + err := errors.New("credentialspec resource: unable to find execution role credentials") + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + _, err := arn.Parse(credentialSpecS3ARN) + if err != nil { + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + bucket, key, err := s3.ParseS3ARN(credentialSpecS3ARN) + if err != nil { + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + s3Client := cs.s3ClientCreator.NewS3Client(cs.region, iamCredentials) + + credSpecJsonStringUnformatted, err := s3.GetObject(bucket, key, s3Client) + + if err != nil { + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + credSpecJsonStringBytes := &bytes.Buffer{} + json.Compact(credSpecJsonStringBytes, []byte(credSpecJsonStringUnformatted)) + credSpecJsonString := credSpecJsonStringBytes.String() + + cs.updateCredSpecMapping(originalCredentialSpec, credSpecJsonString) +} + +func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredentialSpec, credentialSpecSSMARN string, iamCredentials credentials.IAMRoleCredentials, wg *sync.WaitGroup, errorEvents chan error) { + defer wg.Done() + + if iamCredentials == (credentials.IAMRoleCredentials{}) { + err := errors.New("credentialspec resource: unable to find execution role credentials") + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + parsedARN, err := arn.Parse(credentialSpecSSMARN) + if err != nil { + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + + // An SSM ARN is in the form of arn:aws:ssm:us-west-2:123456789012:parameter/a/b. The parsed ARN value + // would be parameter/a/b. The following code gets the SSM parameter by passing "/a/b" value to the + // GetParametersFromSSM method to retrieve the value in the parameter. + ssmParam := strings.SplitAfterN(parsedARN.Resource, "parameter", 2) + if len(ssmParam) != 2 { + err := fmt.Errorf("the provided SSM parameter:%s is in an invalid format", parsedARN.Resource) + cs.setTerminalReason(err.Error()) + errorEvents <- err + return + } + ssmParams := []string{ssmParam[1]} + + ssmClient := cs.ssmClientCreator.NewSSMClient(cs.region, iamCredentials) + seelog.Errorf("ssm secret resource: retrieving resource for secrets %v in region [%s] in task: [%s]", cs.region, ssmParams) + ssmParamMap, err := ssm.GetSecretsFromSSM(ssmParams, ssmClient) + if err != nil { + errorEvents <- fmt.Errorf("fetching secret data from SSM Parameter Store in %s: %v", ssmParamMap, err) + return + } + + ssmParamData := ssmParamMap[ssmParam[1]] + cs.updateCredSpecMapping(originalCredentialSpec, ssmParamData) +} + +// updateCredSpecMapping updates the mapping of credentialSpec input and the corresponding service account info(serviceAccountName, DomainNAme) +func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, credSpecContent string) { + cs.lock.Lock() + defer cs.lock.Unlock() + + //parse json to extract the service account name and the domain name + var credentialSpecSchema CredentialSpecSchema + + // Unmarshal or Decode the JSON to the interface. + err := json.Unmarshal([]byte(credSpecContent), &credentialSpecSchema) + + if err != nil { + seelog.Errorf("Error unmarshalling credentialspec data %s", credSpecContent) + return + } + + serviceAccountName := credentialSpecSchema.DomainJoinConfig.MachineAccountName + domainName := credentialSpecSchema.DomainJoinConfig.DNSName + + if len(serviceAccountName) > 0 && len(domainName) > 0 { + cs.ServiceAccountInfoMap[credSpecInput] = ServiceAccountInfo{ + serviceAccountName: serviceAccountName, + domainName: domainName, + } + + //build request array for credentials fetcher daemon + cs.credentialsFetcherRequest = append(cs.credentialsFetcherRequest, credSpecContent) + } +} + +// Cleanup removes the credentialSpec created for the task +func (cs *CredentialSpecResource) Cleanup() error { + cs.clearKerberosTickets() + return nil +} + +// clearKerberosTickets cycles through the lease directory in the host machine +// and removes the associated kerberos tickets +func (cs *CredentialSpecResource) clearKerberosTickets() { + cs.lock.Lock() + defer cs.lock.Unlock() + + if cs.leaseID != "" { + //set up server connection to communicate with credentials fetcher daemon + conn, err := credentialsfetcherclient.GetGrpcClientConnection() + if err != nil { + seelog.Errorf("failed to connect with credentials fetcher daemon: %s", err) + } + _, err = credentialsfetcherclient.NewCredentialsFetcherClient(conn, time.Minute).DeleteKerberosLease(context.Background(), cs.leaseID) + if err != nil { + seelog.Errorf("Unable to cleanup kerberos tickets associated with leaseid: %s, error: %s", cs.leaseID, err) + } + } + + for key := range cs.CredSpecMap { + if len(key) > 0 { + delete(cs.CredSpecMap, key) + delete(cs.ServiceAccountInfoMap, key) + } + } +} + +// CredentialSpecResourceJSON is the json representation of the credentialspec resource +type CredentialSpecResourceJSON struct { + *CredentialSpecResourceJSONCommon + LeaseID string `json:"leaseID"` +} + +func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecResourceJSON *CredentialSpecResourceJSON) { + credentialSpecResourceJSON.LeaseID = cs.leaseID +} + +func (cs *CredentialSpecResource) UnmarshallPlatformSpecificFields(credentialSpecResourceJSON CredentialSpecResourceJSON) { + cs.leaseID = credentialSpecResourceJSON.LeaseID +} diff --git a/agent/taskresource/credentialspec/credentialspec_linux_test.go b/agent/taskresource/credentialspec/credentialspec_linux_test.go new file mode 100644 index 00000000000..941ffc6b628 --- /dev/null +++ b/agent/taskresource/credentialspec/credentialspec_linux_test.go @@ -0,0 +1,635 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package credentialspec + +import ( + "io" + "io/ioutil" + "os" + "strings" + "sync" + "testing" + + apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/credentials" + mockcredentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" + mock_s3_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" + mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks" + mockfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" + mockssmiface "github.com/aws/amazon-ecs-agent/agent/ssm/mocks" + "github.com/aws/amazon-ecs-agent/agent/taskresource" + resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" + "github.com/aws/aws-sdk-go/aws" + s3sdk "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +const ( + taskARN = "arn:aws:ecs:us-west-2:123456789012:task/12345-678901234-56789" +) + +func TestClearCredentialSpecDataHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credSpecMapData := map[string]string{ + "ssmARN": "/var/credentials-fetcher/krbdir/123456/webapp01", + "asmARN": "/var/credentials-fetcher/krbdir/123456/webapp02", + } + + credentialsFetcherInfoMap := map[string]ServiceAccountInfo{ + "ssmARN": {serviceAccountName: "webapp01", domainName: "contoso.com"}, + "asmARN": {serviceAccountName: "webapp02", domainName: "contoso.com"}, + } + + credspecRes := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, + ServiceAccountInfoMap: credentialsFetcherInfoMap, + } + + err := credspecRes.Cleanup() + assert.NoError(t, err) + assert.Equal(t, 0, len(credspecRes.CredSpecMap)) + assert.Equal(t, 0, len(credspecRes.ServiceAccountInfoMap)) +} + +func TestInitialize(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + ssmClientCreator := mockfactory.NewMockSSMClientCreator(ctrl) + credspecRes := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + }, + } + credspecRes.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + assert.NotNil(t, credspecRes.credentialsManager) + assert.NotNil(t, credspecRes.ssmClientCreator) + assert.NotNil(t, credspecRes.resourceStatusToTransitionFunction) +} + +func TestHandleSSMCredentialspecFile(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + ssmClientCreator := mockfactory.NewMockSSMClientCreator(ctrl) + mockSSMClient := mockssmiface.NewMockSSMClient(ctrl) + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + + containerName := "webapp" + + credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" + ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" + + credentialSpecContainerMap := map[string]string{ + credentialSpecSSMARN: containerName, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + testData := "{\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{\"Sid\":\"S-1-5-21-4217655605-3681839426-3493040985\",\"MachineAccountName\":\"WebApp01\",\"Guid\":\"af602f85-d754-4eea-9fa8-fd76810485f1\",\"DnsTreeName\":\"contoso.com\",\"DnsName\":\"contoso.com\",\"NetBiosName\":\"contoso\"},\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"WebApp01\",\"Scope\":\"contoso.com\"},{\"Name\":\"WebApp01\",\"Scope\":\"contoso\"}]}}" + ssmClientOutput := &ssm.GetParametersOutput{ + InvalidParameters: []*string{}, + Parameters: []*ssm.Parameter{ + { + Name: aws.String("/test"), + Value: aws.String(testData), + }, + }, + } + expectedKerberosTicketPath := "/var/credentials-fetcher/krbdir/123456/webapp01" + + gomock.InOrder( + ssmClientCreator.EXPECT().NewSSMClient(gomock.Any(), gomock.Any()).Return(mockSSMClient), + mockSSMClient.EXPECT().GetParameters(gomock.Any()).Return(ssmClientOutput, nil).Times(1), + ) + + var wg sync.WaitGroup + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + wg.Add(1) + go cs.handleSSMCredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials, &wg, errorEvents) + + wg.Wait() + close(errorEvents) + err := <-errorEvents + assert.NoError(t, err) + + cs.CredSpecMap[credentialSpecSSMARN] = expectedKerberosTicketPath + + actualKerberosTicketPath, err := cs.GetTargetMapping(credentialSpecSSMARN) + assert.NoError(t, err) + assert.Equal(t, expectedKerberosTicketPath, actualKerberosTicketPath) +} + +func TestHandleSSMCredentialspecFileARNParseErr(t *testing.T) { + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + credentialSpecSSMARN := "arn:aws:ssm:parameter/test" + ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + terminalReason: "failed", + }, + } + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, 1) + go cs.handleSSMCredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials, &wg, errorEvents) + + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.Error(t, err) +} + +func TestHandleSSMCredentialspecFileGetSSMParamErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + ssmClientCreator := mockfactory.NewMockSSMClientCreator(ctrl) + mockSSMClient := mockssmiface.NewMockSSMClient(ctrl) + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" + ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" + + credentialSpecContainerMap := map[string]string{credentialSpecSSMARN: "webapp"} + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + gomock.InOrder( + ssmClientCreator.EXPECT().NewSSMClient(gomock.Any(), gomock.Any()).Return(mockSSMClient), + mockSSMClient.EXPECT().GetParameters(gomock.Any()).Return(nil, errors.New("test-error")).Times(1), + ) + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + go cs.handleSSMCredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials, &wg, errorEvents) + + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.Error(t, err) +} + +func TestHandleS3CredentialSpecFileGetS3SecretValue(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) + mockS3Client := mock_s3.NewMockS3Client(ctrl) + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + + containerName := "webapp" + + credentialSpecS3ARN := "arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + s3CredentialSpec := "credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + + credentialSpecContainerMap := map[string]string{ + credentialSpecS3ARN: containerName, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + expectedKerberosTicketPath := "/var/credentials-fetcher/krbdir/123456/webapp01" + testData := "{\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{\"Sid\":\"S-1-5-21-4217655605-3681839426-3493040985\",\"MachineAccountName\":\"WebApp01\",\"Guid\":\"af602f85-d754-4eea-9fa8-fd76810485f1\",\"DnsTreeName\":\"contoso.com\",\"DnsName\":\"contoso.com\",\"NetBiosName\":\"contoso\"},\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"WebApp01\",\"Scope\":\"contoso.com\"},{\"Name\":\"WebApp01\",\"Scope\":\"contoso\"}]}}" + + s3GetObjectResponse := &s3sdk.GetObjectOutput{ + Body: io.NopCloser(strings.NewReader(testData)), + } + gomock.InOrder( + s3ClientCreator.EXPECT().NewS3Client(gomock.Any(), gomock.Any()).Return(mockS3Client), + mockS3Client.EXPECT().GetObject(gomock.Any()).Return(s3GetObjectResponse, nil).Times(1), + ) + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + go cs.handleS3CredentialspecFile(s3CredentialSpec, credentialSpecS3ARN, iamCredentials, &wg, errorEvents) + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.NoError(t, err) + + cs.CredSpecMap[credentialSpecS3ARN] = expectedKerberosTicketPath + actualKerberosTicketPath, err := cs.GetTargetMapping(credentialSpecS3ARN) + assert.NoError(t, err) + assert.Equal(t, expectedKerberosTicketPath, actualKerberosTicketPath) +} + +func TestHandleS3CredentialSpecFileGetS3SecretValueErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) + mockS3Client := mock_s3.NewMockS3Client(ctrl) + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + + credentialSpecS3ARN := "arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + s3CredentialSpec := "credentialspec:arn:aws:s3:::gmsacredspec/contoso_webapp01.json" + + credentialSpecContainerMap := map[string]string{credentialSpecS3ARN: "webapp"} + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + gomock.InOrder( + s3ClientCreator.EXPECT().NewS3Client(gomock.Any(), gomock.Any()).Return(mockS3Client), + mockS3Client.EXPECT().GetObject(gomock.Any()).Return(nil, errors.New("test-error")).Times(1), + ) + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + go cs.handleS3CredentialspecFile(s3CredentialSpec, credentialSpecS3ARN, iamCredentials, &wg, errorEvents) + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.Error(t, err) +} + +func TestHandleS3CredentialspecFileARNParseErr(t *testing.T) { + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + credentialSpecSSMARN := "arn:aws:s3:::contoso_webapp01.json" + ssmCredentialSpec := "credentialspec:arn:aws:s3:::contoso_webapp01.json" + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + terminalReason: "failed", + }, + } + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, 1) + go cs.handleS3CredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials, &wg, errorEvents) + + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.Error(t, err) +} + +func TestHandleCredentialSpecFile(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testCredSpecFilePath := "/tmp/webapp01.json" + credentialsManager := mockcredentials.NewMockManager(ctrl) + + credentialSpecARN := "file:///tmp/webapp01.json" + CredentialSpec := "credentialspec:file:///tmp/webapp01.json" + + testCredSpecData := []byte(`{ + "CmsPlugins": [ + "ActiveDirectory" + ], + "DomainJoinConfig": { + "Sid": "S-1-5-21-975084816-3050680612-2826754290", + "MachineAccountName": "WebApp01", + "Guid": "92a07e28-bd9f-4bf3-b1f7-0894815a5257", + "DnsTreeName": "contoso.com", + "DnsName": "contoso.com", + "NetBiosName": "contoso" + }, + "ActiveDirectoryConfig": { + "GroupManagedServiceAccounts": [ + { + "Name": "WebApp01", + "Scope": "contoso.com" + } + ] + } +}`) + + writeErr := ioutil.WriteFile(testCredSpecFilePath, testCredSpecData, 0755) + assert.NoError(t, writeErr) + + credentialSpecContainerMap := map[string]string{credentialSpecARN: "webapp"} + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + go cs.handleCredentialspecFile(CredentialSpec, &wg, errorEvents) + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.NoError(t, err) + + expectedOutput := ServiceAccountInfo{ + serviceAccountName: "WebApp01", + domainName: "contoso.com", + } + + assert.Equal(t, cs.ServiceAccountInfoMap[CredentialSpec], expectedOutput) + + // Cleanup the test file + err = os.RemoveAll(testCredSpecFilePath) + assert.NoError(t, err) +} + +func TestHandleCredentialSpecFileErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + + credentialSpecARN := "/tmp/webapp01.json" + CredentialSpec := "credentialspec:/tmp/webapp01.json" + + credentialSpecContainerMap := map[string]string{credentialSpecARN: "webapp"} + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + var wg sync.WaitGroup + wg.Add(1) + errorEvents := make(chan error, len(cs.credentialSpecContainerMap)) + go cs.handleCredentialspecFile(CredentialSpec, &wg, errorEvents) + wg.Wait() + close(errorEvents) + + err := <-errorEvents + assert.Error(t, err) +} + +func TestGetName(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + assert.Equal(t, ResourceName, cs.GetName()) +} + +func TestGetTargetMapping(t *testing.T) { + inputCredSpec := "credentialspec:ssmARN" + outputKerberosTicketPath := "/var/credentials-fetcher/krbdir/123456/webapp01" + + credSpecMapData := map[string]string{ + "credentialspec:ssmARN": "/var/credentials-fetcher/krbdir/123456/webapp01", + } + credentialsFetcherInfoMap := map[string]ServiceAccountInfo{ + "credentialspec:ssmARN": {serviceAccountName: "webapp01", domainName: "contoso.com"}, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, + ServiceAccountInfoMap: credentialsFetcherInfoMap, + } + + targetKerberosTicketPath, err := cs.GetTargetMapping(inputCredSpec) + assert.NoError(t, err) + assert.Equal(t, outputKerberosTicketPath, targetKerberosTicketPath) +} + +func TestGetTargetMappingErr(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: map[string]string{}, + }, + } + + targetKerberosTicketPath, err := cs.GetTargetMapping("testcredspec") + assert.Error(t, err) + assert.Empty(t, targetKerberosTicketPath) +} + +func TestUpdateTargetMapping(t *testing.T) { + inputCredSpec := "credentialspec:ssmARN" + credSpecData := "{\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{\"Sid\":\"S-1-5-21-4217655605-3681839426-3493040985\",\"MachineAccountName\":\"WebApp01\",\"Guid\":\"af602f85-d754-4eea-9fa8-fd76810485f1\",\"DnsTreeName\":\"contoso.com\",\"DnsName\":\"contoso.com\",\"NetBiosName\":\"contoso\"},\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"WebApp01\",\"Scope\":\"contoso.com\"},{\"Name\":\"WebApp01\",\"Scope\":\"contoso\"}]}}" + + credSpecMapData := map[string]string{ + "credentialspec:ssmARN": "/var/credentials-fetcher/krbdir/123456/webapp01", + } + credentialsFetcherInfoMap := map[string]ServiceAccountInfo{ + "credentialspec:ssmARN": {serviceAccountName: "webapp01", domainName: "contoso.com"}, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, + ServiceAccountInfoMap: credentialsFetcherInfoMap, + } + + cs.updateCredSpecMapping(inputCredSpec, credSpecData) + + expectedOutput := ServiceAccountInfo{ + serviceAccountName: "WebApp01", + domainName: "contoso.com", + } + + assert.Equal(t, cs.ServiceAccountInfoMap[inputCredSpec], expectedOutput) +} + +func TestSkipCredentialFetcherInvocation(t *testing.T) { + t.Setenv("ZZZ_SKIP_CREDENTIALS_FETCHER_INVOCATION_CHECK_NOT_SUPPORTED_IN_PRODUCTION", "True") + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mockcredentials.NewMockManager(ctrl) + ssmClientCreator := mockfactory.NewMockSSMClientCreator(ctrl) + mockSSMClient := mockssmiface.NewMockSSMClient(ctrl) + taskRoleCredentials := credentials.TaskIAMRoleCredentials{ + IAMRoleCredentials: credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + }, + } + + containerName := "webapp" + + credentialSpecSSMARN := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" + + credentialSpecContainerMap := map[string]string{ + credentialSpecSSMARN: containerName, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ServiceAccountInfoMap: map[string]ServiceAccountInfo{}, + } + + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + testData := "{\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{\"Sid\":\"S-1-5-21-4217655605-3681839426-3493040985\",\"MachineAccountName\":\"WebApp01\",\"Guid\":\"af602f85-d754-4eea-9fa8-fd76810485f1\",\"DnsTreeName\":\"contoso.com\",\"DnsName\":\"contoso.com\",\"NetBiosName\":\"contoso\"},\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"WebApp01\",\"Scope\":\"contoso.com\"},{\"Name\":\"WebApp01\",\"Scope\":\"contoso\"}]}}" + ssmClientOutput := &ssm.GetParametersOutput{ + InvalidParameters: []*string{}, + Parameters: []*ssm.Parameter{ + { + Name: aws.String("/test"), + Value: aws.String(testData), + }, + }, + } + expectedKerberosTicketPath := "/tmp/tgt" + + gomock.InOrder( + credentialsManager.EXPECT().GetTaskCredentials(gomock.Any()).Return(taskRoleCredentials, true).Times(1), + ssmClientCreator.EXPECT().NewSSMClient(gomock.Any(), gomock.Any()).Return(mockSSMClient), + mockSSMClient.EXPECT().GetParameters(gomock.Any()).Return(ssmClientOutput, nil).Times(1), + ) + + err := cs.Create() + + assert.NoError(t, err) + + cs.CredSpecMap[credentialSpecSSMARN] = expectedKerberosTicketPath + + actualKerberosTicketPath, err := cs.GetTargetMapping(credentialSpecSSMARN) + assert.NoError(t, err) + assert.Equal(t, expectedKerberosTicketPath, actualKerberosTicketPath) +} diff --git a/agent/taskresource/credentialspec/credentialspec_test.go b/agent/taskresource/credentialspec/credentialspec_test.go new file mode 100644 index 00000000000..e9e05df99bd --- /dev/null +++ b/agent/taskresource/credentialspec/credentialspec_test.go @@ -0,0 +1,107 @@ +//go:build unit +// +build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package credentialspec + +import ( + "testing" + "time" + + resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" + "github.com/stretchr/testify/assert" +) + +func TestGetResourceName(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + assert.Equal(t, ResourceName, cs.GetName()) +} + +func TestGetDesiredStatus(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + cs.SetDesiredStatus(resourcestatus.ResourceCreated) + assert.Equal(t, resourcestatus.ResourceCreated, cs.GetDesiredStatus()) +} + +func TestGetTerminalReason(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + reason := "failed to read credentialspec" + cs.setTerminalReason(reason) + assert.Equal(t, reason, cs.GetTerminalReason()) +} + +func TestKnownCreated(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + cs.SetKnownStatus(resourcestatus.ResourceCreated) + assert.True(t, cs.KnownCreated()) +} + +func TestNextKnownState(t *testing.T) { + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + cs.SetKnownStatus(resourcestatus.ResourceCreated) + assert.Equal(t, resourcestatus.ResourceRemoved, cs.NextKnownState()) +} + +func TestCreatedAt(t *testing.T) { + time := time.Now() + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } + + cs.SetCreatedAt(time) + assert.Equal(t, time, cs.GetCreatedAt()) +} + +func TestMarshallandUnMarshallCredSpec(t *testing.T) { + containerName := "webapp" + + credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" + + credentialSpecContainerMap := map[string]string{ + credentialSpecSSMARN: containerName, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + } + + parsedBytes, err := cs.MarshalJSON() + assert.NoError(t, err) + + err = cs.UnmarshalJSON(parsedBytes) + assert.NoError(t, err) +} diff --git a/agent/taskresource/credentialspec/credentialspec_unsupported.go b/agent/taskresource/credentialspec/credentialspec_unsupported.go deleted file mode 100644 index fc48ea5ecdb..00000000000 --- a/agent/taskresource/credentialspec/credentialspec_unsupported.go +++ /dev/null @@ -1,170 +0,0 @@ -//go:build !windows - -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package credentialspec - -import ( - "time" - - apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" - apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" - "github.com/aws/amazon-ecs-agent/agent/api/task/status" - "github.com/aws/amazon-ecs-agent/agent/credentials" - s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" - ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" - "github.com/aws/amazon-ecs-agent/agent/taskresource" - resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" - "github.com/pkg/errors" -) - -// CredentialSpecResource is the abstraction for credentialspec resources -type CredentialSpecResource struct { -} - -// NewCredentialSpecResource creates a new CredentialSpecResource object -func NewCredentialSpecResource(taskARN, region string, - credentialSpecs []string, - executionCredentialsID string, - credentialsManager credentials.Manager, - ssmClientCreator ssmfactory.SSMClientCreator, - s3ClientCreator s3factory.S3ClientCreator) (*CredentialSpecResource, error) { - return nil, errors.New("not supported") -} - -func (cs *CredentialSpecResource) Initialize(resourceFields *taskresource.ResourceFields, - taskKnownStatus status.TaskStatus, - taskDesiredStatus status.TaskStatus) { -} - -// GetTerminalReason returns an error string to propagate up through to task -// state change messages -func (cs *CredentialSpecResource) GetTerminalReason() string { - return "undefined" -} - -// GetDesiredStatus safely returns the desired status of the task -func (cs *CredentialSpecResource) GetDesiredStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -// SetDesiredStatus safely sets the desired status of the resource -func (cs *CredentialSpecResource) SetDesiredStatus(status resourcestatus.ResourceStatus) { -} - -// DesiredTerminal returns true if the credentialspec's desired status is REMOVED -func (cs *CredentialSpecResource) DesiredTerminal() bool { - return false -} - -// KnownCreated returns true if the credentialspec's known status is CREATED -func (cs *CredentialSpecResource) KnownCreated() bool { - return false -} - -// TerminalStatus returns the last transition state of credentialspec -func (cs *CredentialSpecResource) TerminalStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -// NextKnownState returns the state that the resource should -// progress to based on its `KnownState`. -func (cs *CredentialSpecResource) NextKnownState() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -// ApplyTransition calls the function required to move to the specified status -func (cs *CredentialSpecResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error { - return errors.New("not implemented") -} - -// SteadyState returns the transition state of the resource defined as "ready" -func (cs *CredentialSpecResource) SteadyState() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -// SetKnownStatus safely sets the currently known status of the resource -func (cs *CredentialSpecResource) SetKnownStatus(status resourcestatus.ResourceStatus) { -} - -// SetAppliedStatus sets the applied status of resource and returns whether -// the resource is already in a transition -func (cs *CredentialSpecResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool { - return false -} - -// GetKnownStatus safely returns the currently known status of the task -func (cs *CredentialSpecResource) GetKnownStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -// StatusString returns the string of the cgroup resource status -func (cs *CredentialSpecResource) StatusString(status resourcestatus.ResourceStatus) string { - return "undefined" -} - -// SetCreatedAt sets the timestamp for resource's creation time -func (cs *CredentialSpecResource) SetCreatedAt(createdAt time.Time) { -} - -// GetCreatedAt sets the timestamp for resource's creation time -func (cs *CredentialSpecResource) GetCreatedAt() time.Time { - return time.Time{} -} - -// GetName safely returns the name of the resource -func (cs *CredentialSpecResource) GetName() string { - return "undefined" -} - -// Create is used to create all the credentialspec resources for a given task -func (cs *CredentialSpecResource) Create() error { - return errors.New("not implemented") -} - -func (cs *CredentialSpecResource) GetTargetMapping(credSpecInput string) (string, error) { - return "", errors.New("not implemented") -} - -// Cleanup removes the credentialspec created for the task -func (cs *CredentialSpecResource) Cleanup() error { - return errors.New("not implemented") -} - -// MarshalJSON serialises the CredentialSpecResourceJSON struct to JSON -func (cs *CredentialSpecResource) MarshalJSON() ([]byte, error) { - return nil, errors.New("not implemented") -} - -// UnmarshalJSON deserialises the raw JSON to a CredentialSpecResourceJSON struct -func (cs *CredentialSpecResource) UnmarshalJSON(b []byte) error { - return errors.New("not implemented") -} - -// GetAppliedStatus safely returns the currently applied status of the resource -func (cs *CredentialSpecResource) GetAppliedStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -func (cs *CredentialSpecResource) DependOnTaskNetwork() bool { - return false -} - -func (cs *CredentialSpecResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus, - dependent resourcestatus.ResourceStatus) { -} - -func (cs *CredentialSpecResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency { - return nil -} diff --git a/agent/taskresource/credentialspec/credentialspec_windows.go b/agent/taskresource/credentialspec/credentialspec_windows.go index 72ac9d2216c..cec190829b4 100644 --- a/agent/taskresource/credentialspec/credentialspec_windows.go +++ b/agent/taskresource/credentialspec/credentialspec_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,24 +17,18 @@ package credentialspec import ( - "encoding/json" + "crypto/sha256" "fmt" "os" "path/filepath" "strings" - "sync" "time" - apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" - apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" - "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/credentials" "github.com/aws/amazon-ecs-agent/agent/s3" s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" "github.com/aws/amazon-ecs-agent/agent/ssm" ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" - "github.com/aws/amazon-ecs-agent/agent/taskresource" - resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper" "github.com/aws/amazon-ecs-agent/agent/utils/oswrapper" "github.com/aws/aws-sdk-go/aws/arn" @@ -55,63 +50,32 @@ const ( // CredentialSpecResource is the abstraction for credentialspec resources type CredentialSpecResource struct { - taskARN string - region string - executionCredentialsID string - credentialsManager credentials.Manager - ioutil ioutilwrapper.IOUtil - createdAt time.Time - desiredStatusUnsafe resourcestatus.ResourceStatus - knownStatusUnsafe resourcestatus.ResourceStatus - // appliedStatus is the status that has been "applied" (e.g., we've called some - // operation such as 'Create' on the resource) but we don't yet know that the - // application was successful, which may then change the known status. This is - // used while progressing resource states in progressTask() of task manager - appliedStatus resourcestatus.ResourceStatus - resourceStatusToTransitionFunction map[resourcestatus.ResourceStatus]func() error - // terminalReason should be set for resource creation failures. This ensures - // the resource object carries some context for why provisioning failed. - terminalReason string - terminalReasonOnce sync.Once - // ssmClientCreator is a factory interface that creates new SSM clients. This is - // needed mostly for testing. - ssmClientCreator ssmfactory.SSMClientCreator - // s3ClientCreator is a factory interface that creates new S3 clients. This is - // needed mostly for testing. - s3ClientCreator s3factory.S3ClientCreator + *CredentialSpecResourceCommon + ioutil ioutilwrapper.IOUtil // credentialSpecResourceLocation is the location for all the tasks' credentialspec artifacts credentialSpecResourceLocation string - // required for processing credentialspecs - // Example item := credentialspec:file://credentialspec.json - requiredCredentialSpecs []string - // map to transform credentialspec values, key is a input credentialspec - // Examples: - // * key := credentialspec:file://credentialspec.json, value := credentialspec=file://credentialspec.json - // * key := credentialspec:s3ARN, value := credentialspec=file://CredentialSpecResourceLocation/s3_taskARN_fileName.json - // * key := credentialspec:ssmARN, value := credentialspec=file://CredentialSpecResourceLocation/ssm_taskARN_param.json - CredSpecMap map[string]string - // lock is used for fields that are accessed and updated concurrently - lock sync.RWMutex } // NewCredentialSpecResource creates a new CredentialSpecResource object func NewCredentialSpecResource(taskARN, region string, - credentialSpecs []string, executionCredentialsID string, credentialsManager credentials.Manager, ssmClientCreator ssmfactory.SSMClientCreator, - s3ClientCreator s3factory.S3ClientCreator) (*CredentialSpecResource, error) { + s3ClientCreator s3factory.S3ClientCreator, + credentialSpecContainerMap map[string]string) (*CredentialSpecResource, error) { s := &CredentialSpecResource{ - taskARN: taskARN, - region: region, - requiredCredentialSpecs: credentialSpecs, - credentialsManager: credentialsManager, - executionCredentialsID: executionCredentialsID, - ssmClientCreator: ssmClientCreator, - s3ClientCreator: s3ClientCreator, - CredSpecMap: make(map[string]string), - ioutil: ioutilwrapper.NewIOUtil(), + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + taskARN: taskARN, + region: region, + credentialsManager: credentialsManager, + executionCredentialsID: executionCredentialsID, + ssmClientCreator: ssmClientCreator, + s3ClientCreator: s3ClientCreator, + CredSpecMap: make(map[string]string), + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: ioutilwrapper.NewIOUtil(), } err := s.setCredentialSpecResourceLocation() @@ -123,189 +87,6 @@ func NewCredentialSpecResource(taskARN, region string, return s, nil } -func (cs *CredentialSpecResource) initStatusToTransition() { - resourceStatusToTransitionFunction := map[resourcestatus.ResourceStatus]func() error{ - resourcestatus.ResourceStatus(CredentialSpecCreated): cs.Create, - } - cs.resourceStatusToTransitionFunction = resourceStatusToTransitionFunction -} - -func (cs *CredentialSpecResource) Initialize(resourceFields *taskresource.ResourceFields, - taskKnownStatus status.TaskStatus, - taskDesiredStatus status.TaskStatus) { - - cs.credentialsManager = resourceFields.CredentialsManager - cs.ssmClientCreator = resourceFields.SSMClientCreator - cs.s3ClientCreator = resourceFields.S3ClientCreator - cs.initStatusToTransition() -} - -// GetTerminalReason returns an error string to propagate up through to task -// state change messages -func (cs *CredentialSpecResource) GetTerminalReason() string { - return cs.terminalReason -} - -func (cs *CredentialSpecResource) setTerminalReason(reason string) { - cs.terminalReasonOnce.Do(func() { - seelog.Debugf("credentialspec resource: setting terminal reason for credentialspec resource in task: [%s]", cs.taskARN) - cs.terminalReason = reason - }) -} - -// GetDesiredStatus safely returns the desired status of the task -func (cs *CredentialSpecResource) GetDesiredStatus() resourcestatus.ResourceStatus { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.desiredStatusUnsafe -} - -// SetDesiredStatus safely sets the desired status of the resource -func (cs *CredentialSpecResource) SetDesiredStatus(status resourcestatus.ResourceStatus) { - cs.lock.Lock() - defer cs.lock.Unlock() - - cs.desiredStatusUnsafe = status -} - -// DesiredTerminal returns true if the credentialspec's desired status is REMOVED -func (cs *CredentialSpecResource) DesiredTerminal() bool { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.desiredStatusUnsafe == resourcestatus.ResourceStatus(CredentialSpecRemoved) -} - -// KnownCreated returns true if the credentialspec's known status is CREATED -func (cs *CredentialSpecResource) KnownCreated() bool { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.knownStatusUnsafe == resourcestatus.ResourceStatus(CredentialSpecCreated) -} - -// TerminalStatus returns the last transition state of credentialspec -func (cs *CredentialSpecResource) TerminalStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatus(CredentialSpecRemoved) -} - -// NextKnownState returns the state that the resource should -// progress to based on its `KnownState`. -func (cs *CredentialSpecResource) NextKnownState() resourcestatus.ResourceStatus { - return cs.GetKnownStatus() + 1 -} - -// ApplyTransition calls the function required to move to the specified status -func (cs *CredentialSpecResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error { - transitionFunc, ok := cs.resourceStatusToTransitionFunction[nextState] - if !ok { - err := errors.Errorf("resource [%s]: transition to %s impossible", cs.GetName(), - cs.StatusString(nextState)) - cs.setTerminalReason(err.Error()) - return err - } - - return transitionFunc() -} - -// SteadyState returns the transition state of the resource defined as "ready" -func (cs *CredentialSpecResource) SteadyState() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatus(CredentialSpecCreated) -} - -// SetKnownStatus safely sets the currently known status of the resource -func (cs *CredentialSpecResource) SetKnownStatus(status resourcestatus.ResourceStatus) { - cs.lock.Lock() - defer cs.lock.Unlock() - - cs.knownStatusUnsafe = status - cs.updateAppliedStatusUnsafe(status) -} - -// updateAppliedStatusUnsafe updates the resource transitioning status -func (cs *CredentialSpecResource) updateAppliedStatusUnsafe(knownStatus resourcestatus.ResourceStatus) { - if cs.appliedStatus == resourcestatus.ResourceStatus(CredentialSpecStatusNone) { - return - } - - // Check if the resource transition has already finished - if cs.appliedStatus <= knownStatus { - cs.appliedStatus = resourcestatus.ResourceStatus(CredentialSpecStatusNone) - } -} - -// SetAppliedStatus sets the applied status of resource and returns whether -// the resource is already in a transition -func (cs *CredentialSpecResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool { - cs.lock.Lock() - defer cs.lock.Unlock() - - if cs.appliedStatus != resourcestatus.ResourceStatus(CredentialSpecStatusNone) { - // return false to indicate the set operation failed - return false - } - - cs.appliedStatus = status - return true -} - -// GetKnownStatus safely returns the currently known status of the task -func (cs *CredentialSpecResource) GetKnownStatus() resourcestatus.ResourceStatus { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.knownStatusUnsafe -} - -// StatusString returns the string of the cgroup resource status -func (cs *CredentialSpecResource) StatusString(status resourcestatus.ResourceStatus) string { - return CredentialSpecStatus(status).String() -} - -// SetCreatedAt sets the timestamp for resource's creation time -func (cs *CredentialSpecResource) SetCreatedAt(createdAt time.Time) { - if createdAt.IsZero() { - return - } - cs.lock.Lock() - defer cs.lock.Unlock() - - cs.createdAt = createdAt -} - -// GetCreatedAt sets the timestamp for resource's creation time -func (cs *CredentialSpecResource) GetCreatedAt() time.Time { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.createdAt -} - -// getRequiredCredentialSpecs returns the requiredCredentialSpecs field of credentialspec task resource -func (cs *CredentialSpecResource) getRequiredCredentialSpecs() []string { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.requiredCredentialSpecs -} - -// getExecutionCredentialsID returns the execution role's credential ID -func (cs *CredentialSpecResource) getExecutionCredentialsID() string { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.executionCredentialsID -} - -// GetName safely returns the name of the resource -func (cs *CredentialSpecResource) GetName() string { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return ResourceName -} - // Create is used to create all the credentialspec resources for a given task func (cs *CredentialSpecResource) Create() error { var err error @@ -316,7 +97,7 @@ func (cs *CredentialSpecResource) Create() error { iamCredentials = executionCredentials.GetIAMRoleCredentials() } - for _, credSpecStr := range cs.requiredCredentialSpecs { + for credSpecStr := range cs.credentialSpecContainerMap { credSpecSplit := strings.SplitAfterN(credSpecStr, "credentialspec:", 2) if len(credSpecSplit) != 2 { seelog.Errorf("Invalid credentialspec: %s", credSpecStr) @@ -402,7 +183,7 @@ func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentials return err } - s3Client, err := cs.s3ClientCreator.NewS3ClientForBucket(bucket, cs.region, iamCredentials) + s3Client, err := cs.s3ClientCreator.NewS3ManagerClient(bucket, cs.region, iamCredentials) if err != nil { cs.setTerminalReason(err.Error()) return err @@ -445,29 +226,48 @@ func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredential ssmClient := cs.ssmClientCreator.NewSSMClient(cs.region, iamCredentials) - ssmParam := filepath.Base(parsedARN.Resource) - ssmParams := []string{ssmParam} - + // An SSM ARN is in the form of arn:aws:ssm:us-west-2:123456789012:parameter/a/b. The parsed ARN value + // would be parameter/a/b. The following code gets the SSM parameter by passing "/a/b" value to the + // GetParametersFromSSM method to retrieve the value in the parameter. + ssmParam := strings.SplitAfterN(parsedARN.Resource, "parameter", 2) + if len(ssmParam) != 2 { + err := fmt.Errorf("the provided SSM parameter:%s is in an invalid format", parsedARN.Resource) + cs.setTerminalReason(err.Error()) + return err + } + ssmParams := []string{ssmParam[1]} ssmParamMap, err := ssm.GetParametersFromSSM(ssmParams, ssmClient) if err != nil { cs.setTerminalReason(err.Error()) return err } - ssmParamData := ssmParamMap[ssmParam] + ssmParamData := ssmParamMap[ssmParam[1]] taskArnSplit := strings.Split(cs.taskARN, "/") length := len(taskArnSplit) if length < 2 { return errors.New("Failed to retrieve taskId from taskArn.") } - localCredSpecFilePath := fmt.Sprintf("%s\\ssm_%v_%s", cs.credentialSpecResourceLocation, taskArnSplit[length-1], ssmParam) + + taskId := taskArnSplit[length-1] + containerName := cs.credentialSpecContainerMap[originalCredentialspec] + + // We compose a string that is a concatenation of the task_id, container name and the ARN of the credential spec + // SSM parameter. This concatenated string is hashed using the SHA-256 hashing scheme to generate a fixed length + // checksum string of 64 characters. This helps with resolving collisions within a host or a task using the same SSM + // parameter. + credSpecFileNameHashFormat := fmt.Sprintf("%s%s%s", taskId, containerName, credentialspecSSMARN) + hashFunction := sha256.New() + hashFunction.Write([]byte(credSpecFileNameHashFormat)) + customCredSpecFileName := fmt.Sprintf("%x", hashFunction.Sum(nil)) + + localCredSpecFilePath := filepath.Join(cs.credentialSpecResourceLocation, customCredSpecFileName) err = cs.writeSSMFile(ssmParamData, localCredSpecFilePath) if err != nil { cs.setTerminalReason(err.Error()) return err } - - dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", filepath.Base(localCredSpecFilePath)) + dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", customCredSpecFileName) cs.updateCredSpecMapping(originalCredentialspec, dockerHostconfigSecOptCredSpec) return nil @@ -504,25 +304,6 @@ func (cs *CredentialSpecResource) writeSSMFile(ssmParamData, filePath string) er return cs.ioutil.WriteFile(filePath, []byte(ssmParamData), filePerm) } -func (cs *CredentialSpecResource) getCredSpecMap() map[string]string { - cs.lock.RLock() - defer cs.lock.RUnlock() - - return cs.CredSpecMap -} - -func (cs *CredentialSpecResource) GetTargetMapping(credSpecInput string) (string, error) { - cs.lock.RLock() - defer cs.lock.RUnlock() - - targetCredSpecMapping, ok := cs.CredSpecMap[credSpecInput] - if !ok { - return "", errors.New("unable to obtain credentialspec mapping") - } - - return targetCredSpecMapping, nil -} - func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, targetCredSpec string) { cs.lock.Lock() defer cs.lock.Unlock() @@ -562,73 +343,9 @@ func (cs *CredentialSpecResource) clearCredentialSpec() { if err != nil { seelog.Warnf("Unable to clear local credential spec file %s for task %s", localCredentialSpecFile, cs.taskARN) } - delete(cs.CredSpecMap, key) - } -} -// CredentialSpecResourceJSON is the json representation of the credentialspec resource -type CredentialSpecResourceJSON struct { - TaskARN string `json:"taskARN"` - CreatedAt *time.Time `json:"createdAt,omitempty"` - DesiredStatus *CredentialSpecStatus `json:"desiredStatus"` - KnownStatus *CredentialSpecStatus `json:"knownStatus"` - RequiredCredentialSpecs []string `json:"credentialSpecResources"` - CredSpecMap map[string]string `json:"CredSpecMap"` - ExecutionCredentialsID string `json:"executionCredentialsID"` -} - -// MarshalJSON serialises the CredentialSpecResourceJSON struct to JSON -func (cs *CredentialSpecResource) MarshalJSON() ([]byte, error) { - if cs == nil { - return nil, errors.New("credential specresource is nil") - } - createdAt := cs.GetCreatedAt() - return json.Marshal(CredentialSpecResourceJSON{ - TaskARN: cs.taskARN, - CreatedAt: &createdAt, - DesiredStatus: func() *CredentialSpecStatus { - desiredState := cs.GetDesiredStatus() - s := CredentialSpecStatus(desiredState) - return &s - }(), - KnownStatus: func() *CredentialSpecStatus { - knownState := cs.GetKnownStatus() - s := CredentialSpecStatus(knownState) - return &s - }(), - RequiredCredentialSpecs: cs.getRequiredCredentialSpecs(), - CredSpecMap: cs.getCredSpecMap(), - ExecutionCredentialsID: cs.getExecutionCredentialsID(), - }) -} - -// UnmarshalJSON deserialises the raw JSON to a CredentialSpecResourceJSON struct -func (cs *CredentialSpecResource) UnmarshalJSON(b []byte) error { - temp := CredentialSpecResourceJSON{} - - if err := json.Unmarshal(b, &temp); err != nil { - return err - } - - if temp.DesiredStatus != nil { - cs.SetDesiredStatus(resourcestatus.ResourceStatus(*temp.DesiredStatus)) - } - if temp.KnownStatus != nil { - cs.SetKnownStatus(resourcestatus.ResourceStatus(*temp.KnownStatus)) - } - if temp.CreatedAt != nil && !temp.CreatedAt.IsZero() { - cs.SetCreatedAt(*temp.CreatedAt) - } - if temp.RequiredCredentialSpecs != nil { - cs.requiredCredentialSpecs = temp.RequiredCredentialSpecs - } - if temp.CredSpecMap != nil { - cs.CredSpecMap = temp.CredSpecMap + delete(cs.CredSpecMap, key) } - cs.taskARN = temp.TaskARN - cs.executionCredentialsID = temp.ExecutionCredentialsID - - return nil } func (cs *CredentialSpecResource) setCredentialSpecResourceLocation() error { @@ -647,19 +364,15 @@ func (cs *CredentialSpecResource) setCredentialSpecResourceLocation() error { return nil } -// GetAppliedStatus safely returns the currently applied status of the resource -func (cs *CredentialSpecResource) GetAppliedStatus() resourcestatus.ResourceStatus { - return resourcestatus.ResourceStatusNone -} - -func (cs *CredentialSpecResource) DependOnTaskNetwork() bool { - return false +// CredentialSpecResourceJSON is the json representation of the credentialspec resource +type CredentialSpecResourceJSON struct { + *CredentialSpecResourceJSONCommon } -func (cs *CredentialSpecResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus, - dependent resourcestatus.ResourceStatus) { +func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecResourceJSON *CredentialSpecResourceJSON) { + return } -func (cs *CredentialSpecResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency { - return nil +func (cs *CredentialSpecResource) UnmarshallPlatformSpecificFields(credentialSpecResourceJSON CredentialSpecResourceJSON) { + return } diff --git a/agent/taskresource/credentialspec/credentialspec_windows_test.go b/agent/taskresource/credentialspec/credentialspec_windows_test.go index dfe6a49aa8c..7e2b99e8d09 100644 --- a/agent/taskresource/credentialspec/credentialspec_windows_test.go +++ b/agent/taskresource/credentialspec/credentialspec_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,18 +17,18 @@ package credentialspec import ( + "crypto/sha256" "encoding/json" + "fmt" "os" "testing" "time" - "github.com/pkg/errors" - apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/credentials" mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" mock_s3_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" - mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks" + mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks/s3manager" mock_factory "github.com/aws/amazon-ecs-agent/agent/ssm/factory/mocks" mock_ssmiface "github.com/aws/amazon-ecs-agent/agent/ssm/mocks" "github.com/aws/amazon-ecs-agent/agent/taskresource" @@ -37,6 +38,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" "github.com/golang/mock/gomock" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -69,7 +71,9 @@ func TestClearCredentialSpecDataHappyPath(t *testing.T) { credentialSpecResourceLocation := "C:/ProgramData/docker/credentialspecs/" credspecRes := &CredentialSpecResource{ - CredSpecMap: credSpecMapData, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, credentialSpecResourceLocation: credentialSpecResourceLocation, } @@ -89,7 +93,9 @@ func TestClearCredentialSpecDataErr(t *testing.T) { credentialSpecResourceLocation := "C:/ProgramData/docker/credentialspecs/" credspecRes := &CredentialSpecResource{ - CredSpecMap: credSpecMapData, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, credentialSpecResourceLocation: credentialSpecResourceLocation, } @@ -113,15 +119,17 @@ func TestInitialize(t *testing.T) { ssmClientCreator := mock_factory.NewMockSSMClientCreator(ctrl) s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) credspecRes := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + }, } credspecRes.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) assert.NotNil(t, credspecRes.credentialsManager) @@ -134,19 +142,21 @@ func TestMarshalUnmarshalJSON(t *testing.T) { testCredSpec := "credentialspec:file://test.json" targetCredSpec := "credentialspec=file://test.json" - requiredCredentialSpecs := []string{testCredSpec} + credentialSpecContainerMap := map[string]string{testCredSpec: "windowsServerCore"} credSpecMap := map[string]string{} credSpecMap[testCredSpec] = targetCredSpec credspecIn := &CredentialSpecResource{ - taskARN: taskARN, - executionCredentialsID: executionCredentialsID, - createdAt: time.Now(), - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredentialSpecs, - CredSpecMap: credSpecMap, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + taskARN: taskARN, + executionCredentialsID: executionCredentialsID, + createdAt: time.Now(), + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + credentialSpecContainerMap: credentialSpecContainerMap, + CredSpecMap: credSpecMap, + }, } bytes, err := json.Marshal(credspecIn) @@ -160,7 +170,7 @@ func TestMarshalUnmarshalJSON(t *testing.T) { assert.Equal(t, credspecIn.desiredStatusUnsafe, credSpecOut.desiredStatusUnsafe) assert.Equal(t, credspecIn.knownStatusUnsafe, credSpecOut.knownStatusUnsafe) assert.Equal(t, credspecIn.executionCredentialsID, credSpecOut.executionCredentialsID) - assert.Equal(t, len(credspecIn.requiredCredentialSpecs), len(credSpecOut.requiredCredentialSpecs)) + assert.Equal(t, len(credspecIn.credentialSpecContainerMap), len(credSpecOut.credentialSpecContainerMap)) assert.Equal(t, len(credspecIn.CredSpecMap), len(credSpecOut.CredSpecMap)) assert.EqualValues(t, credspecIn.CredSpecMap, credSpecOut.CredSpecMap) } @@ -169,11 +179,13 @@ func TestHandleCredentialspecFile(t *testing.T) { fileCredentialSpec := "credentialspec:file://test.json" expectedFileCredentialSpec := "credentialspec=file://test.json" - requiredCredSpec := []string{fileCredentialSpec} + credentialSpecContainerMap := map[string]string{fileCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + credentialSpecContainerMap: credentialSpecContainerMap, + CredSpecMap: map[string]string{}, + }, } err := cs.handleCredentialspecFile(fileCredentialSpec) @@ -186,10 +198,12 @@ func TestHandleCredentialspecFile(t *testing.T) { func TestHandleCredentialspecFileErr(t *testing.T) { fileCredentialSpec := "credentialspec:invalid-file://test.json" - requiredCredSpec := []string{fileCredentialSpec} + credentialSpecContainerMap := map[string]string{fileCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - requiredCredentialSpecs: requiredCredSpec, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + credentialSpecContainerMap: credentialSpecContainerMap, + }, } err := cs.handleCredentialspecFile(fileCredentialSpec) @@ -208,27 +222,36 @@ func TestHandleSSMCredentialspecFile(t *testing.T) { iamCredentials := credentials.IAMRoleCredentials{ CredentialsID: "test-cred-id", } + containerName := "webapp" credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" - expectedFileCredentialSpec := "credentialspec=file://ssm_12345-678901234-56789_test" + customCredSpecFileName := fmt.Sprintf("%s%s%s", "12345-678901234-56789", containerName, credentialSpecSSMARN) + hasher := sha256.New() + hasher.Write([]byte(customCredSpecFileName)) + customCredSpecFileName = fmt.Sprintf("%x", hasher.Sum(nil)) + expectedFileCredentialSpec := fmt.Sprintf("credentialspec=file://%s", customCredSpecFileName) - requiredCredSpec := []string{ssmCredentialSpec} + credentialSpecContainerMap := map[string]string{ + ssmCredentialSpec: containerName, + } cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) testData := "test-cred-spec-data" @@ -256,6 +279,77 @@ func TestHandleSSMCredentialspecFile(t *testing.T) { assert.Equal(t, expectedFileCredentialSpec, targetCredentialSpecFile) } +func TestHandleSSMCredentialspecFileWithHierarchicalPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + credentialsManager := mock_credentials.NewMockManager(ctrl) + ssmClientCreator := mock_factory.NewMockSSMClientCreator(ctrl) + s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) + mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) + mockSSMClient := mock_ssmiface.NewMockSSMClient(ctrl) + iamCredentials := credentials.IAMRoleCredentials{ + CredentialsID: "test-cred-id", + } + + containerName := "webapp" + + credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/x/y/test" + ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" + customCredSpecFileName := fmt.Sprintf("%s%s%s", "12345-678901234-56789", containerName, credentialSpecSSMARN) + hasher := sha256.New() + hasher.Write([]byte(customCredSpecFileName)) + customCredSpecFileName = fmt.Sprintf("%x", hasher.Sum(nil)) + expectedFileCredentialSpec := fmt.Sprintf("credentialspec=file://%s", customCredSpecFileName) + + credentialSpecContainerMap := map[string]string{ + ssmCredentialSpec: containerName, + } + + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, + } + + cs.Initialize(&taskresource.ResourceFields{ + ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ + SSMClientCreator: ssmClientCreator, + CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, + }, + }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) + + testData := "test-cred-spec-data" + ssmClientOutput := &ssm.GetParametersOutput{ + InvalidParameters: []*string{}, + Parameters: []*ssm.Parameter{ + &ssm.Parameter{ + Name: aws.String("x/y/test"), + Value: aws.String(testData), + }, + }, + } + + gomock.InOrder( + ssmClientCreator.EXPECT().NewSSMClient(gomock.Any(), gomock.Any()).Return(mockSSMClient), + mockSSMClient.EXPECT().GetParameters(gomock.Any()).Return(ssmClientOutput, nil).Times(1), + mockIO.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil), + ) + + err := cs.handleSSMCredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials) + assert.NoError(t, err) + + targetCredentialSpecFile, err := cs.GetTargetMapping(ssmCredentialSpec) + assert.NoError(t, err) + assert.Equal(t, expectedFileCredentialSpec, targetCredentialSpecFile) +} + func TestHandleSSMCredentialspecFileARNParseErr(t *testing.T) { iamCredentials := credentials.IAMRoleCredentials{ CredentialsID: "test-cred-id", @@ -265,7 +359,9 @@ func TestHandleSSMCredentialspecFileARNParseErr(t *testing.T) { var termReason string cs := &CredentialSpecResource{ - terminalReason: termReason, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + terminalReason: termReason, + }, } err := cs.handleSSMCredentialspecFile(ssmCredentialSpec, credentialSpecSSMARN, iamCredentials) @@ -286,21 +382,23 @@ func TestHandleSSMCredentialspecFileGetSSMParamErr(t *testing.T) { credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" - requiredCredSpec := []string{ssmCredentialSpec} + credentialSpecContainerMap := map[string]string{ssmCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) gomock.InOrder( @@ -327,22 +425,24 @@ func TestHandleSSMCredentialspecFileIOErr(t *testing.T) { credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" - requiredCredSpec := []string{ssmCredentialSpec} + credentialSpecContainerMap := map[string]string{ssmCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) testData := "test-cred-spec-data" @@ -367,7 +467,9 @@ func TestHandleSSMCredentialspecFileIOErr(t *testing.T) { } func TestHandlerSSMCredentialspecCredMissingErr(t *testing.T) { - cs := &CredentialSpecResource{} + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" credentialSpecSSMARN := "arn:aws:ssm:us-west-2:123456789012:parameter/test" @@ -386,7 +488,7 @@ func TestHandleS3CredentialspecFile(t *testing.T) { s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) mockFile := mock_oswrapper.NewMockFile() - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) iamCredentials := credentials.IAMRoleCredentials{ CredentialsID: "test-cred-id", } @@ -394,22 +496,24 @@ func TestHandleS3CredentialspecFile(t *testing.T) { s3CredentialSpec := "credentialspec:arn:aws:s3:::bucket_name/test" expectedFileCredentialSpec := "credentialspec=file://s3_12345-678901234-56789_test" - requiredCredSpec := []string{s3CredentialSpec} + credentialSpecContainerMap := map[string]string{s3CredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) defer mockRename()() @@ -417,7 +521,7 @@ func TestHandleS3CredentialspecFile(t *testing.T) { return testTempFile } gomock.InOrder( - s3ClientCreator.EXPECT().NewS3ClientForBucket(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), + s3ClientCreator.EXPECT().NewS3ManagerClient(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), mockIO.EXPECT().TempFile(gomock.Any(), gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), nil), ) @@ -439,7 +543,9 @@ func TestHandleS3CredentialspecFileARNParseErr(t *testing.T) { var termReason string cs := &CredentialSpecResource{ - terminalReason: termReason, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + terminalReason: termReason, + }, } err := cs.handleS3CredentialspecFile(s3CredentialSpec, credentialSpecS3ARN, iamCredentials) @@ -453,7 +559,7 @@ func TestHandleS3CredentialspecFileS3ClientErr(t *testing.T) { credentialsManager := mock_credentials.NewMockManager(ctrl) ssmClientCreator := mock_factory.NewMockSSMClientCreator(ctrl) s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) iamCredentials := credentials.IAMRoleCredentials{ CredentialsID: "test-cred-id", } @@ -462,18 +568,20 @@ func TestHandleS3CredentialspecFileS3ClientErr(t *testing.T) { var termReason string cs := &CredentialSpecResource{ - terminalReason: termReason, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + terminalReason: termReason, + }, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) gomock.InOrder( - s3ClientCreator.EXPECT().NewS3ClientForBucket(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, errors.New("test-error")), + s3ClientCreator.EXPECT().NewS3ManagerClient(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, errors.New("test-error")), ) err := cs.handleS3CredentialspecFile(s3CredentialSpec, credentialSpecS3ARN, iamCredentials) @@ -489,7 +597,7 @@ func TestHandleS3CredentialspecFileWriteErr(t *testing.T) { s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) mockFile := mock_oswrapper.NewMockFile() - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) iamCredentials := credentials.IAMRoleCredentials{ CredentialsID: "test-cred-id", @@ -497,22 +605,24 @@ func TestHandleS3CredentialspecFileWriteErr(t *testing.T) { credentialSpecS3ARN := "arn:aws:s3:::bucket_name/test" s3CredentialSpec := "credentialspec:arn:aws:s3:::bucket_name/test" - requiredCredSpec := []string{s3CredentialSpec} + credentialSpecContainerMap := map[string]string{s3CredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) mockFile.(*mock_oswrapper.MockFile).NameImpl = func() string { @@ -527,7 +637,7 @@ func TestHandleS3CredentialspecFileWriteErr(t *testing.T) { }() gomock.InOrder( - s3ClientCreator.EXPECT().NewS3ClientForBucket(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), + s3ClientCreator.EXPECT().NewS3ManagerClient(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), mockIO.EXPECT().TempFile(gomock.Any(), gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), nil), ) @@ -537,7 +647,9 @@ func TestHandleS3CredentialspecFileWriteErr(t *testing.T) { } func TestHandlerS3CredentialspecCredMissingErr(t *testing.T) { - cs := &CredentialSpecResource{} + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } credentialSpecS3ARN := "arn:aws:s3:::bucket_name/test" s3CredentialSpec := "credentialspec:arn:aws:s3:::bucket_name/test" @@ -558,22 +670,24 @@ func TestCreateSSM(t *testing.T) { mockSSMClient := mock_ssmiface.NewMockSSMClient(ctrl) ssmCredentialSpec := "credentialspec:arn:aws:ssm:us-west-2:123456789012:parameter/test" - requiredCredSpec := []string{ssmCredentialSpec} + credentialSpecContainerMap := map[string]string{ssmCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) testData := "test-cred-spec-data" @@ -614,26 +728,28 @@ func TestCreateS3(t *testing.T) { s3ClientCreator := mock_s3_factory.NewMockS3ClientCreator(ctrl) mockIO := mock_ioutilwrapper.NewMockIOUtil(ctrl) mockFile := mock_oswrapper.NewMockFile() - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) s3CredentialSpec := "credentialspec:arn:aws:s3:::bucket_name/test" - requiredCredSpec := []string{s3CredentialSpec} + credentialSpecContainerMap := map[string]string{s3CredentialSpec: "webapp"} cs := &CredentialSpecResource{ - knownStatusUnsafe: resourcestatus.ResourceCreated, - desiredStatusUnsafe: resourcestatus.ResourceCreated, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, - taskARN: taskARN, - ioutil: mockIO, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + knownStatusUnsafe: resourcestatus.ResourceCreated, + desiredStatusUnsafe: resourcestatus.ResourceCreated, + CredSpecMap: map[string]string{}, + taskARN: taskARN, + credentialSpecContainerMap: credentialSpecContainerMap, + }, + ioutil: mockIO, } cs.Initialize(&taskresource.ResourceFields{ ResourceFieldsCommon: &taskresource.ResourceFieldsCommon{ SSMClientCreator: ssmClientCreator, CredentialsManager: credentialsManager, + S3ClientCreator: s3ClientCreator, }, - S3ClientCreator: s3ClientCreator, }, apitaskstatus.TaskStatusNone, apitaskstatus.TaskRunning) creds := credentials.TaskIAMRoleCredentials{ @@ -647,7 +763,7 @@ func TestCreateS3(t *testing.T) { defer mockRename()() gomock.InOrder( credentialsManager.EXPECT().GetTaskCredentials(gomock.Any()).Return(creds, true), - s3ClientCreator.EXPECT().NewS3ClientForBucket(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), + s3ClientCreator.EXPECT().NewS3ManagerClient(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockS3Client, nil), mockIO.EXPECT().TempFile(gomock.Any(), gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), nil), ) @@ -662,12 +778,14 @@ func TestCreateFile(t *testing.T) { credentialsManager := mock_credentials.NewMockManager(ctrl) fileCredentialSpec := "credentialspec:file://test.json" - requiredCredSpec := []string{fileCredentialSpec} + credentialSpecContainerMap := map[string]string{fileCredentialSpec: "webapp"} cs := &CredentialSpecResource{ - credentialsManager: credentialsManager, - requiredCredentialSpecs: requiredCredSpec, - CredSpecMap: map[string]string{}, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + credentialsManager: credentialsManager, + CredSpecMap: map[string]string{}, + credentialSpecContainerMap: credentialSpecContainerMap, + }, } creds := credentials.TaskIAMRoleCredentials{ @@ -686,7 +804,9 @@ func TestCreateFile(t *testing.T) { } func TestGetName(t *testing.T) { - cs := &CredentialSpecResource{} + cs := &CredentialSpecResource{ + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{}, + } assert.Equal(t, ResourceName, cs.GetName()) } @@ -698,7 +818,9 @@ func TestGetTargetMapping(t *testing.T) { } cs := &CredentialSpecResource{ - CredSpecMap: credSpecMapData, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: credSpecMapData, + }, } targetCredSpec, err := cs.GetTargetMapping(inputCredSpec) @@ -708,7 +830,9 @@ func TestGetTargetMapping(t *testing.T) { func TestGetTargetMappingErr(t *testing.T) { cs := &CredentialSpecResource{ - CredSpecMap: map[string]string{}, + CredentialSpecResourceCommon: &CredentialSpecResourceCommon{ + CredSpecMap: map[string]string{}, + }, } targetCredSpec, err := cs.GetTargetMapping("testcredspec") diff --git a/agent/taskresource/envFiles/envfile.go b/agent/taskresource/envFiles/envfile.go index fe56ad67410..546844d02b8 100644 --- a/agent/taskresource/envFiles/envfile.go +++ b/agent/taskresource/envFiles/envfile.go @@ -354,7 +354,7 @@ func (envfile *EnvironmentFileResource) downloadEnvfileFromS3(envFilePath string return } - s3Client, err := envfile.s3ClientCreator.NewS3ClientForBucket(bucket, envfile.region, iamCredentials) + s3Client, err := envfile.s3ClientCreator.NewS3ManagerClient(bucket, envfile.region, iamCredentials) if err != nil { errorEvents <- fmt.Errorf("unable to initialize s3 client for bucket %s, error: %v", bucket, err) return diff --git a/agent/taskresource/envFiles/envfile_test.go b/agent/taskresource/envFiles/envfile_test.go index ad7943c14e4..ea94c32da11 100644 --- a/agent/taskresource/envFiles/envfile_test.go +++ b/agent/taskresource/envFiles/envfile_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -28,7 +29,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/credentials" mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" mock_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" - mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks" + mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks/s3manager" "github.com/aws/amazon-ecs-agent/agent/taskresource" mock_bufio "github.com/aws/amazon-ecs-agent/agent/utils/bufiowrapper/mocks" mock_ioutilwrapper "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper/mocks" @@ -57,14 +58,14 @@ const ( ) func setup(t *testing.T) (oswrapper.File, *mock_ioutilwrapper.MockIOUtil, - *mock_credentials.MockManager, *mock_factory.MockS3ClientCreator, *mock_s3.MockS3Client, func()) { + *mock_credentials.MockManager, *mock_factory.MockS3ClientCreator, *mock_s3.MockS3ManagerClient, func()) { ctrl := gomock.NewController(t) mockFile := mock_oswrapper.NewMockFile() mockIOUtil := mock_ioutilwrapper.NewMockIOUtil(ctrl) mockCredentialsManager := mock_credentials.NewMockManager(ctrl) mockS3ClientCreator := mock_factory.NewMockS3ClientCreator(ctrl) - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) return mockFile, mockIOUtil, mockCredentialsManager, mockS3ClientCreator, mockS3Client, ctrl.Finish } @@ -138,7 +139,7 @@ func TestCreateWithEnvVarFile(t *testing.T) { gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(executionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), mockIOUtil.EXPECT().TempFile(resourceDir, gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Do( func(ctx aws.Context, w io.WriterAt, input *s3.GetObjectInput) { @@ -192,7 +193,7 @@ func TestCreateUnableToRetrieveDataFromS3(t *testing.T) { gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(executionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), mockIOUtil.EXPECT().TempFile(resourceDir, gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), errors.New("error response")), ) @@ -220,7 +221,7 @@ func TestCreateUnableToCreateTmpFile(t *testing.T) { gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(executionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), mockIOUtil.EXPECT().TempFile(resourceDir, gomock.Any()).Return(nil, errors.New("error response")), ) @@ -255,7 +256,7 @@ func TestCreateRenameFileError(t *testing.T) { gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(executionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient(s3Bucket, region, creds.IAMRoleCredentials).Return(mockS3Client, nil), mockIOUtil.EXPECT().TempFile(resourceDir, gomock.Any()).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), nil), ) diff --git a/agent/taskresource/envFiles/envfilestatus_test.go b/agent/taskresource/envFiles/envfilestatus_test.go index 1bbbb7a0ad8..2c87ad70c39 100644 --- a/agent/taskresource/envFiles/envfilestatus_test.go +++ b/agent/taskresource/envFiles/envfilestatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/firelens/firelens_unimplemented.go b/agent/taskresource/firelens/firelens_unimplemented.go index b6a69a67485..36adcf28a68 100644 --- a/agent/taskresource/firelens/firelens_unimplemented.go +++ b/agent/taskresource/firelens/firelens_unimplemented.go @@ -1,4 +1,5 @@ //go:build !linux +// +build !linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/firelens/firelens_unix.go b/agent/taskresource/firelens/firelens_unix.go index 1646ed71eb3..e76f8433903 100644 --- a/agent/taskresource/firelens/firelens_unix.go +++ b/agent/taskresource/firelens/firelens_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -431,11 +432,12 @@ func (firelens *FirelensResource) Create() error { var mkdirAll = os.MkdirAll // createDirectories creates two directories: -// - $(DATA_DIR)/firelens/$(TASK_ID)/config: used to store firelens config file. The config file under this directory -// will be mounted to the firelens container at an expected path. -// - $(DATA_DIR)/firelens/$(TASK_ID)/socket: used to store the unix socket. This directory will be mounted to -// the firelens container and it will generate a socket file under this directory. Containers that use firelens to -// send logs will then use this socket to send logs to the firelens container. +// - $(DATA_DIR)/firelens/$(TASK_ID)/config: used to store firelens config file. The config file under this directory +// will be mounted to the firelens container at an expected path. +// - $(DATA_DIR)/firelens/$(TASK_ID)/socket: used to store the unix socket. This directory will be mounted to +// the firelens container and it will generate a socket file under this directory. Containers that use firelens to +// send logs will then use this socket to send logs to the firelens container. +// // Note: socket path has a limit of at most 108 characters on Linux. If using default data dir, the // resulting socket path will be 79 characters (/var/lib/ecs/data/firelens//socket/fluent.sock) which is fine. // However if ECS_HOST_DATA_DIR is specified to be a longer path, we will exceed the limit and fail. I don't really @@ -492,7 +494,7 @@ func (firelens *FirelensResource) downloadConfigFromS3() error { return errors.Wrap(err, "unable to parse bucket and key from s3 arn") } - s3Client, err := firelens.s3ClientCreator.NewS3ClientForBucket(bucket, firelens.region, creds.GetIAMRoleCredentials()) + s3Client, err := firelens.s3ClientCreator.NewS3ManagerClient(bucket, firelens.region, creds.GetIAMRoleCredentials()) if err != nil { return errors.Wrapf(err, "unable to initialize s3 client for bucket %s", bucket) } diff --git a/agent/taskresource/firelens/firelens_unix_test.go b/agent/taskresource/firelens/firelens_unix_test.go index a4573152c24..0dc550417c5 100644 --- a/agent/taskresource/firelens/firelens_unix_test.go +++ b/agent/taskresource/firelens/firelens_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -31,7 +32,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/credentials" mock_credentials "github.com/aws/amazon-ecs-agent/agent/credentials/mocks" mock_factory "github.com/aws/amazon-ecs-agent/agent/s3/factory/mocks" - mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks" + mock_s3 "github.com/aws/amazon-ecs-agent/agent/s3/mocks/s3manager" "github.com/aws/amazon-ecs-agent/agent/taskresource" resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status" mock_ioutilwrapper "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper/mocks" @@ -69,14 +70,14 @@ var ( ) func setup(t *testing.T) (oswrapper.File, *mock_ioutilwrapper.MockIOUtil, - *mock_credentials.MockManager, *mock_factory.MockS3ClientCreator, *mock_s3.MockS3Client, func()) { + *mock_credentials.MockManager, *mock_factory.MockS3ClientCreator, *mock_s3.MockS3ManagerClient, func()) { ctrl := gomock.NewController(t) mockFile := mock_oswrapper.NewMockFile() mockIOUtil := mock_ioutilwrapper.NewMockIOUtil(ctrl) mockCredentialsManager := mock_credentials.NewMockManager(ctrl) mockS3ClientCreator := mock_factory.NewMockS3ClientCreator(ctrl) - mockS3Client := mock_s3.NewMockS3Client(ctrl) + mockS3Client := mock_s3.NewMockS3ManagerClient(ctrl) return mockFile, mockIOUtil, mockCredentialsManager, mockS3ClientCreator, mockS3Client, ctrl.Finish } @@ -362,7 +363,7 @@ func TestCreateFirelensResourceWithS3Config(t *testing.T) { gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(testExecutionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket("bucket", testRegion, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient("bucket", testRegion, creds.IAMRoleCredentials).Return(mockS3Client, nil), // write external config file downloaded from s3 mockIOUtil.EXPECT().TempFile(testResourceDir, tempFile).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Do( @@ -434,7 +435,7 @@ func TestCreateFirelensResourceWithS3ConfigDownloadFailure(t *testing.T) { } gomock.InOrder( mockCredentialsManager.EXPECT().GetTaskCredentials(testExecutionCredentialsID).Return(creds, true), - mockS3ClientCreator.EXPECT().NewS3ClientForBucket("bucket", testRegion, creds.IAMRoleCredentials).Return(mockS3Client, nil), + mockS3ClientCreator.EXPECT().NewS3ManagerClient("bucket", testRegion, creds.IAMRoleCredentials).Return(mockS3Client, nil), mockIOUtil.EXPECT().TempFile(testResourceDir, tempFile).Return(mockFile, nil), mockS3Client.EXPECT().DownloadWithContext(gomock.Any(), mockFile, gomock.Any()).Return(int64(0), errors.New("test error")), ) diff --git a/agent/taskresource/firelens/firelensconfig_unix.go b/agent/taskresource/firelens/firelensconfig_unix.go index 1ed4d75be95..30bd5c03214 100644 --- a/agent/taskresource/firelens/firelensconfig_unix.go +++ b/agent/taskresource/firelens/firelensconfig_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -223,12 +224,12 @@ func (firelens *FirelensResource) addHealthcheckSections(config generator.Fluent // addOutputSection adds an output section to the firelens container's config that specifies how it routes another // container's logs. It's constructed based on that container's log options. // logOptions is a set of key-value pairs, which includes the following: -// 1. The name of the output plugin (required when there are output options specified, i.e. the ones in 4). For +// 1. The name of the output plugin (required when there are output options specified, i.e. the ones in 4). For // fluentd, the key is "@type", for fluentbit, the key is "Name". -// 2. include-pattern (optional): a regex specifying the logs to be included. -// 3. exclude-pattern (optional): a regex specifying the logs to be excluded. -// 4. All other key-value pairs are customer specified options for the plugin. They are unique for each plugin and -// we don't check them. +// 2. include-pattern (optional): a regex specifying the logs to be included. +// 3. exclude-pattern (optional): a regex specifying the logs to be excluded. +// 4. All other key-value pairs are customer specified options for the plugin. They are unique for each plugin and +// we don't check them. func addOutputSection(tag, firelensConfigType string, logOptions map[string]string, config generator.FluentConfig) (generator.FluentConfig, error) { var outputKey string if firelensConfigType == FirelensConfigTypeFluentd { diff --git a/agent/taskresource/firelens/firelensconfig_unix_test.go b/agent/taskresource/firelens/firelensconfig_unix_test.go index 9fed0fae327..1a18f0cf49c 100644 --- a/agent/taskresource/firelens/firelensconfig_unix_test.go +++ b/agent/taskresource/firelens/firelensconfig_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/firelens/firelensstatus_test.go b/agent/taskresource/firelens/firelensstatus_test.go index d128f54af82..5a171aa56db 100644 --- a/agent/taskresource/firelens/firelensstatus_test.go +++ b/agent/taskresource/firelens/firelensstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/firelens/json_unix.go b/agent/taskresource/firelens/json_unix.go index 8ee89d2c55d..ba3d8c6be24 100644 --- a/agent/taskresource/firelens/json_unix.go +++ b/agent/taskresource/firelens/json_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/firelens/json_unix_test.go b/agent/taskresource/firelens/json_unix_test.go index 4cd5ba12775..dec3cdbfed1 100644 --- a/agent/taskresource/firelens/json_unix_test.go +++ b/agent/taskresource/firelens/json_unix_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_unsupported.go b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_unsupported.go index 6d45f55dc8b..1e2bee5ea2f 100644 --- a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_unsupported.go +++ b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_unsupported.go @@ -1,4 +1,5 @@ //go:build !windows +// +build !windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows.go b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows.go index 1350d52098b..c868d78c589 100644 --- a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows.go +++ b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -552,18 +553,23 @@ func (fv *FSxWindowsFileServerResource) performHostMount(remotePath string, user // New-SmbGlobalMapping cmdlet creates an SMB mapping between the container instance // and SMB share (FSx for Windows File Server file-system) - cmd := execCommand("powershell.exe", + + args := []string{ "New-SmbGlobalMapping", localPathArg, remotePathArg, creds, "-Persistent $true", "-RequirePrivacy $true", - "-ErrorAction Stop") + "-ErrorAction Stop", + } + seelog.Debugf("Executing mapping of fsxwindowsfileserver with cmd: %v %v", strings.Join(args[:3], " "), strings.Join(args[4:], " ")) - _, err = cmd.CombinedOutput() + cmd := execCommand("powershell.exe", args...) + out, err := cmd.CombinedOutput() if err != nil { - seelog.Errorf("Failed to map fsxwindowsfileserver resource on the container instance: %v", err) + safeOutput := strings.ReplaceAll(string(out), password, "") + seelog.Errorf("Failed to map fsxwindowsfileserver resource on the container instance error: %v, out: %v", err, safeOutput) fv.setTerminalReason(err.Error()) return err } diff --git a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows_test.go b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows_test.go index 0d369c76509..e55116ad3ed 100644 --- a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows_test.go +++ b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserver_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserverstatus_test.go b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserverstatus_test.go index 276d29e326e..0f9e0bce848 100644 --- a/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserverstatus_test.go +++ b/agent/taskresource/fsxwindowsfileserver/fsxwindowsfileserverstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.pb.go b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.pb.go new file mode 100644 index 00000000000..d6f28d48481 --- /dev/null +++ b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.pb.go @@ -0,0 +1,384 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.19.4 +// source: credentialsfetcher/credentialsfetcher.proto + +package credentialsfetcher + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateKerberosLeaseRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CredspecContents []string `protobuf:"bytes,1,rep,name=credspec_contents,json=credspecContents,proto3" json:"credspec_contents,omitempty"` +} + +func (x *CreateKerberosLeaseRequest) Reset() { + *x = CreateKerberosLeaseRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateKerberosLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateKerberosLeaseRequest) ProtoMessage() {} + +func (x *CreateKerberosLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateKerberosLeaseRequest.ProtoReflect.Descriptor instead. +func (*CreateKerberosLeaseRequest) Descriptor() ([]byte, []int) { + return file_credentialsfetcher_credentialsfetcher_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateKerberosLeaseRequest) GetCredspecContents() []string { + if x != nil { + return x.CredspecContents + } + return nil +} + +type CreateKerberosLeaseResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LeaseId string `protobuf:"bytes,1,opt,name=lease_id,json=leaseId,proto3" json:"lease_id,omitempty"` + CreatedKerberosFilePaths []string `protobuf:"bytes,2,rep,name=created_kerberos_file_paths,json=createdKerberosFilePaths,proto3" json:"created_kerberos_file_paths,omitempty"` +} + +func (x *CreateKerberosLeaseResponse) Reset() { + *x = CreateKerberosLeaseResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateKerberosLeaseResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateKerberosLeaseResponse) ProtoMessage() {} + +func (x *CreateKerberosLeaseResponse) ProtoReflect() protoreflect.Message { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateKerberosLeaseResponse.ProtoReflect.Descriptor instead. +func (*CreateKerberosLeaseResponse) Descriptor() ([]byte, []int) { + return file_credentialsfetcher_credentialsfetcher_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateKerberosLeaseResponse) GetLeaseId() string { + if x != nil { + return x.LeaseId + } + return "" +} + +func (x *CreateKerberosLeaseResponse) GetCreatedKerberosFilePaths() []string { + if x != nil { + return x.CreatedKerberosFilePaths + } + return nil +} + +type DeleteKerberosLeaseRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LeaseId string `protobuf:"bytes,1,opt,name=lease_id,json=leaseId,proto3" json:"lease_id,omitempty"` +} + +func (x *DeleteKerberosLeaseRequest) Reset() { + *x = DeleteKerberosLeaseRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteKerberosLeaseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteKerberosLeaseRequest) ProtoMessage() {} + +func (x *DeleteKerberosLeaseRequest) ProtoReflect() protoreflect.Message { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteKerberosLeaseRequest.ProtoReflect.Descriptor instead. +func (*DeleteKerberosLeaseRequest) Descriptor() ([]byte, []int) { + return file_credentialsfetcher_credentialsfetcher_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteKerberosLeaseRequest) GetLeaseId() string { + if x != nil { + return x.LeaseId + } + return "" +} + +type DeleteKerberosLeaseResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LeaseId string `protobuf:"bytes,1,opt,name=lease_id,json=leaseId,proto3" json:"lease_id,omitempty"` + DeletedKerberosFilePaths []string `protobuf:"bytes,2,rep,name=deleted_kerberos_file_paths,json=deletedKerberosFilePaths,proto3" json:"deleted_kerberos_file_paths,omitempty"` +} + +func (x *DeleteKerberosLeaseResponse) Reset() { + *x = DeleteKerberosLeaseResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteKerberosLeaseResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteKerberosLeaseResponse) ProtoMessage() {} + +func (x *DeleteKerberosLeaseResponse) ProtoReflect() protoreflect.Message { + mi := &file_credentialsfetcher_credentialsfetcher_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteKerberosLeaseResponse.ProtoReflect.Descriptor instead. +func (*DeleteKerberosLeaseResponse) Descriptor() ([]byte, []int) { + return file_credentialsfetcher_credentialsfetcher_proto_rawDescGZIP(), []int{3} +} + +func (x *DeleteKerberosLeaseResponse) GetLeaseId() string { + if x != nil { + return x.LeaseId + } + return "" +} + +func (x *DeleteKerberosLeaseResponse) GetDeletedKerberosFilePaths() []string { + if x != nil { + return x.DeletedKerberosFilePaths + } + return nil +} + +var File_credentialsfetcher_credentialsfetcher_proto protoreflect.FileDescriptor + +var file_credentialsfetcher_credentialsfetcher_proto_rawDesc = []byte{ + 0x0a, 0x2b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, 0x65, 0x74, + 0x63, 0x68, 0x65, 0x72, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, + 0x66, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x63, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, 0x65, 0x74, 0x63, 0x68, 0x65, + 0x72, 0x22, 0x49, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, + 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2b, 0x0a, 0x11, 0x63, 0x72, 0x65, 0x64, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x63, 0x72, 0x65, 0x64, + 0x73, 0x70, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x77, 0x0a, 0x1b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, + 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x1b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x6b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, + 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x18, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x46, 0x69, 0x6c, 0x65, + 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0x37, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, + 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x22, 0x77, + 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, + 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x1b, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x5f, 0x66, 0x69, 0x6c, + 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x18, 0x64, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x46, 0x69, + 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x32, 0x88, 0x02, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x4b, 0x65, 0x72, 0x62, + 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, + 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, + 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, + 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, + 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x72, + 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, + 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4b, 0x65, 0x72, + 0x62, 0x65, 0x72, 0x6f, 0x73, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x1f, 0x5a, 0x1d, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x66, 0x65, 0x74, 0x63, + 0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_credentialsfetcher_credentialsfetcher_proto_rawDescOnce sync.Once + file_credentialsfetcher_credentialsfetcher_proto_rawDescData = file_credentialsfetcher_credentialsfetcher_proto_rawDesc +) + +func file_credentialsfetcher_credentialsfetcher_proto_rawDescGZIP() []byte { + file_credentialsfetcher_credentialsfetcher_proto_rawDescOnce.Do(func() { + file_credentialsfetcher_credentialsfetcher_proto_rawDescData = protoimpl.X.CompressGZIP(file_credentialsfetcher_credentialsfetcher_proto_rawDescData) + }) + return file_credentialsfetcher_credentialsfetcher_proto_rawDescData +} + +var file_credentialsfetcher_credentialsfetcher_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_credentialsfetcher_credentialsfetcher_proto_goTypes = []interface{}{ + (*CreateKerberosLeaseRequest)(nil), // 0: credentialsfetcher.CreateKerberosLeaseRequest + (*CreateKerberosLeaseResponse)(nil), // 1: credentialsfetcher.CreateKerberosLeaseResponse + (*DeleteKerberosLeaseRequest)(nil), // 2: credentialsfetcher.DeleteKerberosLeaseRequest + (*DeleteKerberosLeaseResponse)(nil), // 3: credentialsfetcher.DeleteKerberosLeaseResponse +} +var file_credentialsfetcher_credentialsfetcher_proto_depIdxs = []int32{ + 0, // 0: credentialsfetcher.CredentialsFetcherService.AddKerberosLease:input_type -> credentialsfetcher.CreateKerberosLeaseRequest + 2, // 1: credentialsfetcher.CredentialsFetcherService.DeleteKerberosLease:input_type -> credentialsfetcher.DeleteKerberosLeaseRequest + 1, // 2: credentialsfetcher.CredentialsFetcherService.AddKerberosLease:output_type -> credentialsfetcher.CreateKerberosLeaseResponse + 3, // 3: credentialsfetcher.CredentialsFetcherService.DeleteKerberosLease:output_type -> credentialsfetcher.DeleteKerberosLeaseResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_credentialsfetcher_credentialsfetcher_proto_init() } +func file_credentialsfetcher_credentialsfetcher_proto_init() { + if File_credentialsfetcher_credentialsfetcher_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_credentialsfetcher_credentialsfetcher_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateKerberosLeaseRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_credentialsfetcher_credentialsfetcher_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateKerberosLeaseResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_credentialsfetcher_credentialsfetcher_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteKerberosLeaseRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_credentialsfetcher_credentialsfetcher_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteKerberosLeaseResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_credentialsfetcher_credentialsfetcher_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_credentialsfetcher_credentialsfetcher_proto_goTypes, + DependencyIndexes: file_credentialsfetcher_credentialsfetcher_proto_depIdxs, + MessageInfos: file_credentialsfetcher_credentialsfetcher_proto_msgTypes, + }.Build() + File_credentialsfetcher_credentialsfetcher_proto = out.File + file_credentialsfetcher_credentialsfetcher_proto_rawDesc = nil + file_credentialsfetcher_credentialsfetcher_proto_goTypes = nil + file_credentialsfetcher_credentialsfetcher_proto_depIdxs = nil +} diff --git a/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.proto b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.proto new file mode 100644 index 00000000000..d54edeb6889 --- /dev/null +++ b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher.proto @@ -0,0 +1,31 @@ +//command to generate gRPC code +//protoc --go_out=. --go_opt=paths=source_relative \ +//--go-grpc_out=. --go-grpc_opt=paths=source_relative credentialsfetcher/credentialsfetcher.proto +// This will generate credentialsfetcher/credentialsfetcher.pb.go and credentialsfetcher/credentialsfetcher_grpc.pb.go files +syntax = "proto3"; + +option go_package = "grpcclient/credentialsfetcher"; +package credentialsfetcher; + +service CredentialsFetcherService { + rpc AddKerberosLease (CreateKerberosLeaseRequest) returns (CreateKerberosLeaseResponse); + rpc DeleteKerberosLease (DeleteKerberosLeaseRequest) returns (DeleteKerberosLeaseResponse); +} + +message CreateKerberosLeaseRequest { + repeated string credspec_contents = 1; +} + +message CreateKerberosLeaseResponse { + string lease_id = 1; + repeated string created_kerberos_file_paths = 2; +} + +message DeleteKerberosLeaseRequest { + string lease_id = 1; +} + +message DeleteKerberosLeaseResponse { + string lease_id = 1; + repeated string deleted_kerberos_file_paths = 2; +} \ No newline at end of file diff --git a/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher_grpc.pb.go b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher_grpc.pb.go new file mode 100644 index 00000000000..02f7df5641b --- /dev/null +++ b/agent/taskresource/grpcclient/credentialsfetcher/credentialsfetcher_grpc.pb.go @@ -0,0 +1,143 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.19.4 +// source: credentialsfetcher/credentialsfetcher.proto + +package credentialsfetcher + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// CredentialsFetcherServiceClient is the client API for CredentialsFetcherService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CredentialsFetcherServiceClient interface { + AddKerberosLease(ctx context.Context, in *CreateKerberosLeaseRequest, opts ...grpc.CallOption) (*CreateKerberosLeaseResponse, error) + DeleteKerberosLease(ctx context.Context, in *DeleteKerberosLeaseRequest, opts ...grpc.CallOption) (*DeleteKerberosLeaseResponse, error) +} + +type credentialsFetcherServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCredentialsFetcherServiceClient(cc grpc.ClientConnInterface) CredentialsFetcherServiceClient { + return &credentialsFetcherServiceClient{cc} +} + +func (c *credentialsFetcherServiceClient) AddKerberosLease(ctx context.Context, in *CreateKerberosLeaseRequest, opts ...grpc.CallOption) (*CreateKerberosLeaseResponse, error) { + out := new(CreateKerberosLeaseResponse) + err := c.cc.Invoke(ctx, "/credentialsfetcher.CredentialsFetcherService/AddKerberosLease", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *credentialsFetcherServiceClient) DeleteKerberosLease(ctx context.Context, in *DeleteKerberosLeaseRequest, opts ...grpc.CallOption) (*DeleteKerberosLeaseResponse, error) { + out := new(DeleteKerberosLeaseResponse) + err := c.cc.Invoke(ctx, "/credentialsfetcher.CredentialsFetcherService/DeleteKerberosLease", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CredentialsFetcherServiceServer is the server API for CredentialsFetcherService service. +// All implementations must embed UnimplementedCredentialsFetcherServiceServer +// for forward compatibility +type CredentialsFetcherServiceServer interface { + AddKerberosLease(context.Context, *CreateKerberosLeaseRequest) (*CreateKerberosLeaseResponse, error) + DeleteKerberosLease(context.Context, *DeleteKerberosLeaseRequest) (*DeleteKerberosLeaseResponse, error) + mustEmbedUnimplementedCredentialsFetcherServiceServer() +} + +// UnimplementedCredentialsFetcherServiceServer must be embedded to have forward compatible implementations. +type UnimplementedCredentialsFetcherServiceServer struct { +} + +func (UnimplementedCredentialsFetcherServiceServer) AddKerberosLease(context.Context, *CreateKerberosLeaseRequest) (*CreateKerberosLeaseResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddKerberosLease not implemented") +} +func (UnimplementedCredentialsFetcherServiceServer) DeleteKerberosLease(context.Context, *DeleteKerberosLeaseRequest) (*DeleteKerberosLeaseResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteKerberosLease not implemented") +} +func (UnimplementedCredentialsFetcherServiceServer) mustEmbedUnimplementedCredentialsFetcherServiceServer() { +} + +// UnsafeCredentialsFetcherServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CredentialsFetcherServiceServer will +// result in compilation errors. +type UnsafeCredentialsFetcherServiceServer interface { + mustEmbedUnimplementedCredentialsFetcherServiceServer() +} + +func RegisterCredentialsFetcherServiceServer(s grpc.ServiceRegistrar, srv CredentialsFetcherServiceServer) { + s.RegisterService(&CredentialsFetcherService_ServiceDesc, srv) +} + +func _CredentialsFetcherService_AddKerberosLease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateKerberosLeaseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CredentialsFetcherServiceServer).AddKerberosLease(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/credentialsfetcher.CredentialsFetcherService/AddKerberosLease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CredentialsFetcherServiceServer).AddKerberosLease(ctx, req.(*CreateKerberosLeaseRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CredentialsFetcherService_DeleteKerberosLease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteKerberosLeaseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CredentialsFetcherServiceServer).DeleteKerberosLease(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/credentialsfetcher.CredentialsFetcherService/DeleteKerberosLease", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CredentialsFetcherServiceServer).DeleteKerberosLease(ctx, req.(*DeleteKerberosLeaseRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CredentialsFetcherService_ServiceDesc is the grpc.ServiceDesc for CredentialsFetcherService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CredentialsFetcherService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "credentialsfetcher.CredentialsFetcherService", + HandlerType: (*CredentialsFetcherServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddKerberosLease", + Handler: _CredentialsFetcherService_AddKerberosLease_Handler, + }, + { + MethodName: "DeleteKerberosLease", + Handler: _CredentialsFetcherService_DeleteKerberosLease_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "credentialsfetcher/credentialsfetcher.proto", +} diff --git a/agent/taskresource/grpcclient/credentialsfetcherclient.go b/agent/taskresource/grpcclient/credentialsfetcherclient.go new file mode 100644 index 00000000000..254b8cd3b28 --- /dev/null +++ b/agent/taskresource/grpcclient/credentialsfetcherclient.go @@ -0,0 +1,122 @@ +package grpcclient + +import ( + "context" + "os" + "time" + + pb "github.com/aws/amazon-ecs-agent/agent/taskresource/grpcclient/credentialsfetcher" + "github.com/cihub/seelog" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type CredentialsFetcherClient struct { + conn *grpc.ClientConn + timeout time.Duration +} + +// GetGrpcClientConnection() returns grpc client connection +func GetGrpcClientConnection() (*grpc.ClientConn, error) { + address, err := getSocketAddress() + if err != nil { + seelog.Errorf("could not find path to credentials fetcher host dir : %v", err) + return nil, err + } + + conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + seelog.Errorf("could not initialize client connection %v", err) + return nil, err + } + return conn, nil + +} + +// getSocketAddress() returns the credentials-fetcher socket dir +func getSocketAddress() (string, error) { + credentialsfetcherHostDir := os.Getenv("CREDENTIALS_FETCHER_HOST_DIR") + + _, err := os.Stat(credentialsfetcherHostDir) + if err != nil { + return "", err + } + return "unix:" + credentialsfetcherHostDir, nil +} + +func NewCredentialsFetcherClient(conn *grpc.ClientConn, timeout time.Duration) CredentialsFetcherClient { + return CredentialsFetcherClient{ + conn: conn, + timeout: timeout, + } +} + +// Credentials fetcher is a daemon running on the host which supports gMSA on linux +type CredentialsFetcherResponse struct { + //lease id is a unique identifier associated with the kerberos tickets created for a container + LeaseID string + //path to the kerberos tickets created for the service accounts + KerberosTicketPaths []string +} + +// AddKerberosLease() invokes credentials fetcher daemon running on the host +// to create kerberos tickets associated with gMSA accounts +func (c CredentialsFetcherClient) AddKerberosLease(ctx context.Context, credentialspecs []string) (CredentialsFetcherResponse, error) { + if len(credentialspecs) == 0 { + seelog.Error("credentialspecs request should not be empty") + return CredentialsFetcherResponse{}, nil + } + + defer c.conn.Close() + client := pb.NewCredentialsFetcherServiceClient(c.conn) + + request := &pb.CreateKerberosLeaseRequest{CredspecContents: credentialspecs} + + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout)) + defer cancel() + + response, err := client.AddKerberosLease(ctx, request) + if err != nil { + seelog.Errorf("could not create kerberos tickets: %v", err) + return CredentialsFetcherResponse{}, err + } + seelog.Infof("created kerberos tickets and associated with LeaseID: %s", response.GetLeaseId()) + + credentialsFetcherResponse := CredentialsFetcherResponse{ + LeaseID: response.GetLeaseId(), + KerberosTicketPaths: response.GetCreatedKerberosFilePaths(), + } + + return credentialsFetcherResponse, nil +} + +// DeleteKerberosLease() invokes credentials fetcher daemon running on the host +// to delete kerberos tickets of gMSA accounts associated with the leaseid +func (c CredentialsFetcherClient) DeleteKerberosLease(ctx context.Context, leaseid string) (CredentialsFetcherResponse, error) { + if len(leaseid) == 0 { + seelog.Error("invalid leaseid provided") + return CredentialsFetcherResponse{}, nil + } + + defer c.conn.Close() + client := pb.NewCredentialsFetcherServiceClient(c.conn) + + request := &pb.DeleteKerberosLeaseRequest{LeaseId: leaseid} + + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout)) + defer cancel() + + response, err := client.DeleteKerberosLease(ctx, request) + if err != nil { + seelog.Errorf("could not delete kerberos tickets: %v", err) + return CredentialsFetcherResponse{}, err + } + seelog.Infof("deleted kerberos associated with LeaseID: %s", response.GetLeaseId()) + + credentialsFetcherResponse := CredentialsFetcherResponse{ + LeaseID: response.GetLeaseId(), + KerberosTicketPaths: response.GetDeletedKerberosFilePaths(), + } + + return credentialsFetcherResponse, nil +} diff --git a/agent/taskresource/grpcclient/credentialsfetcherclient_test.go b/agent/taskresource/grpcclient/credentialsfetcherclient_test.go new file mode 100644 index 00000000000..e75f4d9035c --- /dev/null +++ b/agent/taskresource/grpcclient/credentialsfetcherclient_test.go @@ -0,0 +1,150 @@ +//go:build unit +// +build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +package grpcclient + +import ( + "context" + "log" + "net" + "testing" + "time" + + pb "github.com/aws/amazon-ecs-agent/agent/taskresource/grpcclient/credentialsfetcher" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" +) + +const ( + leaseid = "123456" + credspec_webapp01 = "{\"CmsPlugins\":[\"ActiveDirectory\"],\"DomainJoinConfig\":{\"Sid\":\"S-1-5-21-4217655605-3681839426-3493040985\",\"MachineAccountName\":\"WebApp01\",\"Guid\":\"af602f85-d754-4eea-9fa8-fd76810485f1\",\"DnsTreeName\":\"contoso.com\",\"DnsName\":\"contoso.com\",\"NetBiosName\":\"contoso\"},\"ActiveDirectoryConfig\":{\"GroupManagedServiceAccounts\":[{\"Name\":\"WebApp01\",\"Scope\":\"contoso.com\"},{\"Name\":\"WebApp01\",\"Scope\":\"contoso\"}]}}" +) + +type mockCredentialsFetcherServer struct { + pb.UnimplementedCredentialsFetcherServiceServer +} + +func (*mockCredentialsFetcherServer) AddKerberosLease(ctx context.Context, req *pb.CreateKerberosLeaseRequest) (*pb.CreateKerberosLeaseResponse, error) { + if len(req.GetCredspecContents()) == 0 { + return &pb.CreateKerberosLeaseResponse{}, status.Errorf(codes.InvalidArgument, "credentialspecs request should not be empty") + } + + return &pb.CreateKerberosLeaseResponse{LeaseId: leaseid, CreatedKerberosFilePaths: []string{"/var/credentials-fetcher/krbdir/123456/webapp01", "/var/credentials-fetcher/krbdir/123456/webapp02"}}, nil +} + +func (*mockCredentialsFetcherServer) DeleteKerberosLease(ctx context.Context, req *pb.DeleteKerberosLeaseRequest) (*pb.DeleteKerberosLeaseResponse, error) { + if len(req.GetLeaseId()) == 0 { + return &pb.DeleteKerberosLeaseResponse{}, status.Errorf(codes.InvalidArgument, "credentialspecs request should not be empty") + } + + return &pb.DeleteKerberosLeaseResponse{LeaseId: leaseid, DeletedKerberosFilePaths: []string{"/var/credentials-fetcher/krbdir/123456/webapp01", "/var/credentials-fetcher/krbdir/123456/webapp02"}}, nil +} + +func dialer() func(context.Context, string) (net.Conn, error) { + listener := bufconn.Listen(1024 * 1024) + + server := grpc.NewServer() + + pb.RegisterCredentialsFetcherServiceServer(server, &mockCredentialsFetcherServer{}) + + go func() { + if err := server.Serve(listener); err != nil { + log.Fatal(err) + } + }() + + return func(context.Context, string) (net.Conn, error) { + return listener.Dial() + } +} + +func TestCredentialsFetcherClient_AddKerberosLease(t *testing.T) { + tests := []struct { + name string + credspecContents []string + response CredentialsFetcherResponse + }{ + { + "invalid request empty credspec contents", + []string{}, + CredentialsFetcherResponse{}, + }, + { + "valid request credspecs associated to gMSA account", + []string{credspec_webapp01}, + CredentialsFetcherResponse{LeaseID: leaseid, KerberosTicketPaths: []string{"/var/credentials-fetcher/krbdir/123456/webapp01", "/var/credentials-fetcher/krbdir/123456/webapp02"}}, + }, + } + + ctx := context.Background() + + conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer())) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, _ := NewCredentialsFetcherClient(conn, time.Minute).AddKerberosLease(context.Background(), tt.credspecContents) + if response.LeaseID != tt.response.LeaseID { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCredentialsFetcherClient_DeleteKerberosLease(t *testing.T) { + tests := []struct { + name string + leaseid string + response CredentialsFetcherResponse + }{ + { + "invalid request empty leaseid input", + "", + CredentialsFetcherResponse{}, + }, + { + "valid request credspecs associated to gMSA account", + leaseid, + CredentialsFetcherResponse{LeaseID: leaseid, KerberosTicketPaths: []string{"/var/credentials-fetcher/krbdir/123456/webapp01", "/var/credentials-fetcher/krbdir/123456/webapp02"}}, + }, + } + + ctx := context.Background() + + conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer())) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := NewCredentialsFetcherClient(conn, time.Minute).DeleteKerberosLease(context.Background(), tt.leaseid) + if response.LeaseID != tt.response.LeaseID { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/agent/taskresource/ssmsecret/ssmsecret_test.go b/agent/taskresource/ssmsecret/ssmsecret_test.go index 8c530bc0be2..de1f13f4c22 100644 --- a/agent/taskresource/ssmsecret/ssmsecret_test.go +++ b/agent/taskresource/ssmsecret/ssmsecret_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/ssmsecret/ssmsecretstatus_test.go b/agent/taskresource/ssmsecret/ssmsecretstatus_test.go index 3e1722fb906..4a86aea20e1 100644 --- a/agent/taskresource/ssmsecret/ssmsecretstatus_test.go +++ b/agent/taskresource/ssmsecret/ssmsecretstatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/types/types_linux_test.go b/agent/taskresource/types/types_linux_test.go index 7f3a683a482..a5facfa1a0e 100644 --- a/agent/taskresource/types/types_linux_test.go +++ b/agent/taskresource/types/types_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/types/types_test.go b/agent/taskresource/types/types_test.go index f90c47540c1..98553b83905 100644 --- a/agent/taskresource/types/types_test.go +++ b/agent/taskresource/types/types_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/types_common.go b/agent/taskresource/types_common.go index e2fb0b8a501..2f748414196 100644 --- a/agent/taskresource/types_common.go +++ b/agent/taskresource/types_common.go @@ -17,6 +17,7 @@ import ( asmfactory "github.com/aws/amazon-ecs-agent/agent/asm/factory" "github.com/aws/amazon-ecs-agent/agent/credentials" fsxfactory "github.com/aws/amazon-ecs-agent/agent/fsx/factory" + s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory" "github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper" ) @@ -26,6 +27,7 @@ type ResourceFieldsCommon struct { ASMClientCreator asmfactory.ClientCreator SSMClientCreator ssmfactory.SSMClientCreator FSxClientCreator fsxfactory.FSxClientCreator + S3ClientCreator s3factory.S3ClientCreator CredentialsManager credentials.Manager EC2InstanceID string } diff --git a/agent/taskresource/types_unix.go b/agent/taskresource/types_unix.go index bf9489b828f..a509a9d99ef 100644 --- a/agent/taskresource/types_unix.go +++ b/agent/taskresource/types_unix.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/types_unsupported.go b/agent/taskresource/types_unsupported.go index 106d78d493c..749ff5b85d9 100644 --- a/agent/taskresource/types_unsupported.go +++ b/agent/taskresource/types_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/types_windows.go b/agent/taskresource/types_windows.go index f189f2318ed..02c344ace27 100644 --- a/agent/taskresource/types_windows.go +++ b/agent/taskresource/types_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -20,14 +21,12 @@ import ( "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" - s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory" ) // ResourceFields is the list of fields required for creation of task resources type ResourceFields struct { *ResourceFieldsCommon - Ctx context.Context - DockerClient dockerapi.DockerClient - S3ClientCreator s3factory.S3ClientCreator - NetworkUtils networkutils.NetworkUtils + Ctx context.Context + DockerClient dockerapi.DockerClient + NetworkUtils networkutils.NetworkUtils } diff --git a/agent/taskresource/volume/dockervolume_test.go b/agent/taskresource/volume/dockervolume_test.go index 2b7b5f2990b..952c0774346 100644 --- a/agent/taskresource/volume/dockervolume_test.go +++ b/agent/taskresource/volume/dockervolume_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/taskresource/volume/volumestatus_test.go b/agent/taskresource/volume/volumestatus_test.go index f2bbc093f23..4fb1fc92132 100644 --- a/agent/taskresource/volume/volumestatus_test.go +++ b/agent/taskresource/volume/volumestatus_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/tcs/client/client.go b/agent/tcs/client/client.go index 6af2a81f597..206b3a60243 100644 --- a/agent/tcs/client/client.go +++ b/agent/tcs/client/client.go @@ -29,6 +29,7 @@ import ( "github.com/aws/amazon-ecs-agent/agent/wsclient" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/private/protocol/json/jsonutil" "github.com/cihub/seelog" "github.com/pborman/uuid" ) @@ -39,13 +40,20 @@ const ( tasksInMetricMessage = 10 // tasksInHealthMessage is the maximum number of tasks that can be sent in a message to the backend tasksInHealthMessage = 10 + // defaultPublishServiceConnectTicker is every 3rd time service connect metrics will be sent to the backend + // Task metrics are published at 20s interval, thus task's service metrics will be published 60s. + defaultPublishServiceConnectTicker = 3 +) + +var ( + // publishMetricRequestSizeLimit is the maximum number of bytes that can be sent in a message to the backend + publishMetricRequestSizeLimit = 1024 * 1024 ) // clientServer implements wsclient.ClientServer interface for metrics backend. type clientServer struct { statsEngine stats.Engine doctor *doctor.Doctor - publishTicker *time.Ticker publishHealthTicker *time.Ticker pullInstanceStatusTicker *time.Ticker ctx context.Context @@ -70,7 +78,6 @@ func New(url string, cs := &clientServer{ statsEngine: statsEngine, doctor: doctor, - publishTicker: nil, publishHealthTicker: nil, pullInstanceStatusTicker: nil, publishMetricsInterval: publishMetricsInterval, @@ -103,13 +110,11 @@ func (cs *clientServer) Serve() error { } // Start the timer function to publish metrics to the backend. - cs.publishTicker = time.NewTicker(cs.publishMetricsInterval) cs.publishHealthTicker = time.NewTicker(cs.publishMetricsInterval) cs.pullInstanceStatusTicker = time.NewTicker(cs.publishMetricsInterval) - if !cs.disableResourceMetrics { - go cs.publishMetrics() - } + go cs.publishMetrics() + go cs.publishHealthMetrics() go cs.publishInstanceStatus() @@ -119,9 +124,6 @@ func (cs *clientServer) Serve() error { // Close closes the underlying connection. func (cs *clientServer) Close() error { - if cs.publishTicker != nil { - cs.publishTicker.Stop() - } if cs.publishHealthTicker != nil { cs.publishHealthTicker.Stop() } @@ -162,7 +164,8 @@ func signRequestFunc(url, region string, credentialProvider *credentials.Credent // publishMetrics invokes the PublishMetricsRequest on the clientserver object. func (cs *clientServer) publishMetrics() { - if cs.publishTicker == nil { + publishTicker := cs.statsEngine.GetPublishMetricsTicker() + if publishTicker == nil { seelog.Debug("Skipping publishing metrics. Publish ticker is uninitialized") return } @@ -170,10 +173,20 @@ func (cs *clientServer) publishMetrics() { // don't simply range over the ticker since its channel doesn't ever get closed for { select { - case <-cs.publishTicker.C: - err := cs.publishMetricsOnce() - if err != nil { - seelog.Warnf("Error publishing metrics: %v", err) + case <-publishTicker.C: + var includeServiceConnectStats bool + metricCounter := cs.statsEngine.GetPublishServiceConnectTickerInterval() + metricCounter++ + if metricCounter == defaultPublishServiceConnectTicker { + includeServiceConnectStats = true + metricCounter = 0 + } + cs.statsEngine.SetPublishServiceConnectTickerInterval(metricCounter) + if !cs.disableResourceMetrics || includeServiceConnectStats { + err := cs.publishMetricsOnce(includeServiceConnectStats) + if err != nil { + seelog.Warnf("Error publishing metrics: %v", err) + } } case <-cs.ctx.Done(): return @@ -182,9 +195,9 @@ func (cs *clientServer) publishMetrics() { } // publishMetricsOnce is invoked by the ticker to periodically publish metrics to backend. -func (cs *clientServer) publishMetricsOnce() error { +func (cs *clientServer) publishMetricsOnce(includeServiceConnectStats bool) error { // Get the list of objects to send to backend. - requests, err := cs.metricsToPublishMetricRequests() + requests, err := cs.metricsToPublishMetricRequests(includeServiceConnectStats) if err != nil { return err } @@ -201,8 +214,8 @@ func (cs *clientServer) publishMetricsOnce() error { // metricsToPublishMetricRequests gets task metrics and converts them to a list of PublishMetricRequest // objects. -func (cs *clientServer) metricsToPublishMetricRequests() ([]*ecstcs.PublishMetricsRequest, error) { - metadata, taskMetrics, err := cs.statsEngine.GetInstanceMetrics() +func (cs *clientServer) metricsToPublishMetricRequests(includeServiceConnectStats bool) ([]*ecstcs.PublishMetricsRequest, error) { + metadata, taskMetrics, err := cs.statsEngine.GetInstanceMetrics(includeServiceConnectStats) if err != nil { return nil, err } @@ -215,18 +228,36 @@ func (cs *clientServer) metricsToPublishMetricRequests() ([]*ecstcs.PublishMetri return requests, nil } var messageTaskMetrics []*ecstcs.TaskMetric + var requestMetadata *ecstcs.MetricsMetadata numTasks := len(taskMetrics) for i, taskMetric := range taskMetrics { + requestMetadata = copyMetricsMetadata(metadata, false) + + // Check if taskMetric without service connect metrics exceed the message size + tempTaskMetric := *taskMetric + tempTaskMetric.ServiceConnectMetricsWrapper = tempTaskMetric.ServiceConnectMetricsWrapper[:0] + + messageTaskMetrics = append(messageTaskMetrics, &tempTaskMetric) + tmsg, _ := jsonutil.BuildJSON(ecstcs.NewPublishMetricsRequest(requestMetadata, copyTaskMetrics(messageTaskMetrics))) + // remove the tempTaskMetric added to messageTaskMetrics after creating tempMessage + messageTaskMetrics = messageTaskMetrics[:len(messageTaskMetrics)-1] + if len(tmsg) > publishMetricRequestSizeLimit { + // Create a new request as the current task metric if added is exceeding the size of the frame. + requests = append(requests, ecstcs.NewPublishMetricsRequest(requestMetadata, copyTaskMetrics(messageTaskMetrics))) + // reset the messageTaskMetrics for the new request + messageTaskMetrics = messageTaskMetrics[:0] + } + + if includeServiceConnectStats { + taskMetric, messageTaskMetrics, requests = cs.serviceConnectMetricsToPublishMetricRequests(requestMetadata, taskMetric, messageTaskMetrics, requests) + } messageTaskMetrics = append(messageTaskMetrics, taskMetric) - var requestMetadata *ecstcs.MetricsMetadata if (i + 1) == numTasks { // If this is the last task to send, set fin to true requestMetadata = copyMetricsMetadata(metadata, true) - } else { - requestMetadata = copyMetricsMetadata(metadata, false) } - if (i+1)%tasksInMetricMessage == 0 { + if len(messageTaskMetrics)%tasksInMetricMessage == 0 { // Construct payload with tasksInMetricMessage number of task metrics and send to backend. requests = append(requests, ecstcs.NewPublishMetricsRequest(requestMetadata, copyTaskMetrics(messageTaskMetrics))) messageTaskMetrics = messageTaskMetrics[:0] @@ -242,9 +273,45 @@ func (cs *clientServer) metricsToPublishMetricRequests() ([]*ecstcs.PublishMetri return requests, nil } +// serviceConnectMetricsToPublishMetricRequests loops over all the SC metrics in a +// task metric to add SC metrics until the message size is within 1 MB. +// If adding a SC metric to the message exceeds the 1 MB limit, it will be sent in the new message +func (cs *clientServer) serviceConnectMetricsToPublishMetricRequests(requestMetadata *ecstcs.MetricsMetadata, taskMetric *ecstcs.TaskMetric, + messageTaskMetrics []*ecstcs.TaskMetric, requests []*ecstcs.PublishMetricsRequest) (*ecstcs.TaskMetric, []*ecstcs.TaskMetric, []*ecstcs.PublishMetricsRequest) { + tempTaskMetric := *taskMetric + tempTaskMetric.ServiceConnectMetricsWrapper = tempTaskMetric.ServiceConnectMetricsWrapper[:0] + + for _, serviceConnectMetric := range taskMetric.ServiceConnectMetricsWrapper { + tempTaskMetric.ServiceConnectMetricsWrapper = append(tempTaskMetric.ServiceConnectMetricsWrapper, serviceConnectMetric) + messageTaskMetrics = append(messageTaskMetrics, &tempTaskMetric) + // TODO [SC]: Load test and profile this since BuildJSON results in lot of CPU and memory consumption. + tempMessage, _ := jsonutil.BuildJSON(ecstcs.NewPublishMetricsRequest(requestMetadata, copyTaskMetrics(messageTaskMetrics))) + // remove the tempTaskMetric added to messageTaskMetrics after creating tempMessage + messageTaskMetrics = messageTaskMetrics[:len(messageTaskMetrics)-1] + if len(tempMessage) > publishMetricRequestSizeLimit { + // since adding this SC metric to the message exceeds the 1 MB limit, remove it from taskMetric and create a request to send it to the backend + tempTaskMetric.ServiceConnectMetricsWrapper = tempTaskMetric.ServiceConnectMetricsWrapper[:len(tempTaskMetric.ServiceConnectMetricsWrapper)-1] + taskMetricTruncated := tempTaskMetric + taskMetricTruncated.ServiceConnectMetricsWrapper = copyServiceConnectMetrics(tempTaskMetric.ServiceConnectMetricsWrapper) + + messageTaskMetrics = append(messageTaskMetrics, &taskMetricTruncated) + requests = append(requests, ecstcs.NewPublishMetricsRequest(requestMetadata, copyTaskMetrics(messageTaskMetrics))) + + // reset the messageTaskMetrics and tempTaskMetric for the new request, + messageTaskMetrics = messageTaskMetrics[:0] + tempTaskMetric.ServiceConnectMetricsWrapper = tempTaskMetric.ServiceConnectMetricsWrapper[:0] + // container metrics will be sent only once for each task metric + tempTaskMetric.ContainerMetrics = tempTaskMetric.ContainerMetrics[:0] + // add the serviceConnectMetric to tempTaskMetric to be sent in the next message + tempTaskMetric.ServiceConnectMetricsWrapper = append(tempTaskMetric.ServiceConnectMetricsWrapper, serviceConnectMetric) + } + } + return &tempTaskMetric, messageTaskMetrics, requests +} + // publishHealthMetrics send the container health information to backend func (cs *clientServer) publishHealthMetrics() { - if cs.publishTicker == nil { + if cs.publishHealthTicker == nil { seelog.Debug("Skipping publishing health metrics. Publish ticker is uninitialized") return } @@ -343,6 +410,17 @@ func copyTaskMetrics(from []*ecstcs.TaskMetric) []*ecstcs.TaskMetric { return to } +// copyServiceConnectMetrics loops over list of GeneralMetricsWrapper obejcts and creates a new GeneralMetricsWrapper list +// and creates a new GeneralMetricsWrapper object from each given GeneralMetricsWrapper object. +func copyServiceConnectMetrics(scMetrics []*ecstcs.GeneralMetricsWrapper) []*ecstcs.GeneralMetricsWrapper { + scMetricsTo := make([]*ecstcs.GeneralMetricsWrapper, len(scMetrics)) + for i, scMetricFrom := range scMetrics { + scMetricTo := *scMetricFrom + scMetricsTo[i] = &scMetricTo + } + return scMetricsTo +} + // copyHealthMetadata performs a deep copy of HealthMetadata object func copyHealthMetadata(metadata *ecstcs.HealthMetadata, fin bool) *ecstcs.HealthMetadata { return &ecstcs.HealthMetadata{ diff --git a/agent/tcs/client/client_test.go b/agent/tcs/client/client_test.go index 883b8c82add..4047ca04762 100644 --- a/agent/tcs/client/client_test.go +++ b/agent/tcs/client/client_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -23,6 +24,7 @@ package tcsclient import ( "fmt" + "math/rand" "strconv" "testing" "time" @@ -42,11 +44,12 @@ import ( ) const ( - testPublishMetricsInterval = 1 * time.Second - testMessageId = "testMessageId" - testCluster = "default" - testContainerInstance = "containerInstance" - rwTimeout = time.Second + testPublishMetricsInterval = 1 * time.Second + testMessageId = "testMessageId" + testCluster = "default" + testContainerInstance = "containerInstance" + rwTimeout = time.Second + testPublishMetricRequestSizeLimit = 1024 ) const ( @@ -104,7 +107,7 @@ var emptyDoctor, _ = doctor.NewDoctor([]doctor.Healthcheck{}, "test-cluster", "t type mockStatsEngine struct{} -func (*mockStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (*mockStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { return nil, nil, fmt.Errorf("uninitialized") } @@ -116,9 +119,21 @@ func (*mockStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstc return nil, nil, nil } +func (*mockStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*mockStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*mockStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + type emptyStatsEngine struct{} -func (*emptyStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (*emptyStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { return nil, nil, fmt.Errorf("empty stats") } @@ -130,9 +145,21 @@ func (*emptyStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecst return nil, nil, nil } +func (*emptyStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*emptyStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*emptyStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + type idleStatsEngine struct{} -func (*idleStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (*idleStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { metadata := &ecstcs.MetricsMetadata{ Cluster: aws.String(testCluster), ContainerInstance: aws.String(testContainerInstance), @@ -150,11 +177,23 @@ func (*idleStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstc return nil, nil, nil } +func (*idleStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*idleStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*idleStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + type nonIdleStatsEngine struct { numTasks int } -func (engine *nonIdleStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (engine *nonIdleStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { metadata := &ecstcs.MetricsMetadata{ Cluster: aws.String(testCluster), ContainerInstance: aws.String(testContainerInstance), @@ -177,10 +216,124 @@ func (*nonIdleStatsEngine) ContainerDockerStats(taskARN string, id string) (*typ func (*nonIdleStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstcs.TaskHealth, error) { return nil, nil, nil } + +func (*nonIdleStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*nonIdleStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*nonIdleStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + func newNonIdleStatsEngine(numTasks int) *nonIdleStatsEngine { return &nonIdleStatsEngine{numTasks: numTasks} } +type serviceConnectStatsEngine struct { + numTasks int +} + +func (engine *serviceConnectStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { + metadata := &ecstcs.MetricsMetadata{ + Cluster: aws.String(testCluster), + ContainerInstance: aws.String(testContainerInstance), + Idle: aws.Bool(false), + MessageId: aws.String(testMessageId), + } + var taskMetrics []*ecstcs.TaskMetric + var i int64 + var fval float64 + fval = rand.Float64() + var ival int64 + ival = rand.Int63n(10) + for i = 0; int(i) < engine.numTasks; i++ { + taskArn := "task/" + strconv.FormatInt(i, 10) + taskMetric := ecstcs.TaskMetric{ + TaskArn: &taskArn, + ContainerMetrics: []*ecstcs.ContainerMetric{ + { + CpuStatsSet: &ecstcs.CWStatsSet{ + Max: &fval, + Min: &fval, + SampleCount: &ival, + Sum: &fval, + }, + MemoryStatsSet: &ecstcs.CWStatsSet{ + Max: &fval, + Min: &fval, + SampleCount: &ival, + Sum: &fval, + }, + }, + }, + } + if includeServiceConnectStats { + var serviceConnectMetrics []*ecstcs.GeneralMetricsWrapper + var generalMetrics []*ecstcs.GeneralMetric + metricType := "2" + dimensionKey := "ClusterName" + dimentsionValue := "TestClusterName" + metricName := "HTTPCode_Target_2XX_Count" + metricValue := 3.0 + var metricCount int64 = 1 + + // generate a task metric with size more than testPublishMetricRequestSizeLimit i.e 1kB + generalMetric := ecstcs.GeneralMetric{ + MetricName: &metricName, + MetricValues: []*float64{&metricValue}, + MetricCounts: []*int64{&metricCount}, + } + generalMetrics = append(generalMetrics, &generalMetric) + generalMetrics = append(generalMetrics, &generalMetric) + generalMetrics = append(generalMetrics, &generalMetric) + generalMetrics = append(generalMetrics, &generalMetric) + generalMetricsWrapper := ecstcs.GeneralMetricsWrapper{ + MetricType: &metricType, + Dimensions: []*ecstcs.Dimension{ + { + Key: &dimensionKey, + Value: &dimentsionValue, + }, + }, + GeneralMetrics: generalMetrics, + } + serviceConnectMetrics = append(serviceConnectMetrics, &generalMetricsWrapper) + serviceConnectMetrics = append(serviceConnectMetrics, &generalMetricsWrapper) + taskMetric.ServiceConnectMetricsWrapper = serviceConnectMetrics + } + taskMetrics = append(taskMetrics, &taskMetric) + } + return metadata, taskMetrics, nil +} + +func (*serviceConnectStatsEngine) ContainerDockerStats(taskARN string, id string) (*types.StatsJSON, *stats.NetworkStatsPerSec, error) { + return nil, nil, fmt.Errorf("not implemented") +} + +func (*serviceConnectStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstcs.TaskHealth, error) { + return nil, nil, nil +} + +func (*serviceConnectStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*serviceConnectStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*serviceConnectStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + +func newServiceConnectStatsEngine(numTasks int) *serviceConnectStatsEngine { + return &serviceConnectStatsEngine{numTasks: numTasks} +} + func TestPayloadHandlerCalled(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -233,7 +386,7 @@ func TestPublishMetricsOnceEmptyStatsError(t *testing.T) { cs := clientServer{ statsEngine: &emptyStatsEngine{}, } - err := cs.publishMetricsOnce() + err := cs.publishMetricsOnce(false) assert.Error(t, err, "Failed: expecting publishMerticOnce return err ") } @@ -242,7 +395,7 @@ func TestPublishOnceIdleStatsEngine(t *testing.T) { cs := clientServer{ statsEngine: &idleStatsEngine{}, } - requests, err := cs.metricsToPublishMetricRequests() + requests, err := cs.metricsToPublishMetricRequests(false) if err != nil { t.Fatal("Error creating publishmetricrequests: ", err) } @@ -263,7 +416,7 @@ func TestPublishOnceNonIdleStatsEngine(t *testing.T) { cs := clientServer{ statsEngine: newNonIdleStatsEngine(numTasks), } - requests, err := cs.metricsToPublishMetricRequests() + requests, err := cs.metricsToPublishMetricRequests(false) if err != nil { t.Fatal("Error creating publishmetricrequests: ", err) } @@ -292,6 +445,67 @@ func TestPublishOnceNonIdleStatsEngine(t *testing.T) { } } +func TestPublishServiceConnectStatsEngine(t *testing.T) { + tempLimit := publishMetricRequestSizeLimit + publishMetricRequestSizeLimit = testPublishMetricRequestSizeLimit + defer func() { + publishMetricRequestSizeLimit = tempLimit + }() + + testCases := []struct { + name string + numTasks int + expectedRequests int + }{ + { + name: "publish metrics requests with under 10 tasks with service connect stats", + numTasks: 3, + expectedRequests: 6, + }, + { + name: "publish metrics requests with more than 10 tasks with service connect stats", + numTasks: 20, + expectedRequests: 40, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cs := clientServer{ + statsEngine: newServiceConnectStatsEngine(tc.numTasks), + } + requests, err := cs.metricsToPublishMetricRequests(true) + if err != nil { + t.Fatal("Error creating publishmetricrequests: ", err) + } + + taskArns := make(map[string]bool) + for _, request := range requests { + for _, taskMetric := range request.TaskMetrics { + _, exists := taskArns[*taskMetric.TaskArn] + // if it is first part of task metric or a complete task metric being sent in this request + // validate that ContainerMetrics is not empty + if !exists { + assert.NotEmpty(t, taskMetric.ContainerMetrics, "Expected Container metrics to be not empty") + } else { + // task metric with remaining service connect metrics being sent in the next request + // validate that ContainerMetrics is empty + assert.Empty(t, taskMetric.ContainerMetrics, "Expected Container metrics to be empty, got %d", len(taskMetric.ContainerMetrics)) + } + taskArns[*taskMetric.TaskArn] = true + } + } + assert.Equal(t, tc.expectedRequests, len(requests), "Wrong number of requests generated") + lastRequest := requests[tc.expectedRequests-1] + assert.True(t, *lastRequest.Metadata.Fin, "Fin not set to true in last request") + requests = requests[:(tc.expectedRequests - 1)] + for i, request := range requests { + assert.False(t, *request.Metadata.Fin, "Fin set to true in request %d/%d", i, (tc.expectedRequests - 1)) + } + }) + } +} + func testCS(conn *mock_wsconn.MockWebsocketConn) wsclient.ClientServer { cfg := &config.Config{ AWSRegion: "us-east-1", @@ -372,6 +586,7 @@ func TestMetricsDisabled(t *testing.T) { readed := make(chan struct{}) // stats engine should only be called for getting health metrics + mockStatsEngine.EXPECT().GetPublishMetricsTicker().Return(time.NewTicker(config.DefaultContainerMetricsPublishInterval)).MinTimes(1) mockStatsEngine.EXPECT().GetTaskHealthMetrics().Return(&ecstcs.HealthMetadata{ Cluster: aws.String("TestMetricsDisabled"), ContainerInstance: aws.String("container_instance"), @@ -387,7 +602,10 @@ func TestMetricsDisabled(t *testing.T) { published <- struct{}{} }).Return(nil).MinTimes(1) - go cs.Serve() + go func() { + err := cs.Serve() + assert.NoError(t, err) + }() <-published <-readed } diff --git a/agent/tcs/handler/handler.go b/agent/tcs/handler/handler.go index b9ef6789431..70cf1ee14d3 100644 --- a/agent/tcs/handler/handler.go +++ b/agent/tcs/handler/handler.go @@ -143,7 +143,7 @@ func startSession( client.AddRequestHandler(ackPublishHealthMetricHandler(timer)) client.AddRequestHandler(ackPublishInstanceStatusHandler(timer)) client.SetAnyRequestHandler(anyMessageHandler(client)) - serveC := make(chan error) + serveC := make(chan error, 1) go func() { serveC <- client.Serve() }() diff --git a/agent/tcs/handler/handler_test.go b/agent/tcs/handler/handler_test.go index e815b148530..68ac8c16dea 100644 --- a/agent/tcs/handler/handler_test.go +++ b/agent/tcs/handler/handler_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -66,7 +67,7 @@ var testCfg = &config.Config{ var emptyDoctor, _ = doctor.NewDoctor([]doctor.Healthcheck{}, "test-cluster", "this:is:an:instance:arn") -func (*mockStatsEngine) GetInstanceMetrics() (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { +func (*mockStatsEngine) GetInstanceMetrics(includeServiceConnectStats bool) (*ecstcs.MetricsMetadata, []*ecstcs.TaskMetric, error) { req := createPublishMetricsRequest() return req.Metadata, req.TaskMetrics, nil } @@ -79,6 +80,18 @@ func (*mockStatsEngine) GetTaskHealthMetrics() (*ecstcs.HealthMetadata, []*ecstc return nil, nil, nil } +func (*mockStatsEngine) GetPublishServiceConnectTickerInterval() int32 { + return 0 +} + +func (*mockStatsEngine) SetPublishServiceConnectTickerInterval(counter int32) { + return +} + +func (*mockStatsEngine) GetPublishMetricsTicker() *time.Ticker { + return time.NewTicker(config.DefaultContainerMetricsPublishInterval) +} + // TestDisableMetrics tests the StartMetricsSession will return immediately if // the metrics was disabled func TestDisableMetrics(t *testing.T) { diff --git a/agent/tcs/handler/types_test.go b/agent/tcs/handler/types_test.go index 98ac4a09a58..6879f5c1bff 100644 --- a/agent/tcs/handler/types_test.go +++ b/agent/tcs/handler/types_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/tcs/model/api/api-2.json b/agent/tcs/model/api/api-2.json index 702f31dc606..00d06ef5d8b 100644 --- a/agent/tcs/model/api/api-2.json +++ b/agent/tcs/model/api/api-2.json @@ -191,7 +191,38 @@ "type":"list", "member":{"shape":"ContainerMetric"} }, + "Dimension":{ + "type":"structure", + "members":{ + "key":{"shape":"String"}, + "value":{"shape":"String"} + } + }, + "Dimensions":{ + "type":"list", + "member":{"shape":"Dimension"} + }, "Double":{"type":"double"}, + "GeneralMetric":{ + "type":"structure", + "members":{ + "metricName":{"shape":"String"}, + "metricValues":{"shape":"MetricValues"}, + "metricCounts":{"shape":"MetricCounts"} + } + }, + "GeneralMetrics":{ + "type":"list", + "member":{"shape":"GeneralMetric"} + }, + "GeneralMetricsWrapper":{ + "type":"structure", + "members":{ + "dimensions":{"shape":"Dimensions"}, + "metricType":{"shape":"MetricType"}, + "generalMetrics":{"shape":"GeneralMetrics"} + } + }, "HealthMetadata":{ "type":"structure", "members":{ @@ -252,6 +283,22 @@ }, "exception":true }, + "Long":{"type":"long"}, + "MetricCounts":{ + "type":"list", + "member":{"shape":"Long"} + }, + "MetricType":{ + "type":"string", + "enum":[ + "1", + "2" + ] + }, + "MetricValues":{ + "type":"list", + "member":{"shape":"Double"} + }, "MetricsMetadata":{ "type":"structure", "members":{ @@ -316,6 +363,10 @@ "exception":true, "fault":true }, + "ServiceConnectMetricsWrapper":{ + "type":"list", + "member":{"shape":"GeneralMetricsWrapper"} + }, "StartTelemetrySessionRequest":{ "type":"structure", "members":{ @@ -358,7 +409,8 @@ "clusterArn":{"shape":"String"}, "taskDefinitionFamily":{"shape":"String"}, "taskDefinitionVersion":{"shape":"String"}, - "containerMetrics":{"shape":"ContainerMetrics"} + "containerMetrics":{"shape":"ContainerMetrics"}, + "serviceConnectMetricsWrapper":{"shape":"ServiceConnectMetricsWrapper"} } }, "TaskMetrics":{ @@ -369,7 +421,7 @@ "Timestamp":{"type":"timestamp"}, "UDouble":{ "type":"double", - "min":0 + "min":0.0 }, "UDoubleCWStatsSet":{ "type":"structure", @@ -413,4 +465,4 @@ } } } -} +} \ No newline at end of file diff --git a/agent/tcs/model/ecstcs/api.go b/agent/tcs/model/ecstcs/api.go index 338414abf56..6c2273329d6 100644 --- a/agent/tcs/model/ecstcs/api.go +++ b/agent/tcs/model/ecstcs/api.go @@ -215,6 +215,64 @@ func (s *ContainerMetric) Validate() error { return nil } +type Dimension struct { + _ struct{} `type:"structure"` + + Key *string `locationName:"key" type:"string"` + + Value *string `locationName:"value" type:"string"` +} + +// String returns the string representation +func (s Dimension) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s Dimension) GoString() string { + return s.String() +} + +type GeneralMetric struct { + _ struct{} `type:"structure"` + + MetricCounts []*int64 `locationName:"metricCounts" type:"list"` + + MetricName *string `locationName:"metricName" type:"string"` + + MetricValues []*float64 `locationName:"metricValues" type:"list"` +} + +// String returns the string representation +func (s GeneralMetric) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s GeneralMetric) GoString() string { + return s.String() +} + +type GeneralMetricsWrapper struct { + _ struct{} `type:"structure"` + + Dimensions []*Dimension `locationName:"dimensions" type:"list"` + + GeneralMetrics []*GeneralMetric `locationName:"generalMetrics" type:"list"` + + MetricType *string `locationName:"metricType" type:"string" enum:"MetricType"` +} + +// String returns the string representation +func (s GeneralMetricsWrapper) String() string { + return awsutil.Prettify(s) +} + +// GoString returns the string representation +func (s GeneralMetricsWrapper) GoString() string { + return s.String() +} + type HealthMetadata struct { _ struct{} `type:"structure"` @@ -933,6 +991,8 @@ type TaskMetric struct { ContainerMetrics []*ContainerMetric `locationName:"containerMetrics" type:"list"` + ServiceConnectMetricsWrapper []*GeneralMetricsWrapper `locationName:"serviceConnectMetricsWrapper" type:"list"` + TaskArn *string `locationName:"taskArn" type:"string"` TaskDefinitionFamily *string `locationName:"taskDefinitionFamily" type:"string"` diff --git a/agent/tools.go b/agent/tools.go index bb248e6b6dc..4a6061cdfdd 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -1,4 +1,5 @@ //go:build tools +// +build tools // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/compare_versions_test.go b/agent/utils/compare_versions_test.go index d9b7883d467..b00b10b1889 100644 --- a/agent/utils/compare_versions_test.go +++ b/agent/utils/compare_versions_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/cpuinfo.go b/agent/utils/cpuinfo.go index 390768043d5..0fa8f845de9 100644 --- a/agent/utils/cpuinfo.go +++ b/agent/utils/cpuinfo.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/cpuinfo_test.go b/agent/utils/cpuinfo_test.go index bf86ee282d6..d629ec94ca3 100644 --- a/agent/utils/cpuinfo_test.go +++ b/agent/utils/cpuinfo_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/ephemeral_ports.go b/agent/utils/ephemeral_ports.go new file mode 100644 index 00000000000..25caaedb6ac --- /dev/null +++ b/agent/utils/ephemeral_ports.go @@ -0,0 +1,184 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package utils + +import ( + "fmt" + "math/rand" + "net" + "strconv" + "sync" + "time" + + "github.com/docker/go-connections/nat" +) + +// From https://www.kernel.org/doc/html/latest//networking/ip-sysctl.html#ip-variables +const ( + EphemeralPortMin = 32768 + EphemeralPortMax = 60999 + maxPortSelectionAttempts = 100 +) + +var ( + // Injection point for UTs + randIntFunc = rand.Intn + // portLock is a mutex lock used to prevent two concurrent tasks to get the same host ports. + portLock sync.Mutex +) + +// GenerateEphemeralPortNumbers generates a list of n unique port numbers in the 32768-60999 range. The resulting port +// number list is guaranteed to not include any port number present in "reserved" parameter. +func GenerateEphemeralPortNumbers(n int, reserved []uint16) ([]uint16, error) { + toExcludeSet := map[uint16]struct{}{} + for _, e := range reserved { + toExcludeSet[e] = struct{}{} + } + rand.Seed(time.Now().UnixNano()) + + var result []uint16 + var portSelectionAttempts int + for len(result) < n { + // The intention of maxPortSelectionAttempts is to avoid a super highly unlikely case where we + // keep getting ports that collide, thus creating an infinite loop. + if portSelectionAttempts > maxPortSelectionAttempts { + return nil, fmt.Errorf("maximum number of attempts to generate unique ports reached") + } + port := uint16(randIntFunc(EphemeralPortMax-EphemeralPortMin+1) + EphemeralPortMin) + if _, ok := toExcludeSet[port]; ok { + portSelectionAttempts++ + continue + } + toExcludeSet[port] = struct{}{} + result = append(result, port) + } + return result, nil +} + +// safePortTracker tracks the host port last assigned to a container port range and is safe to use concurrently. +// TODO: implement a port manager that does synchronization and integrates with a configurable option to modify ephemeral range +type safePortTracker struct { + mu sync.Mutex + lastAssignedHostPort int +} + +// SetLastAssignedHostPort sets the last assigned host port +func (pt *safePortTracker) SetLastAssignedHostPort(port int) { + pt.mu.Lock() + defer pt.mu.Unlock() + + pt.lastAssignedHostPort = port +} + +// GetLastAssignedHostPort returns the last assigned host port +func (pt *safePortTracker) GetLastAssignedHostPort() int { + pt.mu.Lock() + defer pt.mu.Unlock() + + return pt.lastAssignedHostPort +} + +var tracker safePortTracker + +// GetHostPortRange gets N contiguous host ports from the ephemeral host port range defined on the host. +func GetHostPortRange(numberOfPorts int, protocol string, dynamicHostPortRange string) (string, error) { + portLock.Lock() + defer portLock.Unlock() + + // get ephemeral port range, either default or if custom-defined + startHostPortRange, endHostPortRange, _ := nat.ParsePortRangeToInt(dynamicHostPortRange) + start := startHostPortRange + end := endHostPortRange + + // get the last assigned host port + lastAssignedHostPort := tracker.GetLastAssignedHostPort() + if lastAssignedHostPort != 0 { + // this implies that this is not the first time we're searching for host ports + // so start searching for new ports from the last tracked port + start = lastAssignedHostPort + 1 + } + + result, lastCheckedPort, err := getHostPortRange(numberOfPorts, start, end, protocol) + if err != nil { + if lastAssignedHostPort != 0 { + // this implies that there are no contiguous host ports available from lastAssignedHostPort to endHostPortRange + // so, we need to loop back to the startHostPortRange and check for contiguous ports until lastCheckedPort + start = startHostPortRange + end = lastCheckedPort - 1 + result, lastCheckedPort, err = getHostPortRange(numberOfPorts, start, end, protocol) + } + } + + if lastCheckedPort == endHostPortRange { + tracker.SetLastAssignedHostPort(startHostPortRange - 1) + } else { + tracker.SetLastAssignedHostPort(lastCheckedPort) + } + + return result, err +} + +func getHostPortRange(numberOfPorts, start, end int, protocol string) (string, int, error) { + var resultStartPort, resultEndPort, n int + for port := start; port <= end; port++ { + portStr := strconv.Itoa(port) + // check if port is available + if protocol == "tcp" { + // net.Listen announces on the local tcp network + ln, err := net.Listen(protocol, ":"+portStr) + // either port is unavailable or some error occurred while listening, we proceed to the next port + if err != nil { + continue + } + // let's close the listener first + err = ln.Close() + if err != nil { + continue + } + } else if protocol == "udp" { + // net.ListenPacket announces on the local udp network + ln, err := net.ListenPacket(protocol, ":"+portStr) + // either port is unavailable or some error occurred while listening, we proceed to the next port + if err != nil { + continue + } + // let's close the listener first + err = ln.Close() + if err != nil { + continue + } + } + + // check if current port is contiguous relative to lastPort + if port-resultEndPort != 1 { + resultStartPort = port + resultEndPort = port + n = 1 + } else { + resultEndPort = port + n += 1 + } + + // we've got contiguous available ephemeral host ports to use, equal to the requested numberOfPorts + if n == numberOfPorts { + break + } + } + + if n != numberOfPorts { + return "", resultEndPort, fmt.Errorf("%v contiguous host ports unavailable", numberOfPorts) + } + + return fmt.Sprintf("%d-%d", resultStartPort, resultEndPort), resultEndPort, nil +} diff --git a/agent/utils/ephemeral_ports_linux.go b/agent/utils/ephemeral_ports_linux.go new file mode 100644 index 00000000000..b959326aef1 --- /dev/null +++ b/agent/utils/ephemeral_ports_linux.go @@ -0,0 +1,52 @@ +//go:build linux +// +build linux + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package utils + +import ( + "bufio" + "fmt" + "os" +) + +const ( + // portRangeKernelParam is a kernel parameter that defines the ephemeral port range + portRangeKernelParam = "/proc/sys/net/ipv4/ip_local_port_range" + // DefaultPortRangeStart indicates the first port in ephemeral port range + DefaultPortRangeStart = 49153 + // DefaultPortRangeEnd indicates the last port in ephemeral port range + DefaultPortRangeEnd = 65535 +) + +// GetDynamicHostPortRange returns the ephemeral port range defined by the "/proc/sys/net/ipv4/ip_local_port_range" +// kernel parameter. Ref: https://github.com/moby/moby/blob/master/libnetwork/portallocator/portallocator_linux.go +func GetDynamicHostPortRange() (start int, end int, err error) { + file, err := os.Open(portRangeKernelParam) + if err != nil { + return 0, 0, err + } + defer file.Close() + + n, err := fmt.Fscanf(bufio.NewReader(file), "%d\t%d", &start, &end) + if n != 2 || err != nil { + if err == nil { + err = fmt.Errorf("unexpected count of parsed numbers (%d)", n) + } + return 0, 0, fmt.Errorf("failed to parse ephemeral port range from %s: %v", + portRangeKernelParam, err) + } + return start, end, nil +} diff --git a/agent/utils/ephemeral_ports_test.go b/agent/utils/ephemeral_ports_test.go new file mode 100644 index 00000000000..a8ef7c9b88f --- /dev/null +++ b/agent/utils/ephemeral_ports_test.go @@ -0,0 +1,164 @@ +package utils + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +import ( + "errors" + "math/rand" + "testing" + "time" + + "github.com/docker/go-connections/nat" + + "github.com/stretchr/testify/assert" +) + +const ( + testTCPProtocol = "tcp" + testUDPProtocol = "udp" +) + +func TestGenerateEphemeralPortNumbers(t *testing.T) { + // This number is just to "stress" this test by increasing the changes of collision, which should not be a problem + // in prod, only around a dozen ports will be needed in the worst case. + expectedPortsGenerated := 1000 + var reservedPorts []uint16 + rand.Seed(time.Now().UnixNano()) + // Since absolute max containers for a task is 20, let's exclude 40 ports at random, 2 per container + // in order to make a somewhat realistic test + for i := 0; i < 40; i++ { + port := uint16(rand.Intn(EphemeralPortMax-EphemeralPortMin+1) + EphemeralPortMin) + reservedPorts = append(reservedPorts, port) + } + toExcludeSet := map[uint16]struct{}{} + for _, e := range reservedPorts { + toExcludeSet[e] = struct{}{} + } + ports, err := GenerateEphemeralPortNumbers(expectedPortsGenerated, reservedPorts) + assert.NoError(t, err) + assert.Len(t, ports, expectedPortsGenerated, "Not enough ports generated") + for _, port := range ports { + _, ok := toExcludeSet[port] + assert.False(t, ok, "Port collision detected") + assert.Conditionf(t, func() (success bool) { + return port >= EphemeralPortMin && port <= EphemeralPortMax + }, "Port was generated outside the ephemeral range [%d-%d]", EphemeralPortMin, EphemeralPortMax) + } +} + +func TestGenerateEphemeralPortNumbers_CollisionError(t *testing.T) { + randIntFuncTmp := randIntFunc + defer func() { + randIntFunc = randIntFuncTmp + }() + // Inject mock rand.Int that always returns the same number in order to test max attempts + randIntFunc = func(n int) int { + return EphemeralPortMin + } + ports, err := GenerateEphemeralPortNumbers(100, []uint16{}) + assert.Nil(t, ports) + assert.Error(t, err) + assert.Equal(t, "maximum number of attempts to generate unique ports reached", err.Error()) +} + +func TestGetHostPortRange(t *testing.T) { + testCases := []struct { + testName string + numberOfPorts int + testDynamicHostPortRange string + protocol string + expectedLastAssignedPort []int + numberOfRequests int + expectedError error + }{ + { + testName: "tcp protocol, contiguous hostPortRange found", + numberOfPorts: 10, + testDynamicHostPortRange: "40001-40080", + protocol: testTCPProtocol, + expectedLastAssignedPort: []int{40010}, + numberOfRequests: 1, + expectedError: nil, + }, + { + testName: "udp protocol, contiguous hostPortRange found", + numberOfPorts: 30, + testDynamicHostPortRange: "40001-40080", + protocol: testUDPProtocol, + expectedLastAssignedPort: []int{40040}, + numberOfRequests: 1, + expectedError: nil, + }, + { + testName: "2 requests for contiguous hostPortRange in succession, success", + numberOfPorts: 20, + testDynamicHostPortRange: "40001-40080", + protocol: testTCPProtocol, + expectedLastAssignedPort: []int{40060, 40000}, + numberOfRequests: 2, + expectedError: nil, + }, + { + testName: "contiguous hostPortRange after looping back, success", + numberOfPorts: 15, + testDynamicHostPortRange: "40001-40080", + protocol: testUDPProtocol, + expectedLastAssignedPort: []int{40015}, + numberOfRequests: 1, + expectedError: nil, + }, + { + testName: "contiguous hostPortRange not found", + numberOfPorts: 20, + testDynamicHostPortRange: "40001-40005", + protocol: testTCPProtocol, + numberOfRequests: 1, + expectedError: errors.New("20 contiguous host ports unavailable"), + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + for i := 0; i < tc.numberOfRequests; i++ { + if tc.expectedError == nil { + + hostPortRange, err := GetHostPortRange(tc.numberOfPorts, tc.protocol, tc.testDynamicHostPortRange) + assert.NoError(t, err) + + numberOfHostPorts, err := getPortRangeLength(hostPortRange) + assert.NoError(t, err) + assert.Equal(t, tc.numberOfPorts, numberOfHostPorts) + + actualLastAssignedHostPort := tracker.GetLastAssignedHostPort() + assert.Equal(t, tc.expectedLastAssignedPort[i], actualLastAssignedHostPort) + } else { + // need to reset the tracker to avoid getting data from previous test cases + tracker.SetLastAssignedHostPort(0) + + hostPortRange, err := GetHostPortRange(tc.numberOfPorts, tc.protocol, tc.testDynamicHostPortRange) + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, "", hostPortRange) + } + } + }) + } +} + +func getPortRangeLength(portRange string) (int, error) { + startPort, endPort, err := nat.ParsePortRangeToInt(portRange) + if err != nil { + return 0, err + } + return endPort - startPort + 1, nil +} diff --git a/agent/utils/ephemeral_ports_unsupported.go b/agent/utils/ephemeral_ports_unsupported.go new file mode 100644 index 00000000000..f59dd3f5e4d --- /dev/null +++ b/agent/utils/ephemeral_ports_unsupported.go @@ -0,0 +1,29 @@ +//go:build !linux && !windows +// +build !linux,!windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package utils + +const ( + // DefaultPortRangeStart indicates the first port in ephemeral port range + DefaultPortRangeStart = 49153 + // DefaultPortRangeEnd indicates the last port in ephemeral port range + DefaultPortRangeEnd = 65535 +) + +// GetDynamicHostPortRange returns the default ephemeral port range +func GetDynamicHostPortRange() (start int, end int, err error) { + return DefaultPortRangeStart, DefaultPortRangeEnd, nil +} diff --git a/agent/utils/ephemeral_ports_windows.go b/agent/utils/ephemeral_ports_windows.go new file mode 100644 index 00000000000..79d5473963c --- /dev/null +++ b/agent/utils/ephemeral_ports_windows.go @@ -0,0 +1,18 @@ +//go:build windows +// +build windows + +package utils + +const ( + // Ref: https://learn.microsoft.com/en-US/troubleshoot/windows-server/networking/default-dynamic-port-range-tcpip-chang + // DefaultPortRangeStart indicates the first port in ephemeral port range + DefaultPortRangeStart = 49152 + // DefaultPortRangeEnd indicates the last port in ephemeral port range + DefaultPortRangeEnd = 65535 +) + +// GetDynamicHostPortRange returns the default ephemeral port range on Windows. +// TODO: instead of sticking to defaults, run netsh commands on the host to get the ranges. +func GetDynamicHostPortRange() (start int, end int, err error) { + return DefaultPortRangeStart, DefaultPortRangeEnd, nil +} diff --git a/agent/utils/json_test.go b/agent/utils/json_test.go index 6e4c6803835..b9746c2e677 100644 --- a/agent/utils/json_test.go +++ b/agent/utils/json_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/license_test.go b/agent/utils/license_test.go index d66f674db4c..7403ce2747a 100644 --- a/agent/utils/license_test.go +++ b/agent/utils/license_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/eni/pause/error.go b/agent/utils/loader/error.go similarity index 80% rename from agent/eni/pause/error.go rename to agent/utils/loader/error.go index 454ad0a29a7..27a054dddbd 100644 --- a/agent/eni/pause/error.go +++ b/agent/utils/loader/error.go @@ -11,20 +11,17 @@ // express or implied. See the License for the specific language governing // permissions and limitations under the License. -package pause +package loader -// https://golang.org/src/syscall/zerrors_linux_386.go#L1382 -const noSuchFile = "no such file or directory" - -// UnsupportedPlatformError indicates an error when loading pause container +// UnsupportedPlatformError indicates an error when loading appnet container // image on an unsupported OS platform type UnsupportedPlatformError struct { error } -// UnsupportedPlatform returns true if the error is of UnsupportedPlatformError +// IsUnsupportedPlatform returns true if the error is of UnsupportedPlatformError // type -func UnsupportedPlatform(err error) bool { +func IsUnsupportedPlatform(err error) bool { _, ok := err.(UnsupportedPlatformError) return ok } diff --git a/agent/eni/pause/error_test.go b/agent/utils/loader/error_test.go similarity index 94% rename from agent/eni/pause/error_test.go rename to agent/utils/loader/error_test.go index 5a7cad12aae..04b422100ad 100644 --- a/agent/eni/pause/error_test.go +++ b/agent/utils/loader/error_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -13,7 +14,7 @@ // express or implied. See the License for the specific language governing // permissions and limitations under the License. -package pause +package loader import ( "errors" @@ -32,7 +33,7 @@ func TestUnsupportedPlatform(t *testing.T) { for err, expected := range testCases { t.Run(fmt.Sprintf("returns %t for type %s", expected, reflect.TypeOf(err)), func(t *testing.T) { - assert.Equal(t, expected, UnsupportedPlatform(err)) + assert.Equal(t, expected, IsUnsupportedPlatform(err)) }) } } diff --git a/agent/utils/loader/generate_mocks.go b/agent/utils/loader/generate_mocks.go new file mode 100644 index 00000000000..46ff3d3bf74 --- /dev/null +++ b/agent/utils/loader/generate_mocks.go @@ -0,0 +1,16 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package loader + +//go:generate mockgen -destination=mocks/load_mocks.go -copyright_file=../../../scripts/copyright_file github.com/aws/amazon-ecs-agent/agent/utils/loader Loader diff --git a/agent/utils/loader/load.go b/agent/utils/loader/load.go new file mode 100644 index 00000000000..8a4dd3c5baa --- /dev/null +++ b/agent/utils/loader/load.go @@ -0,0 +1,87 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package loader + +import ( + "context" + "fmt" + "os" + + "github.com/aws/amazon-ecs-agent/agent/logger/field" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + "github.com/aws/amazon-ecs-agent/agent/logger" + "github.com/docker/docker/api/types" +) + +// Loader defines an interface for loading the container images. This is mostly +// to facilitate mocking and testing +type Loader interface { + LoadImage(ctx context.Context, cfg *config.Config, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) + IsLoaded(dockerClient dockerapi.DockerClient) (bool, error) +} + +// GetContainerImage This function uses the DockerClient to inspect the image with the given name and tag. +func GetContainerImage(imageName string, dockerClient dockerapi.DockerClient) (*types.ImageInspect, error) { + logger.Debug("Inspecting container image: ", logger.Fields{ + field.Image: imageName, + }) + + image, err := dockerClient.InspectImage(imageName) + if err != nil { + return nil, fmt.Errorf( + "container load: failed to inspect image: %s, : %w", imageName, err) + } + + return image, nil +} + +// IsImageLoaded Common function for to check if a container image has been loaded +func IsImageLoaded(imageName string, dockerClient dockerapi.DockerClient) (bool, error) { + image, err := GetContainerImage(imageName, dockerClient) + + if err != nil { + return false, err + } + + if image == nil || image.ID == "" { + return false, nil + } + + return true, nil +} + +var open = os.Open + +// LoadFromFile This function supports loading a container from a local file into docker +func LoadFromFile(ctx context.Context, path string, dockerClient dockerapi.DockerClient) error { + containerReader, err := open(path) + if err != nil { + if os.IsNotExist(err) { + return NewNoSuchFileError(fmt.Errorf( + "container load: failed to read container image: %s : %w", path, err)) + } + return fmt.Errorf( + "container load: failed to read container image: %s : %w", path, err) + } + if err := dockerClient.LoadImage(ctx, containerReader, dockerclient.LoadImageTimeout); err != nil { + return fmt.Errorf( + "container load: failed to load container image: %s : %w", path, err) + } + + return nil + +} diff --git a/agent/utils/loader/load_test.go b/agent/utils/loader/load_test.go new file mode 100644 index 00000000000..107df67351e --- /dev/null +++ b/agent/utils/loader/load_test.go @@ -0,0 +1,234 @@ +//go:build unit +// +build unit + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package loader + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/aws/amazon-ecs-agent/agent/config" + "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" + mock_sdkclient "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclient/mocks" + mock_sdkclientfactory "github.com/aws/amazon-ecs-agent/agent/dockerclient/sdkclientfactory/mocks" + + "github.com/docker/docker/api/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +const ( + imageName = "container:tag" + tarballPath = "/path/to/container.tar" +) + +var defaultConfig = config.DefaultConfig() + +func TestGetContainerImageInspectImageError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), imageName).Return( + types.ImageInspect{}, nil, errors.New("error")) + + _, err = GetContainerImage(imageName, client) + assert.Error(t, err) +} + +func TestGetContainerHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), imageName).Return(types.ImageInspect{}, nil, nil) + + _, err = GetContainerImage(imageName, client) + assert.NoError(t, err) +} + +func TestIsImageLoadedHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{ID: "test123"}, nil, nil) + + isLoaded, err := IsImageLoaded(imageName, client) + assert.NoError(t, err) + assert.True(t, isLoaded) +} + +func TestIsImageLoadedNotLoaded(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(types.ImageInspect{}, nil, nil) + + isLoaded, err := IsImageLoaded(imageName, client) + assert.NoError(t, err) + assert.False(t, isLoaded) +} + +func TestIsImageLoadedError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return( + types.ImageInspect{}, nil, errors.New("error")) + + isLoaded, err := IsImageLoaded(imageName, client) + assert.Error(t, err) + assert.False(t, isLoaded) +} + +func mockOpen(file *os.File, err error) func() { + open = func(name string) (*os.File, error) { + return file, err + } + return func() { + open = os.Open + } +} + +// TestLoadFromFileWithReaderError tests loadFromFile with reader error +func TestLoadFromFileWithReaderError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + + mockedOpenReset := mockOpen(nil, errors.New("Dummy Reader Error")) + defer mockedOpenReset() + + err = LoadFromFile(ctx, tarballPath, client) + assert.Error(t, err) +} + +// TestLoadFromFileHappyPath tests loadFromFile against happy path +func TestLoadFromFileHappyPath(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{}, nil) + mockedOpenReset := mockOpen(nil, nil) + defer mockedOpenReset() + + err = LoadFromFile(ctx, tarballPath, client) + assert.NoError(t, err) +} + +// TestLoadFromFileDockerLoadImageError tests loadFromFile against error +// from Docker clients LoadImage +func TestLoadFromFileDockerLoadImageError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Docker SDK tests + mockDockerSDK := mock_sdkclient.NewMockClient(ctrl) + mockDockerSDK.EXPECT().Ping(gomock.Any()).Return(types.Ping{}, nil) + sdkFactory := mock_sdkclientfactory.NewMockFactory(ctrl) + sdkFactory.EXPECT().GetDefaultClient().AnyTimes().Return(mockDockerSDK, nil) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + client, err := dockerapi.NewDockerGoClient(sdkFactory, &defaultConfig, ctx) + assert.NoError(t, err) + mockDockerSDK.EXPECT().ImageLoad(gomock.Any(), gomock.Any(), false).Return(types.ImageLoadResponse{}, + errors.New("Dummy Load Image Error")) + + mockedOpenReset := mockOpen(nil, nil) + defer mockedOpenReset() + + err = LoadFromFile(ctx, tarballPath, client) + assert.Error(t, err) +} diff --git a/agent/eni/pause/mocks/load_mocks.go b/agent/utils/loader/mocks/load_mocks.go similarity index 94% rename from agent/eni/pause/mocks/load_mocks.go rename to agent/utils/loader/mocks/load_mocks.go index 2e2c768bc9c..e8996880513 100644 --- a/agent/eni/pause/mocks/load_mocks.go +++ b/agent/utils/loader/mocks/load_mocks.go @@ -13,10 +13,10 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/aws/amazon-ecs-agent/agent/eni/pause (interfaces: Loader) +// Source: github.com/aws/amazon-ecs-agent/agent/utils/loader (interfaces: Loader) -// Package mock_pause is a generated GoMock package. -package mock_pause +// Package mock_loader is a generated GoMock package. +package mock_loader import ( context "context" diff --git a/agent/utils/nswrapper/ns_linux.go b/agent/utils/nswrapper/ns_linux.go index 03e7b2f6285..630adfc5666 100644 --- a/agent/utils/nswrapper/ns_linux.go +++ b/agent/utils/nswrapper/ns_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/retry/backoff_test.go b/agent/utils/retry/backoff_test.go index 232349650ce..d68ccde8aef 100644 --- a/agent/utils/retry/backoff_test.go +++ b/agent/utils/retry/backoff_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/retry/exponential_backoff_test.go b/agent/utils/retry/exponential_backoff_test.go index 7583eb9f2ef..5571f49051b 100644 --- a/agent/utils/retry/exponential_backoff_test.go +++ b/agent/utils/retry/exponential_backoff_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/retry/retry.go b/agent/utils/retry/retry.go index f8acac21a17..bbb77cbf4d2 100644 --- a/agent/utils/retry/retry.go +++ b/agent/utils/retry/retry.go @@ -14,9 +14,10 @@ package retry import ( + "context" + apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors" "github.com/aws/amazon-ecs-agent/agent/utils/ttime" - "golang.org/x/net/context" ) var _time ttime.Time = &ttime.DefaultTime{} diff --git a/agent/utils/retry/retry_test.go b/agent/utils/retry/retry_test.go index 4a5546bc7e7..ae602f787e5 100644 --- a/agent/utils/retry/retry_test.go +++ b/agent/utils/retry/retry_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -16,6 +17,7 @@ package retry import ( + "context" "errors" "testing" "time" @@ -25,7 +27,6 @@ import ( mock_ttime "github.com/aws/amazon-ecs-agent/agent/utils/ttime/mocks" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" ) func TestRetryWithBackoff(t *testing.T) { diff --git a/agent/utils/sync/sequential_waitgroup_test.go b/agent/utils/sync/sequential_waitgroup_test.go index 31f713e2395..ff289f364c6 100644 --- a/agent/utils/sync/sequential_waitgroup_test.go +++ b/agent/utils/sync/sequential_waitgroup_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/ticker_test.go b/agent/utils/ticker_test.go index 535eb629b5e..6dc65e89b9e 100644 --- a/agent/utils/ticker_test.go +++ b/agent/utils/ticker_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/utils.go b/agent/utils/utils.go index 341e3bc5dfb..c5faa53c8f2 100644 --- a/agent/utils/utils.go +++ b/agent/utils/utils.go @@ -110,6 +110,18 @@ func Strptr(s string) *string { return &s } +func IntPtr(i int) *int { + return &i +} + +func Int64Ptr(i int64) *int64 { + return &i +} + +func BoolPtr(b bool) *bool { + return &b +} + // Uint16SliceToStringSlice converts a slice of type uint16 to a slice of type // *string. It uses strconv.Itoa on each element func Uint16SliceToStringSlice(slice []uint16) []*string { @@ -151,6 +163,11 @@ func ParseBool(str string, default_ bool) bool { return res } +// Removes element at a particular index in the slice +func Remove(slice []string, s int) []string { + return append(slice[:s], slice[s+1:]...) +} + // IsAWSErrorCodeEqual returns true if the err implements Error // interface of awserr and it has the same error code as // the passed in error code. diff --git a/agent/utils/utils_linux.go b/agent/utils/utils_linux.go index 281f413527d..aaa2931a15e 100644 --- a/agent/utils/utils_linux.go +++ b/agent/utils/utils_linux.go @@ -1,4 +1,5 @@ //go:build linux +// +build linux // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/utils_linux_test.go b/agent/utils/utils_linux_test.go index 73b24be11e0..ea369097c94 100644 --- a/agent/utils/utils_linux_test.go +++ b/agent/utils/utils_linux_test.go @@ -1,4 +1,5 @@ //go:build linux && unit +// +build linux,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/utils_test.go b/agent/utils/utils_test.go index 1e477b6bfd9..dc17d8395cf 100644 --- a/agent/utils/utils_test.go +++ b/agent/utils/utils_test.go @@ -1,4 +1,5 @@ //go:build unit +// +build unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -105,6 +106,16 @@ func TestSlicesDeepEqual(t *testing.T) { } } +func TestRemove(t *testing.T) { + testSlice := []string{"cat", "dog", "cat"} + removeElementAtIndex := 0 + + expectedValue := []string{"dog", "cat"} + actualValue := Remove(testSlice, removeElementAtIndex) + + assert.Equal(t, expectedValue, actualValue) +} + func TestParseBool(t *testing.T) { truthyStrings := []string{"true", "1", "t", "true\r", "true ", "true \r"} falsyStrings := []string{"false", "0", "f", "false\r", "false ", "false \r"} diff --git a/agent/utils/utils_unsupported.go b/agent/utils/utils_unsupported.go index 6109dff49d1..cc93b15e9bd 100644 --- a/agent/utils/utils_unsupported.go +++ b/agent/utils/utils_unsupported.go @@ -1,4 +1,5 @@ //go:build !linux && !windows +// +build !linux,!windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/utils_windows.go b/agent/utils/utils_windows.go index 072df48b470..938db186bf8 100644 --- a/agent/utils/utils_windows.go +++ b/agent/utils/utils_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/utils/utils_windows_test.go b/agent/utils/utils_windows_test.go index ad22e500fc3..6b09d902bce 100644 --- a/agent/utils/utils_windows_test.go +++ b/agent/utils/utils_windows_test.go @@ -1,4 +1,5 @@ //go:build windows && unit +// +build windows,unit // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // diff --git a/agent/vendor/github.com/cilium/ebpf/.clang-format b/agent/vendor/github.com/cilium/ebpf/.clang-format new file mode 100644 index 00000000000..4eb94b1baa8 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/.clang-format @@ -0,0 +1,17 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: true +AlignEscapedNewlines: DontAlign +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortFunctionsOnASingleLine: false +BreakBeforeBraces: Attach +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +TabWidth: 4 +UseTab: ForContinuationAndIndentation +ColumnLimit: 1000 +... diff --git a/agent/vendor/github.com/cilium/ebpf/.gitignore b/agent/vendor/github.com/cilium/ebpf/.gitignore new file mode 100644 index 00000000000..38b15653c0f --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.o + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/agent/vendor/github.com/cilium/ebpf/ARCHITECTURE.md b/agent/vendor/github.com/cilium/ebpf/ARCHITECTURE.md new file mode 100644 index 00000000000..aee9c0a0d4d --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/ARCHITECTURE.md @@ -0,0 +1,80 @@ +Architecture of the library +=== + + ELF -> Specifications -> Objects -> Links + +ELF +--- + +BPF is usually produced by using Clang to compile a subset of C. Clang outputs +an ELF file which contains program byte code (aka BPF), but also metadata for +maps used by the program. The metadata follows the conventions set by libbpf +shipped with the kernel. Certain ELF sections have special meaning +and contain structures defined by libbpf. Newer versions of clang emit +additional metadata in BPF Type Format (aka BTF). + +The library aims to be compatible with libbpf so that moving from a C toolchain +to a Go one creates little friction. To that end, the [ELF reader](elf_reader.go) +is tested against the Linux selftests and avoids introducing custom behaviour +if possible. + +The output of the ELF reader is a `CollectionSpec` which encodes +all of the information contained in the ELF in a form that is easy to work with +in Go. + +### BTF + +The BPF Type Format describes more than just the types used by a BPF program. It +includes debug aids like which source line corresponds to which instructions and +what global variables are used. + +[BTF parsing](internal/btf/) lives in a separate internal package since exposing +it would mean an additional maintenance burden, and because the API still +has sharp corners. The most important concept is the `btf.Type` interface, which +also describes things that aren't really types like `.rodata` or `.bss` sections. +`btf.Type`s can form cyclical graphs, which can easily lead to infinite loops if +one is not careful. Hopefully a safe pattern to work with `btf.Type` emerges as +we write more code that deals with it. + +Specifications +--- + +`CollectionSpec`, `ProgramSpec` and `MapSpec` are blueprints for in-kernel +objects and contain everything necessary to execute the relevant `bpf(2)` +syscalls. Since the ELF reader outputs a `CollectionSpec` it's possible to +modify clang-compiled BPF code, for example to rewrite constants. At the same +time the [asm](asm/) package provides an assembler that can be used to generate +`ProgramSpec` on the fly. + +Creating a spec should never require any privileges or be restricted in any way, +for example by only allowing programs in native endianness. This ensures that +the library stays flexible. + +Objects +--- + +`Program` and `Map` are the result of loading specs into the kernel. Sometimes +loading a spec will fail because the kernel is too old, or a feature is not +enabled. There are multiple ways the library deals with that: + +* Fallback: older kernels don't allowing naming programs and maps. The library + automatically detects support for names, and omits them during load if + necessary. This works since name is primarily a debug aid. + +* Sentinel error: sometimes it's possible to detect that a feature isn't available. + In that case the library will return an error wrapping `ErrNotSupported`. + This is also useful to skip tests that can't run on the current kernel. + +Once program and map objects are loaded they expose the kernel's low-level API, +e.g. `NextKey`. Often this API is awkward to use in Go, so there are safer +wrappers on top of the low-level API, like `MapIterator`. The low-level API is +useful as an out when our higher-level API doesn't support a particular use case. + +Links +--- + +BPF can be attached to many different points in the kernel and newer BPF hooks +tend to use bpf_link to do so. Older hooks unfortunately use a combination of +syscalls, netlink messages, etc. Adding support for a new link type should not +pull in large dependencies like netlink, so XDP programs or tracepoints are +out of scope. diff --git a/agent/vendor/github.com/cilium/ebpf/CODE_OF_CONDUCT.md b/agent/vendor/github.com/cilium/ebpf/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..8e42838c5ac --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at nathanjsweet at gmail dot com or i at lmb dot io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/agent/vendor/github.com/cilium/ebpf/CONTRIBUTING.md b/agent/vendor/github.com/cilium/ebpf/CONTRIBUTING.md new file mode 100644 index 00000000000..97c794f3a9b --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to contribute + +Development is on [GitHub](https://github.com/cilium/ebpf) and contributions in +the form of pull requests and issues reporting bugs or suggesting new features +are welcome. Please take a look at [the architecture](ARCHITECTURE.md) to get +a better understanding for the high-level goals. + +New features must be accompanied by tests. Before starting work on any large +feature, please [join](https://cilium.herokuapp.com/) the +[#libbpf-go](https://cilium.slack.com/messages/libbpf-go) channel on Slack to +discuss the design first. + +When submitting pull requests, consider writing details about what problem you +are solving and why the proposed approach solves that problem in commit messages +and/or pull request description to help future library users and maintainers to +reason about the proposed changes. + +## Running the tests + +Many of the tests require privileges to set resource limits and load eBPF code. +The easiest way to obtain these is to run the tests with `sudo`: + + sudo go test ./... \ No newline at end of file diff --git a/agent/vendor/github.com/cilium/ebpf/LICENSE b/agent/vendor/github.com/cilium/ebpf/LICENSE new file mode 100644 index 00000000000..c637ae99c26 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2017 Nathan Sweet +Copyright (c) 2018, 2019 Cloudflare +Copyright (c) 2019 Authors of Cilium + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/agent/vendor/github.com/cilium/ebpf/Makefile b/agent/vendor/github.com/cilium/ebpf/Makefile new file mode 100644 index 00000000000..5d4195833ca --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/Makefile @@ -0,0 +1,67 @@ +# The development version of clang is distributed as the 'clang' binary, +# while stable/released versions have a version number attached. +# Pin the default clang to a stable version. +CLANG ?= clang-11 +CFLAGS := -target bpf -O2 -g -Wall -Werror $(CFLAGS) + +# Obtain an absolute path to the directory of the Makefile. +# Assume the Makefile is in the root of the repository. +REPODIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +UIDGID := $(shell stat -c '%u:%g' ${REPODIR}) + +IMAGE := $(shell cat ${REPODIR}/testdata/docker/IMAGE) +VERSION := $(shell cat ${REPODIR}/testdata/docker/VERSION) + +# clang <8 doesn't tag relocs properly (STT_NOTYPE) +# clang 9 is the first version emitting BTF +TARGETS := \ + testdata/loader-clang-7 \ + testdata/loader-clang-9 \ + testdata/loader-clang-11 \ + testdata/invalid_map \ + testdata/raw_tracepoint \ + testdata/invalid_map_static \ + testdata/initialized_btf_map \ + testdata/strings \ + internal/btf/testdata/relocs + +.PHONY: all clean docker-all docker-shell + +.DEFAULT_TARGET = docker-all + +# Build all ELF binaries using a Dockerized LLVM toolchain. +docker-all: + docker run --rm --user "${UIDGID}" \ + -v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \ + "${IMAGE}:${VERSION}" \ + make all + +# (debug) Drop the user into a shell inside the Docker container as root. +docker-shell: + docker run --rm -ti \ + -v "${REPODIR}":/ebpf -w /ebpf \ + "${IMAGE}:${VERSION}" + +clean: + -$(RM) testdata/*.elf + -$(RM) internal/btf/testdata/*.elf + +all: $(addsuffix -el.elf,$(TARGETS)) $(addsuffix -eb.elf,$(TARGETS)) + +testdata/loader-%-el.elf: testdata/loader.c + $* $(CFLAGS) -mlittle-endian -c $< -o $@ + +testdata/loader-%-eb.elf: testdata/loader.c + $* $(CFLAGS) -mbig-endian -c $< -o $@ + +%-el.elf: %.c + $(CLANG) $(CFLAGS) -mlittle-endian -c $< -o $@ + +%-eb.elf : %.c + $(CLANG) $(CFLAGS) -mbig-endian -c $< -o $@ + +# Usage: make VMLINUX=/path/to/vmlinux vmlinux-btf +.PHONY: vmlinux-btf +vmlinux-btf: internal/btf/testdata/vmlinux-btf.gz +internal/btf/testdata/vmlinux-btf.gz: $(VMLINUX) + objcopy --dump-section .BTF=/dev/stdout "$<" /dev/null | gzip > "$@" diff --git a/agent/vendor/github.com/cilium/ebpf/README.md b/agent/vendor/github.com/cilium/ebpf/README.md new file mode 100644 index 00000000000..7f504d33467 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/README.md @@ -0,0 +1,54 @@ +# eBPF + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/cilium/ebpf)](https://pkg.go.dev/github.com/cilium/ebpf) + +eBPF is a pure Go library that provides utilities for loading, compiling, and +debugging eBPF programs. It has minimal external dependencies and is intended to +be used in long running processes. + +* [asm](https://pkg.go.dev/github.com/cilium/ebpf/asm) contains a basic + assembler +* [link](https://pkg.go.dev/github.com/cilium/ebpf/link) allows attaching eBPF + to various hooks +* [perf](https://pkg.go.dev/github.com/cilium/ebpf/perf) allows reading from a + `PERF_EVENT_ARRAY` +* [cmd/bpf2go](https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go) allows + embedding eBPF in Go + +The library is maintained by [Cloudflare](https://www.cloudflare.com) and +[Cilium](https://www.cilium.io). Feel free to +[join](https://cilium.herokuapp.com/) the +[#libbpf-go](https://cilium.slack.com/messages/libbpf-go) channel on Slack. + +## Current status + +The package is production ready, but **the API is explicitly unstable right +now**. Expect to update your code if you want to follow along. + +## Requirements + +* A version of Go that is [supported by + upstream](https://golang.org/doc/devel/release.html#policy) +* Linux 4.9, 4.19 or 5.4 (versions in-between should work, but are not tested) + +## Useful resources + +* [eBPF.io](https://ebpf.io) (recommended) +* [Cilium eBPF documentation](https://docs.cilium.io/en/latest/bpf/#bpf-guide) + (recommended) +* [Linux documentation on + BPF](https://www.kernel.org/doc/html/latest/networking/filter.html) +* [eBPF features by Linux + version](https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md) + +## Regenerating Testdata + +Run `make` in the root of this repository to rebuild testdata in all +subpackages. This requires Docker, as it relies on a standardized build +environment to keep the build output stable. + +The toolchain image build files are kept in [testdata/docker/](testdata/docker/). + +## License + +MIT diff --git a/agent/vendor/github.com/cilium/ebpf/asm/alu.go b/agent/vendor/github.com/cilium/ebpf/asm/alu.go new file mode 100644 index 00000000000..70ccc4d1518 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/alu.go @@ -0,0 +1,149 @@ +package asm + +//go:generate stringer -output alu_string.go -type=Source,Endianness,ALUOp + +// Source of ALU / ALU64 / Branch operations +// +// msb lsb +// +----+-+---+ +// |op |S|cls| +// +----+-+---+ +type Source uint8 + +const sourceMask OpCode = 0x08 + +// Source bitmask +const ( + // InvalidSource is returned by getters when invoked + // on non ALU / branch OpCodes. + InvalidSource Source = 0xff + // ImmSource src is from constant + ImmSource Source = 0x00 + // RegSource src is from register + RegSource Source = 0x08 +) + +// The Endianness of a byte swap instruction. +type Endianness uint8 + +const endianMask = sourceMask + +// Endian flags +const ( + InvalidEndian Endianness = 0xff + // Convert to little endian + LE Endianness = 0x00 + // Convert to big endian + BE Endianness = 0x08 +) + +// ALUOp are ALU / ALU64 operations +// +// msb lsb +// +----+-+---+ +// |OP |s|cls| +// +----+-+---+ +type ALUOp uint8 + +const aluMask OpCode = 0xf0 + +const ( + // InvalidALUOp is returned by getters when invoked + // on non ALU OpCodes + InvalidALUOp ALUOp = 0xff + // Add - addition + Add ALUOp = 0x00 + // Sub - subtraction + Sub ALUOp = 0x10 + // Mul - multiplication + Mul ALUOp = 0x20 + // Div - division + Div ALUOp = 0x30 + // Or - bitwise or + Or ALUOp = 0x40 + // And - bitwise and + And ALUOp = 0x50 + // LSh - bitwise shift left + LSh ALUOp = 0x60 + // RSh - bitwise shift right + RSh ALUOp = 0x70 + // Neg - sign/unsign signing bit + Neg ALUOp = 0x80 + // Mod - modulo + Mod ALUOp = 0x90 + // Xor - bitwise xor + Xor ALUOp = 0xa0 + // Mov - move value from one place to another + Mov ALUOp = 0xb0 + // ArSh - arithmatic shift + ArSh ALUOp = 0xc0 + // Swap - endian conversions + Swap ALUOp = 0xd0 +) + +// HostTo converts from host to another endianness. +func HostTo(endian Endianness, dst Register, size Size) Instruction { + var imm int64 + switch size { + case Half: + imm = 16 + case Word: + imm = 32 + case DWord: + imm = 64 + default: + return Instruction{OpCode: InvalidOpCode} + } + + return Instruction{ + OpCode: OpCode(ALUClass).SetALUOp(Swap).SetSource(Source(endian)), + Dst: dst, + Constant: imm, + } +} + +// Op returns the OpCode for an ALU operation with a given source. +func (op ALUOp) Op(source Source) OpCode { + return OpCode(ALU64Class).SetALUOp(op).SetSource(source) +} + +// Reg emits `dst (op) src`. +func (op ALUOp) Reg(dst, src Register) Instruction { + return Instruction{ + OpCode: op.Op(RegSource), + Dst: dst, + Src: src, + } +} + +// Imm emits `dst (op) value`. +func (op ALUOp) Imm(dst Register, value int32) Instruction { + return Instruction{ + OpCode: op.Op(ImmSource), + Dst: dst, + Constant: int64(value), + } +} + +// Op32 returns the OpCode for a 32-bit ALU operation with a given source. +func (op ALUOp) Op32(source Source) OpCode { + return OpCode(ALUClass).SetALUOp(op).SetSource(source) +} + +// Reg32 emits `dst (op) src`, zeroing the upper 32 bit of dst. +func (op ALUOp) Reg32(dst, src Register) Instruction { + return Instruction{ + OpCode: op.Op32(RegSource), + Dst: dst, + Src: src, + } +} + +// Imm32 emits `dst (op) value`, zeroing the upper 32 bit of dst. +func (op ALUOp) Imm32(dst Register, value int32) Instruction { + return Instruction{ + OpCode: op.Op32(ImmSource), + Dst: dst, + Constant: int64(value), + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/alu_string.go b/agent/vendor/github.com/cilium/ebpf/asm/alu_string.go new file mode 100644 index 00000000000..72d3fe6292e --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/alu_string.go @@ -0,0 +1,107 @@ +// Code generated by "stringer -output alu_string.go -type=Source,Endianness,ALUOp"; DO NOT EDIT. + +package asm + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidSource-255] + _ = x[ImmSource-0] + _ = x[RegSource-8] +} + +const ( + _Source_name_0 = "ImmSource" + _Source_name_1 = "RegSource" + _Source_name_2 = "InvalidSource" +) + +func (i Source) String() string { + switch { + case i == 0: + return _Source_name_0 + case i == 8: + return _Source_name_1 + case i == 255: + return _Source_name_2 + default: + return "Source(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidEndian-255] + _ = x[LE-0] + _ = x[BE-8] +} + +const ( + _Endianness_name_0 = "LE" + _Endianness_name_1 = "BE" + _Endianness_name_2 = "InvalidEndian" +) + +func (i Endianness) String() string { + switch { + case i == 0: + return _Endianness_name_0 + case i == 8: + return _Endianness_name_1 + case i == 255: + return _Endianness_name_2 + default: + return "Endianness(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidALUOp-255] + _ = x[Add-0] + _ = x[Sub-16] + _ = x[Mul-32] + _ = x[Div-48] + _ = x[Or-64] + _ = x[And-80] + _ = x[LSh-96] + _ = x[RSh-112] + _ = x[Neg-128] + _ = x[Mod-144] + _ = x[Xor-160] + _ = x[Mov-176] + _ = x[ArSh-192] + _ = x[Swap-208] +} + +const _ALUOp_name = "AddSubMulDivOrAndLShRShNegModXorMovArShSwapInvalidALUOp" + +var _ALUOp_map = map[ALUOp]string{ + 0: _ALUOp_name[0:3], + 16: _ALUOp_name[3:6], + 32: _ALUOp_name[6:9], + 48: _ALUOp_name[9:12], + 64: _ALUOp_name[12:14], + 80: _ALUOp_name[14:17], + 96: _ALUOp_name[17:20], + 112: _ALUOp_name[20:23], + 128: _ALUOp_name[23:26], + 144: _ALUOp_name[26:29], + 160: _ALUOp_name[29:32], + 176: _ALUOp_name[32:35], + 192: _ALUOp_name[35:39], + 208: _ALUOp_name[39:43], + 255: _ALUOp_name[43:55], +} + +func (i ALUOp) String() string { + if str, ok := _ALUOp_map[i]; ok { + return str + } + return "ALUOp(" + strconv.FormatInt(int64(i), 10) + ")" +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/doc.go b/agent/vendor/github.com/cilium/ebpf/asm/doc.go new file mode 100644 index 00000000000..7031bdc2768 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/doc.go @@ -0,0 +1,2 @@ +// Package asm is an assembler for eBPF bytecode. +package asm diff --git a/agent/vendor/github.com/cilium/ebpf/asm/func.go b/agent/vendor/github.com/cilium/ebpf/asm/func.go new file mode 100644 index 00000000000..97f794cdb2a --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/func.go @@ -0,0 +1,143 @@ +package asm + +//go:generate stringer -output func_string.go -type=BuiltinFunc + +// BuiltinFunc is a built-in eBPF function. +type BuiltinFunc int32 + +// eBPF built-in functions +// +// You can renegerate this list using the following gawk script: +// +// /FN\(.+\),/ { +// match($1, /\((.+)\)/, r) +// split(r[1], p, "_") +// printf "Fn" +// for (i in p) { +// printf "%s%s", toupper(substr(p[i], 1, 1)), substr(p[i], 2) +// } +// print "" +// } +// +// The script expects include/uapi/linux/bpf.h as it's input. +const ( + FnUnspec BuiltinFunc = iota + FnMapLookupElem + FnMapUpdateElem + FnMapDeleteElem + FnProbeRead + FnKtimeGetNs + FnTracePrintk + FnGetPrandomU32 + FnGetSmpProcessorId + FnSkbStoreBytes + FnL3CsumReplace + FnL4CsumReplace + FnTailCall + FnCloneRedirect + FnGetCurrentPidTgid + FnGetCurrentUidGid + FnGetCurrentComm + FnGetCgroupClassid + FnSkbVlanPush + FnSkbVlanPop + FnSkbGetTunnelKey + FnSkbSetTunnelKey + FnPerfEventRead + FnRedirect + FnGetRouteRealm + FnPerfEventOutput + FnSkbLoadBytes + FnGetStackid + FnCsumDiff + FnSkbGetTunnelOpt + FnSkbSetTunnelOpt + FnSkbChangeProto + FnSkbChangeType + FnSkbUnderCgroup + FnGetHashRecalc + FnGetCurrentTask + FnProbeWriteUser + FnCurrentTaskUnderCgroup + FnSkbChangeTail + FnSkbPullData + FnCsumUpdate + FnSetHashInvalid + FnGetNumaNodeId + FnSkbChangeHead + FnXdpAdjustHead + FnProbeReadStr + FnGetSocketCookie + FnGetSocketUid + FnSetHash + FnSetsockopt + FnSkbAdjustRoom + FnRedirectMap + FnSkRedirectMap + FnSockMapUpdate + FnXdpAdjustMeta + FnPerfEventReadValue + FnPerfProgReadValue + FnGetsockopt + FnOverrideReturn + FnSockOpsCbFlagsSet + FnMsgRedirectMap + FnMsgApplyBytes + FnMsgCorkBytes + FnMsgPullData + FnBind + FnXdpAdjustTail + FnSkbGetXfrmState + FnGetStack + FnSkbLoadBytesRelative + FnFibLookup + FnSockHashUpdate + FnMsgRedirectHash + FnSkRedirectHash + FnLwtPushEncap + FnLwtSeg6StoreBytes + FnLwtSeg6AdjustSrh + FnLwtSeg6Action + FnRcRepeat + FnRcKeydown + FnSkbCgroupId + FnGetCurrentCgroupId + FnGetLocalStorage + FnSkSelectReuseport + FnSkbAncestorCgroupId + FnSkLookupTcp + FnSkLookupUdp + FnSkRelease + FnMapPushElem + FnMapPopElem + FnMapPeekElem + FnMsgPushData + FnMsgPopData + FnRcPointerRel + FnSpinLock + FnSpinUnlock + FnSkFullsock + FnTcpSock + FnSkbEcnSetCe + FnGetListenerSock + FnSkcLookupTcp + FnTcpCheckSyncookie + FnSysctlGetName + FnSysctlGetCurrentValue + FnSysctlGetNewValue + FnSysctlSetNewValue + FnStrtol + FnStrtoul + FnSkStorageGet + FnSkStorageDelete + FnSendSignal + FnTcpGenSyncookie +) + +// Call emits a function call. +func (fn BuiltinFunc) Call() Instruction { + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(Call), + Constant: int64(fn), + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/func_string.go b/agent/vendor/github.com/cilium/ebpf/asm/func_string.go new file mode 100644 index 00000000000..8860b9fdb42 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/func_string.go @@ -0,0 +1,133 @@ +// Code generated by "stringer -output func_string.go -type=BuiltinFunc"; DO NOT EDIT. + +package asm + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[FnUnspec-0] + _ = x[FnMapLookupElem-1] + _ = x[FnMapUpdateElem-2] + _ = x[FnMapDeleteElem-3] + _ = x[FnProbeRead-4] + _ = x[FnKtimeGetNs-5] + _ = x[FnTracePrintk-6] + _ = x[FnGetPrandomU32-7] + _ = x[FnGetSmpProcessorId-8] + _ = x[FnSkbStoreBytes-9] + _ = x[FnL3CsumReplace-10] + _ = x[FnL4CsumReplace-11] + _ = x[FnTailCall-12] + _ = x[FnCloneRedirect-13] + _ = x[FnGetCurrentPidTgid-14] + _ = x[FnGetCurrentUidGid-15] + _ = x[FnGetCurrentComm-16] + _ = x[FnGetCgroupClassid-17] + _ = x[FnSkbVlanPush-18] + _ = x[FnSkbVlanPop-19] + _ = x[FnSkbGetTunnelKey-20] + _ = x[FnSkbSetTunnelKey-21] + _ = x[FnPerfEventRead-22] + _ = x[FnRedirect-23] + _ = x[FnGetRouteRealm-24] + _ = x[FnPerfEventOutput-25] + _ = x[FnSkbLoadBytes-26] + _ = x[FnGetStackid-27] + _ = x[FnCsumDiff-28] + _ = x[FnSkbGetTunnelOpt-29] + _ = x[FnSkbSetTunnelOpt-30] + _ = x[FnSkbChangeProto-31] + _ = x[FnSkbChangeType-32] + _ = x[FnSkbUnderCgroup-33] + _ = x[FnGetHashRecalc-34] + _ = x[FnGetCurrentTask-35] + _ = x[FnProbeWriteUser-36] + _ = x[FnCurrentTaskUnderCgroup-37] + _ = x[FnSkbChangeTail-38] + _ = x[FnSkbPullData-39] + _ = x[FnCsumUpdate-40] + _ = x[FnSetHashInvalid-41] + _ = x[FnGetNumaNodeId-42] + _ = x[FnSkbChangeHead-43] + _ = x[FnXdpAdjustHead-44] + _ = x[FnProbeReadStr-45] + _ = x[FnGetSocketCookie-46] + _ = x[FnGetSocketUid-47] + _ = x[FnSetHash-48] + _ = x[FnSetsockopt-49] + _ = x[FnSkbAdjustRoom-50] + _ = x[FnRedirectMap-51] + _ = x[FnSkRedirectMap-52] + _ = x[FnSockMapUpdate-53] + _ = x[FnXdpAdjustMeta-54] + _ = x[FnPerfEventReadValue-55] + _ = x[FnPerfProgReadValue-56] + _ = x[FnGetsockopt-57] + _ = x[FnOverrideReturn-58] + _ = x[FnSockOpsCbFlagsSet-59] + _ = x[FnMsgRedirectMap-60] + _ = x[FnMsgApplyBytes-61] + _ = x[FnMsgCorkBytes-62] + _ = x[FnMsgPullData-63] + _ = x[FnBind-64] + _ = x[FnXdpAdjustTail-65] + _ = x[FnSkbGetXfrmState-66] + _ = x[FnGetStack-67] + _ = x[FnSkbLoadBytesRelative-68] + _ = x[FnFibLookup-69] + _ = x[FnSockHashUpdate-70] + _ = x[FnMsgRedirectHash-71] + _ = x[FnSkRedirectHash-72] + _ = x[FnLwtPushEncap-73] + _ = x[FnLwtSeg6StoreBytes-74] + _ = x[FnLwtSeg6AdjustSrh-75] + _ = x[FnLwtSeg6Action-76] + _ = x[FnRcRepeat-77] + _ = x[FnRcKeydown-78] + _ = x[FnSkbCgroupId-79] + _ = x[FnGetCurrentCgroupId-80] + _ = x[FnGetLocalStorage-81] + _ = x[FnSkSelectReuseport-82] + _ = x[FnSkbAncestorCgroupId-83] + _ = x[FnSkLookupTcp-84] + _ = x[FnSkLookupUdp-85] + _ = x[FnSkRelease-86] + _ = x[FnMapPushElem-87] + _ = x[FnMapPopElem-88] + _ = x[FnMapPeekElem-89] + _ = x[FnMsgPushData-90] + _ = x[FnMsgPopData-91] + _ = x[FnRcPointerRel-92] + _ = x[FnSpinLock-93] + _ = x[FnSpinUnlock-94] + _ = x[FnSkFullsock-95] + _ = x[FnTcpSock-96] + _ = x[FnSkbEcnSetCe-97] + _ = x[FnGetListenerSock-98] + _ = x[FnSkcLookupTcp-99] + _ = x[FnTcpCheckSyncookie-100] + _ = x[FnSysctlGetName-101] + _ = x[FnSysctlGetCurrentValue-102] + _ = x[FnSysctlGetNewValue-103] + _ = x[FnSysctlSetNewValue-104] + _ = x[FnStrtol-105] + _ = x[FnStrtoul-106] + _ = x[FnSkStorageGet-107] + _ = x[FnSkStorageDelete-108] + _ = x[FnSendSignal-109] + _ = x[FnTcpGenSyncookie-110] +} + +const _BuiltinFunc_name = "FnUnspecFnMapLookupElemFnMapUpdateElemFnMapDeleteElemFnProbeReadFnKtimeGetNsFnTracePrintkFnGetPrandomU32FnGetSmpProcessorIdFnSkbStoreBytesFnL3CsumReplaceFnL4CsumReplaceFnTailCallFnCloneRedirectFnGetCurrentPidTgidFnGetCurrentUidGidFnGetCurrentCommFnGetCgroupClassidFnSkbVlanPushFnSkbVlanPopFnSkbGetTunnelKeyFnSkbSetTunnelKeyFnPerfEventReadFnRedirectFnGetRouteRealmFnPerfEventOutputFnSkbLoadBytesFnGetStackidFnCsumDiffFnSkbGetTunnelOptFnSkbSetTunnelOptFnSkbChangeProtoFnSkbChangeTypeFnSkbUnderCgroupFnGetHashRecalcFnGetCurrentTaskFnProbeWriteUserFnCurrentTaskUnderCgroupFnSkbChangeTailFnSkbPullDataFnCsumUpdateFnSetHashInvalidFnGetNumaNodeIdFnSkbChangeHeadFnXdpAdjustHeadFnProbeReadStrFnGetSocketCookieFnGetSocketUidFnSetHashFnSetsockoptFnSkbAdjustRoomFnRedirectMapFnSkRedirectMapFnSockMapUpdateFnXdpAdjustMetaFnPerfEventReadValueFnPerfProgReadValueFnGetsockoptFnOverrideReturnFnSockOpsCbFlagsSetFnMsgRedirectMapFnMsgApplyBytesFnMsgCorkBytesFnMsgPullDataFnBindFnXdpAdjustTailFnSkbGetXfrmStateFnGetStackFnSkbLoadBytesRelativeFnFibLookupFnSockHashUpdateFnMsgRedirectHashFnSkRedirectHashFnLwtPushEncapFnLwtSeg6StoreBytesFnLwtSeg6AdjustSrhFnLwtSeg6ActionFnRcRepeatFnRcKeydownFnSkbCgroupIdFnGetCurrentCgroupIdFnGetLocalStorageFnSkSelectReuseportFnSkbAncestorCgroupIdFnSkLookupTcpFnSkLookupUdpFnSkReleaseFnMapPushElemFnMapPopElemFnMapPeekElemFnMsgPushDataFnMsgPopDataFnRcPointerRelFnSpinLockFnSpinUnlockFnSkFullsockFnTcpSockFnSkbEcnSetCeFnGetListenerSockFnSkcLookupTcpFnTcpCheckSyncookieFnSysctlGetNameFnSysctlGetCurrentValueFnSysctlGetNewValueFnSysctlSetNewValueFnStrtolFnStrtoulFnSkStorageGetFnSkStorageDeleteFnSendSignalFnTcpGenSyncookie" + +var _BuiltinFunc_index = [...]uint16{0, 8, 23, 38, 53, 64, 76, 89, 104, 123, 138, 153, 168, 178, 193, 212, 230, 246, 264, 277, 289, 306, 323, 338, 348, 363, 380, 394, 406, 416, 433, 450, 466, 481, 497, 512, 528, 544, 568, 583, 596, 608, 624, 639, 654, 669, 683, 700, 714, 723, 735, 750, 763, 778, 793, 808, 828, 847, 859, 875, 894, 910, 925, 939, 952, 958, 973, 990, 1000, 1022, 1033, 1049, 1066, 1082, 1096, 1115, 1133, 1148, 1158, 1169, 1182, 1202, 1219, 1238, 1259, 1272, 1285, 1296, 1309, 1321, 1334, 1347, 1359, 1373, 1383, 1395, 1407, 1416, 1429, 1446, 1460, 1479, 1494, 1517, 1536, 1555, 1563, 1572, 1586, 1603, 1615, 1632} + +func (i BuiltinFunc) String() string { + if i < 0 || i >= BuiltinFunc(len(_BuiltinFunc_index)-1) { + return "BuiltinFunc(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _BuiltinFunc_name[_BuiltinFunc_index[i]:_BuiltinFunc_index[i+1]] +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/instruction.go b/agent/vendor/github.com/cilium/ebpf/asm/instruction.go new file mode 100644 index 00000000000..5d9d820e54f --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/instruction.go @@ -0,0 +1,498 @@ +package asm + +import ( + "crypto/sha1" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "math" + "strings" + + "github.com/cilium/ebpf/internal/unix" +) + +// InstructionSize is the size of a BPF instruction in bytes +const InstructionSize = 8 + +// RawInstructionOffset is an offset in units of raw BPF instructions. +type RawInstructionOffset uint64 + +// Bytes returns the offset of an instruction in bytes. +func (rio RawInstructionOffset) Bytes() uint64 { + return uint64(rio) * InstructionSize +} + +// Instruction is a single eBPF instruction. +type Instruction struct { + OpCode OpCode + Dst Register + Src Register + Offset int16 + Constant int64 + Reference string + Symbol string +} + +// Sym creates a symbol. +func (ins Instruction) Sym(name string) Instruction { + ins.Symbol = name + return ins +} + +// Unmarshal decodes a BPF instruction. +func (ins *Instruction) Unmarshal(r io.Reader, bo binary.ByteOrder) (uint64, error) { + var bi bpfInstruction + err := binary.Read(r, bo, &bi) + if err != nil { + return 0, err + } + + ins.OpCode = bi.OpCode + ins.Offset = bi.Offset + ins.Constant = int64(bi.Constant) + ins.Dst, ins.Src, err = bi.Registers.Unmarshal(bo) + if err != nil { + return 0, fmt.Errorf("can't unmarshal registers: %s", err) + } + + if !bi.OpCode.isDWordLoad() { + return InstructionSize, nil + } + + var bi2 bpfInstruction + if err := binary.Read(r, bo, &bi2); err != nil { + // No Wrap, to avoid io.EOF clash + return 0, errors.New("64bit immediate is missing second half") + } + if bi2.OpCode != 0 || bi2.Offset != 0 || bi2.Registers != 0 { + return 0, errors.New("64bit immediate has non-zero fields") + } + ins.Constant = int64(uint64(uint32(bi2.Constant))<<32 | uint64(uint32(bi.Constant))) + + return 2 * InstructionSize, nil +} + +// Marshal encodes a BPF instruction. +func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error) { + if ins.OpCode == InvalidOpCode { + return 0, errors.New("invalid opcode") + } + + isDWordLoad := ins.OpCode.isDWordLoad() + + cons := int32(ins.Constant) + if isDWordLoad { + // Encode least significant 32bit first for 64bit operations. + cons = int32(uint32(ins.Constant)) + } + + regs, err := newBPFRegisters(ins.Dst, ins.Src, bo) + if err != nil { + return 0, fmt.Errorf("can't marshal registers: %s", err) + } + + bpfi := bpfInstruction{ + ins.OpCode, + regs, + ins.Offset, + cons, + } + + if err := binary.Write(w, bo, &bpfi); err != nil { + return 0, err + } + + if !isDWordLoad { + return InstructionSize, nil + } + + bpfi = bpfInstruction{ + Constant: int32(ins.Constant >> 32), + } + + if err := binary.Write(w, bo, &bpfi); err != nil { + return 0, err + } + + return 2 * InstructionSize, nil +} + +// RewriteMapPtr changes an instruction to use a new map fd. +// +// Returns an error if the instruction doesn't load a map. +func (ins *Instruction) RewriteMapPtr(fd int) error { + if !ins.OpCode.isDWordLoad() { + return fmt.Errorf("%s is not a 64 bit load", ins.OpCode) + } + + if ins.Src != PseudoMapFD && ins.Src != PseudoMapValue { + return errors.New("not a load from a map") + } + + // Preserve the offset value for direct map loads. + offset := uint64(ins.Constant) & (math.MaxUint32 << 32) + rawFd := uint64(uint32(fd)) + ins.Constant = int64(offset | rawFd) + return nil +} + +func (ins *Instruction) mapPtr() uint32 { + return uint32(uint64(ins.Constant) & math.MaxUint32) +} + +// RewriteMapOffset changes the offset of a direct load from a map. +// +// Returns an error if the instruction is not a direct load. +func (ins *Instruction) RewriteMapOffset(offset uint32) error { + if !ins.OpCode.isDWordLoad() { + return fmt.Errorf("%s is not a 64 bit load", ins.OpCode) + } + + if ins.Src != PseudoMapValue { + return errors.New("not a direct load from a map") + } + + fd := uint64(ins.Constant) & math.MaxUint32 + ins.Constant = int64(uint64(offset)<<32 | fd) + return nil +} + +func (ins *Instruction) mapOffset() uint32 { + return uint32(uint64(ins.Constant) >> 32) +} + +// isLoadFromMap returns true if the instruction loads from a map. +// +// This covers both loading the map pointer and direct map value loads. +func (ins *Instruction) isLoadFromMap() bool { + return ins.OpCode == LoadImmOp(DWord) && (ins.Src == PseudoMapFD || ins.Src == PseudoMapValue) +} + +// IsFunctionCall returns true if the instruction calls another BPF function. +// +// This is not the same thing as a BPF helper call. +func (ins *Instruction) IsFunctionCall() bool { + return ins.OpCode.JumpOp() == Call && ins.Src == PseudoCall +} + +// Format implements fmt.Formatter. +func (ins Instruction) Format(f fmt.State, c rune) { + if c != 'v' { + fmt.Fprintf(f, "{UNRECOGNIZED: %c}", c) + return + } + + op := ins.OpCode + + if op == InvalidOpCode { + fmt.Fprint(f, "INVALID") + return + } + + // Omit trailing space for Exit + if op.JumpOp() == Exit { + fmt.Fprint(f, op) + return + } + + if ins.isLoadFromMap() { + fd := int32(ins.mapPtr()) + switch ins.Src { + case PseudoMapFD: + fmt.Fprintf(f, "LoadMapPtr dst: %s fd: %d", ins.Dst, fd) + + case PseudoMapValue: + fmt.Fprintf(f, "LoadMapValue dst: %s, fd: %d off: %d", ins.Dst, fd, ins.mapOffset()) + } + + goto ref + } + + fmt.Fprintf(f, "%v ", op) + switch cls := op.Class(); cls { + case LdClass, LdXClass, StClass, StXClass: + switch op.Mode() { + case ImmMode: + fmt.Fprintf(f, "dst: %s imm: %d", ins.Dst, ins.Constant) + case AbsMode: + fmt.Fprintf(f, "imm: %d", ins.Constant) + case IndMode: + fmt.Fprintf(f, "dst: %s src: %s imm: %d", ins.Dst, ins.Src, ins.Constant) + case MemMode: + fmt.Fprintf(f, "dst: %s src: %s off: %d imm: %d", ins.Dst, ins.Src, ins.Offset, ins.Constant) + case XAddMode: + fmt.Fprintf(f, "dst: %s src: %s", ins.Dst, ins.Src) + } + + case ALU64Class, ALUClass: + fmt.Fprintf(f, "dst: %s ", ins.Dst) + if op.ALUOp() == Swap || op.Source() == ImmSource { + fmt.Fprintf(f, "imm: %d", ins.Constant) + } else { + fmt.Fprintf(f, "src: %s", ins.Src) + } + + case JumpClass: + switch jop := op.JumpOp(); jop { + case Call: + if ins.Src == PseudoCall { + // bpf-to-bpf call + fmt.Fprint(f, ins.Constant) + } else { + fmt.Fprint(f, BuiltinFunc(ins.Constant)) + } + + default: + fmt.Fprintf(f, "dst: %s off: %d ", ins.Dst, ins.Offset) + if op.Source() == ImmSource { + fmt.Fprintf(f, "imm: %d", ins.Constant) + } else { + fmt.Fprintf(f, "src: %s", ins.Src) + } + } + } + +ref: + if ins.Reference != "" { + fmt.Fprintf(f, " <%s>", ins.Reference) + } +} + +// Instructions is an eBPF program. +type Instructions []Instruction + +func (insns Instructions) String() string { + return fmt.Sprint(insns) +} + +// RewriteMapPtr rewrites all loads of a specific map pointer to a new fd. +// +// Returns an error if the symbol isn't used, see IsUnreferencedSymbol. +func (insns Instructions) RewriteMapPtr(symbol string, fd int) error { + if symbol == "" { + return errors.New("empty symbol") + } + + found := false + for i := range insns { + ins := &insns[i] + if ins.Reference != symbol { + continue + } + + if err := ins.RewriteMapPtr(fd); err != nil { + return err + } + + found = true + } + + if !found { + return &unreferencedSymbolError{symbol} + } + + return nil +} + +// SymbolOffsets returns the set of symbols and their offset in +// the instructions. +func (insns Instructions) SymbolOffsets() (map[string]int, error) { + offsets := make(map[string]int) + + for i, ins := range insns { + if ins.Symbol == "" { + continue + } + + if _, ok := offsets[ins.Symbol]; ok { + return nil, fmt.Errorf("duplicate symbol %s", ins.Symbol) + } + + offsets[ins.Symbol] = i + } + + return offsets, nil +} + +// ReferenceOffsets returns the set of references and their offset in +// the instructions. +func (insns Instructions) ReferenceOffsets() map[string][]int { + offsets := make(map[string][]int) + + for i, ins := range insns { + if ins.Reference == "" { + continue + } + + offsets[ins.Reference] = append(offsets[ins.Reference], i) + } + + return offsets +} + +// Format implements fmt.Formatter. +// +// You can control indentation of symbols by +// specifying a width. Setting a precision controls the indentation of +// instructions. +// The default character is a tab, which can be overriden by specifying +// the ' ' space flag. +func (insns Instructions) Format(f fmt.State, c rune) { + if c != 's' && c != 'v' { + fmt.Fprintf(f, "{UNKNOWN FORMAT '%c'}", c) + return + } + + // Precision is better in this case, because it allows + // specifying 0 padding easily. + padding, ok := f.Precision() + if !ok { + padding = 1 + } + + indent := strings.Repeat("\t", padding) + if f.Flag(' ') { + indent = strings.Repeat(" ", padding) + } + + symPadding, ok := f.Width() + if !ok { + symPadding = padding - 1 + } + if symPadding < 0 { + symPadding = 0 + } + + symIndent := strings.Repeat("\t", symPadding) + if f.Flag(' ') { + symIndent = strings.Repeat(" ", symPadding) + } + + // Guess how many digits we need at most, by assuming that all instructions + // are double wide. + highestOffset := len(insns) * 2 + offsetWidth := int(math.Ceil(math.Log10(float64(highestOffset)))) + + iter := insns.Iterate() + for iter.Next() { + if iter.Ins.Symbol != "" { + fmt.Fprintf(f, "%s%s:\n", symIndent, iter.Ins.Symbol) + } + fmt.Fprintf(f, "%s%*d: %v\n", indent, offsetWidth, iter.Offset, iter.Ins) + } + + return +} + +// Marshal encodes a BPF program into the kernel format. +func (insns Instructions) Marshal(w io.Writer, bo binary.ByteOrder) error { + for i, ins := range insns { + _, err := ins.Marshal(w, bo) + if err != nil { + return fmt.Errorf("instruction %d: %w", i, err) + } + } + return nil +} + +// Tag calculates the kernel tag for a series of instructions. +// +// It mirrors bpf_prog_calc_tag in the kernel and so can be compared +// to ProgramInfo.Tag to figure out whether a loaded program matches +// certain instructions. +func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) { + h := sha1.New() + for i, ins := range insns { + if ins.isLoadFromMap() { + ins.Constant = 0 + } + _, err := ins.Marshal(h, bo) + if err != nil { + return "", fmt.Errorf("instruction %d: %w", i, err) + } + } + return hex.EncodeToString(h.Sum(nil)[:unix.BPF_TAG_SIZE]), nil +} + +// Iterate allows iterating a BPF program while keeping track of +// various offsets. +// +// Modifying the instruction slice will lead to undefined behaviour. +func (insns Instructions) Iterate() *InstructionIterator { + return &InstructionIterator{insns: insns} +} + +// InstructionIterator iterates over a BPF program. +type InstructionIterator struct { + insns Instructions + // The instruction in question. + Ins *Instruction + // The index of the instruction in the original instruction slice. + Index int + // The offset of the instruction in raw BPF instructions. This accounts + // for double-wide instructions. + Offset RawInstructionOffset +} + +// Next returns true as long as there are any instructions remaining. +func (iter *InstructionIterator) Next() bool { + if len(iter.insns) == 0 { + return false + } + + if iter.Ins != nil { + iter.Index++ + iter.Offset += RawInstructionOffset(iter.Ins.OpCode.rawInstructions()) + } + iter.Ins = &iter.insns[0] + iter.insns = iter.insns[1:] + return true +} + +type bpfInstruction struct { + OpCode OpCode + Registers bpfRegisters + Offset int16 + Constant int32 +} + +type bpfRegisters uint8 + +func newBPFRegisters(dst, src Register, bo binary.ByteOrder) (bpfRegisters, error) { + switch bo { + case binary.LittleEndian: + return bpfRegisters((src << 4) | (dst & 0xF)), nil + case binary.BigEndian: + return bpfRegisters((dst << 4) | (src & 0xF)), nil + default: + return 0, fmt.Errorf("unrecognized ByteOrder %T", bo) + } +} + +func (r bpfRegisters) Unmarshal(bo binary.ByteOrder) (dst, src Register, err error) { + switch bo { + case binary.LittleEndian: + return Register(r & 0xF), Register(r >> 4), nil + case binary.BigEndian: + return Register(r >> 4), Register(r & 0xf), nil + default: + return 0, 0, fmt.Errorf("unrecognized ByteOrder %T", bo) + } +} + +type unreferencedSymbolError struct { + symbol string +} + +func (use *unreferencedSymbolError) Error() string { + return fmt.Sprintf("unreferenced symbol %s", use.symbol) +} + +// IsUnreferencedSymbol returns true if err was caused by +// an unreferenced symbol. +func IsUnreferencedSymbol(err error) bool { + _, ok := err.(*unreferencedSymbolError) + return ok +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/jump.go b/agent/vendor/github.com/cilium/ebpf/asm/jump.go new file mode 100644 index 00000000000..7757179de64 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/jump.go @@ -0,0 +1,109 @@ +package asm + +//go:generate stringer -output jump_string.go -type=JumpOp + +// JumpOp affect control flow. +// +// msb lsb +// +----+-+---+ +// |OP |s|cls| +// +----+-+---+ +type JumpOp uint8 + +const jumpMask OpCode = aluMask + +const ( + // InvalidJumpOp is returned by getters when invoked + // on non branch OpCodes + InvalidJumpOp JumpOp = 0xff + // Ja jumps by offset unconditionally + Ja JumpOp = 0x00 + // JEq jumps by offset if r == imm + JEq JumpOp = 0x10 + // JGT jumps by offset if r > imm + JGT JumpOp = 0x20 + // JGE jumps by offset if r >= imm + JGE JumpOp = 0x30 + // JSet jumps by offset if r & imm + JSet JumpOp = 0x40 + // JNE jumps by offset if r != imm + JNE JumpOp = 0x50 + // JSGT jumps by offset if signed r > signed imm + JSGT JumpOp = 0x60 + // JSGE jumps by offset if signed r >= signed imm + JSGE JumpOp = 0x70 + // Call builtin or user defined function from imm + Call JumpOp = 0x80 + // Exit ends execution, with value in r0 + Exit JumpOp = 0x90 + // JLT jumps by offset if r < imm + JLT JumpOp = 0xa0 + // JLE jumps by offset if r <= imm + JLE JumpOp = 0xb0 + // JSLT jumps by offset if signed r < signed imm + JSLT JumpOp = 0xc0 + // JSLE jumps by offset if signed r <= signed imm + JSLE JumpOp = 0xd0 +) + +// Return emits an exit instruction. +// +// Requires a return value in R0. +func Return() Instruction { + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(Exit), + } +} + +// Op returns the OpCode for a given jump source. +func (op JumpOp) Op(source Source) OpCode { + return OpCode(JumpClass).SetJumpOp(op).SetSource(source) +} + +// Imm compares dst to value, and adjusts PC by offset if the condition is fulfilled. +func (op JumpOp) Imm(dst Register, value int32, label string) Instruction { + if op == Exit || op == Call || op == Ja { + return Instruction{OpCode: InvalidOpCode} + } + + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(op).SetSource(ImmSource), + Dst: dst, + Offset: -1, + Constant: int64(value), + Reference: label, + } +} + +// Reg compares dst to src, and adjusts PC by offset if the condition is fulfilled. +func (op JumpOp) Reg(dst, src Register, label string) Instruction { + if op == Exit || op == Call || op == Ja { + return Instruction{OpCode: InvalidOpCode} + } + + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(op).SetSource(RegSource), + Dst: dst, + Src: src, + Offset: -1, + Reference: label, + } +} + +// Label adjusts PC to the address of the label. +func (op JumpOp) Label(label string) Instruction { + if op == Call { + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(Call), + Src: PseudoCall, + Constant: -1, + Reference: label, + } + } + + return Instruction{ + OpCode: OpCode(JumpClass).SetJumpOp(op), + Offset: -1, + Reference: label, + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/jump_string.go b/agent/vendor/github.com/cilium/ebpf/asm/jump_string.go new file mode 100644 index 00000000000..85a4aaffa57 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/jump_string.go @@ -0,0 +1,53 @@ +// Code generated by "stringer -output jump_string.go -type=JumpOp"; DO NOT EDIT. + +package asm + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidJumpOp-255] + _ = x[Ja-0] + _ = x[JEq-16] + _ = x[JGT-32] + _ = x[JGE-48] + _ = x[JSet-64] + _ = x[JNE-80] + _ = x[JSGT-96] + _ = x[JSGE-112] + _ = x[Call-128] + _ = x[Exit-144] + _ = x[JLT-160] + _ = x[JLE-176] + _ = x[JSLT-192] + _ = x[JSLE-208] +} + +const _JumpOp_name = "JaJEqJGTJGEJSetJNEJSGTJSGECallExitJLTJLEJSLTJSLEInvalidJumpOp" + +var _JumpOp_map = map[JumpOp]string{ + 0: _JumpOp_name[0:2], + 16: _JumpOp_name[2:5], + 32: _JumpOp_name[5:8], + 48: _JumpOp_name[8:11], + 64: _JumpOp_name[11:15], + 80: _JumpOp_name[15:18], + 96: _JumpOp_name[18:22], + 112: _JumpOp_name[22:26], + 128: _JumpOp_name[26:30], + 144: _JumpOp_name[30:34], + 160: _JumpOp_name[34:37], + 176: _JumpOp_name[37:40], + 192: _JumpOp_name[40:44], + 208: _JumpOp_name[44:48], + 255: _JumpOp_name[48:61], +} + +func (i JumpOp) String() string { + if str, ok := _JumpOp_map[i]; ok { + return str + } + return "JumpOp(" + strconv.FormatInt(int64(i), 10) + ")" +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/load_store.go b/agent/vendor/github.com/cilium/ebpf/asm/load_store.go new file mode 100644 index 00000000000..2d0ec648e88 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/load_store.go @@ -0,0 +1,204 @@ +package asm + +//go:generate stringer -output load_store_string.go -type=Mode,Size + +// Mode for load and store operations +// +// msb lsb +// +---+--+---+ +// |MDE|sz|cls| +// +---+--+---+ +type Mode uint8 + +const modeMask OpCode = 0xe0 + +const ( + // InvalidMode is returned by getters when invoked + // on non load / store OpCodes + InvalidMode Mode = 0xff + // ImmMode - immediate value + ImmMode Mode = 0x00 + // AbsMode - immediate value + offset + AbsMode Mode = 0x20 + // IndMode - indirect (imm+src) + IndMode Mode = 0x40 + // MemMode - load from memory + MemMode Mode = 0x60 + // XAddMode - add atomically across processors. + XAddMode Mode = 0xc0 +) + +// Size of load and store operations +// +// msb lsb +// +---+--+---+ +// |mde|SZ|cls| +// +---+--+---+ +type Size uint8 + +const sizeMask OpCode = 0x18 + +const ( + // InvalidSize is returned by getters when invoked + // on non load / store OpCodes + InvalidSize Size = 0xff + // DWord - double word; 64 bits + DWord Size = 0x18 + // Word - word; 32 bits + Word Size = 0x00 + // Half - half-word; 16 bits + Half Size = 0x08 + // Byte - byte; 8 bits + Byte Size = 0x10 +) + +// Sizeof returns the size in bytes. +func (s Size) Sizeof() int { + switch s { + case DWord: + return 8 + case Word: + return 4 + case Half: + return 2 + case Byte: + return 1 + default: + return -1 + } +} + +// LoadMemOp returns the OpCode to load a value of given size from memory. +func LoadMemOp(size Size) OpCode { + return OpCode(LdXClass).SetMode(MemMode).SetSize(size) +} + +// LoadMem emits `dst = *(size *)(src + offset)`. +func LoadMem(dst, src Register, offset int16, size Size) Instruction { + return Instruction{ + OpCode: LoadMemOp(size), + Dst: dst, + Src: src, + Offset: offset, + } +} + +// LoadImmOp returns the OpCode to load an immediate of given size. +// +// As of kernel 4.20, only DWord size is accepted. +func LoadImmOp(size Size) OpCode { + return OpCode(LdClass).SetMode(ImmMode).SetSize(size) +} + +// LoadImm emits `dst = (size)value`. +// +// As of kernel 4.20, only DWord size is accepted. +func LoadImm(dst Register, value int64, size Size) Instruction { + return Instruction{ + OpCode: LoadImmOp(size), + Dst: dst, + Constant: value, + } +} + +// LoadMapPtr stores a pointer to a map in dst. +func LoadMapPtr(dst Register, fd int) Instruction { + if fd < 0 { + return Instruction{OpCode: InvalidOpCode} + } + + return Instruction{ + OpCode: LoadImmOp(DWord), + Dst: dst, + Src: PseudoMapFD, + Constant: int64(fd), + } +} + +// LoadMapValue stores a pointer to the value at a certain offset of a map. +func LoadMapValue(dst Register, fd int, offset uint32) Instruction { + if fd < 0 { + return Instruction{OpCode: InvalidOpCode} + } + + fdAndOffset := (uint64(offset) << 32) | uint64(uint32(fd)) + return Instruction{ + OpCode: LoadImmOp(DWord), + Dst: dst, + Src: PseudoMapValue, + Constant: int64(fdAndOffset), + } +} + +// LoadIndOp returns the OpCode for loading a value of given size from an sk_buff. +func LoadIndOp(size Size) OpCode { + return OpCode(LdClass).SetMode(IndMode).SetSize(size) +} + +// LoadInd emits `dst = ntoh(*(size *)(((sk_buff *)R6)->data + src + offset))`. +func LoadInd(dst, src Register, offset int32, size Size) Instruction { + return Instruction{ + OpCode: LoadIndOp(size), + Dst: dst, + Src: src, + Constant: int64(offset), + } +} + +// LoadAbsOp returns the OpCode for loading a value of given size from an sk_buff. +func LoadAbsOp(size Size) OpCode { + return OpCode(LdClass).SetMode(AbsMode).SetSize(size) +} + +// LoadAbs emits `r0 = ntoh(*(size *)(((sk_buff *)R6)->data + offset))`. +func LoadAbs(offset int32, size Size) Instruction { + return Instruction{ + OpCode: LoadAbsOp(size), + Dst: R0, + Constant: int64(offset), + } +} + +// StoreMemOp returns the OpCode for storing a register of given size in memory. +func StoreMemOp(size Size) OpCode { + return OpCode(StXClass).SetMode(MemMode).SetSize(size) +} + +// StoreMem emits `*(size *)(dst + offset) = src` +func StoreMem(dst Register, offset int16, src Register, size Size) Instruction { + return Instruction{ + OpCode: StoreMemOp(size), + Dst: dst, + Src: src, + Offset: offset, + } +} + +// StoreImmOp returns the OpCode for storing an immediate of given size in memory. +func StoreImmOp(size Size) OpCode { + return OpCode(StClass).SetMode(MemMode).SetSize(size) +} + +// StoreImm emits `*(size *)(dst + offset) = value`. +func StoreImm(dst Register, offset int16, value int64, size Size) Instruction { + return Instruction{ + OpCode: StoreImmOp(size), + Dst: dst, + Offset: offset, + Constant: value, + } +} + +// StoreXAddOp returns the OpCode to atomically add a register to a value in memory. +func StoreXAddOp(size Size) OpCode { + return OpCode(StXClass).SetMode(XAddMode).SetSize(size) +} + +// StoreXAdd atomically adds src to *dst. +func StoreXAdd(dst, src Register, size Size) Instruction { + return Instruction{ + OpCode: StoreXAddOp(size), + Dst: dst, + Src: src, + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/load_store_string.go b/agent/vendor/github.com/cilium/ebpf/asm/load_store_string.go new file mode 100644 index 00000000000..76d29a0756c --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/load_store_string.go @@ -0,0 +1,80 @@ +// Code generated by "stringer -output load_store_string.go -type=Mode,Size"; DO NOT EDIT. + +package asm + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidMode-255] + _ = x[ImmMode-0] + _ = x[AbsMode-32] + _ = x[IndMode-64] + _ = x[MemMode-96] + _ = x[XAddMode-192] +} + +const ( + _Mode_name_0 = "ImmMode" + _Mode_name_1 = "AbsMode" + _Mode_name_2 = "IndMode" + _Mode_name_3 = "MemMode" + _Mode_name_4 = "XAddMode" + _Mode_name_5 = "InvalidMode" +) + +func (i Mode) String() string { + switch { + case i == 0: + return _Mode_name_0 + case i == 32: + return _Mode_name_1 + case i == 64: + return _Mode_name_2 + case i == 96: + return _Mode_name_3 + case i == 192: + return _Mode_name_4 + case i == 255: + return _Mode_name_5 + default: + return "Mode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidSize-255] + _ = x[DWord-24] + _ = x[Word-0] + _ = x[Half-8] + _ = x[Byte-16] +} + +const ( + _Size_name_0 = "Word" + _Size_name_1 = "Half" + _Size_name_2 = "Byte" + _Size_name_3 = "DWord" + _Size_name_4 = "InvalidSize" +) + +func (i Size) String() string { + switch { + case i == 0: + return _Size_name_0 + case i == 8: + return _Size_name_1 + case i == 16: + return _Size_name_2 + case i == 24: + return _Size_name_3 + case i == 255: + return _Size_name_4 + default: + return "Size(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/opcode.go b/agent/vendor/github.com/cilium/ebpf/asm/opcode.go new file mode 100644 index 00000000000..dc4564a98d2 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/opcode.go @@ -0,0 +1,237 @@ +package asm + +import ( + "fmt" + "strings" +) + +//go:generate stringer -output opcode_string.go -type=Class + +type encoding int + +const ( + unknownEncoding encoding = iota + loadOrStore + jumpOrALU +) + +// Class of operations +// +// msb lsb +// +---+--+---+ +// | ?? |CLS| +// +---+--+---+ +type Class uint8 + +const classMask OpCode = 0x07 + +const ( + // LdClass load memory + LdClass Class = 0x00 + // LdXClass load memory from constant + LdXClass Class = 0x01 + // StClass load register from memory + StClass Class = 0x02 + // StXClass load register from constant + StXClass Class = 0x03 + // ALUClass arithmetic operators + ALUClass Class = 0x04 + // JumpClass jump operators + JumpClass Class = 0x05 + // ALU64Class arithmetic in 64 bit mode + ALU64Class Class = 0x07 +) + +func (cls Class) encoding() encoding { + switch cls { + case LdClass, LdXClass, StClass, StXClass: + return loadOrStore + case ALU64Class, ALUClass, JumpClass: + return jumpOrALU + default: + return unknownEncoding + } +} + +// OpCode is a packed eBPF opcode. +// +// Its encoding is defined by a Class value: +// +// msb lsb +// +----+-+---+ +// | ???? |CLS| +// +----+-+---+ +type OpCode uint8 + +// InvalidOpCode is returned by setters on OpCode +const InvalidOpCode OpCode = 0xff + +// rawInstructions returns the number of BPF instructions required +// to encode this opcode. +func (op OpCode) rawInstructions() int { + if op.isDWordLoad() { + return 2 + } + return 1 +} + +func (op OpCode) isDWordLoad() bool { + return op == LoadImmOp(DWord) +} + +// Class returns the class of operation. +func (op OpCode) Class() Class { + return Class(op & classMask) +} + +// Mode returns the mode for load and store operations. +func (op OpCode) Mode() Mode { + if op.Class().encoding() != loadOrStore { + return InvalidMode + } + return Mode(op & modeMask) +} + +// Size returns the size for load and store operations. +func (op OpCode) Size() Size { + if op.Class().encoding() != loadOrStore { + return InvalidSize + } + return Size(op & sizeMask) +} + +// Source returns the source for branch and ALU operations. +func (op OpCode) Source() Source { + if op.Class().encoding() != jumpOrALU || op.ALUOp() == Swap { + return InvalidSource + } + return Source(op & sourceMask) +} + +// ALUOp returns the ALUOp. +func (op OpCode) ALUOp() ALUOp { + if op.Class().encoding() != jumpOrALU { + return InvalidALUOp + } + return ALUOp(op & aluMask) +} + +// Endianness returns the Endianness for a byte swap instruction. +func (op OpCode) Endianness() Endianness { + if op.ALUOp() != Swap { + return InvalidEndian + } + return Endianness(op & endianMask) +} + +// JumpOp returns the JumpOp. +func (op OpCode) JumpOp() JumpOp { + if op.Class().encoding() != jumpOrALU { + return InvalidJumpOp + } + return JumpOp(op & jumpMask) +} + +// SetMode sets the mode on load and store operations. +// +// Returns InvalidOpCode if op is of the wrong class. +func (op OpCode) SetMode(mode Mode) OpCode { + if op.Class().encoding() != loadOrStore || !valid(OpCode(mode), modeMask) { + return InvalidOpCode + } + return (op & ^modeMask) | OpCode(mode) +} + +// SetSize sets the size on load and store operations. +// +// Returns InvalidOpCode if op is of the wrong class. +func (op OpCode) SetSize(size Size) OpCode { + if op.Class().encoding() != loadOrStore || !valid(OpCode(size), sizeMask) { + return InvalidOpCode + } + return (op & ^sizeMask) | OpCode(size) +} + +// SetSource sets the source on jump and ALU operations. +// +// Returns InvalidOpCode if op is of the wrong class. +func (op OpCode) SetSource(source Source) OpCode { + if op.Class().encoding() != jumpOrALU || !valid(OpCode(source), sourceMask) { + return InvalidOpCode + } + return (op & ^sourceMask) | OpCode(source) +} + +// SetALUOp sets the ALUOp on ALU operations. +// +// Returns InvalidOpCode if op is of the wrong class. +func (op OpCode) SetALUOp(alu ALUOp) OpCode { + class := op.Class() + if (class != ALUClass && class != ALU64Class) || !valid(OpCode(alu), aluMask) { + return InvalidOpCode + } + return (op & ^aluMask) | OpCode(alu) +} + +// SetJumpOp sets the JumpOp on jump operations. +// +// Returns InvalidOpCode if op is of the wrong class. +func (op OpCode) SetJumpOp(jump JumpOp) OpCode { + if op.Class() != JumpClass || !valid(OpCode(jump), jumpMask) { + return InvalidOpCode + } + return (op & ^jumpMask) | OpCode(jump) +} + +func (op OpCode) String() string { + var f strings.Builder + + switch class := op.Class(); class { + case LdClass, LdXClass, StClass, StXClass: + f.WriteString(strings.TrimSuffix(class.String(), "Class")) + + mode := op.Mode() + f.WriteString(strings.TrimSuffix(mode.String(), "Mode")) + + switch op.Size() { + case DWord: + f.WriteString("DW") + case Word: + f.WriteString("W") + case Half: + f.WriteString("H") + case Byte: + f.WriteString("B") + } + + case ALU64Class, ALUClass: + f.WriteString(op.ALUOp().String()) + + if op.ALUOp() == Swap { + // Width for Endian is controlled by Constant + f.WriteString(op.Endianness().String()) + } else { + if class == ALUClass { + f.WriteString("32") + } + + f.WriteString(strings.TrimSuffix(op.Source().String(), "Source")) + } + + case JumpClass: + f.WriteString(op.JumpOp().String()) + if jop := op.JumpOp(); jop != Exit && jop != Call { + f.WriteString(strings.TrimSuffix(op.Source().String(), "Source")) + } + + default: + fmt.Fprintf(&f, "OpCode(%#x)", uint8(op)) + } + + return f.String() +} + +// valid returns true if all bits in value are covered by mask. +func valid(value, mask OpCode) bool { + return value & ^mask == 0 +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/opcode_string.go b/agent/vendor/github.com/cilium/ebpf/asm/opcode_string.go new file mode 100644 index 00000000000..079ce1db0b8 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/opcode_string.go @@ -0,0 +1,38 @@ +// Code generated by "stringer -output opcode_string.go -type=Class"; DO NOT EDIT. + +package asm + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[LdClass-0] + _ = x[LdXClass-1] + _ = x[StClass-2] + _ = x[StXClass-3] + _ = x[ALUClass-4] + _ = x[JumpClass-5] + _ = x[ALU64Class-7] +} + +const ( + _Class_name_0 = "LdClassLdXClassStClassStXClassALUClassJumpClass" + _Class_name_1 = "ALU64Class" +) + +var ( + _Class_index_0 = [...]uint8{0, 7, 15, 22, 30, 38, 47} +) + +func (i Class) String() string { + switch { + case 0 <= i && i <= 5: + return _Class_name_0[_Class_index_0[i]:_Class_index_0[i+1]] + case i == 7: + return _Class_name_1 + default: + return "Class(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/asm/register.go b/agent/vendor/github.com/cilium/ebpf/asm/register.go new file mode 100644 index 00000000000..76cb44bffc7 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/asm/register.go @@ -0,0 +1,49 @@ +package asm + +import ( + "fmt" +) + +// Register is the source or destination of most operations. +type Register uint8 + +// R0 contains return values. +const R0 Register = 0 + +// Registers for function arguments. +const ( + R1 Register = R0 + 1 + iota + R2 + R3 + R4 + R5 +) + +// Callee saved registers preserved by function calls. +const ( + R6 Register = R5 + 1 + iota + R7 + R8 + R9 +) + +// Read-only frame pointer to access stack. +const ( + R10 Register = R9 + 1 + RFP = R10 +) + +// Pseudo registers used by 64bit loads and jumps +const ( + PseudoMapFD = R1 // BPF_PSEUDO_MAP_FD + PseudoMapValue = R2 // BPF_PSEUDO_MAP_VALUE + PseudoCall = R1 // BPF_PSEUDO_CALL +) + +func (r Register) String() string { + v := uint8(r) + if v == 10 { + return "rfp" + } + return fmt.Sprintf("r%d", v) +} diff --git a/agent/vendor/github.com/cilium/ebpf/collection.go b/agent/vendor/github.com/cilium/ebpf/collection.go new file mode 100644 index 00000000000..8e362900326 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/collection.go @@ -0,0 +1,589 @@ +package ebpf + +import ( + "errors" + "fmt" + "math" + "reflect" + "strings" + + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" +) + +// CollectionOptions control loading a collection into the kernel. +// +// Maps and Programs are passed to NewMapWithOptions and NewProgramsWithOptions. +type CollectionOptions struct { + Maps MapOptions + Programs ProgramOptions +} + +// CollectionSpec describes a collection. +type CollectionSpec struct { + Maps map[string]*MapSpec + Programs map[string]*ProgramSpec +} + +// Copy returns a recursive copy of the spec. +func (cs *CollectionSpec) Copy() *CollectionSpec { + if cs == nil { + return nil + } + + cpy := CollectionSpec{ + Maps: make(map[string]*MapSpec, len(cs.Maps)), + Programs: make(map[string]*ProgramSpec, len(cs.Programs)), + } + + for name, spec := range cs.Maps { + cpy.Maps[name] = spec.Copy() + } + + for name, spec := range cs.Programs { + cpy.Programs[name] = spec.Copy() + } + + return &cpy +} + +// RewriteMaps replaces all references to specific maps. +// +// Use this function to use pre-existing maps instead of creating new ones +// when calling NewCollection. Any named maps are removed from CollectionSpec.Maps. +// +// Returns an error if a named map isn't used in at least one program. +func (cs *CollectionSpec) RewriteMaps(maps map[string]*Map) error { + for symbol, m := range maps { + // have we seen a program that uses this symbol / map + seen := false + fd := m.FD() + for progName, progSpec := range cs.Programs { + err := progSpec.Instructions.RewriteMapPtr(symbol, fd) + + switch { + case err == nil: + seen = true + + case asm.IsUnreferencedSymbol(err): + // Not all programs need to use the map + + default: + return fmt.Errorf("program %s: %w", progName, err) + } + } + + if !seen { + return fmt.Errorf("map %s not referenced by any programs", symbol) + } + + // Prevent NewCollection from creating rewritten maps + delete(cs.Maps, symbol) + } + + return nil +} + +// RewriteConstants replaces the value of multiple constants. +// +// The constant must be defined like so in the C program: +// +// static volatile const type foobar; +// static volatile const type foobar = default; +// +// Replacement values must be of the same length as the C sizeof(type). +// If necessary, they are marshalled according to the same rules as +// map values. +// +// From Linux 5.5 the verifier will use constants to eliminate dead code. +// +// Returns an error if a constant doesn't exist. +func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error { + rodata := cs.Maps[".rodata"] + if rodata == nil { + return errors.New("missing .rodata section") + } + + if rodata.BTF == nil { + return errors.New(".rodata section has no BTF") + } + + if n := len(rodata.Contents); n != 1 { + return fmt.Errorf("expected one key in .rodata, found %d", n) + } + + kv := rodata.Contents[0] + value, ok := kv.Value.([]byte) + if !ok { + return fmt.Errorf("first value in .rodata is %T not []byte", kv.Value) + } + + buf := make([]byte, len(value)) + copy(buf, value) + + err := patchValue(buf, btf.MapValue(rodata.BTF), consts) + if err != nil { + return err + } + + rodata.Contents[0] = MapKV{kv.Key, buf} + return nil +} + +// Assign the contents of a CollectionSpec to a struct. +// +// This function is a short-cut to manually checking the presence +// of maps and programs in a collection spec. Consider using bpf2go if this +// sounds useful. +// +// The argument to must be a pointer to a struct. A field of the +// struct is updated with values from Programs or Maps if it +// has an `ebpf` tag and its type is *ProgramSpec or *MapSpec. +// The tag gives the name of the program or map as found in +// the CollectionSpec. +// +// struct { +// Foo *ebpf.ProgramSpec `ebpf:"xdp_foo"` +// Bar *ebpf.MapSpec `ebpf:"bar_map"` +// Ignored int +// } +// +// Returns an error if any of the fields can't be found, or +// if the same map or program is assigned multiple times. +func (cs *CollectionSpec) Assign(to interface{}) error { + valueOf := func(typ reflect.Type, name string) (reflect.Value, error) { + switch typ { + case reflect.TypeOf((*ProgramSpec)(nil)): + p := cs.Programs[name] + if p == nil { + return reflect.Value{}, fmt.Errorf("missing program %q", name) + } + return reflect.ValueOf(p), nil + case reflect.TypeOf((*MapSpec)(nil)): + m := cs.Maps[name] + if m == nil { + return reflect.Value{}, fmt.Errorf("missing map %q", name) + } + return reflect.ValueOf(m), nil + default: + return reflect.Value{}, fmt.Errorf("unsupported type %s", typ) + } + } + + return assignValues(to, valueOf) +} + +// LoadAndAssign maps and programs into the kernel and assign them to a struct. +// +// This function is a short-cut to manually checking the presence +// of maps and programs in a collection spec. Consider using bpf2go if this +// sounds useful. +// +// The argument to must be a pointer to a struct. A field of the +// struct is updated with values from Programs or Maps if it +// has an `ebpf` tag and its type is *Program or *Map. +// The tag gives the name of the program or map as found in +// the CollectionSpec. +// +// struct { +// Foo *ebpf.Program `ebpf:"xdp_foo"` +// Bar *ebpf.Map `ebpf:"bar_map"` +// Ignored int +// } +// +// opts may be nil. +// +// Returns an error if any of the fields can't be found, or +// if the same map or program is assigned multiple times. +func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) error { + if opts == nil { + opts = &CollectionOptions{} + } + + loadMap, loadProgram, done, cleanup := lazyLoadCollection(cs, opts) + defer cleanup() + + valueOf := func(typ reflect.Type, name string) (reflect.Value, error) { + switch typ { + case reflect.TypeOf((*Program)(nil)): + p, err := loadProgram(name) + if err != nil { + return reflect.Value{}, err + } + return reflect.ValueOf(p), nil + case reflect.TypeOf((*Map)(nil)): + m, err := loadMap(name) + if err != nil { + return reflect.Value{}, err + } + return reflect.ValueOf(m), nil + default: + return reflect.Value{}, fmt.Errorf("unsupported type %s", typ) + } + } + + if err := assignValues(to, valueOf); err != nil { + return err + } + + done() + return nil +} + +// Collection is a collection of Programs and Maps associated +// with their symbols +type Collection struct { + Programs map[string]*Program + Maps map[string]*Map +} + +// NewCollection creates a Collection from a specification. +func NewCollection(spec *CollectionSpec) (*Collection, error) { + return NewCollectionWithOptions(spec, CollectionOptions{}) +} + +// NewCollectionWithOptions creates a Collection from a specification. +func NewCollectionWithOptions(spec *CollectionSpec, opts CollectionOptions) (*Collection, error) { + loadMap, loadProgram, done, cleanup := lazyLoadCollection(spec, &opts) + defer cleanup() + + for mapName := range spec.Maps { + _, err := loadMap(mapName) + if err != nil { + return nil, err + } + } + + for progName := range spec.Programs { + _, err := loadProgram(progName) + if err != nil { + return nil, err + } + } + + maps, progs := done() + return &Collection{ + progs, + maps, + }, nil +} + +type btfHandleCache map[*btf.Spec]*btf.Handle + +func (btfs btfHandleCache) load(spec *btf.Spec) (*btf.Handle, error) { + if btfs[spec] != nil { + return btfs[spec], nil + } + + handle, err := btf.NewHandle(spec) + if err != nil { + return nil, err + } + + btfs[spec] = handle + return handle, nil +} + +func (btfs btfHandleCache) close() { + for _, handle := range btfs { + handle.Close() + } +} + +func lazyLoadCollection(coll *CollectionSpec, opts *CollectionOptions) ( + loadMap func(string) (*Map, error), + loadProgram func(string) (*Program, error), + done func() (map[string]*Map, map[string]*Program), + cleanup func(), +) { + var ( + maps = make(map[string]*Map) + progs = make(map[string]*Program) + btfs = make(btfHandleCache) + skipMapsAndProgs = false + ) + + cleanup = func() { + btfs.close() + + if skipMapsAndProgs { + return + } + + for _, m := range maps { + m.Close() + } + + for _, p := range progs { + p.Close() + } + } + + done = func() (map[string]*Map, map[string]*Program) { + skipMapsAndProgs = true + return maps, progs + } + + loadMap = func(mapName string) (*Map, error) { + if m := maps[mapName]; m != nil { + return m, nil + } + + mapSpec := coll.Maps[mapName] + if mapSpec == nil { + return nil, fmt.Errorf("missing map %s", mapName) + } + + m, err := newMapWithOptions(mapSpec, opts.Maps, btfs) + if err != nil { + return nil, fmt.Errorf("map %s: %w", mapName, err) + } + + maps[mapName] = m + return m, nil + } + + loadProgram = func(progName string) (*Program, error) { + if prog := progs[progName]; prog != nil { + return prog, nil + } + + progSpec := coll.Programs[progName] + if progSpec == nil { + return nil, fmt.Errorf("unknown program %s", progName) + } + + progSpec = progSpec.Copy() + + // Rewrite any reference to a valid map. + for i := range progSpec.Instructions { + ins := &progSpec.Instructions[i] + + if ins.OpCode != asm.LoadImmOp(asm.DWord) || ins.Reference == "" { + continue + } + + if uint32(ins.Constant) != math.MaxUint32 { + // Don't overwrite maps already rewritten, users can + // rewrite programs in the spec themselves + continue + } + + m, err := loadMap(ins.Reference) + if err != nil { + return nil, fmt.Errorf("program %s: %s", progName, err) + } + + fd := m.FD() + if fd < 0 { + return nil, fmt.Errorf("map %s: %w", ins.Reference, internal.ErrClosedFd) + } + if err := ins.RewriteMapPtr(m.FD()); err != nil { + return nil, fmt.Errorf("progam %s: map %s: %w", progName, ins.Reference, err) + } + } + + prog, err := newProgramWithOptions(progSpec, opts.Programs, btfs) + if err != nil { + return nil, fmt.Errorf("program %s: %w", progName, err) + } + + progs[progName] = prog + return prog, nil + } + + return +} + +// LoadCollection parses an object file and converts it to a collection. +func LoadCollection(file string) (*Collection, error) { + spec, err := LoadCollectionSpec(file) + if err != nil { + return nil, err + } + return NewCollection(spec) +} + +// Close frees all maps and programs associated with the collection. +// +// The collection mustn't be used afterwards. +func (coll *Collection) Close() { + for _, prog := range coll.Programs { + prog.Close() + } + for _, m := range coll.Maps { + m.Close() + } +} + +// DetachMap removes the named map from the Collection. +// +// This means that a later call to Close() will not affect this map. +// +// Returns nil if no map of that name exists. +func (coll *Collection) DetachMap(name string) *Map { + m := coll.Maps[name] + delete(coll.Maps, name) + return m +} + +// DetachProgram removes the named program from the Collection. +// +// This means that a later call to Close() will not affect this program. +// +// Returns nil if no program of that name exists. +func (coll *Collection) DetachProgram(name string) *Program { + p := coll.Programs[name] + delete(coll.Programs, name) + return p +} + +// Assign the contents of a collection to a struct. +// +// Deprecated: use CollectionSpec.Assign instead. It provides the same +// functionality but creates only the maps and programs requested. +func (coll *Collection) Assign(to interface{}) error { + assignedMaps := make(map[string]struct{}) + assignedPrograms := make(map[string]struct{}) + valueOf := func(typ reflect.Type, name string) (reflect.Value, error) { + switch typ { + case reflect.TypeOf((*Program)(nil)): + p := coll.Programs[name] + if p == nil { + return reflect.Value{}, fmt.Errorf("missing program %q", name) + } + assignedPrograms[name] = struct{}{} + return reflect.ValueOf(p), nil + case reflect.TypeOf((*Map)(nil)): + m := coll.Maps[name] + if m == nil { + return reflect.Value{}, fmt.Errorf("missing map %q", name) + } + assignedMaps[name] = struct{}{} + return reflect.ValueOf(m), nil + default: + return reflect.Value{}, fmt.Errorf("unsupported type %s", typ) + } + } + + if err := assignValues(to, valueOf); err != nil { + return err + } + + for name := range assignedPrograms { + coll.DetachProgram(name) + } + + for name := range assignedMaps { + coll.DetachMap(name) + } + + return nil +} + +func assignValues(to interface{}, valueOf func(reflect.Type, string) (reflect.Value, error)) error { + type structField struct { + reflect.StructField + value reflect.Value + } + + var ( + fields []structField + visitedTypes = make(map[reflect.Type]bool) + flattenStruct func(reflect.Value) error + ) + + flattenStruct = func(structVal reflect.Value) error { + structType := structVal.Type() + if structType.Kind() != reflect.Struct { + return fmt.Errorf("%s is not a struct", structType) + } + + if visitedTypes[structType] { + return fmt.Errorf("recursion on type %s", structType) + } + + for i := 0; i < structType.NumField(); i++ { + field := structField{structType.Field(i), structVal.Field(i)} + + name := field.Tag.Get("ebpf") + if name != "" { + fields = append(fields, field) + continue + } + + var err error + switch field.Type.Kind() { + case reflect.Ptr: + if field.Type.Elem().Kind() != reflect.Struct { + continue + } + + if field.value.IsNil() { + return fmt.Errorf("nil pointer to %s", structType) + } + + err = flattenStruct(field.value.Elem()) + + case reflect.Struct: + err = flattenStruct(field.value) + + default: + continue + } + + if err != nil { + return fmt.Errorf("field %s: %s", field.Name, err) + } + } + + return nil + } + + toValue := reflect.ValueOf(to) + if toValue.Type().Kind() != reflect.Ptr { + return fmt.Errorf("%T is not a pointer to struct", to) + } + + if toValue.IsNil() { + return fmt.Errorf("nil pointer to %T", to) + } + + if err := flattenStruct(toValue.Elem()); err != nil { + return err + } + + type elem struct { + // Either *Map or *Program + typ reflect.Type + name string + } + + assignedTo := make(map[elem]string) + for _, field := range fields { + name := field.Tag.Get("ebpf") + if strings.Contains(name, ",") { + return fmt.Errorf("field %s: ebpf tag contains a comma", field.Name) + } + + e := elem{field.Type, name} + if assignedField := assignedTo[e]; assignedField != "" { + return fmt.Errorf("field %s: %q was already assigned to %s", field.Name, name, assignedField) + } + + value, err := valueOf(field.Type, name) + if err != nil { + return fmt.Errorf("field %s: %w", field.Name, err) + } + + if !field.value.CanSet() { + return fmt.Errorf("field %s: can't set value", field.Name) + } + + field.value.Set(value) + assignedTo[e] = field.Name + } + + return nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/doc.go b/agent/vendor/github.com/cilium/ebpf/doc.go new file mode 100644 index 00000000000..f7f34da8f44 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/doc.go @@ -0,0 +1,16 @@ +// Package ebpf is a toolkit for working with eBPF programs. +// +// eBPF programs are small snippets of code which are executed directly +// in a VM in the Linux kernel, which makes them very fast and flexible. +// Many Linux subsystems now accept eBPF programs. This makes it possible +// to implement highly application specific logic inside the kernel, +// without having to modify the actual kernel itself. +// +// This package is designed for long-running processes which +// want to use eBPF to implement part of their application logic. It has no +// run-time dependencies outside of the library and the Linux kernel itself. +// eBPF code should be compiled ahead of time using clang, and shipped with +// your application as any other resource. +// +// Use the link subpackage to attach a loaded program to a hook in the kernel. +package ebpf diff --git a/agent/vendor/github.com/cilium/ebpf/elf_reader.go b/agent/vendor/github.com/cilium/ebpf/elf_reader.go new file mode 100644 index 00000000000..943142c49e6 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/elf_reader.go @@ -0,0 +1,930 @@ +package ebpf + +import ( + "bufio" + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "os" + "strings" + + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" + "github.com/cilium/ebpf/internal/unix" +) + +// elfCode is a convenience to reduce the amount of arguments that have to +// be passed around explicitly. You should treat it's contents as immutable. +type elfCode struct { + *internal.SafeELFFile + sections map[elf.SectionIndex]*elfSection + license string + version uint32 + btf *btf.Spec +} + +// LoadCollectionSpec parses an ELF file into a CollectionSpec. +func LoadCollectionSpec(file string) (*CollectionSpec, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + + spec, err := LoadCollectionSpecFromReader(f) + if err != nil { + return nil, fmt.Errorf("file %s: %w", file, err) + } + return spec, nil +} + +// LoadCollectionSpecFromReader parses an ELF file into a CollectionSpec. +func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) { + f, err := internal.NewSafeELFFile(rd) + if err != nil { + return nil, err + } + defer f.Close() + + var ( + licenseSection *elf.Section + versionSection *elf.Section + sections = make(map[elf.SectionIndex]*elfSection) + relSections = make(map[elf.SectionIndex]*elf.Section) + ) + + // This is the target of relocations generated by inline assembly. + sections[elf.SHN_UNDEF] = newElfSection(new(elf.Section), undefSection) + + // Collect all the sections we're interested in. This includes relocations + // which we parse later. + for i, sec := range f.Sections { + idx := elf.SectionIndex(i) + + switch { + case strings.HasPrefix(sec.Name, "license"): + licenseSection = sec + case strings.HasPrefix(sec.Name, "version"): + versionSection = sec + case strings.HasPrefix(sec.Name, "maps"): + sections[idx] = newElfSection(sec, mapSection) + case sec.Name == ".maps": + sections[idx] = newElfSection(sec, btfMapSection) + case sec.Name == ".bss" || sec.Name == ".data" || strings.HasPrefix(sec.Name, ".rodata"): + sections[idx] = newElfSection(sec, dataSection) + case sec.Type == elf.SHT_REL: + // Store relocations under the section index of the target + relSections[elf.SectionIndex(sec.Info)] = sec + case sec.Type == elf.SHT_PROGBITS && (sec.Flags&elf.SHF_EXECINSTR) != 0 && sec.Size > 0: + sections[idx] = newElfSection(sec, programSection) + } + } + + license, err := loadLicense(licenseSection) + if err != nil { + return nil, fmt.Errorf("load license: %w", err) + } + + version, err := loadVersion(versionSection, f.ByteOrder) + if err != nil { + return nil, fmt.Errorf("load version: %w", err) + } + + btfSpec, err := btf.LoadSpecFromReader(rd) + if err != nil { + return nil, fmt.Errorf("load BTF: %w", err) + } + + // Assign symbols to all the sections we're interested in. + symbols, err := f.Symbols() + if err != nil { + return nil, fmt.Errorf("load symbols: %v", err) + } + + for _, symbol := range symbols { + idx := symbol.Section + symType := elf.ST_TYPE(symbol.Info) + + section := sections[idx] + if section == nil { + continue + } + + // Older versions of LLVM don't tag symbols correctly, so keep + // all NOTYPE ones. + keep := symType == elf.STT_NOTYPE + switch section.kind { + case mapSection, btfMapSection, dataSection: + keep = keep || symType == elf.STT_OBJECT + case programSection: + keep = keep || symType == elf.STT_FUNC + } + if !keep || symbol.Name == "" { + continue + } + + section.symbols[symbol.Value] = symbol + } + + ec := &elfCode{ + SafeELFFile: f, + sections: sections, + license: license, + version: version, + btf: btfSpec, + } + + // Go through relocation sections, and parse the ones for sections we're + // interested in. Make sure that relocations point at valid sections. + for idx, relSection := range relSections { + section := sections[idx] + if section == nil { + continue + } + + rels, err := ec.loadRelocations(relSection, symbols) + if err != nil { + return nil, fmt.Errorf("relocation for section %q: %w", section.Name, err) + } + + for _, rel := range rels { + target := sections[rel.Section] + if target == nil { + return nil, fmt.Errorf("section %q: reference to %q in section %s: %w", section.Name, rel.Name, rel.Section, ErrNotSupported) + } + + if target.Flags&elf.SHF_STRINGS > 0 { + return nil, fmt.Errorf("section %q: string %q is not stack allocated: %w", section.Name, rel.Name, ErrNotSupported) + } + + target.references++ + } + + section.relocations = rels + } + + // Collect all the various ways to define maps. + maps := make(map[string]*MapSpec) + if err := ec.loadMaps(maps); err != nil { + return nil, fmt.Errorf("load maps: %w", err) + } + + if err := ec.loadBTFMaps(maps); err != nil { + return nil, fmt.Errorf("load BTF maps: %w", err) + } + + if err := ec.loadDataSections(maps); err != nil { + return nil, fmt.Errorf("load data sections: %w", err) + } + + // Finally, collect programs and link them. + progs, err := ec.loadPrograms() + if err != nil { + return nil, fmt.Errorf("load programs: %w", err) + } + + return &CollectionSpec{maps, progs}, nil +} + +func loadLicense(sec *elf.Section) (string, error) { + if sec == nil { + return "", nil + } + + data, err := sec.Data() + if err != nil { + return "", fmt.Errorf("section %s: %v", sec.Name, err) + } + return string(bytes.TrimRight(data, "\000")), nil +} + +func loadVersion(sec *elf.Section, bo binary.ByteOrder) (uint32, error) { + if sec == nil { + return 0, nil + } + + var version uint32 + if err := binary.Read(sec.Open(), bo, &version); err != nil { + return 0, fmt.Errorf("section %s: %v", sec.Name, err) + } + return version, nil +} + +type elfSectionKind int + +const ( + undefSection elfSectionKind = iota + mapSection + btfMapSection + programSection + dataSection +) + +type elfSection struct { + *elf.Section + kind elfSectionKind + // Offset from the start of the section to a symbol + symbols map[uint64]elf.Symbol + // Offset from the start of the section to a relocation, which points at + // a symbol in another section. + relocations map[uint64]elf.Symbol + // The number of relocations pointing at this section. + references int +} + +func newElfSection(section *elf.Section, kind elfSectionKind) *elfSection { + return &elfSection{ + section, + kind, + make(map[uint64]elf.Symbol), + make(map[uint64]elf.Symbol), + 0, + } +} + +func (ec *elfCode) loadPrograms() (map[string]*ProgramSpec, error) { + var ( + progs []*ProgramSpec + libs []*ProgramSpec + ) + + for _, sec := range ec.sections { + if sec.kind != programSection { + continue + } + + if len(sec.symbols) == 0 { + return nil, fmt.Errorf("section %v: missing symbols", sec.Name) + } + + funcSym, ok := sec.symbols[0] + if !ok { + return nil, fmt.Errorf("section %v: no label at start", sec.Name) + } + + insns, length, err := ec.loadInstructions(sec) + if err != nil { + return nil, fmt.Errorf("program %s: %w", funcSym.Name, err) + } + + progType, attachType, attachTo := getProgType(sec.Name) + + spec := &ProgramSpec{ + Name: funcSym.Name, + Type: progType, + AttachType: attachType, + AttachTo: attachTo, + License: ec.license, + KernelVersion: ec.version, + Instructions: insns, + ByteOrder: ec.ByteOrder, + } + + if ec.btf != nil { + spec.BTF, err = ec.btf.Program(sec.Name, length) + if err != nil && !errors.Is(err, btf.ErrNoExtendedInfo) { + return nil, fmt.Errorf("program %s: %w", funcSym.Name, err) + } + } + + if spec.Type == UnspecifiedProgram { + // There is no single name we can use for "library" sections, + // since they may contain multiple functions. We'll decode the + // labels they contain later on, and then link sections that way. + libs = append(libs, spec) + } else { + progs = append(progs, spec) + } + } + + res := make(map[string]*ProgramSpec, len(progs)) + for _, prog := range progs { + err := link(prog, libs) + if err != nil { + return nil, fmt.Errorf("program %s: %w", prog.Name, err) + } + res[prog.Name] = prog + } + + return res, nil +} + +func (ec *elfCode) loadInstructions(section *elfSection) (asm.Instructions, uint64, error) { + var ( + r = bufio.NewReader(section.Open()) + insns asm.Instructions + offset uint64 + ) + for { + var ins asm.Instruction + n, err := ins.Unmarshal(r, ec.ByteOrder) + if err == io.EOF { + return insns, offset, nil + } + if err != nil { + return nil, 0, fmt.Errorf("offset %d: %w", offset, err) + } + + ins.Symbol = section.symbols[offset].Name + + if rel, ok := section.relocations[offset]; ok { + if err = ec.relocateInstruction(&ins, rel); err != nil { + return nil, 0, fmt.Errorf("offset %d: relocate instruction: %w", offset, err) + } + } + + insns = append(insns, ins) + offset += n + } +} + +func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) error { + var ( + typ = elf.ST_TYPE(rel.Info) + bind = elf.ST_BIND(rel.Info) + name = rel.Name + ) + + target := ec.sections[rel.Section] + + switch target.kind { + case mapSection, btfMapSection: + if bind != elf.STB_GLOBAL { + return fmt.Errorf("possible erroneous static qualifier on map definition: found reference to %q", name) + } + + if typ != elf.STT_OBJECT && typ != elf.STT_NOTYPE { + // STT_NOTYPE is generated on clang < 8 which doesn't tag + // relocations appropriately. + return fmt.Errorf("map load: incorrect relocation type %v", typ) + } + + ins.Src = asm.PseudoMapFD + + // Mark the instruction as needing an update when creating the + // collection. + if err := ins.RewriteMapPtr(-1); err != nil { + return err + } + + case dataSection: + switch typ { + case elf.STT_SECTION: + if bind != elf.STB_LOCAL { + return fmt.Errorf("direct load: %s: unsupported relocation %s", name, bind) + } + + case elf.STT_OBJECT: + if bind != elf.STB_GLOBAL { + return fmt.Errorf("direct load: %s: unsupported relocation %s", name, bind) + } + + default: + return fmt.Errorf("incorrect relocation type %v for direct map load", typ) + } + + // We rely on using the name of the data section as the reference. It + // would be nicer to keep the real name in case of an STT_OBJECT, but + // it's not clear how to encode that into Instruction. + name = target.Name + + // For some reason, clang encodes the offset of the symbol its + // section in the first basic BPF instruction, while the kernel + // expects it in the second one. + ins.Constant <<= 32 + ins.Src = asm.PseudoMapValue + + // Mark the instruction as needing an update when creating the + // collection. + if err := ins.RewriteMapPtr(-1); err != nil { + return err + } + + case programSection: + if ins.OpCode.JumpOp() != asm.Call { + return fmt.Errorf("not a call instruction: %s", ins) + } + + if ins.Src != asm.PseudoCall { + return fmt.Errorf("call: %s: incorrect source register", name) + } + + switch typ { + case elf.STT_NOTYPE, elf.STT_FUNC: + if bind != elf.STB_GLOBAL { + return fmt.Errorf("call: %s: unsupported binding: %s", name, bind) + } + + case elf.STT_SECTION: + if bind != elf.STB_LOCAL { + return fmt.Errorf("call: %s: unsupported binding: %s", name, bind) + } + + // The function we want to call is in the indicated section, + // at the offset encoded in the instruction itself. Reverse + // the calculation to find the real function we're looking for. + // A value of -1 references the first instruction in the section. + offset := int64(int32(ins.Constant)+1) * asm.InstructionSize + if offset < 0 { + return fmt.Errorf("call: %s: invalid offset %d", name, offset) + } + + sym, ok := target.symbols[uint64(offset)] + if !ok { + return fmt.Errorf("call: %s: no symbol at offset %d", name, offset) + } + + ins.Constant = -1 + name = sym.Name + + default: + return fmt.Errorf("call: %s: invalid symbol type %s", name, typ) + } + + case undefSection: + if bind != elf.STB_GLOBAL { + return fmt.Errorf("asm relocation: %s: unsupported binding: %s", name, bind) + } + + if typ != elf.STT_NOTYPE { + return fmt.Errorf("asm relocation: %s: unsupported type %s", name, typ) + } + + // There is nothing to do here but set ins.Reference. + + default: + return fmt.Errorf("relocation to %q: %w", target.Name, ErrNotSupported) + } + + ins.Reference = name + return nil +} + +func (ec *elfCode) loadMaps(maps map[string]*MapSpec) error { + for _, sec := range ec.sections { + if sec.kind != mapSection { + continue + } + + nSym := len(sec.symbols) + if nSym == 0 { + return fmt.Errorf("section %v: no symbols", sec.Name) + } + + if sec.Size%uint64(nSym) != 0 { + return fmt.Errorf("section %v: map descriptors are not of equal size", sec.Name) + } + + var ( + r = bufio.NewReader(sec.Open()) + size = sec.Size / uint64(nSym) + ) + for i, offset := 0, uint64(0); i < nSym; i, offset = i+1, offset+size { + mapSym, ok := sec.symbols[offset] + if !ok { + return fmt.Errorf("section %s: missing symbol for map at offset %d", sec.Name, offset) + } + + if maps[mapSym.Name] != nil { + return fmt.Errorf("section %v: map %v already exists", sec.Name, mapSym) + } + + lr := io.LimitReader(r, int64(size)) + + spec := MapSpec{ + Name: SanitizeName(mapSym.Name, -1), + } + switch { + case binary.Read(lr, ec.ByteOrder, &spec.Type) != nil: + return fmt.Errorf("map %v: missing type", mapSym) + case binary.Read(lr, ec.ByteOrder, &spec.KeySize) != nil: + return fmt.Errorf("map %v: missing key size", mapSym) + case binary.Read(lr, ec.ByteOrder, &spec.ValueSize) != nil: + return fmt.Errorf("map %v: missing value size", mapSym) + case binary.Read(lr, ec.ByteOrder, &spec.MaxEntries) != nil: + return fmt.Errorf("map %v: missing max entries", mapSym) + case binary.Read(lr, ec.ByteOrder, &spec.Flags) != nil: + return fmt.Errorf("map %v: missing flags", mapSym) + } + + if _, err := io.Copy(internal.DiscardZeroes{}, lr); err != nil { + return fmt.Errorf("map %v: unknown and non-zero fields in definition", mapSym) + } + + maps[mapSym.Name] = &spec + } + } + + return nil +} + +func (ec *elfCode) loadBTFMaps(maps map[string]*MapSpec) error { + for _, sec := range ec.sections { + if sec.kind != btfMapSection { + continue + } + + if ec.btf == nil { + return fmt.Errorf("missing BTF") + } + + if len(sec.symbols) == 0 { + return fmt.Errorf("section %v: no symbols", sec.Name) + } + + _, err := io.Copy(internal.DiscardZeroes{}, bufio.NewReader(sec.Open())) + if err != nil { + return fmt.Errorf("section %v: initializing BTF map definitions: %w", sec.Name, internal.ErrNotSupported) + } + + for _, sym := range sec.symbols { + name := sym.Name + if maps[name] != nil { + return fmt.Errorf("section %v: map %v already exists", sec.Name, sym) + } + + // A global Var is created by declaring a struct with a 'structure variable', + // as is common in eBPF C to declare eBPF maps. For example, + // `struct { ... } map_name ...;` emits a global variable `map_name` + // with the type of said struct (which can be anonymous). + var v btf.Var + if err := ec.btf.FindType(name, &v); err != nil { + return fmt.Errorf("cannot find global variable '%s' in BTF: %w", name, err) + } + + mapStruct, ok := v.Type.(*btf.Struct) + if !ok { + return fmt.Errorf("expected struct, got %s", v.Type) + } + + mapSpec, err := mapSpecFromBTF(name, mapStruct, false, ec.btf) + if err != nil { + return fmt.Errorf("map %v: %w", name, err) + } + + maps[name] = mapSpec + } + } + + return nil +} + +// mapSpecFromBTF produces a MapSpec based on a btf.Struct def representing +// a BTF map definition. The name and spec arguments will be copied to the +// resulting MapSpec, and inner must be true on any resursive invocations. +func mapSpecFromBTF(name string, def *btf.Struct, inner bool, spec *btf.Spec) (*MapSpec, error) { + + var ( + key, value btf.Type + keySize, valueSize uint32 + mapType, flags, maxEntries uint32 + pinType PinType + innerMapSpec *MapSpec + err error + ) + + for i, member := range def.Members { + switch member.Name { + case "type": + mapType, err = uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get type: %w", err) + } + + case "map_flags": + flags, err = uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get BTF map flags: %w", err) + } + + case "max_entries": + maxEntries, err = uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get BTF map max entries: %w", err) + } + + case "key": + if keySize != 0 { + return nil, errors.New("both key and key_size given") + } + + pk, ok := member.Type.(*btf.Pointer) + if !ok { + return nil, fmt.Errorf("key type is not a pointer: %T", member.Type) + } + + key = pk.Target + + size, err := btf.Sizeof(pk.Target) + if err != nil { + return nil, fmt.Errorf("can't get size of BTF key: %w", err) + } + + keySize = uint32(size) + + case "value": + if valueSize != 0 { + return nil, errors.New("both value and value_size given") + } + + vk, ok := member.Type.(*btf.Pointer) + if !ok { + return nil, fmt.Errorf("value type is not a pointer: %T", member.Type) + } + + value = vk.Target + + size, err := btf.Sizeof(vk.Target) + if err != nil { + return nil, fmt.Errorf("can't get size of BTF value: %w", err) + } + + valueSize = uint32(size) + + case "key_size": + // Key needs to be nil and keySize needs to be 0 for key_size to be + // considered a valid member. + if key != nil || keySize != 0 { + return nil, errors.New("both key and key_size given") + } + + keySize, err = uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get BTF key size: %w", err) + } + + case "value_size": + // Value needs to be nil and valueSize needs to be 0 for value_size to be + // considered a valid member. + if value != nil || valueSize != 0 { + return nil, errors.New("both value and value_size given") + } + + valueSize, err = uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get BTF value size: %w", err) + } + + case "pinning": + if inner { + return nil, errors.New("inner maps can't be pinned") + } + + pinning, err := uintFromBTF(member.Type) + if err != nil { + return nil, fmt.Errorf("can't get pinning: %w", err) + } + + pinType = PinType(pinning) + + case "values": + // The 'values' field in BTF map definitions is used for declaring map + // value types that are references to other BPF objects, like other maps + // or programs. It is always expected to be an array of pointers. + if i != len(def.Members)-1 { + return nil, errors.New("'values' must be the last member in a BTF map definition") + } + + if valueSize != 0 && valueSize != 4 { + return nil, errors.New("value_size must be 0 or 4") + } + valueSize = 4 + + valueType, err := resolveBTFArrayMacro(member.Type) + if err != nil { + return nil, fmt.Errorf("can't resolve type of member 'values': %w", err) + } + + switch t := valueType.(type) { + case *btf.Struct: + // The values member pointing to an array of structs means we're expecting + // a map-in-map declaration. + if MapType(mapType) != ArrayOfMaps && MapType(mapType) != HashOfMaps { + return nil, errors.New("outer map needs to be an array or a hash of maps") + } + if inner { + return nil, fmt.Errorf("nested inner maps are not supported") + } + + // This inner map spec is used as a map template, but it needs to be + // created as a traditional map before it can be used to do so. + // libbpf names the inner map template '.inner', but we + // opted for _inner to simplify validation logic. (dots only supported + // on kernels 5.2 and up) + // Pass the BTF spec from the parent object, since both parent and + // child must be created from the same BTF blob (on kernels that support BTF). + innerMapSpec, err = mapSpecFromBTF(name+"_inner", t, true, spec) + if err != nil { + return nil, fmt.Errorf("can't parse BTF map definition of inner map: %w", err) + } + + default: + return nil, fmt.Errorf("unsupported value type %q in 'values' field", t) + } + + default: + return nil, fmt.Errorf("unrecognized field %s in BTF map definition", member.Name) + } + } + + bm := btf.NewMap(spec, key, value) + + return &MapSpec{ + Name: SanitizeName(name, -1), + Type: MapType(mapType), + KeySize: keySize, + ValueSize: valueSize, + MaxEntries: maxEntries, + Flags: flags, + BTF: &bm, + Pinning: pinType, + InnerMap: innerMapSpec, + }, nil +} + +// uintFromBTF resolves the __uint macro, which is a pointer to a sized +// array, e.g. for int (*foo)[10], this function will return 10. +func uintFromBTF(typ btf.Type) (uint32, error) { + ptr, ok := typ.(*btf.Pointer) + if !ok { + return 0, fmt.Errorf("not a pointer: %v", typ) + } + + arr, ok := ptr.Target.(*btf.Array) + if !ok { + return 0, fmt.Errorf("not a pointer to array: %v", typ) + } + + return arr.Nelems, nil +} + +// resolveBTFArrayMacro resolves the __array macro, which declares an array +// of pointers to a given type. This function returns the target Type of +// the pointers in the array. +func resolveBTFArrayMacro(typ btf.Type) (btf.Type, error) { + arr, ok := typ.(*btf.Array) + if !ok { + return nil, fmt.Errorf("not an array: %v", typ) + } + + ptr, ok := arr.Type.(*btf.Pointer) + if !ok { + return nil, fmt.Errorf("not an array of pointers: %v", typ) + } + + return ptr.Target, nil +} + +func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error { + for _, sec := range ec.sections { + if sec.kind != dataSection { + continue + } + + if sec.references == 0 { + // Prune data sections which are not referenced by any + // instructions. + continue + } + + if ec.btf == nil { + return errors.New("data sections require BTF, make sure all consts are marked as static") + } + + btfMap, err := ec.btf.Datasec(sec.Name) + if err != nil { + return err + } + + data, err := sec.Data() + if err != nil { + return fmt.Errorf("data section %s: can't get contents: %w", sec.Name, err) + } + + if uint64(len(data)) > math.MaxUint32 { + return fmt.Errorf("data section %s: contents exceed maximum size", sec.Name) + } + + mapSpec := &MapSpec{ + Name: SanitizeName(sec.Name, -1), + Type: Array, + KeySize: 4, + ValueSize: uint32(len(data)), + MaxEntries: 1, + Contents: []MapKV{{uint32(0), data}}, + BTF: btfMap, + } + + switch sec.Name { + case ".rodata": + mapSpec.Flags = unix.BPF_F_RDONLY_PROG + mapSpec.Freeze = true + case ".bss": + // The kernel already zero-initializes the map + mapSpec.Contents = nil + } + + maps[sec.Name] = mapSpec + } + return nil +} + +func getProgType(sectionName string) (ProgramType, AttachType, string) { + types := map[string]struct { + progType ProgramType + attachType AttachType + }{ + // From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/lib/bpf/libbpf.c + "socket": {SocketFilter, AttachNone}, + "seccomp": {SocketFilter, AttachNone}, + "kprobe/": {Kprobe, AttachNone}, + "uprobe/": {Kprobe, AttachNone}, + "kretprobe/": {Kprobe, AttachNone}, + "uretprobe/": {Kprobe, AttachNone}, + "tracepoint/": {TracePoint, AttachNone}, + "raw_tracepoint/": {RawTracepoint, AttachNone}, + "xdp": {XDP, AttachNone}, + "perf_event": {PerfEvent, AttachNone}, + "lwt_in": {LWTIn, AttachNone}, + "lwt_out": {LWTOut, AttachNone}, + "lwt_xmit": {LWTXmit, AttachNone}, + "lwt_seg6local": {LWTSeg6Local, AttachNone}, + "sockops": {SockOps, AttachCGroupSockOps}, + "sk_skb/stream_parser": {SkSKB, AttachSkSKBStreamParser}, + "sk_skb/stream_verdict": {SkSKB, AttachSkSKBStreamParser}, + "sk_msg": {SkMsg, AttachSkSKBStreamVerdict}, + "lirc_mode2": {LircMode2, AttachLircMode2}, + "flow_dissector": {FlowDissector, AttachFlowDissector}, + "iter/": {Tracing, AttachTraceIter}, + "sk_lookup/": {SkLookup, AttachSkLookup}, + "lsm/": {LSM, AttachLSMMac}, + + "cgroup_skb/ingress": {CGroupSKB, AttachCGroupInetIngress}, + "cgroup_skb/egress": {CGroupSKB, AttachCGroupInetEgress}, + "cgroup/dev": {CGroupDevice, AttachCGroupDevice}, + "cgroup/skb": {CGroupSKB, AttachNone}, + "cgroup/sock": {CGroupSock, AttachCGroupInetSockCreate}, + "cgroup/post_bind4": {CGroupSock, AttachCGroupInet4PostBind}, + "cgroup/post_bind6": {CGroupSock, AttachCGroupInet6PostBind}, + "cgroup/bind4": {CGroupSockAddr, AttachCGroupInet4Bind}, + "cgroup/bind6": {CGroupSockAddr, AttachCGroupInet6Bind}, + "cgroup/connect4": {CGroupSockAddr, AttachCGroupInet4Connect}, + "cgroup/connect6": {CGroupSockAddr, AttachCGroupInet6Connect}, + "cgroup/sendmsg4": {CGroupSockAddr, AttachCGroupUDP4Sendmsg}, + "cgroup/sendmsg6": {CGroupSockAddr, AttachCGroupUDP6Sendmsg}, + "cgroup/recvmsg4": {CGroupSockAddr, AttachCGroupUDP4Recvmsg}, + "cgroup/recvmsg6": {CGroupSockAddr, AttachCGroupUDP6Recvmsg}, + "cgroup/sysctl": {CGroupSysctl, AttachCGroupSysctl}, + "cgroup/getsockopt": {CGroupSockopt, AttachCGroupGetsockopt}, + "cgroup/setsockopt": {CGroupSockopt, AttachCGroupSetsockopt}, + "classifier": {SchedCLS, AttachNone}, + "action": {SchedACT, AttachNone}, + } + + for prefix, t := range types { + if !strings.HasPrefix(sectionName, prefix) { + continue + } + + if !strings.HasSuffix(prefix, "/") { + return t.progType, t.attachType, "" + } + + return t.progType, t.attachType, sectionName[len(prefix):] + } + + return UnspecifiedProgram, AttachNone, "" +} + +func (ec *elfCode) loadRelocations(sec *elf.Section, symbols []elf.Symbol) (map[uint64]elf.Symbol, error) { + rels := make(map[uint64]elf.Symbol) + + if sec.Entsize < 16 { + return nil, fmt.Errorf("section %s: relocations are less than 16 bytes", sec.Name) + } + + r := bufio.NewReader(sec.Open()) + for off := uint64(0); off < sec.Size; off += sec.Entsize { + ent := io.LimitReader(r, int64(sec.Entsize)) + + var rel elf.Rel64 + if binary.Read(ent, ec.ByteOrder, &rel) != nil { + return nil, fmt.Errorf("can't parse relocation at offset %v", off) + } + + symNo := int(elf.R_SYM64(rel.Info) - 1) + if symNo >= len(symbols) { + return nil, fmt.Errorf("offset %d: symbol %d doesn't exist", off, symNo) + } + + symbol := symbols[symNo] + rels[rel.Off] = symbol + } + + return rels, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/elf_reader_fuzz.go b/agent/vendor/github.com/cilium/ebpf/elf_reader_fuzz.go new file mode 100644 index 00000000000..d46d135f2fc --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/elf_reader_fuzz.go @@ -0,0 +1,21 @@ +// +build gofuzz + +// Use with https://github.com/dvyukov/go-fuzz + +package ebpf + +import "bytes" + +func FuzzLoadCollectionSpec(data []byte) int { + spec, err := LoadCollectionSpecFromReader(bytes.NewReader(data)) + if err != nil { + if spec != nil { + panic("spec is not nil") + } + return 0 + } + if spec == nil { + panic("spec is nil") + } + return 1 +} diff --git a/agent/vendor/github.com/cilium/ebpf/go.mod b/agent/vendor/github.com/cilium/ebpf/go.mod new file mode 100644 index 00000000000..df8139621c3 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/go.mod @@ -0,0 +1,9 @@ +module github.com/cilium/ebpf + +go 1.15 + +require ( + github.com/frankban/quicktest v1.11.3 + github.com/google/go-cmp v0.5.4 + golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c +) diff --git a/agent/vendor/github.com/cilium/ebpf/go.sum b/agent/vendor/github.com/cilium/ebpf/go.sum new file mode 100644 index 00000000000..a5039262aab --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/go.sum @@ -0,0 +1,13 @@ +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/agent/vendor/github.com/cilium/ebpf/info.go b/agent/vendor/github.com/cilium/ebpf/info.go new file mode 100644 index 00000000000..b95131ef572 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/info.go @@ -0,0 +1,239 @@ +package ebpf + +import ( + "bufio" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "strings" + "syscall" + "time" + + "github.com/cilium/ebpf/internal" +) + +// MapInfo describes a map. +type MapInfo struct { + Type MapType + id MapID + KeySize uint32 + ValueSize uint32 + MaxEntries uint32 + Flags uint32 + // Name as supplied by user space at load time. + Name string +} + +func newMapInfoFromFd(fd *internal.FD) (*MapInfo, error) { + info, err := bpfGetMapInfoByFD(fd) + if errors.Is(err, syscall.EINVAL) { + return newMapInfoFromProc(fd) + } + if err != nil { + return nil, err + } + + return &MapInfo{ + MapType(info.map_type), + MapID(info.id), + info.key_size, + info.value_size, + info.max_entries, + info.map_flags, + // name is available from 4.15. + internal.CString(info.name[:]), + }, nil +} + +func newMapInfoFromProc(fd *internal.FD) (*MapInfo, error) { + var mi MapInfo + err := scanFdInfo(fd, map[string]interface{}{ + "map_type": &mi.Type, + "key_size": &mi.KeySize, + "value_size": &mi.ValueSize, + "max_entries": &mi.MaxEntries, + "map_flags": &mi.Flags, + }) + if err != nil { + return nil, err + } + return &mi, nil +} + +// ID returns the map ID. +// +// Available from 4.13. +// +// The bool return value indicates whether this optional field is available. +func (mi *MapInfo) ID() (MapID, bool) { + return mi.id, mi.id > 0 +} + +// programStats holds statistics of a program. +type programStats struct { + // Total accumulated runtime of the program ins ns. + runtime time.Duration + // Total number of times the program was called. + runCount uint64 +} + +// ProgramInfo describes a program. +type ProgramInfo struct { + Type ProgramType + id ProgramID + // Truncated hash of the BPF bytecode. + Tag string + // Name as supplied by user space at load time. + Name string + + stats *programStats +} + +func newProgramInfoFromFd(fd *internal.FD) (*ProgramInfo, error) { + info, err := bpfGetProgInfoByFD(fd) + if errors.Is(err, syscall.EINVAL) { + return newProgramInfoFromProc(fd) + } + if err != nil { + return nil, err + } + + return &ProgramInfo{ + Type: ProgramType(info.prog_type), + id: ProgramID(info.id), + // tag is available if the kernel supports BPF_PROG_GET_INFO_BY_FD. + Tag: hex.EncodeToString(info.tag[:]), + // name is available from 4.15. + Name: internal.CString(info.name[:]), + stats: &programStats{ + runtime: time.Duration(info.run_time_ns), + runCount: info.run_cnt, + }, + }, nil +} + +func newProgramInfoFromProc(fd *internal.FD) (*ProgramInfo, error) { + var info ProgramInfo + err := scanFdInfo(fd, map[string]interface{}{ + "prog_type": &info.Type, + "prog_tag": &info.Tag, + }) + if errors.Is(err, errMissingFields) { + return nil, &internal.UnsupportedFeatureError{ + Name: "reading program info from /proc/self/fdinfo", + MinimumVersion: internal.Version{4, 10, 0}, + } + } + if err != nil { + return nil, err + } + + return &info, nil +} + +// ID returns the program ID. +// +// Available from 4.13. +// +// The bool return value indicates whether this optional field is available. +func (pi *ProgramInfo) ID() (ProgramID, bool) { + return pi.id, pi.id > 0 +} + +// RunCount returns the total number of times the program was called. +// +// Can return 0 if the collection of statistics is not enabled. See EnableStats(). +// The bool return value indicates whether this optional field is available. +func (pi *ProgramInfo) RunCount() (uint64, bool) { + if pi.stats != nil { + return pi.stats.runCount, true + } + return 0, false +} + +// Runtime returns the total accumulated runtime of the program. +// +// Can return 0 if the collection of statistics is not enabled. See EnableStats(). +// The bool return value indicates whether this optional field is available. +func (pi *ProgramInfo) Runtime() (time.Duration, bool) { + if pi.stats != nil { + return pi.stats.runtime, true + } + return time.Duration(0), false +} + +func scanFdInfo(fd *internal.FD, fields map[string]interface{}) error { + raw, err := fd.Value() + if err != nil { + return err + } + + fh, err := os.Open(fmt.Sprintf("/proc/self/fdinfo/%d", raw)) + if err != nil { + return err + } + defer fh.Close() + + if err := scanFdInfoReader(fh, fields); err != nil { + return fmt.Errorf("%s: %w", fh.Name(), err) + } + return nil +} + +var errMissingFields = errors.New("missing fields") + +func scanFdInfoReader(r io.Reader, fields map[string]interface{}) error { + var ( + scanner = bufio.NewScanner(r) + scanned int + ) + + for scanner.Scan() { + parts := strings.SplitN(scanner.Text(), "\t", 2) + if len(parts) != 2 { + continue + } + + name := strings.TrimSuffix(parts[0], ":") + field, ok := fields[string(name)] + if !ok { + continue + } + + if n, err := fmt.Sscanln(parts[1], field); err != nil || n != 1 { + return fmt.Errorf("can't parse field %s: %v", name, err) + } + + scanned++ + } + + if err := scanner.Err(); err != nil { + return err + } + + if scanned != len(fields) { + return errMissingFields + } + + return nil +} + +// EnableStats starts the measuring of the runtime +// and run counts of eBPF programs. +// +// Collecting statistics can have an impact on the performance. +// +// Requires at least 5.8. +func EnableStats(which uint32) (io.Closer, error) { + attr := internal.BPFEnableStatsAttr{ + StatsType: which, + } + + fd, err := internal.BPFEnableStats(&attr) + if err != nil { + return nil, err + } + return fd, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/btf.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/btf.go new file mode 100644 index 00000000000..1e66d94765a --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/btf.go @@ -0,0 +1,791 @@ +package btf + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "os" + "reflect" + "sync" + "unsafe" + + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/unix" +) + +const btfMagic = 0xeB9F + +// Errors returned by BTF functions. +var ( + ErrNotSupported = internal.ErrNotSupported + ErrNotFound = errors.New("not found") + ErrNoExtendedInfo = errors.New("no extended info") +) + +// Spec represents decoded BTF. +type Spec struct { + rawTypes []rawType + strings stringTable + types []Type + namedTypes map[string][]namedType + funcInfos map[string]extInfo + lineInfos map[string]extInfo + coreRelos map[string]bpfCoreRelos + byteOrder binary.ByteOrder +} + +type btfHeader struct { + Magic uint16 + Version uint8 + Flags uint8 + HdrLen uint32 + + TypeOff uint32 + TypeLen uint32 + StringOff uint32 + StringLen uint32 +} + +// LoadSpecFromReader reads BTF sections from an ELF. +// +// Returns a nil Spec and no error if no BTF was present. +func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) { + file, err := internal.NewSafeELFFile(rd) + if err != nil { + return nil, err + } + defer file.Close() + + btfSection, btfExtSection, sectionSizes, err := findBtfSections(file) + if err != nil { + return nil, err + } + + if btfSection == nil { + return nil, nil + } + + symbols, err := file.Symbols() + if err != nil { + return nil, fmt.Errorf("can't read symbols: %v", err) + } + + variableOffsets := make(map[variable]uint32) + for _, symbol := range symbols { + if idx := symbol.Section; idx >= elf.SHN_LORESERVE && idx <= elf.SHN_HIRESERVE { + // Ignore things like SHN_ABS + continue + } + + if int(symbol.Section) >= len(file.Sections) { + return nil, fmt.Errorf("symbol %s: invalid section %d", symbol.Name, symbol.Section) + } + + secName := file.Sections[symbol.Section].Name + if _, ok := sectionSizes[secName]; !ok { + continue + } + + if symbol.Value > math.MaxUint32 { + return nil, fmt.Errorf("section %s: symbol %s: size exceeds maximum", secName, symbol.Name) + } + + variableOffsets[variable{secName, symbol.Name}] = uint32(symbol.Value) + } + + spec, err := loadNakedSpec(btfSection.Open(), file.ByteOrder, sectionSizes, variableOffsets) + if err != nil { + return nil, err + } + + if btfExtSection == nil { + return spec, nil + } + + spec.funcInfos, spec.lineInfos, spec.coreRelos, err = parseExtInfos(btfExtSection.Open(), file.ByteOrder, spec.strings) + if err != nil { + return nil, fmt.Errorf("can't read ext info: %w", err) + } + + return spec, nil +} + +func findBtfSections(file *internal.SafeELFFile) (*elf.Section, *elf.Section, map[string]uint32, error) { + var ( + btfSection *elf.Section + btfExtSection *elf.Section + sectionSizes = make(map[string]uint32) + ) + + for _, sec := range file.Sections { + switch sec.Name { + case ".BTF": + btfSection = sec + case ".BTF.ext": + btfExtSection = sec + default: + if sec.Type != elf.SHT_PROGBITS && sec.Type != elf.SHT_NOBITS { + break + } + + if sec.Size > math.MaxUint32 { + return nil, nil, nil, fmt.Errorf("section %s exceeds maximum size", sec.Name) + } + + sectionSizes[sec.Name] = uint32(sec.Size) + } + } + return btfSection, btfExtSection, sectionSizes, nil +} + +func loadSpecFromVmlinux(rd io.ReaderAt) (*Spec, error) { + file, err := internal.NewSafeELFFile(rd) + if err != nil { + return nil, err + } + defer file.Close() + + btfSection, _, _, err := findBtfSections(file) + if err != nil { + return nil, fmt.Errorf(".BTF ELF section: %s", err) + } + if btfSection == nil { + return nil, fmt.Errorf("unable to find .BTF ELF section") + } + return loadNakedSpec(btfSection.Open(), file.ByteOrder, nil, nil) +} + +func loadNakedSpec(btf io.ReadSeeker, bo binary.ByteOrder, sectionSizes map[string]uint32, variableOffsets map[variable]uint32) (*Spec, error) { + rawTypes, rawStrings, err := parseBTF(btf, bo) + if err != nil { + return nil, err + } + + err = fixupDatasec(rawTypes, rawStrings, sectionSizes, variableOffsets) + if err != nil { + return nil, err + } + + types, typesByName, err := inflateRawTypes(rawTypes, rawStrings) + if err != nil { + return nil, err + } + + return &Spec{ + rawTypes: rawTypes, + namedTypes: typesByName, + types: types, + strings: rawStrings, + byteOrder: bo, + }, nil +} + +var kernelBTF struct { + sync.Mutex + *Spec +} + +// LoadKernelSpec returns the current kernel's BTF information. +// +// Requires a >= 5.5 kernel with CONFIG_DEBUG_INFO_BTF enabled. Returns +// ErrNotSupported if BTF is not enabled. +func LoadKernelSpec() (*Spec, error) { + kernelBTF.Lock() + defer kernelBTF.Unlock() + + if kernelBTF.Spec != nil { + return kernelBTF.Spec, nil + } + + var err error + kernelBTF.Spec, err = loadKernelSpec() + return kernelBTF.Spec, err +} + +func loadKernelSpec() (*Spec, error) { + release, err := unix.KernelRelease() + if err != nil { + return nil, fmt.Errorf("can't read kernel release number: %w", err) + } + + fh, err := os.Open("/sys/kernel/btf/vmlinux") + if err == nil { + defer fh.Close() + + return loadNakedSpec(fh, internal.NativeEndian, nil, nil) + } + + // use same list of locations as libbpf + // https://github.com/libbpf/libbpf/blob/9a3a42608dbe3731256a5682a125ac1e23bced8f/src/btf.c#L3114-L3122 + locations := []string{ + "/boot/vmlinux-%s", + "/lib/modules/%s/vmlinux-%[1]s", + "/lib/modules/%s/build/vmlinux", + "/usr/lib/modules/%s/kernel/vmlinux", + "/usr/lib/debug/boot/vmlinux-%s", + "/usr/lib/debug/boot/vmlinux-%s.debug", + "/usr/lib/debug/lib/modules/%s/vmlinux", + } + + for _, loc := range locations { + path := fmt.Sprintf(loc, release) + + fh, err := os.Open(path) + if err != nil { + continue + } + defer fh.Close() + + return loadSpecFromVmlinux(fh) + } + + return nil, fmt.Errorf("no BTF for kernel version %s: %w", release, internal.ErrNotSupported) +} + +func parseBTF(btf io.ReadSeeker, bo binary.ByteOrder) ([]rawType, stringTable, error) { + rawBTF, err := ioutil.ReadAll(btf) + if err != nil { + return nil, nil, fmt.Errorf("can't read BTF: %v", err) + } + + rd := bytes.NewReader(rawBTF) + + var header btfHeader + if err := binary.Read(rd, bo, &header); err != nil { + return nil, nil, fmt.Errorf("can't read header: %v", err) + } + + if header.Magic != btfMagic { + return nil, nil, fmt.Errorf("incorrect magic value %v", header.Magic) + } + + if header.Version != 1 { + return nil, nil, fmt.Errorf("unexpected version %v", header.Version) + } + + if header.Flags != 0 { + return nil, nil, fmt.Errorf("unsupported flags %v", header.Flags) + } + + remainder := int64(header.HdrLen) - int64(binary.Size(&header)) + if remainder < 0 { + return nil, nil, errors.New("header is too short") + } + + if _, err := io.CopyN(internal.DiscardZeroes{}, rd, remainder); err != nil { + return nil, nil, fmt.Errorf("header padding: %v", err) + } + + if _, err := rd.Seek(int64(header.HdrLen+header.StringOff), io.SeekStart); err != nil { + return nil, nil, fmt.Errorf("can't seek to start of string section: %v", err) + } + + rawStrings, err := readStringTable(io.LimitReader(rd, int64(header.StringLen))) + if err != nil { + return nil, nil, fmt.Errorf("can't read type names: %w", err) + } + + if _, err := rd.Seek(int64(header.HdrLen+header.TypeOff), io.SeekStart); err != nil { + return nil, nil, fmt.Errorf("can't seek to start of type section: %v", err) + } + + rawTypes, err := readTypes(io.LimitReader(rd, int64(header.TypeLen)), bo) + if err != nil { + return nil, nil, fmt.Errorf("can't read types: %w", err) + } + + return rawTypes, rawStrings, nil +} + +type variable struct { + section string + name string +} + +func fixupDatasec(rawTypes []rawType, rawStrings stringTable, sectionSizes map[string]uint32, variableOffsets map[variable]uint32) error { + for i, rawType := range rawTypes { + if rawType.Kind() != kindDatasec { + continue + } + + name, err := rawStrings.Lookup(rawType.NameOff) + if err != nil { + return err + } + + if name == ".kconfig" || name == ".ksyms" { + return fmt.Errorf("reference to %s: %w", name, ErrNotSupported) + } + + if rawTypes[i].SizeType != 0 { + continue + } + + size, ok := sectionSizes[name] + if !ok { + return fmt.Errorf("data section %s: missing size", name) + } + + rawTypes[i].SizeType = size + + secinfos := rawType.data.([]btfVarSecinfo) + for j, secInfo := range secinfos { + id := int(secInfo.Type - 1) + if id >= len(rawTypes) { + return fmt.Errorf("data section %s: invalid type id %d for variable %d", name, id, j) + } + + varName, err := rawStrings.Lookup(rawTypes[id].NameOff) + if err != nil { + return fmt.Errorf("data section %s: can't get name for type %d: %w", name, id, err) + } + + offset, ok := variableOffsets[variable{name, varName}] + if !ok { + return fmt.Errorf("data section %s: missing offset for variable %s", name, varName) + } + + secinfos[j].Offset = offset + } + } + + return nil +} + +type marshalOpts struct { + ByteOrder binary.ByteOrder + StripFuncLinkage bool +} + +func (s *Spec) marshal(opts marshalOpts) ([]byte, error) { + var ( + buf bytes.Buffer + header = new(btfHeader) + headerLen = binary.Size(header) + ) + + // Reserve space for the header. We have to write it last since + // we don't know the size of the type section yet. + _, _ = buf.Write(make([]byte, headerLen)) + + // Write type section, just after the header. + for _, raw := range s.rawTypes { + switch { + case opts.StripFuncLinkage && raw.Kind() == kindFunc: + raw.SetLinkage(linkageStatic) + } + + if err := raw.Marshal(&buf, opts.ByteOrder); err != nil { + return nil, fmt.Errorf("can't marshal BTF: %w", err) + } + } + + typeLen := uint32(buf.Len() - headerLen) + + // Write string section after type section. + _, _ = buf.Write(s.strings) + + // Fill out the header, and write it out. + header = &btfHeader{ + Magic: btfMagic, + Version: 1, + Flags: 0, + HdrLen: uint32(headerLen), + TypeOff: 0, + TypeLen: typeLen, + StringOff: typeLen, + StringLen: uint32(len(s.strings)), + } + + raw := buf.Bytes() + err := binary.Write(sliceWriter(raw[:headerLen]), opts.ByteOrder, header) + if err != nil { + return nil, fmt.Errorf("can't write header: %v", err) + } + + return raw, nil +} + +type sliceWriter []byte + +func (sw sliceWriter) Write(p []byte) (int, error) { + if len(p) != len(sw) { + return 0, errors.New("size doesn't match") + } + + return copy(sw, p), nil +} + +// Program finds the BTF for a specific section. +// +// Length is the number of bytes in the raw BPF instruction stream. +// +// Returns an error which may wrap ErrNoExtendedInfo if the Spec doesn't +// contain extended BTF info. +func (s *Spec) Program(name string, length uint64) (*Program, error) { + if length == 0 { + return nil, errors.New("length musn't be zero") + } + + if s.funcInfos == nil && s.lineInfos == nil && s.coreRelos == nil { + return nil, fmt.Errorf("BTF for section %s: %w", name, ErrNoExtendedInfo) + } + + funcInfos, funcOK := s.funcInfos[name] + lineInfos, lineOK := s.lineInfos[name] + coreRelos, coreOK := s.coreRelos[name] + + if !funcOK && !lineOK && !coreOK { + return nil, fmt.Errorf("no extended BTF info for section %s", name) + } + + return &Program{s, length, funcInfos, lineInfos, coreRelos}, nil +} + +// Datasec returns the BTF required to create maps which represent data sections. +func (s *Spec) Datasec(name string) (*Map, error) { + var datasec Datasec + if err := s.FindType(name, &datasec); err != nil { + return nil, fmt.Errorf("data section %s: can't get BTF: %w", name, err) + } + + m := NewMap(s, &Void{}, &datasec) + return &m, nil +} + +// FindType searches for a type with a specific name. +// +// hint determines the type of the returned Type. +// +// Returns an error wrapping ErrNotFound if no matching +// type exists in spec. +func (s *Spec) FindType(name string, typ Type) error { + var ( + wanted = reflect.TypeOf(typ) + candidate Type + ) + + for _, typ := range s.namedTypes[essentialName(name)] { + if reflect.TypeOf(typ) != wanted { + continue + } + + // Match against the full name, not just the essential one. + if typ.name() != name { + continue + } + + if candidate != nil { + return fmt.Errorf("type %s: multiple candidates for %T", name, typ) + } + + candidate = typ + } + + if candidate == nil { + return fmt.Errorf("type %s: %w", name, ErrNotFound) + } + + value := reflect.Indirect(reflect.ValueOf(copyType(candidate))) + reflect.Indirect(reflect.ValueOf(typ)).Set(value) + return nil +} + +// Handle is a reference to BTF loaded into the kernel. +type Handle struct { + fd *internal.FD +} + +// NewHandle loads BTF into the kernel. +// +// Returns ErrNotSupported if BTF is not supported. +func NewHandle(spec *Spec) (*Handle, error) { + if err := haveBTF(); err != nil { + return nil, err + } + + if spec.byteOrder != internal.NativeEndian { + return nil, fmt.Errorf("can't load %s BTF on %s", spec.byteOrder, internal.NativeEndian) + } + + btf, err := spec.marshal(marshalOpts{ + ByteOrder: internal.NativeEndian, + StripFuncLinkage: haveFuncLinkage() != nil, + }) + if err != nil { + return nil, fmt.Errorf("can't marshal BTF: %w", err) + } + + if uint64(len(btf)) > math.MaxUint32 { + return nil, errors.New("BTF exceeds the maximum size") + } + + attr := &bpfLoadBTFAttr{ + btf: internal.NewSlicePointer(btf), + btfSize: uint32(len(btf)), + } + + fd, err := bpfLoadBTF(attr) + if err != nil { + logBuf := make([]byte, 64*1024) + attr.logBuf = internal.NewSlicePointer(logBuf) + attr.btfLogSize = uint32(len(logBuf)) + attr.btfLogLevel = 1 + _, logErr := bpfLoadBTF(attr) + return nil, internal.ErrorWithLog(err, logBuf, logErr) + } + + return &Handle{fd}, nil +} + +// Close destroys the handle. +// +// Subsequent calls to FD will return an invalid value. +func (h *Handle) Close() error { + return h.fd.Close() +} + +// FD returns the file descriptor for the handle. +func (h *Handle) FD() int { + value, err := h.fd.Value() + if err != nil { + return -1 + } + + return int(value) +} + +// Map is the BTF for a map. +type Map struct { + spec *Spec + key, value Type +} + +// NewMap returns a new Map containing the given values. +// The key and value arguments are initialized to Void if nil values are given. +func NewMap(spec *Spec, key Type, value Type) Map { + if key == nil { + key = &Void{} + } + if value == nil { + value = &Void{} + } + + return Map{ + spec: spec, + key: key, + value: value, + } +} + +// MapSpec should be a method on Map, but is a free function +// to hide it from users of the ebpf package. +func MapSpec(m *Map) *Spec { + return m.spec +} + +// MapKey should be a method on Map, but is a free function +// to hide it from users of the ebpf package. +func MapKey(m *Map) Type { + return m.key +} + +// MapValue should be a method on Map, but is a free function +// to hide it from users of the ebpf package. +func MapValue(m *Map) Type { + return m.value +} + +// Program is the BTF information for a stream of instructions. +type Program struct { + spec *Spec + length uint64 + funcInfos, lineInfos extInfo + coreRelos bpfCoreRelos +} + +// ProgramSpec returns the Spec needed for loading function and line infos into the kernel. +// +// This is a free function instead of a method to hide it from users +// of package ebpf. +func ProgramSpec(s *Program) *Spec { + return s.spec +} + +// ProgramAppend the information from other to the Program. +// +// This is a free function instead of a method to hide it from users +// of package ebpf. +func ProgramAppend(s, other *Program) error { + funcInfos, err := s.funcInfos.append(other.funcInfos, s.length) + if err != nil { + return fmt.Errorf("func infos: %w", err) + } + + lineInfos, err := s.lineInfos.append(other.lineInfos, s.length) + if err != nil { + return fmt.Errorf("line infos: %w", err) + } + + s.funcInfos = funcInfos + s.lineInfos = lineInfos + s.coreRelos = s.coreRelos.append(other.coreRelos, s.length) + s.length += other.length + return nil +} + +// ProgramFuncInfos returns the binary form of BTF function infos. +// +// This is a free function instead of a method to hide it from users +// of package ebpf. +func ProgramFuncInfos(s *Program) (recordSize uint32, bytes []byte, err error) { + bytes, err = s.funcInfos.MarshalBinary() + if err != nil { + return 0, nil, err + } + + return s.funcInfos.recordSize, bytes, nil +} + +// ProgramLineInfos returns the binary form of BTF line infos. +// +// This is a free function instead of a method to hide it from users +// of package ebpf. +func ProgramLineInfos(s *Program) (recordSize uint32, bytes []byte, err error) { + bytes, err = s.lineInfos.MarshalBinary() + if err != nil { + return 0, nil, err + } + + return s.lineInfos.recordSize, bytes, nil +} + +// ProgramRelocations returns the CO-RE relocations required to adjust the +// program to the target. +// +// This is a free function instead of a method to hide it from users +// of package ebpf. +func ProgramRelocations(s *Program, target *Spec) (map[uint64]Relocation, error) { + if len(s.coreRelos) == 0 { + return nil, nil + } + + return coreRelocate(s.spec, target, s.coreRelos) +} + +type bpfLoadBTFAttr struct { + btf internal.Pointer + logBuf internal.Pointer + btfSize uint32 + btfLogSize uint32 + btfLogLevel uint32 +} + +func bpfLoadBTF(attr *bpfLoadBTFAttr) (*internal.FD, error) { + fd, err := internal.BPF(internal.BPF_BTF_LOAD, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err != nil { + return nil, err + } + + return internal.NewFD(uint32(fd)), nil +} + +func marshalBTF(types interface{}, strings []byte, bo binary.ByteOrder) []byte { + const minHeaderLength = 24 + + typesLen := uint32(binary.Size(types)) + header := btfHeader{ + Magic: btfMagic, + Version: 1, + HdrLen: minHeaderLength, + TypeOff: 0, + TypeLen: typesLen, + StringOff: typesLen, + StringLen: uint32(len(strings)), + } + + buf := new(bytes.Buffer) + _ = binary.Write(buf, bo, &header) + _ = binary.Write(buf, bo, types) + buf.Write(strings) + + return buf.Bytes() +} + +var haveBTF = internal.FeatureTest("BTF", "5.1", func() error { + var ( + types struct { + Integer btfType + Var btfType + btfVar struct{ Linkage uint32 } + } + strings = []byte{0, 'a', 0} + ) + + // We use a BTF_KIND_VAR here, to make sure that + // the kernel understands BTF at least as well as we + // do. BTF_KIND_VAR was introduced ~5.1. + types.Integer.SetKind(kindPointer) + types.Var.NameOff = 1 + types.Var.SetKind(kindVar) + types.Var.SizeType = 1 + + btf := marshalBTF(&types, strings, internal.NativeEndian) + + fd, err := bpfLoadBTF(&bpfLoadBTFAttr{ + btf: internal.NewSlicePointer(btf), + btfSize: uint32(len(btf)), + }) + if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) { + // Treat both EINVAL and EPERM as not supported: loading the program + // might still succeed without BTF. + return internal.ErrNotSupported + } + if err != nil { + return err + } + + fd.Close() + return nil +}) + +var haveFuncLinkage = internal.FeatureTest("BTF func linkage", "5.6", func() error { + if err := haveBTF(); err != nil { + return err + } + + var ( + types struct { + FuncProto btfType + Func btfType + } + strings = []byte{0, 'a', 0} + ) + + types.FuncProto.SetKind(kindFuncProto) + types.Func.SetKind(kindFunc) + types.Func.SizeType = 1 // aka FuncProto + types.Func.NameOff = 1 + types.Func.SetLinkage(linkageGlobal) + + btf := marshalBTF(&types, strings, internal.NativeEndian) + + fd, err := bpfLoadBTF(&bpfLoadBTFAttr{ + btf: internal.NewSlicePointer(btf), + btfSize: uint32(len(btf)), + }) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if err != nil { + return err + } + + fd.Close() + return nil +}) diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/btf_types.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/btf_types.go new file mode 100644 index 00000000000..a4cde3fe827 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/btf_types.go @@ -0,0 +1,269 @@ +package btf + +import ( + "encoding/binary" + "fmt" + "io" +) + +// btfKind describes a Type. +type btfKind uint8 + +// Equivalents of the BTF_KIND_* constants. +const ( + kindUnknown btfKind = iota + kindInt + kindPointer + kindArray + kindStruct + kindUnion + kindEnum + kindForward + kindTypedef + kindVolatile + kindConst + kindRestrict + // Added ~4.20 + kindFunc + kindFuncProto + // Added ~5.1 + kindVar + kindDatasec +) + +type btfFuncLinkage uint8 + +const ( + linkageStatic btfFuncLinkage = iota + linkageGlobal + linkageExtern +) + +const ( + btfTypeKindShift = 24 + btfTypeKindLen = 4 + btfTypeVlenShift = 0 + btfTypeVlenMask = 16 + btfTypeKindFlagShift = 31 + btfTypeKindFlagMask = 1 +) + +// btfType is equivalent to struct btf_type in Documentation/bpf/btf.rst. +type btfType struct { + NameOff uint32 + /* "info" bits arrangement + * bits 0-15: vlen (e.g. # of struct's members), linkage + * bits 16-23: unused + * bits 24-27: kind (e.g. int, ptr, array...etc) + * bits 28-30: unused + * bit 31: kind_flag, currently used by + * struct, union and fwd + */ + Info uint32 + /* "size" is used by INT, ENUM, STRUCT and UNION. + * "size" tells the size of the type it is describing. + * + * "type" is used by PTR, TYPEDEF, VOLATILE, CONST, RESTRICT, + * FUNC and FUNC_PROTO. + * "type" is a type_id referring to another type. + */ + SizeType uint32 +} + +func (k btfKind) String() string { + switch k { + case kindUnknown: + return "Unknown" + case kindInt: + return "Integer" + case kindPointer: + return "Pointer" + case kindArray: + return "Array" + case kindStruct: + return "Struct" + case kindUnion: + return "Union" + case kindEnum: + return "Enumeration" + case kindForward: + return "Forward" + case kindTypedef: + return "Typedef" + case kindVolatile: + return "Volatile" + case kindConst: + return "Const" + case kindRestrict: + return "Restrict" + case kindFunc: + return "Function" + case kindFuncProto: + return "Function Proto" + case kindVar: + return "Variable" + case kindDatasec: + return "Section" + default: + return fmt.Sprintf("Unknown (%d)", k) + } +} + +func mask(len uint32) uint32 { + return (1 << len) - 1 +} + +func (bt *btfType) info(len, shift uint32) uint32 { + return (bt.Info >> shift) & mask(len) +} + +func (bt *btfType) setInfo(value, len, shift uint32) { + bt.Info &^= mask(len) << shift + bt.Info |= (value & mask(len)) << shift +} + +func (bt *btfType) Kind() btfKind { + return btfKind(bt.info(btfTypeKindLen, btfTypeKindShift)) +} + +func (bt *btfType) SetKind(kind btfKind) { + bt.setInfo(uint32(kind), btfTypeKindLen, btfTypeKindShift) +} + +func (bt *btfType) Vlen() int { + return int(bt.info(btfTypeVlenMask, btfTypeVlenShift)) +} + +func (bt *btfType) SetVlen(vlen int) { + bt.setInfo(uint32(vlen), btfTypeVlenMask, btfTypeVlenShift) +} + +func (bt *btfType) KindFlag() bool { + return bt.info(btfTypeKindFlagMask, btfTypeKindFlagShift) == 1 +} + +func (bt *btfType) Linkage() btfFuncLinkage { + return btfFuncLinkage(bt.info(btfTypeVlenMask, btfTypeVlenShift)) +} + +func (bt *btfType) SetLinkage(linkage btfFuncLinkage) { + bt.setInfo(uint32(linkage), btfTypeVlenMask, btfTypeVlenShift) +} + +func (bt *btfType) Type() TypeID { + // TODO: Panic here if wrong kind? + return TypeID(bt.SizeType) +} + +func (bt *btfType) Size() uint32 { + // TODO: Panic here if wrong kind? + return bt.SizeType +} + +type rawType struct { + btfType + data interface{} +} + +func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error { + if err := binary.Write(w, bo, &rt.btfType); err != nil { + return err + } + + if rt.data == nil { + return nil + } + + return binary.Write(w, bo, rt.data) +} + +type btfArray struct { + Type TypeID + IndexType TypeID + Nelems uint32 +} + +type btfMember struct { + NameOff uint32 + Type TypeID + Offset uint32 +} + +type btfVarSecinfo struct { + Type TypeID + Offset uint32 + Size uint32 +} + +type btfVariable struct { + Linkage uint32 +} + +type btfEnum struct { + NameOff uint32 + Val int32 +} + +type btfParam struct { + NameOff uint32 + Type TypeID +} + +func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) { + var ( + header btfType + types []rawType + ) + + for id := TypeID(1); ; id++ { + if err := binary.Read(r, bo, &header); err == io.EOF { + return types, nil + } else if err != nil { + return nil, fmt.Errorf("can't read type info for id %v: %v", id, err) + } + + var data interface{} + switch header.Kind() { + case kindInt: + data = new(uint32) + case kindPointer: + case kindArray: + data = new(btfArray) + case kindStruct: + fallthrough + case kindUnion: + data = make([]btfMember, header.Vlen()) + case kindEnum: + data = make([]btfEnum, header.Vlen()) + case kindForward: + case kindTypedef: + case kindVolatile: + case kindConst: + case kindRestrict: + case kindFunc: + case kindFuncProto: + data = make([]btfParam, header.Vlen()) + case kindVar: + data = new(btfVariable) + case kindDatasec: + data = make([]btfVarSecinfo, header.Vlen()) + default: + return nil, fmt.Errorf("type id %v: unknown kind: %v", id, header.Kind()) + } + + if data == nil { + types = append(types, rawType{header, nil}) + continue + } + + if err := binary.Read(r, bo, data); err != nil { + return nil, fmt.Errorf("type id %d: kind %v: can't read %T: %v", id, header.Kind(), data, err) + } + + types = append(types, rawType{header, data}) + } +} + +func intEncoding(raw uint32) (IntEncoding, uint32, byte) { + return IntEncoding((raw & 0x0f000000) >> 24), (raw & 0x00ff0000) >> 16, byte(raw & 0x000000ff) +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/core.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/core.go new file mode 100644 index 00000000000..52b59ed189f --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/core.go @@ -0,0 +1,388 @@ +package btf + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" +) + +// Code in this file is derived from libbpf, which is available under a BSD +// 2-Clause license. + +// Relocation describes a CO-RE relocation. +type Relocation struct { + Current uint32 + New uint32 +} + +func (r Relocation) equal(other Relocation) bool { + return r.Current == other.Current && r.New == other.New +} + +// coreReloKind is the type of CO-RE relocation +type coreReloKind uint32 + +const ( + reloFieldByteOffset coreReloKind = iota /* field byte offset */ + reloFieldByteSize /* field size in bytes */ + reloFieldExists /* field existence in target kernel */ + reloFieldSigned /* field signedness (0 - unsigned, 1 - signed) */ + reloFieldLShiftU64 /* bitfield-specific left bitshift */ + reloFieldRShiftU64 /* bitfield-specific right bitshift */ + reloTypeIDLocal /* type ID in local BPF object */ + reloTypeIDTarget /* type ID in target kernel */ + reloTypeExists /* type existence in target kernel */ + reloTypeSize /* type size in bytes */ + reloEnumvalExists /* enum value existence in target kernel */ + reloEnumvalValue /* enum value integer value */ +) + +func (k coreReloKind) String() string { + switch k { + case reloFieldByteOffset: + return "byte_off" + case reloFieldByteSize: + return "byte_sz" + case reloFieldExists: + return "field_exists" + case reloFieldSigned: + return "signed" + case reloFieldLShiftU64: + return "lshift_u64" + case reloFieldRShiftU64: + return "rshift_u64" + case reloTypeIDLocal: + return "local_type_id" + case reloTypeIDTarget: + return "target_type_id" + case reloTypeExists: + return "type_exists" + case reloTypeSize: + return "type_size" + case reloEnumvalExists: + return "enumval_exists" + case reloEnumvalValue: + return "enumval_value" + default: + return "unknown" + } +} + +func coreRelocate(local, target *Spec, coreRelos bpfCoreRelos) (map[uint64]Relocation, error) { + if target == nil { + var err error + target, err = loadKernelSpec() + if err != nil { + return nil, err + } + } + + if local.byteOrder != target.byteOrder { + return nil, fmt.Errorf("can't relocate %s against %s", local.byteOrder, target.byteOrder) + } + + relocations := make(map[uint64]Relocation, len(coreRelos)) + for _, relo := range coreRelos { + accessorStr, err := local.strings.Lookup(relo.AccessStrOff) + if err != nil { + return nil, err + } + + accessor, err := parseCoreAccessor(accessorStr) + if err != nil { + return nil, fmt.Errorf("accessor %q: %s", accessorStr, err) + } + + if int(relo.TypeID) >= len(local.types) { + return nil, fmt.Errorf("invalid type id %d", relo.TypeID) + } + + typ := local.types[relo.TypeID] + + if relo.ReloKind == reloTypeIDLocal { + relocations[uint64(relo.InsnOff)] = Relocation{ + uint32(typ.ID()), + uint32(typ.ID()), + } + continue + } + + named, ok := typ.(namedType) + if !ok || named.name() == "" { + return nil, fmt.Errorf("relocate anonymous type %s: %w", typ.String(), ErrNotSupported) + } + + name := essentialName(named.name()) + res, err := coreCalculateRelocation(typ, target.namedTypes[name], relo.ReloKind, accessor) + if err != nil { + return nil, fmt.Errorf("relocate %s: %w", name, err) + } + + relocations[uint64(relo.InsnOff)] = res + } + + return relocations, nil +} + +var errAmbiguousRelocation = errors.New("ambiguous relocation") + +func coreCalculateRelocation(local Type, targets []namedType, kind coreReloKind, localAccessor coreAccessor) (Relocation, error) { + var relos []Relocation + var matches []Type + for _, target := range targets { + switch kind { + case reloTypeIDTarget: + if localAccessor[0] != 0 { + return Relocation{}, fmt.Errorf("%s: unexpected non-zero accessor", kind) + } + + if compat, err := coreAreTypesCompatible(local, target); err != nil { + return Relocation{}, fmt.Errorf("%s: %s", kind, err) + } else if !compat { + continue + } + + relos = append(relos, Relocation{uint32(target.ID()), uint32(target.ID())}) + + default: + return Relocation{}, fmt.Errorf("relocation %s: %w", kind, ErrNotSupported) + } + matches = append(matches, target) + } + + if len(relos) == 0 { + // TODO: Add switch for existence checks like reloEnumvalExists here. + + // TODO: This might have to be poisoned. + return Relocation{}, fmt.Errorf("no relocation found, tried %v", targets) + } + + relo := relos[0] + for _, altRelo := range relos[1:] { + if !altRelo.equal(relo) { + return Relocation{}, fmt.Errorf("multiple types %v match: %w", matches, errAmbiguousRelocation) + } + } + + return relo, nil +} + +/* coreAccessor contains a path through a struct. It contains at least one index. + * + * The interpretation depends on the kind of the relocation. The following is + * taken from struct bpf_core_relo in libbpf_internal.h: + * + * - for field-based relocations, string encodes an accessed field using + * a sequence of field and array indices, separated by colon (:). It's + * conceptually very close to LLVM's getelementptr ([0]) instruction's + * arguments for identifying offset to a field. + * - for type-based relocations, strings is expected to be just "0"; + * - for enum value-based relocations, string contains an index of enum + * value within its enum type; + * + * Example to provide a better feel. + * + * struct sample { + * int a; + * struct { + * int b[10]; + * }; + * }; + * + * struct sample s = ...; + * int x = &s->a; // encoded as "0:0" (a is field #0) + * int y = &s->b[5]; // encoded as "0:1:0:5" (anon struct is field #1, + * // b is field #0 inside anon struct, accessing elem #5) + * int z = &s[10]->b; // encoded as "10:1" (ptr is used as an array) + */ +type coreAccessor []int + +func parseCoreAccessor(accessor string) (coreAccessor, error) { + if accessor == "" { + return nil, fmt.Errorf("empty accessor") + } + + var result coreAccessor + parts := strings.Split(accessor, ":") + for _, part := range parts { + // 31 bits to avoid overflowing int on 32 bit platforms. + index, err := strconv.ParseUint(part, 10, 31) + if err != nil { + return nil, fmt.Errorf("accessor index %q: %s", part, err) + } + + result = append(result, int(index)) + } + + return result, nil +} + +/* The comment below is from bpf_core_types_are_compat in libbpf.c: + * + * Check local and target types for compatibility. This check is used for + * type-based CO-RE relocations and follow slightly different rules than + * field-based relocations. This function assumes that root types were already + * checked for name match. Beyond that initial root-level name check, names + * are completely ignored. Compatibility rules are as follows: + * - any two STRUCTs/UNIONs/FWDs/ENUMs/INTs are considered compatible, but + * kind should match for local and target types (i.e., STRUCT is not + * compatible with UNION); + * - for ENUMs, the size is ignored; + * - for INT, size and signedness are ignored; + * - for ARRAY, dimensionality is ignored, element types are checked for + * compatibility recursively; + * - CONST/VOLATILE/RESTRICT modifiers are ignored; + * - TYPEDEFs/PTRs are compatible if types they pointing to are compatible; + * - FUNC_PROTOs are compatible if they have compatible signature: same + * number of input args and compatible return and argument types. + * These rules are not set in stone and probably will be adjusted as we get + * more experience with using BPF CO-RE relocations. + */ +func coreAreTypesCompatible(localType Type, targetType Type) (bool, error) { + var ( + localTs, targetTs typeDeque + l, t = &localType, &targetType + depth = 0 + ) + + for ; l != nil && t != nil; l, t = localTs.shift(), targetTs.shift() { + if depth >= maxTypeDepth { + return false, errors.New("types are nested too deep") + } + + localType = skipQualifierAndTypedef(*l) + targetType = skipQualifierAndTypedef(*t) + + if reflect.TypeOf(localType) != reflect.TypeOf(targetType) { + return false, nil + } + + switch lv := (localType).(type) { + case *Void, *Struct, *Union, *Enum, *Fwd: + // Nothing to do here + + case *Int: + tv := targetType.(*Int) + if lv.isBitfield() || tv.isBitfield() { + return false, nil + } + + case *Pointer, *Array: + depth++ + localType.walk(&localTs) + targetType.walk(&targetTs) + + case *FuncProto: + tv := targetType.(*FuncProto) + if len(lv.Params) != len(tv.Params) { + return false, nil + } + + depth++ + localType.walk(&localTs) + targetType.walk(&targetTs) + + default: + return false, fmt.Errorf("unsupported type %T", localType) + } + } + + if l != nil { + return false, fmt.Errorf("dangling local type %T", *l) + } + + if t != nil { + return false, fmt.Errorf("dangling target type %T", *t) + } + + return true, nil +} + +/* The comment below is from bpf_core_fields_are_compat in libbpf.c: + * + * Check two types for compatibility for the purpose of field access + * relocation. const/volatile/restrict and typedefs are skipped to ensure we + * are relocating semantically compatible entities: + * - any two STRUCTs/UNIONs are compatible and can be mixed; + * - any two FWDs are compatible, if their names match (modulo flavor suffix); + * - any two PTRs are always compatible; + * - for ENUMs, names should be the same (ignoring flavor suffix) or at + * least one of enums should be anonymous; + * - for ENUMs, check sizes, names are ignored; + * - for INT, size and signedness are ignored; + * - for ARRAY, dimensionality is ignored, element types are checked for + * compatibility recursively; + * - everything else shouldn't be ever a target of relocation. + * These rules are not set in stone and probably will be adjusted as we get + * more experience with using BPF CO-RE relocations. + */ +func coreAreMembersCompatible(localType Type, targetType Type) (bool, error) { + doNamesMatch := func(a, b string) bool { + if a == "" || b == "" { + // allow anonymous and named type to match + return true + } + + return essentialName(a) == essentialName(b) + } + + for depth := 0; depth <= maxTypeDepth; depth++ { + localType = skipQualifierAndTypedef(localType) + targetType = skipQualifierAndTypedef(targetType) + + _, lok := localType.(composite) + _, tok := targetType.(composite) + if lok && tok { + return true, nil + } + + if reflect.TypeOf(localType) != reflect.TypeOf(targetType) { + return false, nil + } + + switch lv := localType.(type) { + case *Pointer: + return true, nil + + case *Enum: + tv := targetType.(*Enum) + return doNamesMatch(lv.name(), tv.name()), nil + + case *Fwd: + tv := targetType.(*Fwd) + return doNamesMatch(lv.name(), tv.name()), nil + + case *Int: + tv := targetType.(*Int) + return !lv.isBitfield() && !tv.isBitfield(), nil + + case *Array: + tv := targetType.(*Array) + + localType = lv.Type + targetType = tv.Type + + default: + return false, fmt.Errorf("unsupported type %T", localType) + } + } + + return false, errors.New("types are nested too deep") +} + +func skipQualifierAndTypedef(typ Type) Type { + result := typ + for depth := 0; depth <= maxTypeDepth; depth++ { + switch v := (result).(type) { + case qualifier: + result = v.qualify() + case *Typedef: + result = v.Type + default: + return result + } + } + return typ +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/doc.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/doc.go new file mode 100644 index 00000000000..ad2576cb23c --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/doc.go @@ -0,0 +1,8 @@ +// Package btf handles data encoded according to the BPF Type Format. +// +// The canonical documentation lives in the Linux kernel repository and is +// available at https://www.kernel.org/doc/html/latest/bpf/btf.html +// +// The API is very much unstable. You should only use this via the main +// ebpf library. +package btf diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/ext_info.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/ext_info.go new file mode 100644 index 00000000000..6a21b6bda5c --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/ext_info.go @@ -0,0 +1,281 @@ +package btf + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" +) + +type btfExtHeader struct { + Magic uint16 + Version uint8 + Flags uint8 + HdrLen uint32 + + FuncInfoOff uint32 + FuncInfoLen uint32 + LineInfoOff uint32 + LineInfoLen uint32 +} + +type btfExtCoreHeader struct { + CoreReloOff uint32 + CoreReloLen uint32 +} + +func parseExtInfos(r io.ReadSeeker, bo binary.ByteOrder, strings stringTable) (funcInfo, lineInfo map[string]extInfo, coreRelos map[string]bpfCoreRelos, err error) { + var header btfExtHeader + var coreHeader btfExtCoreHeader + if err := binary.Read(r, bo, &header); err != nil { + return nil, nil, nil, fmt.Errorf("can't read header: %v", err) + } + + if header.Magic != btfMagic { + return nil, nil, nil, fmt.Errorf("incorrect magic value %v", header.Magic) + } + + if header.Version != 1 { + return nil, nil, nil, fmt.Errorf("unexpected version %v", header.Version) + } + + if header.Flags != 0 { + return nil, nil, nil, fmt.Errorf("unsupported flags %v", header.Flags) + } + + remainder := int64(header.HdrLen) - int64(binary.Size(&header)) + if remainder < 0 { + return nil, nil, nil, errors.New("header is too short") + } + + coreHdrSize := int64(binary.Size(&coreHeader)) + if remainder >= coreHdrSize { + if err := binary.Read(r, bo, &coreHeader); err != nil { + return nil, nil, nil, fmt.Errorf("can't read CO-RE relocation header: %v", err) + } + remainder -= coreHdrSize + } + + // Of course, the .BTF.ext header has different semantics than the + // .BTF ext header. We need to ignore non-null values. + _, err = io.CopyN(ioutil.Discard, r, remainder) + if err != nil { + return nil, nil, nil, fmt.Errorf("header padding: %v", err) + } + + if _, err := r.Seek(int64(header.HdrLen+header.FuncInfoOff), io.SeekStart); err != nil { + return nil, nil, nil, fmt.Errorf("can't seek to function info section: %v", err) + } + + buf := bufio.NewReader(io.LimitReader(r, int64(header.FuncInfoLen))) + funcInfo, err = parseExtInfo(buf, bo, strings) + if err != nil { + return nil, nil, nil, fmt.Errorf("function info: %w", err) + } + + if _, err := r.Seek(int64(header.HdrLen+header.LineInfoOff), io.SeekStart); err != nil { + return nil, nil, nil, fmt.Errorf("can't seek to line info section: %v", err) + } + + buf = bufio.NewReader(io.LimitReader(r, int64(header.LineInfoLen))) + lineInfo, err = parseExtInfo(buf, bo, strings) + if err != nil { + return nil, nil, nil, fmt.Errorf("line info: %w", err) + } + + if coreHeader.CoreReloOff > 0 && coreHeader.CoreReloLen > 0 { + if _, err := r.Seek(int64(header.HdrLen+coreHeader.CoreReloOff), io.SeekStart); err != nil { + return nil, nil, nil, fmt.Errorf("can't seek to CO-RE relocation section: %v", err) + } + + coreRelos, err = parseExtInfoRelos(io.LimitReader(r, int64(coreHeader.CoreReloLen)), bo, strings) + if err != nil { + return nil, nil, nil, fmt.Errorf("CO-RE relocation info: %w", err) + } + } + + return funcInfo, lineInfo, coreRelos, nil +} + +type btfExtInfoSec struct { + SecNameOff uint32 + NumInfo uint32 +} + +type extInfoRecord struct { + InsnOff uint64 + Opaque []byte +} + +type extInfo struct { + recordSize uint32 + records []extInfoRecord +} + +func (ei extInfo) append(other extInfo, offset uint64) (extInfo, error) { + if other.recordSize != ei.recordSize { + return extInfo{}, fmt.Errorf("ext_info record size mismatch, want %d (got %d)", ei.recordSize, other.recordSize) + } + + records := make([]extInfoRecord, 0, len(ei.records)+len(other.records)) + records = append(records, ei.records...) + for _, info := range other.records { + records = append(records, extInfoRecord{ + InsnOff: info.InsnOff + offset, + Opaque: info.Opaque, + }) + } + return extInfo{ei.recordSize, records}, nil +} + +func (ei extInfo) MarshalBinary() ([]byte, error) { + if len(ei.records) == 0 { + return nil, nil + } + + buf := bytes.NewBuffer(make([]byte, 0, int(ei.recordSize)*len(ei.records))) + for _, info := range ei.records { + // The kernel expects offsets in number of raw bpf instructions, + // while the ELF tracks it in bytes. + insnOff := uint32(info.InsnOff / asm.InstructionSize) + if err := binary.Write(buf, internal.NativeEndian, insnOff); err != nil { + return nil, fmt.Errorf("can't write instruction offset: %v", err) + } + + buf.Write(info.Opaque) + } + + return buf.Bytes(), nil +} + +func parseExtInfo(r io.Reader, bo binary.ByteOrder, strings stringTable) (map[string]extInfo, error) { + const maxRecordSize = 256 + + var recordSize uint32 + if err := binary.Read(r, bo, &recordSize); err != nil { + return nil, fmt.Errorf("can't read record size: %v", err) + } + + if recordSize < 4 { + // Need at least insnOff + return nil, errors.New("record size too short") + } + if recordSize > maxRecordSize { + return nil, fmt.Errorf("record size %v exceeds %v", recordSize, maxRecordSize) + } + + result := make(map[string]extInfo) + for { + secName, infoHeader, err := parseExtInfoHeader(r, bo, strings) + if errors.Is(err, io.EOF) { + return result, nil + } + + var records []extInfoRecord + for i := uint32(0); i < infoHeader.NumInfo; i++ { + var byteOff uint32 + if err := binary.Read(r, bo, &byteOff); err != nil { + return nil, fmt.Errorf("section %v: can't read extended info offset: %v", secName, err) + } + + buf := make([]byte, int(recordSize-4)) + if _, err := io.ReadFull(r, buf); err != nil { + return nil, fmt.Errorf("section %v: can't read record: %v", secName, err) + } + + if byteOff%asm.InstructionSize != 0 { + return nil, fmt.Errorf("section %v: offset %v is not aligned with instruction size", secName, byteOff) + } + + records = append(records, extInfoRecord{uint64(byteOff), buf}) + } + + result[secName] = extInfo{ + recordSize, + records, + } + } +} + +// bpfCoreRelo matches `struct bpf_core_relo` from the kernel +type bpfCoreRelo struct { + InsnOff uint32 + TypeID TypeID + AccessStrOff uint32 + ReloKind coreReloKind +} + +type bpfCoreRelos []bpfCoreRelo + +// append two slices of extInfoRelo to each other. The InsnOff of b are adjusted +// by offset. +func (r bpfCoreRelos) append(other bpfCoreRelos, offset uint64) bpfCoreRelos { + result := make([]bpfCoreRelo, 0, len(r)+len(other)) + result = append(result, r...) + for _, relo := range other { + relo.InsnOff += uint32(offset) + result = append(result, relo) + } + return result +} + +var extInfoReloSize = binary.Size(bpfCoreRelo{}) + +func parseExtInfoRelos(r io.Reader, bo binary.ByteOrder, strings stringTable) (map[string]bpfCoreRelos, error) { + var recordSize uint32 + if err := binary.Read(r, bo, &recordSize); err != nil { + return nil, fmt.Errorf("read record size: %v", err) + } + + if recordSize != uint32(extInfoReloSize) { + return nil, fmt.Errorf("expected record size %d, got %d", extInfoReloSize, recordSize) + } + + result := make(map[string]bpfCoreRelos) + for { + secName, infoHeader, err := parseExtInfoHeader(r, bo, strings) + if errors.Is(err, io.EOF) { + return result, nil + } + + var relos []bpfCoreRelo + for i := uint32(0); i < infoHeader.NumInfo; i++ { + var relo bpfCoreRelo + if err := binary.Read(r, bo, &relo); err != nil { + return nil, fmt.Errorf("section %v: read record: %v", secName, err) + } + + if relo.InsnOff%asm.InstructionSize != 0 { + return nil, fmt.Errorf("section %v: offset %v is not aligned with instruction size", secName, relo.InsnOff) + } + + relos = append(relos, relo) + } + + result[secName] = relos + } +} + +func parseExtInfoHeader(r io.Reader, bo binary.ByteOrder, strings stringTable) (string, *btfExtInfoSec, error) { + var infoHeader btfExtInfoSec + if err := binary.Read(r, bo, &infoHeader); err != nil { + return "", nil, fmt.Errorf("read ext info header: %w", err) + } + + secName, err := strings.Lookup(infoHeader.SecNameOff) + if err != nil { + return "", nil, fmt.Errorf("get section name: %w", err) + } + + if infoHeader.NumInfo == 0 { + return "", nil, fmt.Errorf("section %s has zero records", secName) + } + + return secName, &infoHeader, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/fuzz.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/fuzz.go new file mode 100644 index 00000000000..37e043fd378 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/fuzz.go @@ -0,0 +1,49 @@ +// +build gofuzz + +// Use with https://github.com/dvyukov/go-fuzz + +package btf + +import ( + "bytes" + "encoding/binary" + + "github.com/cilium/ebpf/internal" +) + +func FuzzSpec(data []byte) int { + if len(data) < binary.Size(btfHeader{}) { + return -1 + } + + spec, err := loadNakedSpec(bytes.NewReader(data), internal.NativeEndian, nil, nil) + if err != nil { + if spec != nil { + panic("spec is not nil") + } + return 0 + } + if spec == nil { + panic("spec is nil") + } + return 1 +} + +func FuzzExtInfo(data []byte) int { + if len(data) < binary.Size(btfExtHeader{}) { + return -1 + } + + table := stringTable("\x00foo\x00barfoo\x00") + info, err := parseExtInfo(bytes.NewReader(data), internal.NativeEndian, table) + if err != nil { + if info != nil { + panic("info is not nil") + } + return 0 + } + if info == nil { + panic("info is nil") + } + return 1 +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/strings.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/strings.go new file mode 100644 index 00000000000..8782643a043 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/strings.go @@ -0,0 +1,60 @@ +package btf + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" +) + +type stringTable []byte + +func readStringTable(r io.Reader) (stringTable, error) { + contents, err := ioutil.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("can't read string table: %v", err) + } + + if len(contents) < 1 { + return nil, errors.New("string table is empty") + } + + if contents[0] != '\x00' { + return nil, errors.New("first item in string table is non-empty") + } + + if contents[len(contents)-1] != '\x00' { + return nil, errors.New("string table isn't null terminated") + } + + return stringTable(contents), nil +} + +func (st stringTable) Lookup(offset uint32) (string, error) { + if int64(offset) > int64(^uint(0)>>1) { + return "", fmt.Errorf("offset %d overflows int", offset) + } + + pos := int(offset) + if pos >= len(st) { + return "", fmt.Errorf("offset %d is out of bounds", offset) + } + + if pos > 0 && st[pos-1] != '\x00' { + return "", fmt.Errorf("offset %d isn't start of a string", offset) + } + + str := st[pos:] + end := bytes.IndexByte(str, '\x00') + if end == -1 { + return "", fmt.Errorf("offset %d isn't null terminated", offset) + } + + return string(str[:end]), nil +} + +func (st stringTable) LookupName(offset uint32) (Name, error) { + str, err := st.Lookup(offset) + return Name(str), err +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/btf/types.go b/agent/vendor/github.com/cilium/ebpf/internal/btf/types.go new file mode 100644 index 00000000000..9e1fd8d0b2d --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/btf/types.go @@ -0,0 +1,871 @@ +package btf + +import ( + "errors" + "fmt" + "math" + "strings" +) + +const maxTypeDepth = 32 + +// TypeID identifies a type in a BTF section. +type TypeID uint32 + +// ID implements part of the Type interface. +func (tid TypeID) ID() TypeID { + return tid +} + +// Type represents a type described by BTF. +type Type interface { + ID() TypeID + + String() string + + // Make a copy of the type, without copying Type members. + copy() Type + + // Enumerate all nested Types. Repeated calls must visit nested + // types in the same order. + walk(*typeDeque) +} + +// namedType is a type with a name. +// +// Most named types simply embed Name. +type namedType interface { + Type + name() string +} + +// Name identifies a type. +// +// Anonymous types have an empty name. +type Name string + +func (n Name) name() string { + return string(n) +} + +// Void is the unit type of BTF. +type Void struct{} + +func (v *Void) ID() TypeID { return 0 } +func (v *Void) String() string { return "void#0" } +func (v *Void) size() uint32 { return 0 } +func (v *Void) copy() Type { return (*Void)(nil) } +func (v *Void) walk(*typeDeque) {} + +type IntEncoding byte + +const ( + Signed IntEncoding = 1 << iota + Char + Bool +) + +// Int is an integer of a given length. +type Int struct { + TypeID + Name + + // The size of the integer in bytes. + Size uint32 + Encoding IntEncoding + // Offset is the starting bit offset. Currently always 0. + // See https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-kind-int + Offset uint32 + Bits byte +} + +var _ namedType = (*Int)(nil) + +func (i *Int) String() string { + var s strings.Builder + + switch { + case i.Encoding&Char != 0: + s.WriteString("char") + case i.Encoding&Bool != 0: + s.WriteString("bool") + default: + if i.Encoding&Signed == 0 { + s.WriteRune('u') + } + s.WriteString("int") + fmt.Fprintf(&s, "%d", i.Size*8) + } + + fmt.Fprintf(&s, "#%d", i.TypeID) + + if i.Bits > 0 { + fmt.Fprintf(&s, "[bits=%d]", i.Bits) + } + + return s.String() +} + +func (i *Int) size() uint32 { return i.Size } +func (i *Int) walk(*typeDeque) {} +func (i *Int) copy() Type { + cpy := *i + return &cpy +} + +func (i *Int) isBitfield() bool { + return i.Offset > 0 +} + +// Pointer is a pointer to another type. +type Pointer struct { + TypeID + Target Type +} + +func (p *Pointer) String() string { + return fmt.Sprintf("pointer#%d[target=#%d]", p.TypeID, p.Target.ID()) +} + +func (p *Pointer) size() uint32 { return 8 } +func (p *Pointer) walk(tdq *typeDeque) { tdq.push(&p.Target) } +func (p *Pointer) copy() Type { + cpy := *p + return &cpy +} + +// Array is an array with a fixed number of elements. +type Array struct { + TypeID + Type Type + Nelems uint32 +} + +func (arr *Array) String() string { + return fmt.Sprintf("array#%d[type=#%d n=%d]", arr.TypeID, arr.Type.ID(), arr.Nelems) +} + +func (arr *Array) walk(tdq *typeDeque) { tdq.push(&arr.Type) } +func (arr *Array) copy() Type { + cpy := *arr + return &cpy +} + +// Struct is a compound type of consecutive members. +type Struct struct { + TypeID + Name + // The size of the struct including padding, in bytes + Size uint32 + Members []Member +} + +func (s *Struct) String() string { + return fmt.Sprintf("struct#%d[%q]", s.TypeID, s.Name) +} + +func (s *Struct) size() uint32 { return s.Size } + +func (s *Struct) walk(tdq *typeDeque) { + for i := range s.Members { + tdq.push(&s.Members[i].Type) + } +} + +func (s *Struct) copy() Type { + cpy := *s + cpy.Members = make([]Member, len(s.Members)) + copy(cpy.Members, s.Members) + return &cpy +} + +func (s *Struct) members() []Member { + return s.Members +} + +// Union is a compound type where members occupy the same memory. +type Union struct { + TypeID + Name + // The size of the union including padding, in bytes. + Size uint32 + Members []Member +} + +func (u *Union) String() string { + return fmt.Sprintf("union#%d[%q]", u.TypeID, u.Name) +} + +func (u *Union) size() uint32 { return u.Size } + +func (u *Union) walk(tdq *typeDeque) { + for i := range u.Members { + tdq.push(&u.Members[i].Type) + } +} + +func (u *Union) copy() Type { + cpy := *u + cpy.Members = make([]Member, len(u.Members)) + copy(cpy.Members, u.Members) + return &cpy +} + +func (u *Union) members() []Member { + return u.Members +} + +type composite interface { + members() []Member +} + +var ( + _ composite = (*Struct)(nil) + _ composite = (*Union)(nil) +) + +// Member is part of a Struct or Union. +// +// It is not a valid Type. +type Member struct { + Name + Type Type + // Offset is the bit offset of this member + Offset uint32 + BitfieldSize uint32 +} + +// Enum lists possible values. +type Enum struct { + TypeID + Name + Values []EnumValue +} + +func (e *Enum) String() string { + return fmt.Sprintf("enum#%d[%q]", e.TypeID, e.Name) +} + +// EnumValue is part of an Enum +// +// Is is not a valid Type +type EnumValue struct { + Name + Value int32 +} + +func (e *Enum) size() uint32 { return 4 } +func (e *Enum) walk(*typeDeque) {} +func (e *Enum) copy() Type { + cpy := *e + cpy.Values = make([]EnumValue, len(e.Values)) + copy(cpy.Values, e.Values) + return &cpy +} + +// FwdKind is the type of forward declaration. +type FwdKind int + +// Valid types of forward declaration. +const ( + FwdStruct FwdKind = iota + FwdUnion +) + +func (fk FwdKind) String() string { + switch fk { + case FwdStruct: + return "struct" + case FwdUnion: + return "union" + default: + return fmt.Sprintf("%T(%d)", fk, int(fk)) + } +} + +// Fwd is a forward declaration of a Type. +type Fwd struct { + TypeID + Name + Kind FwdKind +} + +func (f *Fwd) String() string { + return fmt.Sprintf("fwd#%d[%s %q]", f.TypeID, f.Kind, f.Name) +} + +func (f *Fwd) walk(*typeDeque) {} +func (f *Fwd) copy() Type { + cpy := *f + return &cpy +} + +// Typedef is an alias of a Type. +type Typedef struct { + TypeID + Name + Type Type +} + +func (td *Typedef) String() string { + return fmt.Sprintf("typedef#%d[%q #%d]", td.TypeID, td.Name, td.Type.ID()) +} + +func (td *Typedef) walk(tdq *typeDeque) { tdq.push(&td.Type) } +func (td *Typedef) copy() Type { + cpy := *td + return &cpy +} + +// Volatile is a qualifier. +type Volatile struct { + TypeID + Type Type +} + +func (v *Volatile) String() string { + return fmt.Sprintf("volatile#%d[#%d]", v.TypeID, v.Type.ID()) +} + +func (v *Volatile) qualify() Type { return v.Type } +func (v *Volatile) walk(tdq *typeDeque) { tdq.push(&v.Type) } +func (v *Volatile) copy() Type { + cpy := *v + return &cpy +} + +// Const is a qualifier. +type Const struct { + TypeID + Type Type +} + +func (c *Const) String() string { + return fmt.Sprintf("const#%d[#%d]", c.TypeID, c.Type.ID()) +} + +func (c *Const) qualify() Type { return c.Type } +func (c *Const) walk(tdq *typeDeque) { tdq.push(&c.Type) } +func (c *Const) copy() Type { + cpy := *c + return &cpy +} + +// Restrict is a qualifier. +type Restrict struct { + TypeID + Type Type +} + +func (r *Restrict) String() string { + return fmt.Sprintf("restrict#%d[#%d]", r.TypeID, r.Type.ID()) +} + +func (r *Restrict) qualify() Type { return r.Type } +func (r *Restrict) walk(tdq *typeDeque) { tdq.push(&r.Type) } +func (r *Restrict) copy() Type { + cpy := *r + return &cpy +} + +// Func is a function definition. +type Func struct { + TypeID + Name + Type Type +} + +func (f *Func) String() string { + return fmt.Sprintf("func#%d[%q proto=#%d]", f.TypeID, f.Name, f.Type.ID()) +} + +func (f *Func) walk(tdq *typeDeque) { tdq.push(&f.Type) } +func (f *Func) copy() Type { + cpy := *f + return &cpy +} + +// FuncProto is a function declaration. +type FuncProto struct { + TypeID + Return Type + Params []FuncParam +} + +func (fp *FuncProto) String() string { + var s strings.Builder + fmt.Fprintf(&s, "proto#%d[", fp.TypeID) + for _, param := range fp.Params { + fmt.Fprintf(&s, "%q=#%d, ", param.Name, param.Type.ID()) + } + fmt.Fprintf(&s, "return=#%d]", fp.Return.ID()) + return s.String() +} + +func (fp *FuncProto) walk(tdq *typeDeque) { + tdq.push(&fp.Return) + for i := range fp.Params { + tdq.push(&fp.Params[i].Type) + } +} + +func (fp *FuncProto) copy() Type { + cpy := *fp + cpy.Params = make([]FuncParam, len(fp.Params)) + copy(cpy.Params, fp.Params) + return &cpy +} + +type FuncParam struct { + Name + Type Type +} + +// Var is a global variable. +type Var struct { + TypeID + Name + Type Type +} + +func (v *Var) String() string { + // TODO: Linkage + return fmt.Sprintf("var#%d[%q]", v.TypeID, v.Name) +} + +func (v *Var) walk(tdq *typeDeque) { tdq.push(&v.Type) } +func (v *Var) copy() Type { + cpy := *v + return &cpy +} + +// Datasec is a global program section containing data. +type Datasec struct { + TypeID + Name + Size uint32 + Vars []VarSecinfo +} + +func (ds *Datasec) String() string { + return fmt.Sprintf("section#%d[%q]", ds.TypeID, ds.Name) +} + +func (ds *Datasec) size() uint32 { return ds.Size } + +func (ds *Datasec) walk(tdq *typeDeque) { + for i := range ds.Vars { + tdq.push(&ds.Vars[i].Type) + } +} + +func (ds *Datasec) copy() Type { + cpy := *ds + cpy.Vars = make([]VarSecinfo, len(ds.Vars)) + copy(cpy.Vars, ds.Vars) + return &cpy +} + +// VarSecinfo describes variable in a Datasec +// +// It is not a valid Type. +type VarSecinfo struct { + Type Type + Offset uint32 + Size uint32 +} + +type sizer interface { + size() uint32 +} + +var ( + _ sizer = (*Int)(nil) + _ sizer = (*Pointer)(nil) + _ sizer = (*Struct)(nil) + _ sizer = (*Union)(nil) + _ sizer = (*Enum)(nil) + _ sizer = (*Datasec)(nil) +) + +type qualifier interface { + qualify() Type +} + +var ( + _ qualifier = (*Const)(nil) + _ qualifier = (*Restrict)(nil) + _ qualifier = (*Volatile)(nil) +) + +// Sizeof returns the size of a type in bytes. +// +// Returns an error if the size can't be computed. +func Sizeof(typ Type) (int, error) { + var ( + n = int64(1) + elem int64 + ) + + for i := 0; i < maxTypeDepth; i++ { + switch v := typ.(type) { + case *Array: + if n > 0 && int64(v.Nelems) > math.MaxInt64/n { + return 0, errors.New("overflow") + } + + // Arrays may be of zero length, which allows + // n to be zero as well. + n *= int64(v.Nelems) + typ = v.Type + continue + + case sizer: + elem = int64(v.size()) + + case *Typedef: + typ = v.Type + continue + + case qualifier: + typ = v.qualify() + continue + + default: + return 0, fmt.Errorf("unrecognized type %T", typ) + } + + if n > 0 && elem > math.MaxInt64/n { + return 0, errors.New("overflow") + } + + size := n * elem + if int64(int(size)) != size { + return 0, errors.New("overflow") + } + + return int(size), nil + } + + return 0, errors.New("exceeded type depth") +} + +// copy a Type recursively. +// +// typ may form a cycle. +func copyType(typ Type) Type { + var ( + copies = make(map[Type]Type) + work typeDeque + ) + + for t := &typ; t != nil; t = work.pop() { + // *t is the identity of the type. + if cpy := copies[*t]; cpy != nil { + *t = cpy + continue + } + + cpy := (*t).copy() + copies[*t] = cpy + *t = cpy + + // Mark any nested types for copying. + cpy.walk(&work) + } + + return typ +} + +// typeDeque keeps track of pointers to types which still +// need to be visited. +type typeDeque struct { + types []*Type + read, write uint64 + mask uint64 +} + +// push adds a type to the stack. +func (dq *typeDeque) push(t *Type) { + if dq.write-dq.read < uint64(len(dq.types)) { + dq.types[dq.write&dq.mask] = t + dq.write++ + return + } + + new := len(dq.types) * 2 + if new == 0 { + new = 8 + } + + types := make([]*Type, new) + pivot := dq.read & dq.mask + n := copy(types, dq.types[pivot:]) + n += copy(types[n:], dq.types[:pivot]) + types[n] = t + + dq.types = types + dq.mask = uint64(new) - 1 + dq.read, dq.write = 0, uint64(n+1) +} + +// shift returns the first element or null. +func (dq *typeDeque) shift() *Type { + if dq.read == dq.write { + return nil + } + + index := dq.read & dq.mask + t := dq.types[index] + dq.types[index] = nil + dq.read++ + return t +} + +// pop returns the last element or null. +func (dq *typeDeque) pop() *Type { + if dq.read == dq.write { + return nil + } + + dq.write-- + index := dq.write & dq.mask + t := dq.types[index] + dq.types[index] = nil + return t +} + +// all returns all elements. +// +// The deque is empty after calling this method. +func (dq *typeDeque) all() []*Type { + length := dq.write - dq.read + types := make([]*Type, 0, length) + for t := dq.shift(); t != nil; t = dq.shift() { + types = append(types, t) + } + return types +} + +// inflateRawTypes takes a list of raw btf types linked via type IDs, and turns +// it into a graph of Types connected via pointers. +// +// Returns a map of named types (so, where NameOff is non-zero) and a slice of types +// indexed by TypeID. Since BTF ignores compilation units, multiple types may share +// the same name. A Type may form a cyclic graph by pointing at itself. +func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type, namedTypes map[string][]namedType, err error) { + type fixupDef struct { + id TypeID + expectedKind btfKind + typ *Type + } + + var fixups []fixupDef + fixup := func(id TypeID, expectedKind btfKind, typ *Type) { + fixups = append(fixups, fixupDef{id, expectedKind, typ}) + } + + convertMembers := func(raw []btfMember, kindFlag bool) ([]Member, error) { + // NB: The fixup below relies on pre-allocating this array to + // work, since otherwise append might re-allocate members. + members := make([]Member, 0, len(raw)) + for i, btfMember := range raw { + name, err := rawStrings.LookupName(btfMember.NameOff) + if err != nil { + return nil, fmt.Errorf("can't get name for member %d: %w", i, err) + } + m := Member{ + Name: name, + Offset: btfMember.Offset, + } + if kindFlag { + m.BitfieldSize = btfMember.Offset >> 24 + m.Offset &= 0xffffff + } + members = append(members, m) + } + for i := range members { + fixup(raw[i].Type, kindUnknown, &members[i].Type) + } + return members, nil + } + + types = make([]Type, 0, len(rawTypes)) + types = append(types, (*Void)(nil)) + namedTypes = make(map[string][]namedType) + + for i, raw := range rawTypes { + var ( + // Void is defined to always be type ID 0, and is thus + // omitted from BTF. + id = TypeID(i + 1) + typ Type + ) + + name, err := rawStrings.LookupName(raw.NameOff) + if err != nil { + return nil, nil, fmt.Errorf("get name for type id %d: %w", id, err) + } + + switch raw.Kind() { + case kindInt: + encoding, offset, bits := intEncoding(*raw.data.(*uint32)) + typ = &Int{id, name, raw.Size(), encoding, offset, bits} + + case kindPointer: + ptr := &Pointer{id, nil} + fixup(raw.Type(), kindUnknown, &ptr.Target) + typ = ptr + + case kindArray: + btfArr := raw.data.(*btfArray) + + // IndexType is unused according to btf.rst. + // Don't make it available right now. + arr := &Array{id, nil, btfArr.Nelems} + fixup(btfArr.Type, kindUnknown, &arr.Type) + typ = arr + + case kindStruct: + members, err := convertMembers(raw.data.([]btfMember), raw.KindFlag()) + if err != nil { + return nil, nil, fmt.Errorf("struct %s (id %d): %w", name, id, err) + } + typ = &Struct{id, name, raw.Size(), members} + + case kindUnion: + members, err := convertMembers(raw.data.([]btfMember), raw.KindFlag()) + if err != nil { + return nil, nil, fmt.Errorf("union %s (id %d): %w", name, id, err) + } + typ = &Union{id, name, raw.Size(), members} + + case kindEnum: + rawvals := raw.data.([]btfEnum) + vals := make([]EnumValue, 0, len(rawvals)) + for i, btfVal := range rawvals { + name, err := rawStrings.LookupName(btfVal.NameOff) + if err != nil { + return nil, nil, fmt.Errorf("get name for enum value %d: %s", i, err) + } + vals = append(vals, EnumValue{ + Name: name, + Value: btfVal.Val, + }) + } + typ = &Enum{id, name, vals} + + case kindForward: + if raw.KindFlag() { + typ = &Fwd{id, name, FwdUnion} + } else { + typ = &Fwd{id, name, FwdStruct} + } + + case kindTypedef: + typedef := &Typedef{id, name, nil} + fixup(raw.Type(), kindUnknown, &typedef.Type) + typ = typedef + + case kindVolatile: + volatile := &Volatile{id, nil} + fixup(raw.Type(), kindUnknown, &volatile.Type) + typ = volatile + + case kindConst: + cnst := &Const{id, nil} + fixup(raw.Type(), kindUnknown, &cnst.Type) + typ = cnst + + case kindRestrict: + restrict := &Restrict{id, nil} + fixup(raw.Type(), kindUnknown, &restrict.Type) + typ = restrict + + case kindFunc: + fn := &Func{id, name, nil} + fixup(raw.Type(), kindFuncProto, &fn.Type) + typ = fn + + case kindFuncProto: + rawparams := raw.data.([]btfParam) + params := make([]FuncParam, 0, len(rawparams)) + for i, param := range rawparams { + name, err := rawStrings.LookupName(param.NameOff) + if err != nil { + return nil, nil, fmt.Errorf("get name for func proto parameter %d: %s", i, err) + } + params = append(params, FuncParam{ + Name: name, + }) + } + for i := range params { + fixup(rawparams[i].Type, kindUnknown, ¶ms[i].Type) + } + + fp := &FuncProto{id, nil, params} + fixup(raw.Type(), kindUnknown, &fp.Return) + typ = fp + + case kindVar: + v := &Var{id, name, nil} + fixup(raw.Type(), kindUnknown, &v.Type) + typ = v + + case kindDatasec: + btfVars := raw.data.([]btfVarSecinfo) + vars := make([]VarSecinfo, 0, len(btfVars)) + for _, btfVar := range btfVars { + vars = append(vars, VarSecinfo{ + Offset: btfVar.Offset, + Size: btfVar.Size, + }) + } + for i := range vars { + fixup(btfVars[i].Type, kindVar, &vars[i].Type) + } + typ = &Datasec{id, name, raw.SizeType, vars} + + default: + return nil, nil, fmt.Errorf("type id %d: unknown kind: %v", id, raw.Kind()) + } + + types = append(types, typ) + + if named, ok := typ.(namedType); ok { + if name := essentialName(named.name()); name != "" { + namedTypes[name] = append(namedTypes[name], named) + } + } + } + + for _, fixup := range fixups { + i := int(fixup.id) + if i >= len(types) { + return nil, nil, fmt.Errorf("reference to invalid type id: %d", fixup.id) + } + + // Default void (id 0) to unknown + rawKind := kindUnknown + if i > 0 { + rawKind = rawTypes[i-1].Kind() + } + + if expected := fixup.expectedKind; expected != kindUnknown && rawKind != expected { + return nil, nil, fmt.Errorf("expected type id %d to have kind %s, found %s", fixup.id, expected, rawKind) + } + + *fixup.typ = types[i] + } + + return types, namedTypes, nil +} + +// essentialName returns name without a ___ suffix. +func essentialName(name string) string { + lastIdx := strings.LastIndex(name, "___") + if lastIdx > 0 { + return name[:lastIdx] + } + return name +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/cpu.go b/agent/vendor/github.com/cilium/ebpf/internal/cpu.go new file mode 100644 index 00000000000..d3424ba4345 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/cpu.go @@ -0,0 +1,62 @@ +package internal + +import ( + "fmt" + "io/ioutil" + "strings" + "sync" +) + +var sysCPU struct { + once sync.Once + err error + num int +} + +// PossibleCPUs returns the max number of CPUs a system may possibly have +// Logical CPU numbers must be of the form 0-n +func PossibleCPUs() (int, error) { + sysCPU.once.Do(func() { + sysCPU.num, sysCPU.err = parseCPUsFromFile("/sys/devices/system/cpu/possible") + }) + + return sysCPU.num, sysCPU.err +} + +func parseCPUsFromFile(path string) (int, error) { + spec, err := ioutil.ReadFile(path) + if err != nil { + return 0, err + } + + n, err := parseCPUs(string(spec)) + if err != nil { + return 0, fmt.Errorf("can't parse %s: %v", path, err) + } + + return n, nil +} + +// parseCPUs parses the number of cpus from a string produced +// by bitmap_list_string() in the Linux kernel. +// Multiple ranges are rejected, since they can't be unified +// into a single number. +// This is the format of /sys/devices/system/cpu/possible, it +// is not suitable for /sys/devices/system/cpu/online, etc. +func parseCPUs(spec string) (int, error) { + if strings.Trim(spec, "\n") == "0" { + return 1, nil + } + + var low, high int + n, err := fmt.Sscanf(spec, "%d-%d\n", &low, &high) + if n != 2 || err != nil { + return 0, fmt.Errorf("invalid format: %s", spec) + } + if low != 0 { + return 0, fmt.Errorf("CPU spec doesn't start at zero: %s", spec) + } + + // cpus is 0 indexed + return high + 1, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/elf.go b/agent/vendor/github.com/cilium/ebpf/internal/elf.go new file mode 100644 index 00000000000..c3f9ea0f8a4 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/elf.go @@ -0,0 +1,52 @@ +package internal + +import ( + "debug/elf" + "fmt" + "io" +) + +type SafeELFFile struct { + *elf.File +} + +// NewSafeELFFile reads an ELF safely. +// +// Any panic during parsing is turned into an error. This is necessary since +// there are a bunch of unfixed bugs in debug/elf. +// +// https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+debug%2Felf+in%3Atitle +func NewSafeELFFile(r io.ReaderAt) (safe *SafeELFFile, err error) { + defer func() { + r := recover() + if r == nil { + return + } + + safe = nil + err = fmt.Errorf("reading ELF file panicked: %s", r) + }() + + file, err := elf.NewFile(r) + if err != nil { + return nil, err + } + + return &SafeELFFile{file}, nil +} + +// Symbols is the safe version of elf.File.Symbols. +func (se *SafeELFFile) Symbols() (syms []elf.Symbol, err error) { + defer func() { + r := recover() + if r == nil { + return + } + + syms = nil + err = fmt.Errorf("reading ELF symbols panicked: %s", r) + }() + + syms, err = se.File.Symbols() + return +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/endian.go b/agent/vendor/github.com/cilium/ebpf/internal/endian.go new file mode 100644 index 00000000000..ac8a94e512a --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/endian.go @@ -0,0 +1,24 @@ +package internal + +import ( + "encoding/binary" + "unsafe" +) + +// NativeEndian is set to either binary.BigEndian or binary.LittleEndian, +// depending on the host's endianness. +var NativeEndian binary.ByteOrder + +func init() { + if isBigEndian() { + NativeEndian = binary.BigEndian + } else { + NativeEndian = binary.LittleEndian + } +} + +func isBigEndian() (ret bool) { + i := int(0x1) + bs := (*[int(unsafe.Sizeof(i))]byte)(unsafe.Pointer(&i)) + return bs[0] == 0 +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/errors.go b/agent/vendor/github.com/cilium/ebpf/internal/errors.go new file mode 100644 index 00000000000..b6aee81f7de --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/errors.go @@ -0,0 +1,47 @@ +package internal + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/cilium/ebpf/internal/unix" +) + +// ErrorWithLog returns an error that includes logs from the +// kernel verifier. +// +// logErr should be the error returned by the syscall that generated +// the log. It is used to check for truncation of the output. +func ErrorWithLog(err error, log []byte, logErr error) error { + logStr := strings.Trim(CString(log), "\t\r\n ") + if errors.Is(logErr, unix.ENOSPC) { + logStr += " (truncated...)" + } + + return &VerifierError{err, logStr} +} + +// VerifierError includes information from the eBPF verifier. +type VerifierError struct { + cause error + log string +} + +func (le *VerifierError) Error() string { + if le.log == "" { + return le.cause.Error() + } + + return fmt.Sprintf("%s: %s", le.cause, le.log) +} + +// CString turns a NUL / zero terminated byte buffer into a string. +func CString(in []byte) string { + inLen := bytes.IndexByte(in, 0) + if inLen == -1 { + return "" + } + return string(in[:inLen]) +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/fd.go b/agent/vendor/github.com/cilium/ebpf/internal/fd.go new file mode 100644 index 00000000000..af04955bd53 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/fd.go @@ -0,0 +1,69 @@ +package internal + +import ( + "errors" + "fmt" + "os" + "runtime" + "strconv" + + "github.com/cilium/ebpf/internal/unix" +) + +var ErrClosedFd = errors.New("use of closed file descriptor") + +type FD struct { + raw int64 +} + +func NewFD(value uint32) *FD { + fd := &FD{int64(value)} + runtime.SetFinalizer(fd, (*FD).Close) + return fd +} + +func (fd *FD) String() string { + return strconv.FormatInt(fd.raw, 10) +} + +func (fd *FD) Value() (uint32, error) { + if fd.raw < 0 { + return 0, ErrClosedFd + } + + return uint32(fd.raw), nil +} + +func (fd *FD) Close() error { + if fd.raw < 0 { + return nil + } + + value := int(fd.raw) + fd.raw = -1 + + fd.Forget() + return unix.Close(value) +} + +func (fd *FD) Forget() { + runtime.SetFinalizer(fd, nil) +} + +func (fd *FD) Dup() (*FD, error) { + if fd.raw < 0 { + return nil, ErrClosedFd + } + + dup, err := unix.FcntlInt(uintptr(fd.raw), unix.F_DUPFD_CLOEXEC, 0) + if err != nil { + return nil, fmt.Errorf("can't dup fd: %v", err) + } + + return NewFD(uint32(dup)), nil +} + +func (fd *FD) File(name string) *os.File { + fd.Forget() + return os.NewFile(uintptr(fd.raw), name) +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/feature.go b/agent/vendor/github.com/cilium/ebpf/internal/feature.go new file mode 100644 index 00000000000..ec62ed39b7d --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/feature.go @@ -0,0 +1,138 @@ +package internal + +import ( + "errors" + "fmt" + "sync" +) + +// ErrNotSupported indicates that a feature is not supported by the current kernel. +var ErrNotSupported = errors.New("not supported") + +// UnsupportedFeatureError is returned by FeatureTest() functions. +type UnsupportedFeatureError struct { + // The minimum Linux mainline version required for this feature. + // Used for the error string, and for sanity checking during testing. + MinimumVersion Version + + // The name of the feature that isn't supported. + Name string +} + +func (ufe *UnsupportedFeatureError) Error() string { + if ufe.MinimumVersion.Unspecified() { + return fmt.Sprintf("%s not supported", ufe.Name) + } + return fmt.Sprintf("%s not supported (requires >= %s)", ufe.Name, ufe.MinimumVersion) +} + +// Is indicates that UnsupportedFeatureError is ErrNotSupported. +func (ufe *UnsupportedFeatureError) Is(target error) bool { + return target == ErrNotSupported +} + +type featureTest struct { + sync.RWMutex + successful bool + result error +} + +// FeatureTestFn is used to determine whether the kernel supports +// a certain feature. +// +// The return values have the following semantics: +// +// err == ErrNotSupported: the feature is not available +// err == nil: the feature is available +// err != nil: the test couldn't be executed +type FeatureTestFn func() error + +// FeatureTest wraps a function so that it is run at most once. +// +// name should identify the tested feature, while version must be in the +// form Major.Minor[.Patch]. +// +// Returns an error wrapping ErrNotSupported if the feature is not supported. +func FeatureTest(name, version string, fn FeatureTestFn) func() error { + v, err := NewVersion(version) + if err != nil { + return func() error { return err } + } + + ft := new(featureTest) + return func() error { + ft.RLock() + if ft.successful { + defer ft.RUnlock() + return ft.result + } + ft.RUnlock() + ft.Lock() + defer ft.Unlock() + // check one more time on the off + // chance that two go routines + // were able to call into the write + // lock + if ft.successful { + return ft.result + } + err := fn() + switch { + case errors.Is(err, ErrNotSupported): + ft.result = &UnsupportedFeatureError{ + MinimumVersion: v, + Name: name, + } + fallthrough + + case err == nil: + ft.successful = true + + default: + // We couldn't execute the feature test to a point + // where it could make a determination. + // Don't cache the result, just return it. + return fmt.Errorf("detect support for %s: %w", name, err) + } + + return ft.result + } +} + +// A Version in the form Major.Minor.Patch. +type Version [3]uint16 + +// NewVersion creates a version from a string like "Major.Minor.Patch". +// +// Patch is optional. +func NewVersion(ver string) (Version, error) { + var major, minor, patch uint16 + n, _ := fmt.Sscanf(ver, "%d.%d.%d", &major, &minor, &patch) + if n < 2 { + return Version{}, fmt.Errorf("invalid version: %s", ver) + } + return Version{major, minor, patch}, nil +} + +func (v Version) String() string { + if v[2] == 0 { + return fmt.Sprintf("v%d.%d", v[0], v[1]) + } + return fmt.Sprintf("v%d.%d.%d", v[0], v[1], v[2]) +} + +// Less returns true if the version is less than another version. +func (v Version) Less(other Version) bool { + for i, a := range v { + if a == other[i] { + continue + } + return a < other[i] + } + return false +} + +// Unspecified returns true if the version is all zero. +func (v Version) Unspecified() bool { + return v[0] == 0 && v[1] == 0 && v[2] == 0 +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/io.go b/agent/vendor/github.com/cilium/ebpf/internal/io.go new file mode 100644 index 00000000000..fa7402782d7 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/io.go @@ -0,0 +1,16 @@ +package internal + +import "errors" + +// DiscardZeroes makes sure that all written bytes are zero +// before discarding them. +type DiscardZeroes struct{} + +func (DiscardZeroes) Write(p []byte) (int, error) { + for _, b := range p { + if b != 0 { + return 0, errors.New("encountered non-zero byte") + } + } + return len(p), nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/ptr.go b/agent/vendor/github.com/cilium/ebpf/internal/ptr.go new file mode 100644 index 00000000000..a7f12b2db4f --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/ptr.go @@ -0,0 +1,30 @@ +package internal + +import "unsafe" + +// NewPointer creates a 64-bit pointer from an unsafe Pointer. +func NewPointer(ptr unsafe.Pointer) Pointer { + return Pointer{ptr: ptr} +} + +// NewSlicePointer creates a 64-bit pointer from a byte slice. +func NewSlicePointer(buf []byte) Pointer { + if len(buf) == 0 { + return Pointer{} + } + + return Pointer{ptr: unsafe.Pointer(&buf[0])} +} + +// NewStringPointer creates a 64-bit pointer from a string. +func NewStringPointer(str string) Pointer { + if str == "" { + return Pointer{} + } + + // The kernel expects strings to be zero terminated + buf := make([]byte, len(str)+1) + copy(buf, str) + + return Pointer{ptr: unsafe.Pointer(&buf[0])} +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_be.go b/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_be.go new file mode 100644 index 00000000000..a56fbcc8e01 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_be.go @@ -0,0 +1,14 @@ +// +build armbe mips mips64p32 + +package internal + +import ( + "unsafe" +) + +// Pointer wraps an unsafe.Pointer to be 64bit to +// conform to the syscall specification. +type Pointer struct { + pad uint32 + ptr unsafe.Pointer +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_le.go b/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_le.go new file mode 100644 index 00000000000..be2ecfca731 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/ptr_32_le.go @@ -0,0 +1,14 @@ +// +build 386 amd64p32 arm mipsle mips64p32le + +package internal + +import ( + "unsafe" +) + +// Pointer wraps an unsafe.Pointer to be 64bit to +// conform to the syscall specification. +type Pointer struct { + ptr unsafe.Pointer + pad uint32 +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/ptr_64.go b/agent/vendor/github.com/cilium/ebpf/internal/ptr_64.go new file mode 100644 index 00000000000..69452dceb9a --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/ptr_64.go @@ -0,0 +1,14 @@ +// +build !386,!amd64p32,!arm,!mipsle,!mips64p32le +// +build !armbe,!mips,!mips64p32 + +package internal + +import ( + "unsafe" +) + +// Pointer wraps an unsafe.Pointer to be 64bit to +// conform to the syscall specification. +type Pointer struct { + ptr unsafe.Pointer +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/syscall.go b/agent/vendor/github.com/cilium/ebpf/internal/syscall.go new file mode 100644 index 00000000000..c808151312a --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/syscall.go @@ -0,0 +1,179 @@ +package internal + +import ( + "fmt" + "path/filepath" + "runtime" + "unsafe" + + "github.com/cilium/ebpf/internal/unix" +) + +//go:generate stringer -output syscall_string.go -type=BPFCmd + +// BPFCmd identifies a subcommand of the bpf syscall. +type BPFCmd int + +// Well known BPF commands. +const ( + BPF_MAP_CREATE BPFCmd = iota + BPF_MAP_LOOKUP_ELEM + BPF_MAP_UPDATE_ELEM + BPF_MAP_DELETE_ELEM + BPF_MAP_GET_NEXT_KEY + BPF_PROG_LOAD + BPF_OBJ_PIN + BPF_OBJ_GET + BPF_PROG_ATTACH + BPF_PROG_DETACH + BPF_PROG_TEST_RUN + BPF_PROG_GET_NEXT_ID + BPF_MAP_GET_NEXT_ID + BPF_PROG_GET_FD_BY_ID + BPF_MAP_GET_FD_BY_ID + BPF_OBJ_GET_INFO_BY_FD + BPF_PROG_QUERY + BPF_RAW_TRACEPOINT_OPEN + BPF_BTF_LOAD + BPF_BTF_GET_FD_BY_ID + BPF_TASK_FD_QUERY + BPF_MAP_LOOKUP_AND_DELETE_ELEM + BPF_MAP_FREEZE + BPF_BTF_GET_NEXT_ID + BPF_MAP_LOOKUP_BATCH + BPF_MAP_LOOKUP_AND_DELETE_BATCH + BPF_MAP_UPDATE_BATCH + BPF_MAP_DELETE_BATCH + BPF_LINK_CREATE + BPF_LINK_UPDATE + BPF_LINK_GET_FD_BY_ID + BPF_LINK_GET_NEXT_ID + BPF_ENABLE_STATS + BPF_ITER_CREATE +) + +// BPF wraps SYS_BPF. +// +// Any pointers contained in attr must use the Pointer type from this package. +func BPF(cmd BPFCmd, attr unsafe.Pointer, size uintptr) (uintptr, error) { + r1, _, errNo := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size) + runtime.KeepAlive(attr) + + var err error + if errNo != 0 { + err = errNo + } + + return r1, err +} + +type BPFProgAttachAttr struct { + TargetFd uint32 + AttachBpfFd uint32 + AttachType uint32 + AttachFlags uint32 + ReplaceBpfFd uint32 +} + +func BPFProgAttach(attr *BPFProgAttachAttr) error { + _, err := BPF(BPF_PROG_ATTACH, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + return err +} + +type BPFProgDetachAttr struct { + TargetFd uint32 + AttachBpfFd uint32 + AttachType uint32 +} + +func BPFProgDetach(attr *BPFProgDetachAttr) error { + _, err := BPF(BPF_PROG_DETACH, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + return err +} + +type BPFEnableStatsAttr struct { + StatsType uint32 +} + +func BPFEnableStats(attr *BPFEnableStatsAttr) (*FD, error) { + ptr, err := BPF(BPF_ENABLE_STATS, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err != nil { + return nil, fmt.Errorf("enable stats: %w", err) + } + return NewFD(uint32(ptr)), nil + +} + +type bpfObjAttr struct { + fileName Pointer + fd uint32 + fileFlags uint32 +} + +const bpfFSType = 0xcafe4a11 + +// BPFObjPin wraps BPF_OBJ_PIN. +func BPFObjPin(fileName string, fd *FD) error { + dirName := filepath.Dir(fileName) + var statfs unix.Statfs_t + if err := unix.Statfs(dirName, &statfs); err != nil { + return err + } + if uint64(statfs.Type) != bpfFSType { + return fmt.Errorf("%s is not on a bpf filesystem", fileName) + } + + value, err := fd.Value() + if err != nil { + return err + } + + attr := bpfObjAttr{ + fileName: NewStringPointer(fileName), + fd: value, + } + _, err = BPF(BPF_OBJ_PIN, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + if err != nil { + return fmt.Errorf("pin object %s: %w", fileName, err) + } + return nil +} + +// BPFObjGet wraps BPF_OBJ_GET. +func BPFObjGet(fileName string) (*FD, error) { + attr := bpfObjAttr{ + fileName: NewStringPointer(fileName), + } + ptr, err := BPF(BPF_OBJ_GET, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + if err != nil { + return nil, fmt.Errorf("get object %s: %w", fileName, err) + } + return NewFD(uint32(ptr)), nil +} + +type bpfObjGetInfoByFDAttr struct { + fd uint32 + infoLen uint32 + info Pointer +} + +// BPFObjGetInfoByFD wraps BPF_OBJ_GET_INFO_BY_FD. +// +// Available from 4.13. +func BPFObjGetInfoByFD(fd *FD, info unsafe.Pointer, size uintptr) error { + value, err := fd.Value() + if err != nil { + return err + } + + attr := bpfObjGetInfoByFDAttr{ + fd: value, + infoLen: uint32(size), + info: NewPointer(info), + } + _, err = BPF(BPF_OBJ_GET_INFO_BY_FD, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + if err != nil { + return fmt.Errorf("fd %v: %w", fd, err) + } + return nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/syscall_string.go b/agent/vendor/github.com/cilium/ebpf/internal/syscall_string.go new file mode 100644 index 00000000000..85df0477973 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/syscall_string.go @@ -0,0 +1,56 @@ +// Code generated by "stringer -output syscall_string.go -type=BPFCmd"; DO NOT EDIT. + +package internal + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[BPF_MAP_CREATE-0] + _ = x[BPF_MAP_LOOKUP_ELEM-1] + _ = x[BPF_MAP_UPDATE_ELEM-2] + _ = x[BPF_MAP_DELETE_ELEM-3] + _ = x[BPF_MAP_GET_NEXT_KEY-4] + _ = x[BPF_PROG_LOAD-5] + _ = x[BPF_OBJ_PIN-6] + _ = x[BPF_OBJ_GET-7] + _ = x[BPF_PROG_ATTACH-8] + _ = x[BPF_PROG_DETACH-9] + _ = x[BPF_PROG_TEST_RUN-10] + _ = x[BPF_PROG_GET_NEXT_ID-11] + _ = x[BPF_MAP_GET_NEXT_ID-12] + _ = x[BPF_PROG_GET_FD_BY_ID-13] + _ = x[BPF_MAP_GET_FD_BY_ID-14] + _ = x[BPF_OBJ_GET_INFO_BY_FD-15] + _ = x[BPF_PROG_QUERY-16] + _ = x[BPF_RAW_TRACEPOINT_OPEN-17] + _ = x[BPF_BTF_LOAD-18] + _ = x[BPF_BTF_GET_FD_BY_ID-19] + _ = x[BPF_TASK_FD_QUERY-20] + _ = x[BPF_MAP_LOOKUP_AND_DELETE_ELEM-21] + _ = x[BPF_MAP_FREEZE-22] + _ = x[BPF_BTF_GET_NEXT_ID-23] + _ = x[BPF_MAP_LOOKUP_BATCH-24] + _ = x[BPF_MAP_LOOKUP_AND_DELETE_BATCH-25] + _ = x[BPF_MAP_UPDATE_BATCH-26] + _ = x[BPF_MAP_DELETE_BATCH-27] + _ = x[BPF_LINK_CREATE-28] + _ = x[BPF_LINK_UPDATE-29] + _ = x[BPF_LINK_GET_FD_BY_ID-30] + _ = x[BPF_LINK_GET_NEXT_ID-31] + _ = x[BPF_ENABLE_STATS-32] + _ = x[BPF_ITER_CREATE-33] +} + +const _BPFCmd_name = "BPF_MAP_CREATEBPF_MAP_LOOKUP_ELEMBPF_MAP_UPDATE_ELEMBPF_MAP_DELETE_ELEMBPF_MAP_GET_NEXT_KEYBPF_PROG_LOADBPF_OBJ_PINBPF_OBJ_GETBPF_PROG_ATTACHBPF_PROG_DETACHBPF_PROG_TEST_RUNBPF_PROG_GET_NEXT_IDBPF_MAP_GET_NEXT_IDBPF_PROG_GET_FD_BY_IDBPF_MAP_GET_FD_BY_IDBPF_OBJ_GET_INFO_BY_FDBPF_PROG_QUERYBPF_RAW_TRACEPOINT_OPENBPF_BTF_LOADBPF_BTF_GET_FD_BY_IDBPF_TASK_FD_QUERYBPF_MAP_LOOKUP_AND_DELETE_ELEMBPF_MAP_FREEZEBPF_BTF_GET_NEXT_IDBPF_MAP_LOOKUP_BATCHBPF_MAP_LOOKUP_AND_DELETE_BATCHBPF_MAP_UPDATE_BATCHBPF_MAP_DELETE_BATCHBPF_LINK_CREATEBPF_LINK_UPDATEBPF_LINK_GET_FD_BY_IDBPF_LINK_GET_NEXT_IDBPF_ENABLE_STATSBPF_ITER_CREATE" + +var _BPFCmd_index = [...]uint16{0, 14, 33, 52, 71, 91, 104, 115, 126, 141, 156, 173, 193, 212, 233, 253, 275, 289, 312, 324, 344, 361, 391, 405, 424, 444, 475, 495, 515, 530, 545, 566, 586, 602, 617} + +func (i BPFCmd) String() string { + if i < 0 || i >= BPFCmd(len(_BPFCmd_index)-1) { + return "BPFCmd(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _BPFCmd_name[_BPFCmd_index[i]:_BPFCmd_index[i+1]] +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/unix/types_linux.go b/agent/vendor/github.com/cilium/ebpf/internal/unix/types_linux.go new file mode 100644 index 00000000000..86d2a10f968 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/unix/types_linux.go @@ -0,0 +1,170 @@ +// +build linux + +package unix + +import ( + "bytes" + "syscall" + + linux "golang.org/x/sys/unix" +) + +const ( + ENOENT = linux.ENOENT + EEXIST = linux.EEXIST + EAGAIN = linux.EAGAIN + ENOSPC = linux.ENOSPC + EINVAL = linux.EINVAL + EPOLLIN = linux.EPOLLIN + EINTR = linux.EINTR + EPERM = linux.EPERM + ESRCH = linux.ESRCH + ENODEV = linux.ENODEV + // ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP + ENOTSUPP = syscall.Errno(0x20c) + + EBADF = linux.EBADF + BPF_F_NO_PREALLOC = linux.BPF_F_NO_PREALLOC + BPF_F_NUMA_NODE = linux.BPF_F_NUMA_NODE + BPF_F_RDONLY_PROG = linux.BPF_F_RDONLY_PROG + BPF_F_WRONLY_PROG = linux.BPF_F_WRONLY_PROG + BPF_OBJ_NAME_LEN = linux.BPF_OBJ_NAME_LEN + BPF_TAG_SIZE = linux.BPF_TAG_SIZE + SYS_BPF = linux.SYS_BPF + F_DUPFD_CLOEXEC = linux.F_DUPFD_CLOEXEC + EPOLL_CTL_ADD = linux.EPOLL_CTL_ADD + EPOLL_CLOEXEC = linux.EPOLL_CLOEXEC + O_CLOEXEC = linux.O_CLOEXEC + O_NONBLOCK = linux.O_NONBLOCK + PROT_READ = linux.PROT_READ + PROT_WRITE = linux.PROT_WRITE + MAP_SHARED = linux.MAP_SHARED + PERF_TYPE_SOFTWARE = linux.PERF_TYPE_SOFTWARE + PERF_COUNT_SW_BPF_OUTPUT = linux.PERF_COUNT_SW_BPF_OUTPUT + PerfBitWatermark = linux.PerfBitWatermark + PERF_SAMPLE_RAW = linux.PERF_SAMPLE_RAW + PERF_FLAG_FD_CLOEXEC = linux.PERF_FLAG_FD_CLOEXEC + RLIM_INFINITY = linux.RLIM_INFINITY + RLIMIT_MEMLOCK = linux.RLIMIT_MEMLOCK + BPF_STATS_RUN_TIME = linux.BPF_STATS_RUN_TIME +) + +// Statfs_t is a wrapper +type Statfs_t = linux.Statfs_t + +// Rlimit is a wrapper +type Rlimit = linux.Rlimit + +// Setrlimit is a wrapper +func Setrlimit(resource int, rlim *Rlimit) (err error) { + return linux.Setrlimit(resource, rlim) +} + +// Syscall is a wrapper +func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) { + return linux.Syscall(trap, a1, a2, a3) +} + +// FcntlInt is a wrapper +func FcntlInt(fd uintptr, cmd, arg int) (int, error) { + return linux.FcntlInt(fd, cmd, arg) +} + +// Statfs is a wrapper +func Statfs(path string, buf *Statfs_t) (err error) { + return linux.Statfs(path, buf) +} + +// Close is a wrapper +func Close(fd int) (err error) { + return linux.Close(fd) +} + +// EpollEvent is a wrapper +type EpollEvent = linux.EpollEvent + +// EpollWait is a wrapper +func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) { + return linux.EpollWait(epfd, events, msec) +} + +// EpollCtl is a wrapper +func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) { + return linux.EpollCtl(epfd, op, fd, event) +} + +// Eventfd is a wrapper +func Eventfd(initval uint, flags int) (fd int, err error) { + return linux.Eventfd(initval, flags) +} + +// Write is a wrapper +func Write(fd int, p []byte) (n int, err error) { + return linux.Write(fd, p) +} + +// EpollCreate1 is a wrapper +func EpollCreate1(flag int) (fd int, err error) { + return linux.EpollCreate1(flag) +} + +// PerfEventMmapPage is a wrapper +type PerfEventMmapPage linux.PerfEventMmapPage + +// SetNonblock is a wrapper +func SetNonblock(fd int, nonblocking bool) (err error) { + return linux.SetNonblock(fd, nonblocking) +} + +// Mmap is a wrapper +func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) { + return linux.Mmap(fd, offset, length, prot, flags) +} + +// Munmap is a wrapper +func Munmap(b []byte) (err error) { + return linux.Munmap(b) +} + +// PerfEventAttr is a wrapper +type PerfEventAttr = linux.PerfEventAttr + +// PerfEventOpen is a wrapper +func PerfEventOpen(attr *PerfEventAttr, pid int, cpu int, groupFd int, flags int) (fd int, err error) { + return linux.PerfEventOpen(attr, pid, cpu, groupFd, flags) +} + +// Utsname is a wrapper +type Utsname = linux.Utsname + +// Uname is a wrapper +func Uname(buf *Utsname) (err error) { + return linux.Uname(buf) +} + +// Getpid is a wrapper +func Getpid() int { + return linux.Getpid() +} + +// Gettid is a wrapper +func Gettid() int { + return linux.Gettid() +} + +// Tgkill is a wrapper +func Tgkill(tgid int, tid int, sig syscall.Signal) (err error) { + return linux.Tgkill(tgid, tid, sig) +} + +func KernelRelease() (string, error) { + var uname Utsname + err := Uname(&uname) + if err != nil { + return "", err + } + + end := bytes.IndexByte(uname.Release[:], 0) + release := string(uname.Release[:end]) + return release, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/internal/unix/types_other.go b/agent/vendor/github.com/cilium/ebpf/internal/unix/types_other.go new file mode 100644 index 00000000000..8c291796a9b --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/internal/unix/types_other.go @@ -0,0 +1,228 @@ +// +build !linux + +package unix + +import ( + "fmt" + "runtime" + "syscall" +) + +var errNonLinux = fmt.Errorf("unsupported platform %s/%s", runtime.GOOS, runtime.GOARCH) + +const ( + ENOENT = syscall.ENOENT + EEXIST = syscall.EEXIST + EAGAIN = syscall.EAGAIN + ENOSPC = syscall.ENOSPC + EINVAL = syscall.EINVAL + EINTR = syscall.EINTR + EPERM = syscall.EPERM + ESRCH = syscall.ESRCH + ENODEV = syscall.ENODEV + EBADF = syscall.Errno(0) + // ENOTSUPP is not the same as ENOTSUP or EOPNOTSUP + ENOTSUPP = syscall.Errno(0x20c) + + BPF_F_NO_PREALLOC = 0 + BPF_F_NUMA_NODE = 0 + BPF_F_RDONLY_PROG = 0 + BPF_F_WRONLY_PROG = 0 + BPF_OBJ_NAME_LEN = 0x10 + BPF_TAG_SIZE = 0x8 + SYS_BPF = 321 + F_DUPFD_CLOEXEC = 0x406 + EPOLLIN = 0x1 + EPOLL_CTL_ADD = 0x1 + EPOLL_CLOEXEC = 0x80000 + O_CLOEXEC = 0x80000 + O_NONBLOCK = 0x800 + PROT_READ = 0x1 + PROT_WRITE = 0x2 + MAP_SHARED = 0x1 + PERF_TYPE_SOFTWARE = 0x1 + PERF_COUNT_SW_BPF_OUTPUT = 0xa + PerfBitWatermark = 0x4000 + PERF_SAMPLE_RAW = 0x400 + PERF_FLAG_FD_CLOEXEC = 0x8 + RLIM_INFINITY = 0x7fffffffffffffff + RLIMIT_MEMLOCK = 8 + BPF_STATS_RUN_TIME = 0 +) + +// Statfs_t is a wrapper +type Statfs_t struct { + Type int64 + Bsize int64 + Blocks uint64 + Bfree uint64 + Bavail uint64 + Files uint64 + Ffree uint64 + Fsid [2]int32 + Namelen int64 + Frsize int64 + Flags int64 + Spare [4]int64 +} + +// Rlimit is a wrapper +type Rlimit struct { + Cur uint64 + Max uint64 +} + +// Setrlimit is a wrapper +func Setrlimit(resource int, rlim *Rlimit) (err error) { + return errNonLinux +} + +// Syscall is a wrapper +func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) { + return 0, 0, syscall.Errno(1) +} + +// FcntlInt is a wrapper +func FcntlInt(fd uintptr, cmd, arg int) (int, error) { + return -1, errNonLinux +} + +// Statfs is a wrapper +func Statfs(path string, buf *Statfs_t) error { + return errNonLinux +} + +// Close is a wrapper +func Close(fd int) (err error) { + return errNonLinux +} + +// EpollEvent is a wrapper +type EpollEvent struct { + Events uint32 + Fd int32 + Pad int32 +} + +// EpollWait is a wrapper +func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) { + return 0, errNonLinux +} + +// EpollCtl is a wrapper +func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) { + return errNonLinux +} + +// Eventfd is a wrapper +func Eventfd(initval uint, flags int) (fd int, err error) { + return 0, errNonLinux +} + +// Write is a wrapper +func Write(fd int, p []byte) (n int, err error) { + return 0, errNonLinux +} + +// EpollCreate1 is a wrapper +func EpollCreate1(flag int) (fd int, err error) { + return 0, errNonLinux +} + +// PerfEventMmapPage is a wrapper +type PerfEventMmapPage struct { + Version uint32 + Compat_version uint32 + Lock uint32 + Index uint32 + Offset int64 + Time_enabled uint64 + Time_running uint64 + Capabilities uint64 + Pmc_width uint16 + Time_shift uint16 + Time_mult uint32 + Time_offset uint64 + Time_zero uint64 + Size uint32 + + Data_head uint64 + Data_tail uint64 + Data_offset uint64 + Data_size uint64 + Aux_head uint64 + Aux_tail uint64 + Aux_offset uint64 + Aux_size uint64 +} + +// SetNonblock is a wrapper +func SetNonblock(fd int, nonblocking bool) (err error) { + return errNonLinux +} + +// Mmap is a wrapper +func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) { + return []byte{}, errNonLinux +} + +// Munmap is a wrapper +func Munmap(b []byte) (err error) { + return errNonLinux +} + +// PerfEventAttr is a wrapper +type PerfEventAttr struct { + Type uint32 + Size uint32 + Config uint64 + Sample uint64 + Sample_type uint64 + Read_format uint64 + Bits uint64 + Wakeup uint32 + Bp_type uint32 + Ext1 uint64 + Ext2 uint64 + Branch_sample_type uint64 + Sample_regs_user uint64 + Sample_stack_user uint32 + Clockid int32 + Sample_regs_intr uint64 + Aux_watermark uint32 + Sample_max_stack uint16 +} + +// PerfEventOpen is a wrapper +func PerfEventOpen(attr *PerfEventAttr, pid int, cpu int, groupFd int, flags int) (fd int, err error) { + return 0, errNonLinux +} + +// Utsname is a wrapper +type Utsname struct { + Release [65]byte +} + +// Uname is a wrapper +func Uname(buf *Utsname) (err error) { + return errNonLinux +} + +// Getpid is a wrapper +func Getpid() int { + return -1 +} + +// Gettid is a wrapper +func Gettid() int { + return -1 +} + +// Tgkill is a wrapper +func Tgkill(tgid int, tid int, sig syscall.Signal) (err error) { + return errNonLinux +} + +func KernelRelease() (string, error) { + return "", errNonLinux +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/cgroup.go b/agent/vendor/github.com/cilium/ebpf/link/cgroup.go new file mode 100644 index 00000000000..16a94393036 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/cgroup.go @@ -0,0 +1,169 @@ +package link + +import ( + "errors" + "fmt" + "os" + + "github.com/cilium/ebpf" +) + +type cgroupAttachFlags uint32 + +// cgroup attach flags +const ( + flagAllowOverride cgroupAttachFlags = 1 << iota + flagAllowMulti + flagReplace +) + +type CgroupOptions struct { + // Path to a cgroupv2 folder. + Path string + // One of the AttachCgroup* constants + Attach ebpf.AttachType + // Program must be of type CGroup*, and the attach type must match Attach. + Program *ebpf.Program +} + +// AttachCgroup links a BPF program to a cgroup. +func AttachCgroup(opts CgroupOptions) (Link, error) { + cgroup, err := os.Open(opts.Path) + if err != nil { + return nil, fmt.Errorf("can't open cgroup: %s", err) + } + + clone, err := opts.Program.Clone() + if err != nil { + cgroup.Close() + return nil, err + } + + var cg Link + cg, err = newLinkCgroup(cgroup, opts.Attach, clone) + if errors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowMulti) + } + if errors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowOverride) + } + if err != nil { + cgroup.Close() + clone.Close() + return nil, err + } + + return cg, nil +} + +// LoadPinnedCgroup loads a pinned cgroup from a bpffs. +func LoadPinnedCgroup(fileName string) (Link, error) { + link, err := LoadPinnedRawLink(fileName) + if err != nil { + return nil, err + } + + return &linkCgroup{link}, nil +} + +type progAttachCgroup struct { + cgroup *os.File + current *ebpf.Program + attachType ebpf.AttachType + flags cgroupAttachFlags +} + +var _ Link = (*progAttachCgroup)(nil) + +func (cg *progAttachCgroup) isLink() {} + +func newProgAttachCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program, flags cgroupAttachFlags) (*progAttachCgroup, error) { + if flags&flagAllowMulti > 0 { + if err := haveProgAttachReplace(); err != nil { + return nil, fmt.Errorf("can't support multiple programs: %w", err) + } + } + + err := RawAttachProgram(RawAttachProgramOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Flags: uint32(flags), + Attach: attach, + }) + if err != nil { + return nil, fmt.Errorf("cgroup: %w", err) + } + + return &progAttachCgroup{cgroup, prog, attach, flags}, nil +} + +func (cg *progAttachCgroup) Close() error { + defer cg.cgroup.Close() + defer cg.current.Close() + + err := RawDetachProgram(RawDetachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: cg.current, + Attach: cg.attachType, + }) + if err != nil { + return fmt.Errorf("close cgroup: %s", err) + } + return nil +} + +func (cg *progAttachCgroup) Update(prog *ebpf.Program) error { + new, err := prog.Clone() + if err != nil { + return err + } + + args := RawAttachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: prog, + Attach: cg.attachType, + Flags: uint32(cg.flags), + } + + if cg.flags&flagAllowMulti > 0 { + // Atomically replacing multiple programs requires at least + // 5.5 (commit 7dd68b3279f17921 "bpf: Support replacing cgroup-bpf + // program in MULTI mode") + args.Flags |= uint32(flagReplace) + args.Replace = cg.current + } + + if err := RawAttachProgram(args); err != nil { + new.Close() + return fmt.Errorf("can't update cgroup: %s", err) + } + + cg.current.Close() + cg.current = new + return nil +} + +func (cg *progAttachCgroup) Pin(string) error { + return fmt.Errorf("can't pin cgroup: %w", ErrNotSupported) +} + +type linkCgroup struct { + *RawLink +} + +var _ Link = (*linkCgroup)(nil) + +func (cg *linkCgroup) isLink() {} + +func newLinkCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program) (*linkCgroup, error) { + link, err := AttachRawLink(RawLinkOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Attach: attach, + }) + if err != nil { + return nil, err + } + + return &linkCgroup{link}, err +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/doc.go b/agent/vendor/github.com/cilium/ebpf/link/doc.go new file mode 100644 index 00000000000..2bde35ed7a2 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/doc.go @@ -0,0 +1,2 @@ +// Package link allows attaching eBPF programs to various kernel hooks. +package link diff --git a/agent/vendor/github.com/cilium/ebpf/link/iter.go b/agent/vendor/github.com/cilium/ebpf/link/iter.go new file mode 100644 index 00000000000..2b5f2846a98 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/iter.go @@ -0,0 +1,91 @@ +package link + +import ( + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type IterOptions struct { + // Program must be of type Tracing with attach type + // AttachTraceIter. The kind of iterator to attach to is + // determined at load time via the AttachTo field. + // + // AttachTo requires the kernel to include BTF of itself, + // and it to be compiled with a recent pahole (>= 1.16). + Program *ebpf.Program +} + +// AttachIter attaches a BPF seq_file iterator. +func AttachIter(opts IterOptions) (*Iter, error) { + link, err := AttachRawLink(RawLinkOptions{ + Program: opts.Program, + Attach: ebpf.AttachTraceIter, + }) + if err != nil { + return nil, fmt.Errorf("can't link iterator: %w", err) + } + + return &Iter{link}, err +} + +// LoadPinnedIter loads a pinned iterator from a bpffs. +func LoadPinnedIter(fileName string) (*Iter, error) { + link, err := LoadPinnedRawLink(fileName) + if err != nil { + return nil, err + } + + return &Iter{link}, err +} + +// Iter represents an attached bpf_iter. +type Iter struct { + link *RawLink +} + +var _ Link = (*Iter)(nil) + +func (it *Iter) isLink() {} + +// FD returns the underlying file descriptor. +func (it *Iter) FD() int { + return it.link.FD() +} + +// Close implements Link. +func (it *Iter) Close() error { + return it.link.Close() +} + +// Pin implements Link. +func (it *Iter) Pin(fileName string) error { + return it.link.Pin(fileName) +} + +// Update implements Link. +func (it *Iter) Update(new *ebpf.Program) error { + return it.link.Update(new) +} + +// Open creates a new instance of the iterator. +// +// Reading from the returned reader triggers the BPF program. +func (it *Iter) Open() (io.ReadCloser, error) { + linkFd, err := it.link.fd.Value() + if err != nil { + return nil, err + } + + attr := &bpfIterCreateAttr{ + linkFd: linkFd, + } + + fd, err := bpfIterCreate(attr) + if err != nil { + return nil, fmt.Errorf("can't create iterator: %w", err) + } + + return fd.File("bpf_iter"), nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/link.go b/agent/vendor/github.com/cilium/ebpf/link/link.go new file mode 100644 index 00000000000..48f1a552975 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/link.go @@ -0,0 +1,214 @@ +package link + +import ( + "fmt" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal" +) + +var ErrNotSupported = internal.ErrNotSupported + +// Link represents a Program attached to a BPF hook. +type Link interface { + // Replace the current program with a new program. + // + // Passing a nil program is an error. May return an error wrapping ErrNotSupported. + Update(*ebpf.Program) error + + // Persist a link by pinning it into a bpffs. + // + // May return an error wrapping ErrNotSupported. + Pin(string) error + + // Close frees resources. + // + // The link will be broken unless it has been pinned. A link + // may continue past the lifetime of the process if Close is + // not called. + Close() error + + // Prevent external users from implementing this interface. + isLink() +} + +// ID uniquely identifies a BPF link. +type ID uint32 + +// RawLinkOptions control the creation of a raw link. +type RawLinkOptions struct { + // File descriptor to attach to. This differs for each attach type. + Target int + // Program to attach. + Program *ebpf.Program + // Attach must match the attach type of Program. + Attach ebpf.AttachType +} + +// RawLinkInfo contains metadata on a link. +type RawLinkInfo struct { + Type Type + ID ID + Program ebpf.ProgramID +} + +// RawLink is the low-level API to bpf_link. +// +// You should consider using the higher level interfaces in this +// package instead. +type RawLink struct { + fd *internal.FD +} + +// AttachRawLink creates a raw link. +func AttachRawLink(opts RawLinkOptions) (*RawLink, error) { + if err := haveBPFLink(); err != nil { + return nil, err + } + + if opts.Target < 0 { + return nil, fmt.Errorf("invalid target: %s", internal.ErrClosedFd) + } + + progFd := opts.Program.FD() + if progFd < 0 { + return nil, fmt.Errorf("invalid program: %s", internal.ErrClosedFd) + } + + attr := bpfLinkCreateAttr{ + targetFd: uint32(opts.Target), + progFd: uint32(progFd), + attachType: opts.Attach, + } + fd, err := bpfLinkCreate(&attr) + if err != nil { + return nil, fmt.Errorf("can't create link: %s", err) + } + + return &RawLink{fd}, nil +} + +// LoadPinnedRawLink loads a persisted link from a bpffs. +func LoadPinnedRawLink(fileName string) (*RawLink, error) { + return loadPinnedRawLink(fileName, UnspecifiedType) +} + +func loadPinnedRawLink(fileName string, typ Type) (*RawLink, error) { + fd, err := internal.BPFObjGet(fileName) + if err != nil { + return nil, fmt.Errorf("load pinned link: %s", err) + } + + link := &RawLink{fd} + if typ == UnspecifiedType { + return link, nil + } + + info, err := link.Info() + if err != nil { + link.Close() + return nil, fmt.Errorf("get pinned link info: %s", err) + } + + if info.Type != typ { + link.Close() + return nil, fmt.Errorf("link type %v doesn't match %v", info.Type, typ) + } + + return link, nil +} + +func (l *RawLink) isLink() {} + +// FD returns the raw file descriptor. +func (l *RawLink) FD() int { + fd, err := l.fd.Value() + if err != nil { + return -1 + } + return int(fd) +} + +// Close breaks the link. +// +// Use Pin if you want to make the link persistent. +func (l *RawLink) Close() error { + return l.fd.Close() +} + +// Pin persists a link past the lifetime of the process. +// +// Calling Close on a pinned Link will not break the link +// until the pin is removed. +func (l *RawLink) Pin(fileName string) error { + if err := internal.BPFObjPin(fileName, l.fd); err != nil { + return fmt.Errorf("can't pin link: %s", err) + } + return nil +} + +// Update implements Link. +func (l *RawLink) Update(new *ebpf.Program) error { + return l.UpdateArgs(RawLinkUpdateOptions{ + New: new, + }) +} + +// RawLinkUpdateOptions control the behaviour of RawLink.UpdateArgs. +type RawLinkUpdateOptions struct { + New *ebpf.Program + Old *ebpf.Program + Flags uint32 +} + +// UpdateArgs updates a link based on args. +func (l *RawLink) UpdateArgs(opts RawLinkUpdateOptions) error { + newFd := opts.New.FD() + if newFd < 0 { + return fmt.Errorf("invalid program: %s", internal.ErrClosedFd) + } + + var oldFd int + if opts.Old != nil { + oldFd = opts.Old.FD() + if oldFd < 0 { + return fmt.Errorf("invalid replacement program: %s", internal.ErrClosedFd) + } + } + + linkFd, err := l.fd.Value() + if err != nil { + return fmt.Errorf("can't update link: %s", err) + } + + attr := bpfLinkUpdateAttr{ + linkFd: linkFd, + newProgFd: uint32(newFd), + oldProgFd: uint32(oldFd), + flags: opts.Flags, + } + return bpfLinkUpdate(&attr) +} + +// struct bpf_link_info +type bpfLinkInfo struct { + typ uint32 + id uint32 + prog_id uint32 +} + +// Info returns metadata about the link. +func (l *RawLink) Info() (*RawLinkInfo, error) { + var info bpfLinkInfo + err := internal.BPFObjGetInfoByFD(l.fd, unsafe.Pointer(&info), unsafe.Sizeof(info)) + if err != nil { + return nil, fmt.Errorf("link info: %s", err) + } + + return &RawLinkInfo{ + Type(info.typ), + ID(info.id), + ebpf.ProgramID(info.prog_id), + }, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/netns.go b/agent/vendor/github.com/cilium/ebpf/link/netns.go new file mode 100644 index 00000000000..3533ff0fa61 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/netns.go @@ -0,0 +1,60 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" +) + +// NetNsInfo contains metadata about a network namespace link. +type NetNsInfo struct { + RawLinkInfo +} + +// NetNsLink is a program attached to a network namespace. +type NetNsLink struct { + *RawLink +} + +// AttachNetNs attaches a program to a network namespace. +func AttachNetNs(ns int, prog *ebpf.Program) (*NetNsLink, error) { + var attach ebpf.AttachType + switch t := prog.Type(); t { + case ebpf.FlowDissector: + attach = ebpf.AttachFlowDissector + case ebpf.SkLookup: + attach = ebpf.AttachSkLookup + default: + return nil, fmt.Errorf("can't attach %v to network namespace", t) + } + + link, err := AttachRawLink(RawLinkOptions{ + Target: ns, + Program: prog, + Attach: attach, + }) + if err != nil { + return nil, err + } + + return &NetNsLink{link}, nil +} + +// LoadPinnedNetNs loads a network namespace link from bpffs. +func LoadPinnedNetNs(fileName string) (*NetNsLink, error) { + link, err := loadPinnedRawLink(fileName, NetNsType) + if err != nil { + return nil, err + } + + return &NetNsLink{link}, nil +} + +// Info returns information about the link. +func (nns *NetNsLink) Info() (*NetNsInfo, error) { + info, err := nns.RawLink.Info() + if err != nil { + return nil, err + } + return &NetNsInfo{*info}, nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/program.go b/agent/vendor/github.com/cilium/ebpf/link/program.go new file mode 100644 index 00000000000..0fe9d37c4f8 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/program.go @@ -0,0 +1,76 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal" +) + +type RawAttachProgramOptions struct { + // File descriptor to attach to. This differs for each attach type. + Target int + // Program to attach. + Program *ebpf.Program + // Program to replace (cgroups). + Replace *ebpf.Program + // Attach must match the attach type of Program (and Replace). + Attach ebpf.AttachType + // Flags control the attach behaviour. This differs for each attach type. + Flags uint32 +} + +// RawAttachProgram is a low level wrapper around BPF_PROG_ATTACH. +// +// You should use one of the higher level abstractions available in this +// package if possible. +func RawAttachProgram(opts RawAttachProgramOptions) error { + if err := haveProgAttach(); err != nil { + return err + } + + var replaceFd uint32 + if opts.Replace != nil { + replaceFd = uint32(opts.Replace.FD()) + } + + attr := internal.BPFProgAttachAttr{ + TargetFd: uint32(opts.Target), + AttachBpfFd: uint32(opts.Program.FD()), + ReplaceBpfFd: replaceFd, + AttachType: uint32(opts.Attach), + AttachFlags: uint32(opts.Flags), + } + + if err := internal.BPFProgAttach(&attr); err != nil { + return fmt.Errorf("can't attach program: %s", err) + } + return nil +} + +type RawDetachProgramOptions struct { + Target int + Program *ebpf.Program + Attach ebpf.AttachType +} + +// RawDetachProgram is a low level wrapper around BPF_PROG_DETACH. +// +// You should use one of the higher level abstractions available in this +// package if possible. +func RawDetachProgram(opts RawDetachProgramOptions) error { + if err := haveProgAttach(); err != nil { + return err + } + + attr := internal.BPFProgDetachAttr{ + TargetFd: uint32(opts.Target), + AttachBpfFd: uint32(opts.Program.FD()), + AttachType: uint32(opts.Attach), + } + if err := internal.BPFProgDetach(&attr); err != nil { + return fmt.Errorf("can't detach program: %s", err) + } + + return nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go b/agent/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go new file mode 100644 index 00000000000..65652486f1c --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go @@ -0,0 +1,57 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal" +) + +type RawTracepointOptions struct { + // Tracepoint name. + Name string + // Program must be of type RawTracepoint* + Program *ebpf.Program +} + +// AttachRawTracepoint links a BPF program to a raw_tracepoint. +// +// Requires at least Linux 4.17. +func AttachRawTracepoint(opts RawTracepointOptions) (Link, error) { + if t := opts.Program.Type(); t != ebpf.RawTracepoint && t != ebpf.RawTracepointWritable { + return nil, fmt.Errorf("invalid program type %s, expected RawTracepoint(Writable)", t) + } + if opts.Program.FD() < 0 { + return nil, fmt.Errorf("invalid program: %w", internal.ErrClosedFd) + } + + fd, err := bpfRawTracepointOpen(&bpfRawTracepointOpenAttr{ + name: internal.NewStringPointer(opts.Name), + fd: uint32(opts.Program.FD()), + }) + if err != nil { + return nil, err + } + + return &progAttachRawTracepoint{fd: fd}, nil +} + +type progAttachRawTracepoint struct { + fd *internal.FD +} + +var _ Link = (*progAttachRawTracepoint)(nil) + +func (rt *progAttachRawTracepoint) isLink() {} + +func (rt *progAttachRawTracepoint) Close() error { + return rt.fd.Close() +} + +func (rt *progAttachRawTracepoint) Update(_ *ebpf.Program) error { + return fmt.Errorf("can't update raw_tracepoint: %w", ErrNotSupported) +} + +func (rt *progAttachRawTracepoint) Pin(_ string) error { + return fmt.Errorf("can't pin raw_tracepoint: %w", ErrNotSupported) +} diff --git a/agent/vendor/github.com/cilium/ebpf/link/syscalls.go b/agent/vendor/github.com/cilium/ebpf/link/syscalls.go new file mode 100644 index 00000000000..19326c8af80 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/link/syscalls.go @@ -0,0 +1,173 @@ +package link + +import ( + "errors" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/unix" +) + +// Type is the kind of link. +type Type uint32 + +// Valid link types. +// +// Equivalent to enum bpf_link_type. +const ( + UnspecifiedType Type = iota + RawTracepointType + TracingType + CgroupType + IterType + NetNsType + XDPType +) + +var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() error { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + AttachType: ebpf.AttachCGroupInetIngress, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return internal.ErrNotSupported + } + + // BPF_PROG_ATTACH was introduced at the same time as CGgroupSKB, + // so being able to load the program is enough to infer that we + // have the syscall. + prog.Close() + return nil +}) + +var haveProgAttachReplace = internal.FeatureTest("BPF_PROG_ATTACH atomic replacement", "5.5", func() error { + if err := haveProgAttach(); err != nil { + return err + } + + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + AttachType: ebpf.AttachCGroupInetIngress, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return internal.ErrNotSupported + } + defer prog.Close() + + // We know that we have BPF_PROG_ATTACH since we can load CGroupSKB programs. + // If passing BPF_F_REPLACE gives us EINVAL we know that the feature isn't + // present. + attr := internal.BPFProgAttachAttr{ + // We rely on this being checked after attachFlags. + TargetFd: ^uint32(0), + AttachBpfFd: uint32(prog.FD()), + AttachType: uint32(ebpf.AttachCGroupInetIngress), + AttachFlags: uint32(flagReplace), + } + + err = internal.BPFProgAttach(&attr) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +type bpfLinkCreateAttr struct { + progFd uint32 + targetFd uint32 + attachType ebpf.AttachType + flags uint32 +} + +func bpfLinkCreate(attr *bpfLinkCreateAttr) (*internal.FD, error) { + ptr, err := internal.BPF(internal.BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err != nil { + return nil, err + } + return internal.NewFD(uint32(ptr)), nil +} + +type bpfLinkUpdateAttr struct { + linkFd uint32 + newProgFd uint32 + flags uint32 + oldProgFd uint32 +} + +func bpfLinkUpdate(attr *bpfLinkUpdateAttr) error { + _, err := internal.BPF(internal.BPF_LINK_UPDATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + return err +} + +var haveBPFLink = internal.FeatureTest("bpf_link", "5.7", func() error { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + AttachType: ebpf.AttachCGroupInetIngress, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return internal.ErrNotSupported + } + defer prog.Close() + + attr := bpfLinkCreateAttr{ + // This is a hopefully invalid file descriptor, which triggers EBADF. + targetFd: ^uint32(0), + progFd: uint32(prog.FD()), + attachType: ebpf.AttachCGroupInetIngress, + } + _, err = bpfLinkCreate(&attr) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +type bpfIterCreateAttr struct { + linkFd uint32 + flags uint32 +} + +func bpfIterCreate(attr *bpfIterCreateAttr) (*internal.FD, error) { + ptr, err := internal.BPF(internal.BPF_ITER_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err == nil { + return internal.NewFD(uint32(ptr)), nil + } + return nil, err +} + +type bpfRawTracepointOpenAttr struct { + name internal.Pointer + fd uint32 + _ uint32 +} + +func bpfRawTracepointOpen(attr *bpfRawTracepointOpenAttr) (*internal.FD, error) { + ptr, err := internal.BPF(internal.BPF_RAW_TRACEPOINT_OPEN, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err == nil { + return internal.NewFD(uint32(ptr)), nil + } + return nil, err +} diff --git a/agent/vendor/github.com/cilium/ebpf/linker.go b/agent/vendor/github.com/cilium/ebpf/linker.go new file mode 100644 index 00000000000..f843bb25e7b --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/linker.go @@ -0,0 +1,133 @@ +package ebpf + +import ( + "fmt" + + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal/btf" +) + +// link resolves bpf-to-bpf calls. +// +// Each library may contain multiple functions / labels, and is only linked +// if prog references one of these functions. +// +// Libraries also linked. +func link(prog *ProgramSpec, libs []*ProgramSpec) error { + var ( + linked = make(map[*ProgramSpec]bool) + pending = []asm.Instructions{prog.Instructions} + insns asm.Instructions + ) + for len(pending) > 0 { + insns, pending = pending[0], pending[1:] + for _, lib := range libs { + if linked[lib] { + continue + } + + needed, err := needSection(insns, lib.Instructions) + if err != nil { + return fmt.Errorf("linking %s: %w", lib.Name, err) + } + + if !needed { + continue + } + + linked[lib] = true + prog.Instructions = append(prog.Instructions, lib.Instructions...) + pending = append(pending, lib.Instructions) + + if prog.BTF != nil && lib.BTF != nil { + if err := btf.ProgramAppend(prog.BTF, lib.BTF); err != nil { + return fmt.Errorf("linking BTF of %s: %w", lib.Name, err) + } + } + } + } + + return nil +} + +func needSection(insns, section asm.Instructions) (bool, error) { + // A map of symbols to the libraries which contain them. + symbols, err := section.SymbolOffsets() + if err != nil { + return false, err + } + + for _, ins := range insns { + if ins.Reference == "" { + continue + } + + if ins.OpCode.JumpOp() != asm.Call || ins.Src != asm.PseudoCall { + continue + } + + if ins.Constant != -1 { + // This is already a valid call, no need to link again. + continue + } + + if _, ok := symbols[ins.Reference]; !ok { + // Symbol isn't available in this section + continue + } + + // At this point we know that at least one function in the + // library is called from insns, so we have to link it. + return true, nil + } + + // None of the functions in the section are called. + return false, nil +} + +func fixupJumpsAndCalls(insns asm.Instructions) error { + symbolOffsets := make(map[string]asm.RawInstructionOffset) + iter := insns.Iterate() + for iter.Next() { + ins := iter.Ins + + if ins.Symbol == "" { + continue + } + + if _, ok := symbolOffsets[ins.Symbol]; ok { + return fmt.Errorf("duplicate symbol %s", ins.Symbol) + } + + symbolOffsets[ins.Symbol] = iter.Offset + } + + iter = insns.Iterate() + for iter.Next() { + i := iter.Index + offset := iter.Offset + ins := iter.Ins + + switch { + case ins.IsFunctionCall() && ins.Constant == -1: + // Rewrite bpf to bpf call + callOffset, ok := symbolOffsets[ins.Reference] + if !ok { + return fmt.Errorf("instruction %d: reference to missing symbol %q", i, ins.Reference) + } + + ins.Constant = int64(callOffset - offset - 1) + + case ins.OpCode.Class() == asm.JumpClass && ins.Offset == -1: + // Rewrite jump to label + jumpOffset, ok := symbolOffsets[ins.Reference] + if !ok { + return fmt.Errorf("instruction %d: reference to missing symbol %q", i, ins.Reference) + } + + ins.Offset = int16(jumpOffset - offset - 1) + } + } + + return nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/map.go b/agent/vendor/github.com/cilium/ebpf/map.go new file mode 100644 index 00000000000..316fc37b1c2 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/map.go @@ -0,0 +1,1188 @@ +package ebpf + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "reflect" + "strings" + + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" + "github.com/cilium/ebpf/internal/unix" +) + +// Errors returned by Map and MapIterator methods. +var ( + ErrKeyNotExist = errors.New("key does not exist") + ErrKeyExist = errors.New("key already exists") + ErrIterationAborted = errors.New("iteration aborted") +) + +// MapOptions control loading a map into the kernel. +type MapOptions struct { + // The base path to pin maps in if requested via PinByName. + // Existing maps will be re-used if they are compatible, otherwise an + // error is returned. + PinPath string +} + +// MapID represents the unique ID of an eBPF map +type MapID uint32 + +// MapSpec defines a Map. +type MapSpec struct { + // Name is passed to the kernel as a debug aid. Must only contain + // alpha numeric and '_' characters. + Name string + Type MapType + KeySize uint32 + ValueSize uint32 + MaxEntries uint32 + Flags uint32 + + // Automatically pin and load a map from MapOptions.PinPath. + // Generates an error if an existing pinned map is incompatible with the MapSpec. + Pinning PinType + + // Specify numa node during map creation + // (effective only if unix.BPF_F_NUMA_NODE flag is set, + // which can be imported from golang.org/x/sys/unix) + NumaNode uint32 + + // The initial contents of the map. May be nil. + Contents []MapKV + + // Whether to freeze a map after setting its initial contents. + Freeze bool + + // InnerMap is used as a template for ArrayOfMaps and HashOfMaps + InnerMap *MapSpec + + // The BTF associated with this map. + BTF *btf.Map +} + +func (ms *MapSpec) String() string { + return fmt.Sprintf("%s(keySize=%d, valueSize=%d, maxEntries=%d, flags=%d)", ms.Type, ms.KeySize, ms.ValueSize, ms.MaxEntries, ms.Flags) +} + +// Copy returns a copy of the spec. +// +// MapSpec.Contents is a shallow copy. +func (ms *MapSpec) Copy() *MapSpec { + if ms == nil { + return nil + } + + cpy := *ms + cpy.Contents = make([]MapKV, len(ms.Contents)) + copy(cpy.Contents, ms.Contents) + cpy.InnerMap = ms.InnerMap.Copy() + return &cpy +} + +// MapKV is used to initialize the contents of a Map. +type MapKV struct { + Key interface{} + Value interface{} +} + +func (ms *MapSpec) checkCompatibility(m *Map) error { + switch { + case m.typ != ms.Type: + return fmt.Errorf("expected type %v, got %v", ms.Type, m.typ) + + case m.keySize != ms.KeySize: + return fmt.Errorf("expected key size %v, got %v", ms.KeySize, m.keySize) + + case m.valueSize != ms.ValueSize: + return fmt.Errorf("expected value size %v, got %v", ms.ValueSize, m.valueSize) + + case m.maxEntries != ms.MaxEntries: + return fmt.Errorf("expected max entries %v, got %v", ms.MaxEntries, m.maxEntries) + + case m.flags != ms.Flags: + return fmt.Errorf("expected flags %v, got %v", ms.Flags, m.flags) + } + return nil +} + +// Map represents a Map file descriptor. +// +// It is not safe to close a map which is used by other goroutines. +// +// Methods which take interface{} arguments by default encode +// them using binary.Read/Write in the machine's native endianness. +// +// Implement encoding.BinaryMarshaler or encoding.BinaryUnmarshaler +// if you require custom encoding. +type Map struct { + name string + fd *internal.FD + typ MapType + keySize uint32 + valueSize uint32 + maxEntries uint32 + flags uint32 + pinnedPath string + // Per CPU maps return values larger than the size in the spec + fullValueSize int +} + +// NewMapFromFD creates a map from a raw fd. +// +// You should not use fd after calling this function. +func NewMapFromFD(fd int) (*Map, error) { + if fd < 0 { + return nil, errors.New("invalid fd") + } + + return newMapFromFD(internal.NewFD(uint32(fd))) +} + +func newMapFromFD(fd *internal.FD) (*Map, error) { + info, err := newMapInfoFromFd(fd) + if err != nil { + fd.Close() + return nil, fmt.Errorf("get map info: %s", err) + } + + return newMap(fd, info.Name, info.Type, info.KeySize, info.ValueSize, info.MaxEntries, info.Flags) +} + +// NewMap creates a new Map. +// +// It's equivalent to calling NewMapWithOptions with default options. +func NewMap(spec *MapSpec) (*Map, error) { + return NewMapWithOptions(spec, MapOptions{}) +} + +// NewMapWithOptions creates a new Map. +// +// Creating a map for the first time will perform feature detection +// by creating small, temporary maps. +// +// The caller is responsible for ensuring the process' rlimit is set +// sufficiently high for locking memory during map creation. This can be done +// by calling unix.Setrlimit with unix.RLIMIT_MEMLOCK prior to calling NewMapWithOptions. +func NewMapWithOptions(spec *MapSpec, opts MapOptions) (*Map, error) { + btfs := make(btfHandleCache) + defer btfs.close() + + return newMapWithOptions(spec, opts, btfs) +} + +func newMapWithOptions(spec *MapSpec, opts MapOptions, btfs btfHandleCache) (*Map, error) { + switch spec.Pinning { + case PinByName: + if spec.Name == "" || opts.PinPath == "" { + return nil, fmt.Errorf("pin by name: missing Name or PinPath") + } + + m, err := LoadPinnedMap(filepath.Join(opts.PinPath, spec.Name)) + if errors.Is(err, unix.ENOENT) { + break + } + if err != nil { + return nil, fmt.Errorf("load pinned map: %s", err) + } + + if err := spec.checkCompatibility(m); err != nil { + m.Close() + return nil, fmt.Errorf("use pinned map %s: %s", spec.Name, err) + } + + return m, nil + + case PinNone: + // Nothing to do here + + default: + return nil, fmt.Errorf("unsupported pin type %d", int(spec.Pinning)) + } + + var innerFd *internal.FD + if spec.Type == ArrayOfMaps || spec.Type == HashOfMaps { + if spec.InnerMap == nil { + return nil, fmt.Errorf("%s requires InnerMap", spec.Type) + } + + if spec.InnerMap.Pinning != PinNone { + return nil, errors.New("inner maps cannot be pinned") + } + + template, err := createMap(spec.InnerMap, nil, opts, btfs) + if err != nil { + return nil, err + } + defer template.Close() + + innerFd = template.fd + } + + m, err := createMap(spec, innerFd, opts, btfs) + if err != nil { + return nil, err + } + + if spec.Pinning == PinByName { + if err := m.Pin(filepath.Join(opts.PinPath, spec.Name)); err != nil { + m.Close() + return nil, fmt.Errorf("pin map: %s", err) + } + } + + return m, nil +} + +func createMap(spec *MapSpec, inner *internal.FD, opts MapOptions, btfs btfHandleCache) (_ *Map, err error) { + closeOnError := func(closer io.Closer) { + if err != nil { + closer.Close() + } + } + + spec = spec.Copy() + + switch spec.Type { + case ArrayOfMaps: + fallthrough + case HashOfMaps: + if err := haveNestedMaps(); err != nil { + return nil, err + } + + if spec.ValueSize != 0 && spec.ValueSize != 4 { + return nil, errors.New("ValueSize must be zero or four for map of map") + } + spec.ValueSize = 4 + + case PerfEventArray: + if spec.KeySize != 0 && spec.KeySize != 4 { + return nil, errors.New("KeySize must be zero or four for perf event array") + } + spec.KeySize = 4 + + if spec.ValueSize != 0 && spec.ValueSize != 4 { + return nil, errors.New("ValueSize must be zero or four for perf event array") + } + spec.ValueSize = 4 + + if spec.MaxEntries == 0 { + n, err := internal.PossibleCPUs() + if err != nil { + return nil, fmt.Errorf("perf event array: %w", err) + } + spec.MaxEntries = uint32(n) + } + } + + if spec.Flags&(unix.BPF_F_RDONLY_PROG|unix.BPF_F_WRONLY_PROG) > 0 || spec.Freeze { + if err := haveMapMutabilityModifiers(); err != nil { + return nil, fmt.Errorf("map create: %w", err) + } + } + + attr := bpfMapCreateAttr{ + mapType: spec.Type, + keySize: spec.KeySize, + valueSize: spec.ValueSize, + maxEntries: spec.MaxEntries, + flags: spec.Flags, + numaNode: spec.NumaNode, + } + + if inner != nil { + var err error + attr.innerMapFd, err = inner.Value() + if err != nil { + return nil, fmt.Errorf("map create: %w", err) + } + } + + if haveObjName() == nil { + attr.mapName = newBPFObjName(spec.Name) + } + + var btfDisabled bool + if spec.BTF != nil { + handle, err := btfs.load(btf.MapSpec(spec.BTF)) + btfDisabled = errors.Is(err, btf.ErrNotSupported) + if err != nil && !btfDisabled { + return nil, fmt.Errorf("load BTF: %w", err) + } + + if handle != nil { + attr.btfFd = uint32(handle.FD()) + attr.btfKeyTypeID = btf.MapKey(spec.BTF).ID() + attr.btfValueTypeID = btf.MapValue(spec.BTF).ID() + } + } + + fd, err := bpfMapCreate(&attr) + if err != nil { + if errors.Is(err, unix.EPERM) { + return nil, fmt.Errorf("map create: RLIMIT_MEMLOCK may be too low: %w", err) + } + if btfDisabled { + return nil, fmt.Errorf("map create without BTF: %w", err) + } + return nil, fmt.Errorf("map create: %w", err) + } + defer closeOnError(fd) + + m, err := newMap(fd, spec.Name, spec.Type, spec.KeySize, spec.ValueSize, spec.MaxEntries, spec.Flags) + if err != nil { + return nil, fmt.Errorf("map create: %w", err) + } + + if err := m.populate(spec.Contents); err != nil { + return nil, fmt.Errorf("map create: can't set initial contents: %w", err) + } + + if spec.Freeze { + if err := m.Freeze(); err != nil { + return nil, fmt.Errorf("can't freeze map: %w", err) + } + } + + return m, nil +} + +func newMap(fd *internal.FD, name string, typ MapType, keySize, valueSize, maxEntries, flags uint32) (*Map, error) { + m := &Map{ + name, + fd, + typ, + keySize, + valueSize, + maxEntries, + flags, + "", + int(valueSize), + } + + if !typ.hasPerCPUValue() { + return m, nil + } + + possibleCPUs, err := internal.PossibleCPUs() + if err != nil { + return nil, err + } + + m.fullValueSize = align(int(valueSize), 8) * possibleCPUs + return m, nil +} + +func (m *Map) String() string { + if m.name != "" { + return fmt.Sprintf("%s(%s)#%v", m.typ, m.name, m.fd) + } + return fmt.Sprintf("%s#%v", m.typ, m.fd) +} + +// Type returns the underlying type of the map. +func (m *Map) Type() MapType { + return m.typ +} + +// KeySize returns the size of the map key in bytes. +func (m *Map) KeySize() uint32 { + return m.keySize +} + +// ValueSize returns the size of the map value in bytes. +func (m *Map) ValueSize() uint32 { + return m.valueSize +} + +// MaxEntries returns the maximum number of elements the map can hold. +func (m *Map) MaxEntries() uint32 { + return m.maxEntries +} + +// Flags returns the flags of the map. +func (m *Map) Flags() uint32 { + return m.flags +} + +// Info returns metadata about the map. +func (m *Map) Info() (*MapInfo, error) { + return newMapInfoFromFd(m.fd) +} + +// Lookup retrieves a value from a Map. +// +// Calls Close() on valueOut if it is of type **Map or **Program, +// and *valueOut is not nil. +// +// Returns an error if the key doesn't exist, see ErrKeyNotExist. +func (m *Map) Lookup(key, valueOut interface{}) error { + valuePtr, valueBytes := makeBuffer(valueOut, m.fullValueSize) + if err := m.lookup(key, valuePtr); err != nil { + return err + } + + return m.unmarshalValue(valueOut, valueBytes) +} + +// LookupAndDelete retrieves and deletes a value from a Map. +// +// Returns ErrKeyNotExist if the key doesn't exist. +func (m *Map) LookupAndDelete(key, valueOut interface{}) error { + valuePtr, valueBytes := makeBuffer(valueOut, m.fullValueSize) + + keyPtr, err := m.marshalKey(key) + if err != nil { + return fmt.Errorf("can't marshal key: %w", err) + } + + if err := bpfMapLookupAndDelete(m.fd, keyPtr, valuePtr); err != nil { + return fmt.Errorf("lookup and delete failed: %w", err) + } + + return m.unmarshalValue(valueOut, valueBytes) +} + +// LookupBytes gets a value from Map. +// +// Returns a nil value if a key doesn't exist. +func (m *Map) LookupBytes(key interface{}) ([]byte, error) { + valueBytes := make([]byte, m.fullValueSize) + valuePtr := internal.NewSlicePointer(valueBytes) + + err := m.lookup(key, valuePtr) + if errors.Is(err, ErrKeyNotExist) { + return nil, nil + } + + return valueBytes, err +} + +func (m *Map) lookup(key interface{}, valueOut internal.Pointer) error { + keyPtr, err := m.marshalKey(key) + if err != nil { + return fmt.Errorf("can't marshal key: %w", err) + } + + if err = bpfMapLookupElem(m.fd, keyPtr, valueOut); err != nil { + return fmt.Errorf("lookup failed: %w", err) + } + return nil +} + +// MapUpdateFlags controls the behaviour of the Map.Update call. +// +// The exact semantics depend on the specific MapType. +type MapUpdateFlags uint64 + +const ( + // UpdateAny creates a new element or update an existing one. + UpdateAny MapUpdateFlags = iota + // UpdateNoExist creates a new element. + UpdateNoExist MapUpdateFlags = 1 << (iota - 1) + // UpdateExist updates an existing element. + UpdateExist +) + +// Put replaces or creates a value in map. +// +// It is equivalent to calling Update with UpdateAny. +func (m *Map) Put(key, value interface{}) error { + return m.Update(key, value, UpdateAny) +} + +// Update changes the value of a key. +func (m *Map) Update(key, value interface{}, flags MapUpdateFlags) error { + keyPtr, err := m.marshalKey(key) + if err != nil { + return fmt.Errorf("can't marshal key: %w", err) + } + + valuePtr, err := m.marshalValue(value) + if err != nil { + return fmt.Errorf("can't marshal value: %w", err) + } + + if err = bpfMapUpdateElem(m.fd, keyPtr, valuePtr, uint64(flags)); err != nil { + return fmt.Errorf("update failed: %w", err) + } + + return nil +} + +// Delete removes a value. +// +// Returns ErrKeyNotExist if the key does not exist. +func (m *Map) Delete(key interface{}) error { + keyPtr, err := m.marshalKey(key) + if err != nil { + return fmt.Errorf("can't marshal key: %w", err) + } + + if err = bpfMapDeleteElem(m.fd, keyPtr); err != nil { + return fmt.Errorf("delete failed: %w", err) + } + return nil +} + +// NextKey finds the key following an initial key. +// +// See NextKeyBytes for details. +// +// Returns ErrKeyNotExist if there is no next key. +func (m *Map) NextKey(key, nextKeyOut interface{}) error { + nextKeyPtr, nextKeyBytes := makeBuffer(nextKeyOut, int(m.keySize)) + + if err := m.nextKey(key, nextKeyPtr); err != nil { + return err + } + + if err := m.unmarshalKey(nextKeyOut, nextKeyBytes); err != nil { + return fmt.Errorf("can't unmarshal next key: %w", err) + } + return nil +} + +// NextKeyBytes returns the key following an initial key as a byte slice. +// +// Passing nil will return the first key. +// +// Use Iterate if you want to traverse all entries in the map. +// +// Returns nil if there are no more keys. +func (m *Map) NextKeyBytes(key interface{}) ([]byte, error) { + nextKey := make([]byte, m.keySize) + nextKeyPtr := internal.NewSlicePointer(nextKey) + + err := m.nextKey(key, nextKeyPtr) + if errors.Is(err, ErrKeyNotExist) { + return nil, nil + } + + return nextKey, err +} + +func (m *Map) nextKey(key interface{}, nextKeyOut internal.Pointer) error { + var ( + keyPtr internal.Pointer + err error + ) + + if key != nil { + keyPtr, err = m.marshalKey(key) + if err != nil { + return fmt.Errorf("can't marshal key: %w", err) + } + } + + if err = bpfMapGetNextKey(m.fd, keyPtr, nextKeyOut); err != nil { + return fmt.Errorf("next key failed: %w", err) + } + return nil +} + +// BatchLookup looks up many elements in a map at once. +// +// "keysOut" and "valuesOut" must be of type slice, a pointer +// to a slice or buffer will not work. +// "prevKey" is the key to start the batch lookup from, it will +// *not* be included in the results. Use nil to start at the first key. +// +// ErrKeyNotExist is returned when the batch lookup has reached +// the end of all possible results, even when partial results +// are returned. It should be used to evaluate when lookup is "done". +func (m *Map) BatchLookup(prevKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) { + return m.batchLookup(internal.BPF_MAP_LOOKUP_BATCH, prevKey, nextKeyOut, keysOut, valuesOut, opts) +} + +// BatchLookupAndDelete looks up many elements in a map at once, +// +// It then deletes all those elements. +// "keysOut" and "valuesOut" must be of type slice, a pointer +// to a slice or buffer will not work. +// "prevKey" is the key to start the batch lookup from, it will +// *not* be included in the results. Use nil to start at the first key. +// +// ErrKeyNotExist is returned when the batch lookup has reached +// the end of all possible results, even when partial results +// are returned. It should be used to evaluate when lookup is "done". +func (m *Map) BatchLookupAndDelete(prevKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) { + return m.batchLookup(internal.BPF_MAP_LOOKUP_AND_DELETE_BATCH, prevKey, nextKeyOut, keysOut, valuesOut, opts) +} + +func (m *Map) batchLookup(cmd internal.BPFCmd, startKey, nextKeyOut, keysOut, valuesOut interface{}, opts *BatchOptions) (int, error) { + if err := haveBatchAPI(); err != nil { + return 0, err + } + if m.typ.hasPerCPUValue() { + return 0, ErrNotSupported + } + keysValue := reflect.ValueOf(keysOut) + if keysValue.Kind() != reflect.Slice { + return 0, fmt.Errorf("keys must be a slice") + } + valuesValue := reflect.ValueOf(valuesOut) + if valuesValue.Kind() != reflect.Slice { + return 0, fmt.Errorf("valuesOut must be a slice") + } + count := keysValue.Len() + if count != valuesValue.Len() { + return 0, fmt.Errorf("keysOut and valuesOut must be the same length") + } + keyBuf := make([]byte, count*int(m.keySize)) + keyPtr := internal.NewSlicePointer(keyBuf) + valueBuf := make([]byte, count*int(m.fullValueSize)) + valuePtr := internal.NewSlicePointer(valueBuf) + + var ( + startPtr internal.Pointer + err error + retErr error + ) + if startKey != nil { + startPtr, err = marshalPtr(startKey, int(m.keySize)) + if err != nil { + return 0, err + } + } + nextPtr, nextBuf := makeBuffer(nextKeyOut, int(m.keySize)) + + ct, err := bpfMapBatch(cmd, m.fd, startPtr, nextPtr, keyPtr, valuePtr, uint32(count), opts) + if err != nil { + if !errors.Is(err, ErrKeyNotExist) { + return 0, err + } + retErr = ErrKeyNotExist + } + + err = m.unmarshalKey(nextKeyOut, nextBuf) + if err != nil { + return 0, err + } + err = unmarshalBytes(keysOut, keyBuf) + if err != nil { + return 0, err + } + err = unmarshalBytes(valuesOut, valueBuf) + if err != nil { + retErr = err + } + return int(ct), retErr +} + +// BatchUpdate updates the map with multiple keys and values +// simultaneously. +// "keys" and "values" must be of type slice, a pointer +// to a slice or buffer will not work. +func (m *Map) BatchUpdate(keys, values interface{}, opts *BatchOptions) (int, error) { + if err := haveBatchAPI(); err != nil { + return 0, err + } + if m.typ.hasPerCPUValue() { + return 0, ErrNotSupported + } + keysValue := reflect.ValueOf(keys) + if keysValue.Kind() != reflect.Slice { + return 0, fmt.Errorf("keys must be a slice") + } + valuesValue := reflect.ValueOf(values) + if valuesValue.Kind() != reflect.Slice { + return 0, fmt.Errorf("values must be a slice") + } + var ( + count = keysValue.Len() + valuePtr internal.Pointer + err error + ) + if count != valuesValue.Len() { + return 0, fmt.Errorf("keys and values must be the same length") + } + keyPtr, err := marshalPtr(keys, count*int(m.keySize)) + if err != nil { + return 0, err + } + valuePtr, err = marshalPtr(values, count*int(m.valueSize)) + if err != nil { + return 0, err + } + var nilPtr internal.Pointer + ct, err := bpfMapBatch(internal.BPF_MAP_UPDATE_BATCH, m.fd, nilPtr, nilPtr, keyPtr, valuePtr, uint32(count), opts) + return int(ct), err +} + +// BatchDelete batch deletes entries in the map by keys. +// "keys" must be of type slice, a pointer to a slice or buffer will not work. +func (m *Map) BatchDelete(keys interface{}, opts *BatchOptions) (int, error) { + if err := haveBatchAPI(); err != nil { + return 0, err + } + if m.typ.hasPerCPUValue() { + return 0, ErrNotSupported + } + keysValue := reflect.ValueOf(keys) + if keysValue.Kind() != reflect.Slice { + return 0, fmt.Errorf("keys must be a slice") + } + count := keysValue.Len() + keyPtr, err := marshalPtr(keys, count*int(m.keySize)) + if err != nil { + return 0, fmt.Errorf("cannot marshal keys: %v", err) + } + var nilPtr internal.Pointer + ct, err := bpfMapBatch(internal.BPF_MAP_DELETE_BATCH, m.fd, nilPtr, nilPtr, keyPtr, nilPtr, uint32(count), opts) + return int(ct), err +} + +// Iterate traverses a map. +// +// It's safe to create multiple iterators at the same time. +// +// It's not possible to guarantee that all keys in a map will be +// returned if there are concurrent modifications to the map. +func (m *Map) Iterate() *MapIterator { + return newMapIterator(m) +} + +// Close removes a Map +func (m *Map) Close() error { + if m == nil { + // This makes it easier to clean up when iterating maps + // of maps / programs. + return nil + } + + return m.fd.Close() +} + +// FD gets the file descriptor of the Map. +// +// Calling this function is invalid after Close has been called. +func (m *Map) FD() int { + fd, err := m.fd.Value() + if err != nil { + // Best effort: -1 is the number most likely to be an + // invalid file descriptor. + return -1 + } + + return int(fd) +} + +// Clone creates a duplicate of the Map. +// +// Closing the duplicate does not affect the original, and vice versa. +// Changes made to the map are reflected by both instances however. +// If the original map was pinned, the cloned map will not be pinned by default. +// +// Cloning a nil Map returns nil. +func (m *Map) Clone() (*Map, error) { + if m == nil { + return nil, nil + } + + dup, err := m.fd.Dup() + if err != nil { + return nil, fmt.Errorf("can't clone map: %w", err) + } + + return &Map{ + m.name, + dup, + m.typ, + m.keySize, + m.valueSize, + m.maxEntries, + m.flags, + "", + m.fullValueSize, + }, nil +} + +// Pin persists the map on the BPF virtual file system past the lifetime of +// the process that created it . +// +// Calling Pin on a previously pinned map will override the path. +// You can Clone a map to pin it to a different path. +// +// This requires bpffs to be mounted above fileName. See https://docs.cilium.io/en/k8s-doc/admin/#admin-mount-bpffs +func (m *Map) Pin(fileName string) error { + if err := pin(m.pinnedPath, fileName, m.fd); err != nil { + return err + } + m.pinnedPath = fileName + return nil +} + +// Unpin removes the persisted state for the map from the BPF virtual filesystem. +// +// Failed calls to Unpin will not alter the state returned by IsPinned. +// +// Unpinning an unpinned Map returns nil. +func (m *Map) Unpin() error { + if err := unpin(m.pinnedPath); err != nil { + return err + } + m.pinnedPath = "" + return nil +} + +// IsPinned returns true if the map has a non-empty pinned path. +func (m *Map) IsPinned() bool { + if m.pinnedPath == "" { + return false + } + return true +} + +// Freeze prevents a map to be modified from user space. +// +// It makes no changes to kernel-side restrictions. +func (m *Map) Freeze() error { + if err := haveMapMutabilityModifiers(); err != nil { + return fmt.Errorf("can't freeze map: %w", err) + } + + if err := bpfMapFreeze(m.fd); err != nil { + return fmt.Errorf("can't freeze map: %w", err) + } + return nil +} + +func (m *Map) populate(contents []MapKV) error { + for _, kv := range contents { + if err := m.Put(kv.Key, kv.Value); err != nil { + return fmt.Errorf("key %v: %w", kv.Key, err) + } + } + return nil +} + +func (m *Map) marshalKey(data interface{}) (internal.Pointer, error) { + if data == nil { + if m.keySize == 0 { + // Queues have a key length of zero, so passing nil here is valid. + return internal.NewPointer(nil), nil + } + return internal.Pointer{}, errors.New("can't use nil as key of map") + } + + return marshalPtr(data, int(m.keySize)) +} + +func (m *Map) unmarshalKey(data interface{}, buf []byte) error { + if buf == nil { + // This is from a makeBuffer call, nothing do do here. + return nil + } + + return unmarshalBytes(data, buf) +} + +func (m *Map) marshalValue(data interface{}) (internal.Pointer, error) { + if m.typ.hasPerCPUValue() { + return marshalPerCPUValue(data, int(m.valueSize)) + } + + var ( + buf []byte + err error + ) + + switch value := data.(type) { + case *Map: + if !m.typ.canStoreMap() { + return internal.Pointer{}, fmt.Errorf("can't store map in %s", m.typ) + } + buf, err = marshalMap(value, int(m.valueSize)) + + case *Program: + if !m.typ.canStoreProgram() { + return internal.Pointer{}, fmt.Errorf("can't store program in %s", m.typ) + } + buf, err = marshalProgram(value, int(m.valueSize)) + + default: + return marshalPtr(data, int(m.valueSize)) + } + + if err != nil { + return internal.Pointer{}, err + } + + return internal.NewSlicePointer(buf), nil +} + +func (m *Map) unmarshalValue(value interface{}, buf []byte) error { + if buf == nil { + // This is from a makeBuffer call, nothing do do here. + return nil + } + + if m.typ.hasPerCPUValue() { + return unmarshalPerCPUValue(value, int(m.valueSize), buf) + } + + switch value := value.(type) { + case **Map: + if !m.typ.canStoreMap() { + return fmt.Errorf("can't read a map from %s", m.typ) + } + + other, err := unmarshalMap(buf) + if err != nil { + return err + } + + (*value).Close() + *value = other + return nil + + case *Map: + if !m.typ.canStoreMap() { + return fmt.Errorf("can't read a map from %s", m.typ) + } + return errors.New("require pointer to *Map") + + case **Program: + if !m.typ.canStoreProgram() { + return fmt.Errorf("can't read a program from %s", m.typ) + } + + other, err := unmarshalProgram(buf) + if err != nil { + return err + } + + (*value).Close() + *value = other + return nil + + case *Program: + if !m.typ.canStoreProgram() { + return fmt.Errorf("can't read a program from %s", m.typ) + } + return errors.New("require pointer to *Program") + } + + return unmarshalBytes(value, buf) +} + +// LoadPinnedMap load a Map from a BPF file. +func LoadPinnedMap(fileName string) (*Map, error) { + fd, err := internal.BPFObjGet(fileName) + if err != nil { + return nil, err + } + + m, err := newMapFromFD(fd) + if err == nil { + m.pinnedPath = fileName + } + + return m, err +} + +// unmarshalMap creates a map from a map ID encoded in host endianness. +func unmarshalMap(buf []byte) (*Map, error) { + if len(buf) != 4 { + return nil, errors.New("map id requires 4 byte value") + } + + id := internal.NativeEndian.Uint32(buf) + return NewMapFromID(MapID(id)) +} + +// marshalMap marshals the fd of a map into a buffer in host endianness. +func marshalMap(m *Map, length int) ([]byte, error) { + if length != 4 { + return nil, fmt.Errorf("can't marshal map to %d bytes", length) + } + + fd, err := m.fd.Value() + if err != nil { + return nil, err + } + + buf := make([]byte, 4) + internal.NativeEndian.PutUint32(buf, fd) + return buf, nil +} + +func patchValue(value []byte, typ btf.Type, replacements map[string]interface{}) error { + replaced := make(map[string]bool) + replace := func(name string, offset, size int, replacement interface{}) error { + if offset+size > len(value) { + return fmt.Errorf("%s: offset %d(+%d) is out of bounds", name, offset, size) + } + + buf, err := marshalBytes(replacement, size) + if err != nil { + return fmt.Errorf("marshal %s: %w", name, err) + } + + copy(value[offset:offset+size], buf) + replaced[name] = true + return nil + } + + switch parent := typ.(type) { + case *btf.Datasec: + for _, secinfo := range parent.Vars { + name := string(secinfo.Type.(*btf.Var).Name) + replacement, ok := replacements[name] + if !ok { + continue + } + + err := replace(name, int(secinfo.Offset), int(secinfo.Size), replacement) + if err != nil { + return err + } + } + + default: + return fmt.Errorf("patching %T is not supported", typ) + } + + if len(replaced) == len(replacements) { + return nil + } + + var missing []string + for name := range replacements { + if !replaced[name] { + missing = append(missing, name) + } + } + + if len(missing) == 1 { + return fmt.Errorf("unknown field: %s", missing[0]) + } + + return fmt.Errorf("unknown fields: %s", strings.Join(missing, ",")) +} + +// MapIterator iterates a Map. +// +// See Map.Iterate. +type MapIterator struct { + target *Map + prevKey interface{} + prevBytes []byte + count, maxEntries uint32 + done bool + err error +} + +func newMapIterator(target *Map) *MapIterator { + return &MapIterator{ + target: target, + maxEntries: target.maxEntries, + prevBytes: make([]byte, target.keySize), + } +} + +// Next decodes the next key and value. +// +// Iterating a hash map from which keys are being deleted is not +// safe. You may see the same key multiple times. Iteration may +// also abort with an error, see IsIterationAborted. +// +// Returns false if there are no more entries. You must check +// the result of Err afterwards. +// +// See Map.Get for further caveats around valueOut. +func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool { + if mi.err != nil || mi.done { + return false + } + + // For array-like maps NextKeyBytes returns nil only on after maxEntries + // iterations. + for mi.count <= mi.maxEntries { + var nextBytes []byte + nextBytes, mi.err = mi.target.NextKeyBytes(mi.prevKey) + if mi.err != nil { + return false + } + + if nextBytes == nil { + mi.done = true + return false + } + + // The user can get access to nextBytes since unmarshalBytes + // does not copy when unmarshaling into a []byte. + // Make a copy to prevent accidental corruption of + // iterator state. + copy(mi.prevBytes, nextBytes) + mi.prevKey = mi.prevBytes + + mi.count++ + mi.err = mi.target.Lookup(nextBytes, valueOut) + if errors.Is(mi.err, ErrKeyNotExist) { + // Even though the key should be valid, we couldn't look up + // its value. If we're iterating a hash map this is probably + // because a concurrent delete removed the value before we + // could get it. This means that the next call to NextKeyBytes + // is very likely to restart iteration. + // If we're iterating one of the fd maps like + // ProgramArray it means that a given slot doesn't have + // a valid fd associated. It's OK to continue to the next slot. + continue + } + if mi.err != nil { + return false + } + + mi.err = mi.target.unmarshalKey(keyOut, nextBytes) + return mi.err == nil + } + + mi.err = fmt.Errorf("%w", ErrIterationAborted) + return false +} + +// Err returns any encountered error. +// +// The method must be called after Next returns nil. +// +// Returns ErrIterationAborted if it wasn't possible to do a full iteration. +func (mi *MapIterator) Err() error { + return mi.err +} + +// MapGetNextID returns the ID of the next eBPF map. +// +// Returns ErrNotExist, if there is no next eBPF map. +func MapGetNextID(startID MapID) (MapID, error) { + id, err := objGetNextID(internal.BPF_MAP_GET_NEXT_ID, uint32(startID)) + return MapID(id), err +} + +// NewMapFromID returns the map for a given id. +// +// Returns ErrNotExist, if there is no eBPF map with the given id. +func NewMapFromID(id MapID) (*Map, error) { + fd, err := bpfObjGetFDByID(internal.BPF_MAP_GET_FD_BY_ID, uint32(id)) + if err != nil { + return nil, err + } + + return newMapFromFD(fd) +} + +// ID returns the systemwide unique ID of the map. +// +// Deprecated: use MapInfo.ID() instead. +func (m *Map) ID() (MapID, error) { + info, err := bpfGetMapInfoByFD(m.fd) + if err != nil { + return MapID(0), err + } + return MapID(info.id), nil +} diff --git a/agent/vendor/github.com/cilium/ebpf/marshalers.go b/agent/vendor/github.com/cilium/ebpf/marshalers.go new file mode 100644 index 00000000000..3ea1021a8d3 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/marshalers.go @@ -0,0 +1,216 @@ +package ebpf + +import ( + "bytes" + "encoding" + "encoding/binary" + "errors" + "fmt" + "reflect" + "runtime" + "unsafe" + + "github.com/cilium/ebpf/internal" +) + +// marshalPtr converts an arbitrary value into a pointer suitable +// to be passed to the kernel. +// +// As an optimization, it returns the original value if it is an +// unsafe.Pointer. +func marshalPtr(data interface{}, length int) (internal.Pointer, error) { + if ptr, ok := data.(unsafe.Pointer); ok { + return internal.NewPointer(ptr), nil + } + + buf, err := marshalBytes(data, length) + if err != nil { + return internal.Pointer{}, err + } + + return internal.NewSlicePointer(buf), nil +} + +// marshalBytes converts an arbitrary value into a byte buffer. +// +// Prefer using Map.marshalKey and Map.marshalValue if possible, since +// those have special cases that allow more types to be encoded. +// +// Returns an error if the given value isn't representable in exactly +// length bytes. +func marshalBytes(data interface{}, length int) (buf []byte, err error) { + switch value := data.(type) { + case encoding.BinaryMarshaler: + buf, err = value.MarshalBinary() + case string: + buf = []byte(value) + case []byte: + buf = value + case unsafe.Pointer: + err = errors.New("can't marshal from unsafe.Pointer") + case Map, *Map, Program, *Program: + err = fmt.Errorf("can't marshal %T", value) + default: + var wr bytes.Buffer + err = binary.Write(&wr, internal.NativeEndian, value) + if err != nil { + err = fmt.Errorf("encoding %T: %v", value, err) + } + buf = wr.Bytes() + } + if err != nil { + return nil, err + } + + if len(buf) != length { + return nil, fmt.Errorf("%T doesn't marshal to %d bytes", data, length) + } + return buf, nil +} + +func makeBuffer(dst interface{}, length int) (internal.Pointer, []byte) { + if ptr, ok := dst.(unsafe.Pointer); ok { + return internal.NewPointer(ptr), nil + } + + buf := make([]byte, length) + return internal.NewSlicePointer(buf), buf +} + +// unmarshalBytes converts a byte buffer into an arbitrary value. +// +// Prefer using Map.unmarshalKey and Map.unmarshalValue if possible, since +// those have special cases that allow more types to be encoded. +func unmarshalBytes(data interface{}, buf []byte) error { + switch value := data.(type) { + case unsafe.Pointer: + sh := &reflect.SliceHeader{ + Data: uintptr(value), + Len: len(buf), + Cap: len(buf), + } + + dst := *(*[]byte)(unsafe.Pointer(sh)) + copy(dst, buf) + runtime.KeepAlive(value) + return nil + case Map, *Map, Program, *Program: + return fmt.Errorf("can't unmarshal into %T", value) + case encoding.BinaryUnmarshaler: + return value.UnmarshalBinary(buf) + case *string: + *value = string(buf) + return nil + case *[]byte: + *value = buf + return nil + case string: + return errors.New("require pointer to string") + case []byte: + return errors.New("require pointer to []byte") + default: + rd := bytes.NewReader(buf) + if err := binary.Read(rd, internal.NativeEndian, value); err != nil { + return fmt.Errorf("decoding %T: %v", value, err) + } + return nil + } +} + +// marshalPerCPUValue encodes a slice containing one value per +// possible CPU into a buffer of bytes. +// +// Values are initialized to zero if the slice has less elements than CPUs. +// +// slice must have a type like []elementType. +func marshalPerCPUValue(slice interface{}, elemLength int) (internal.Pointer, error) { + sliceType := reflect.TypeOf(slice) + if sliceType.Kind() != reflect.Slice { + return internal.Pointer{}, errors.New("per-CPU value requires slice") + } + + possibleCPUs, err := internal.PossibleCPUs() + if err != nil { + return internal.Pointer{}, err + } + + sliceValue := reflect.ValueOf(slice) + sliceLen := sliceValue.Len() + if sliceLen > possibleCPUs { + return internal.Pointer{}, fmt.Errorf("per-CPU value exceeds number of CPUs") + } + + alignedElemLength := align(elemLength, 8) + buf := make([]byte, alignedElemLength*possibleCPUs) + + for i := 0; i < sliceLen; i++ { + elem := sliceValue.Index(i).Interface() + elemBytes, err := marshalBytes(elem, elemLength) + if err != nil { + return internal.Pointer{}, err + } + + offset := i * alignedElemLength + copy(buf[offset:offset+elemLength], elemBytes) + } + + return internal.NewSlicePointer(buf), nil +} + +// unmarshalPerCPUValue decodes a buffer into a slice containing one value per +// possible CPU. +// +// valueOut must have a type like *[]elementType +func unmarshalPerCPUValue(slicePtr interface{}, elemLength int, buf []byte) error { + slicePtrType := reflect.TypeOf(slicePtr) + if slicePtrType.Kind() != reflect.Ptr || slicePtrType.Elem().Kind() != reflect.Slice { + return fmt.Errorf("per-cpu value requires pointer to slice") + } + + possibleCPUs, err := internal.PossibleCPUs() + if err != nil { + return err + } + + sliceType := slicePtrType.Elem() + slice := reflect.MakeSlice(sliceType, possibleCPUs, possibleCPUs) + + sliceElemType := sliceType.Elem() + sliceElemIsPointer := sliceElemType.Kind() == reflect.Ptr + if sliceElemIsPointer { + sliceElemType = sliceElemType.Elem() + } + + step := len(buf) / possibleCPUs + if step < elemLength { + return fmt.Errorf("per-cpu element length is larger than available data") + } + for i := 0; i < possibleCPUs; i++ { + var elem interface{} + if sliceElemIsPointer { + newElem := reflect.New(sliceElemType) + slice.Index(i).Set(newElem) + elem = newElem.Interface() + } else { + elem = slice.Index(i).Addr().Interface() + } + + // Make a copy, since unmarshal can hold on to itemBytes + elemBytes := make([]byte, elemLength) + copy(elemBytes, buf[:elemLength]) + + err := unmarshalBytes(elem, elemBytes) + if err != nil { + return fmt.Errorf("cpu %d: %w", i, err) + } + + buf = buf[step:] + } + + reflect.ValueOf(slicePtr).Elem().Set(slice) + return nil +} + +func align(n, alignment int) int { + return (int(n) + alignment - 1) / alignment * alignment +} diff --git a/agent/vendor/github.com/cilium/ebpf/pinning.go b/agent/vendor/github.com/cilium/ebpf/pinning.go new file mode 100644 index 00000000000..78812364ac7 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/pinning.go @@ -0,0 +1,42 @@ +package ebpf + +import ( + "errors" + "fmt" + "os" + + "github.com/cilium/ebpf/internal" +) + +func pin(currentPath, newPath string, fd *internal.FD) error { + if newPath == "" { + return errors.New("given pinning path cannot be empty") + } + if currentPath == "" { + return internal.BPFObjPin(newPath, fd) + } + if currentPath == newPath { + return nil + } + var err error + // Object is now moved to the new pinning path. + if err = os.Rename(currentPath, newPath); err == nil { + return nil + } + if !os.IsNotExist(err) { + return fmt.Errorf("unable to move pinned object to new path %v: %w", newPath, err) + } + // Internal state not in sync with the file system so let's fix it. + return internal.BPFObjPin(newPath, fd) +} + +func unpin(pinnedPath string) error { + if pinnedPath == "" { + return nil + } + err := os.Remove(pinnedPath) + if err == nil || os.IsNotExist(err) { + return nil + } + return err +} diff --git a/agent/vendor/github.com/cilium/ebpf/prog.go b/agent/vendor/github.com/cilium/ebpf/prog.go new file mode 100644 index 00000000000..4b65f23b206 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/prog.go @@ -0,0 +1,698 @@ +package ebpf + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "path/filepath" + "strings" + "time" + + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" + "github.com/cilium/ebpf/internal/unix" +) + +// ErrNotSupported is returned whenever the kernel doesn't support a feature. +var ErrNotSupported = internal.ErrNotSupported + +// ProgramID represents the unique ID of an eBPF program. +type ProgramID uint32 + +const ( + // Number of bytes to pad the output buffer for BPF_PROG_TEST_RUN. + // This is currently the maximum of spare space allocated for SKB + // and XDP programs, and equal to XDP_PACKET_HEADROOM + NET_IP_ALIGN. + outputPad = 256 + 2 +) + +// DefaultVerifierLogSize is the default number of bytes allocated for the +// verifier log. +const DefaultVerifierLogSize = 64 * 1024 + +// ProgramOptions control loading a program into the kernel. +type ProgramOptions struct { + // Controls the detail emitted by the kernel verifier. Set to non-zero + // to enable logging. + LogLevel uint32 + // Controls the output buffer size for the verifier. Defaults to + // DefaultVerifierLogSize. + LogSize int +} + +// ProgramSpec defines a Program. +type ProgramSpec struct { + // Name is passed to the kernel as a debug aid. Must only contain + // alpha numeric and '_' characters. + Name string + // Type determines at which hook in the kernel a program will run. + Type ProgramType + AttachType AttachType + // Name of a kernel data structure to attach to. It's interpretation + // depends on Type and AttachType. + AttachTo string + Instructions asm.Instructions + + // License of the program. Some helpers are only available if + // the license is deemed compatible with the GPL. + // + // See https://www.kernel.org/doc/html/latest/process/license-rules.html#id1 + License string + + // Version used by tracing programs. + // + // Deprecated: superseded by BTF. + KernelVersion uint32 + + // The BTF associated with this program. Changing Instructions + // will most likely invalidate the contained data, and may + // result in errors when attempting to load it into the kernel. + BTF *btf.Program + + // The byte order this program was compiled for, may be nil. + ByteOrder binary.ByteOrder +} + +// Copy returns a copy of the spec. +func (ps *ProgramSpec) Copy() *ProgramSpec { + if ps == nil { + return nil + } + + cpy := *ps + cpy.Instructions = make(asm.Instructions, len(ps.Instructions)) + copy(cpy.Instructions, ps.Instructions) + return &cpy +} + +// Tag calculates the kernel tag for a series of instructions. +// +// Use asm.Instructions.Tag if you need to calculate for non-native endianness. +func (ps *ProgramSpec) Tag() (string, error) { + return ps.Instructions.Tag(internal.NativeEndian) +} + +// Program represents BPF program loaded into the kernel. +// +// It is not safe to close a Program which is used by other goroutines. +type Program struct { + // Contains the output of the kernel verifier if enabled, + // otherwise it is empty. + VerifierLog string + + fd *internal.FD + name string + pinnedPath string + typ ProgramType +} + +// NewProgram creates a new Program. +// +// Loading a program for the first time will perform +// feature detection by loading small, temporary programs. +func NewProgram(spec *ProgramSpec) (*Program, error) { + return NewProgramWithOptions(spec, ProgramOptions{}) +} + +// NewProgramWithOptions creates a new Program. +// +// Loading a program for the first time will perform +// feature detection by loading small, temporary programs. +func NewProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, error) { + btfs := make(btfHandleCache) + defer btfs.close() + + return newProgramWithOptions(spec, opts, btfs) +} + +func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, btfs btfHandleCache) (*Program, error) { + if len(spec.Instructions) == 0 { + return nil, errors.New("Instructions cannot be empty") + } + + if len(spec.License) == 0 { + return nil, errors.New("License cannot be empty") + } + + if spec.ByteOrder != nil && spec.ByteOrder != internal.NativeEndian { + return nil, fmt.Errorf("can't load %s program on %s", spec.ByteOrder, internal.NativeEndian) + } + + insns := make(asm.Instructions, len(spec.Instructions)) + copy(insns, spec.Instructions) + + if err := fixupJumpsAndCalls(insns); err != nil { + return nil, err + } + + buf := bytes.NewBuffer(make([]byte, 0, len(spec.Instructions)*asm.InstructionSize)) + err := insns.Marshal(buf, internal.NativeEndian) + if err != nil { + return nil, err + } + + bytecode := buf.Bytes() + insCount := uint32(len(bytecode) / asm.InstructionSize) + attr := &bpfProgLoadAttr{ + progType: spec.Type, + expectedAttachType: spec.AttachType, + insCount: insCount, + instructions: internal.NewSlicePointer(bytecode), + license: internal.NewStringPointer(spec.License), + kernelVersion: spec.KernelVersion, + } + + if haveObjName() == nil { + attr.progName = newBPFObjName(spec.Name) + } + + var btfDisabled bool + if spec.BTF != nil { + if relos, err := btf.ProgramRelocations(spec.BTF, nil); err != nil { + return nil, fmt.Errorf("CO-RE relocations: %s", err) + } else if len(relos) > 0 { + return nil, fmt.Errorf("applying CO-RE relocations: %w", ErrNotSupported) + } + + handle, err := btfs.load(btf.ProgramSpec(spec.BTF)) + btfDisabled = errors.Is(err, btf.ErrNotSupported) + if err != nil && !btfDisabled { + return nil, fmt.Errorf("load BTF: %w", err) + } + + if handle != nil { + attr.progBTFFd = uint32(handle.FD()) + + recSize, bytes, err := btf.ProgramLineInfos(spec.BTF) + if err != nil { + return nil, fmt.Errorf("get BTF line infos: %w", err) + } + attr.lineInfoRecSize = recSize + attr.lineInfoCnt = uint32(uint64(len(bytes)) / uint64(recSize)) + attr.lineInfo = internal.NewSlicePointer(bytes) + + recSize, bytes, err = btf.ProgramFuncInfos(spec.BTF) + if err != nil { + return nil, fmt.Errorf("get BTF function infos: %w", err) + } + attr.funcInfoRecSize = recSize + attr.funcInfoCnt = uint32(uint64(len(bytes)) / uint64(recSize)) + attr.funcInfo = internal.NewSlicePointer(bytes) + } + } + + if spec.AttachTo != "" { + target, err := resolveBTFType(spec.AttachTo, spec.Type, spec.AttachType) + if err != nil { + return nil, err + } + if target != nil { + attr.attachBTFID = target.ID() + } + } + + logSize := DefaultVerifierLogSize + if opts.LogSize > 0 { + logSize = opts.LogSize + } + + var logBuf []byte + if opts.LogLevel > 0 { + logBuf = make([]byte, logSize) + attr.logLevel = opts.LogLevel + attr.logSize = uint32(len(logBuf)) + attr.logBuf = internal.NewSlicePointer(logBuf) + } + + fd, err := bpfProgLoad(attr) + if err == nil { + return &Program{internal.CString(logBuf), fd, spec.Name, "", spec.Type}, nil + } + + logErr := err + if opts.LogLevel == 0 { + // Re-run with the verifier enabled to get better error messages. + logBuf = make([]byte, logSize) + attr.logLevel = 1 + attr.logSize = uint32(len(logBuf)) + attr.logBuf = internal.NewSlicePointer(logBuf) + + _, logErr = bpfProgLoad(attr) + } + + if errors.Is(logErr, unix.EPERM) && logBuf[0] == 0 { + // EPERM due to RLIMIT_MEMLOCK happens before the verifier, so we can + // check that the log is empty to reduce false positives. + return nil, fmt.Errorf("load program: RLIMIT_MEMLOCK may be too low: %w", logErr) + } + + err = internal.ErrorWithLog(err, logBuf, logErr) + if btfDisabled { + return nil, fmt.Errorf("load program without BTF: %w", err) + } + return nil, fmt.Errorf("load program: %w", err) +} + +// NewProgramFromFD creates a program from a raw fd. +// +// You should not use fd after calling this function. +// +// Requires at least Linux 4.10. +func NewProgramFromFD(fd int) (*Program, error) { + if fd < 0 { + return nil, errors.New("invalid fd") + } + + return newProgramFromFD(internal.NewFD(uint32(fd))) +} + +// NewProgramFromID returns the program for a given id. +// +// Returns ErrNotExist, if there is no eBPF program with the given id. +func NewProgramFromID(id ProgramID) (*Program, error) { + fd, err := bpfObjGetFDByID(internal.BPF_PROG_GET_FD_BY_ID, uint32(id)) + if err != nil { + return nil, fmt.Errorf("get program by id: %w", err) + } + + return newProgramFromFD(fd) +} + +func newProgramFromFD(fd *internal.FD) (*Program, error) { + info, err := newProgramInfoFromFd(fd) + if err != nil { + fd.Close() + return nil, fmt.Errorf("discover program type: %w", err) + } + + return &Program{"", fd, "", "", info.Type}, nil +} + +func (p *Program) String() string { + if p.name != "" { + return fmt.Sprintf("%s(%s)#%v", p.typ, p.name, p.fd) + } + return fmt.Sprintf("%s(%v)", p.typ, p.fd) +} + +// Type returns the underlying type of the program. +func (p *Program) Type() ProgramType { + return p.typ +} + +// Info returns metadata about the program. +// +// Requires at least 4.10. +func (p *Program) Info() (*ProgramInfo, error) { + return newProgramInfoFromFd(p.fd) +} + +// FD gets the file descriptor of the Program. +// +// It is invalid to call this function after Close has been called. +func (p *Program) FD() int { + fd, err := p.fd.Value() + if err != nil { + // Best effort: -1 is the number most likely to be an + // invalid file descriptor. + return -1 + } + + return int(fd) +} + +// Clone creates a duplicate of the Program. +// +// Closing the duplicate does not affect the original, and vice versa. +// +// Cloning a nil Program returns nil. +func (p *Program) Clone() (*Program, error) { + if p == nil { + return nil, nil + } + + dup, err := p.fd.Dup() + if err != nil { + return nil, fmt.Errorf("can't clone program: %w", err) + } + + return &Program{p.VerifierLog, dup, p.name, "", p.typ}, nil +} + +// Pin persists the Program on the BPF virtual file system past the lifetime of +// the process that created it +// +// This requires bpffs to be mounted above fileName. See https://docs.cilium.io/en/k8s-doc/admin/#admin-mount-bpffs +func (p *Program) Pin(fileName string) error { + if err := pin(p.pinnedPath, fileName, p.fd); err != nil { + return err + } + p.pinnedPath = fileName + return nil +} + +// Unpin removes the persisted state for the Program from the BPF virtual filesystem. +// +// Failed calls to Unpin will not alter the state returned by IsPinned. +// +// Unpinning an unpinned Program returns nil. +func (p *Program) Unpin() error { + if err := unpin(p.pinnedPath); err != nil { + return err + } + p.pinnedPath = "" + return nil +} + +// IsPinned returns true if the Program has a non-empty pinned path. +func (p *Program) IsPinned() bool { + if p.pinnedPath == "" { + return false + } + return true +} + +// Close unloads the program from the kernel. +func (p *Program) Close() error { + if p == nil { + return nil + } + + return p.fd.Close() +} + +// Test runs the Program in the kernel with the given input and returns the +// value returned by the eBPF program. outLen may be zero. +// +// Note: the kernel expects at least 14 bytes input for an ethernet header for +// XDP and SKB programs. +// +// This function requires at least Linux 4.12. +func (p *Program) Test(in []byte) (uint32, []byte, error) { + ret, out, _, err := p.testRun(in, 1, nil) + if err != nil { + return ret, nil, fmt.Errorf("can't test program: %w", err) + } + return ret, out, nil +} + +// Benchmark runs the Program with the given input for a number of times +// and returns the time taken per iteration. +// +// Returns the result of the last execution of the program and the time per +// run or an error. reset is called whenever the benchmark syscall is +// interrupted, and should be set to testing.B.ResetTimer or similar. +// +// Note: profiling a call to this function will skew it's results, see +// https://github.com/cilium/ebpf/issues/24 +// +// This function requires at least Linux 4.12. +func (p *Program) Benchmark(in []byte, repeat int, reset func()) (uint32, time.Duration, error) { + ret, _, total, err := p.testRun(in, repeat, reset) + if err != nil { + return ret, total, fmt.Errorf("can't benchmark program: %w", err) + } + return ret, total, nil +} + +var haveProgTestRun = internal.FeatureTest("BPF_PROG_TEST_RUN", "4.12", func() error { + prog, err := NewProgram(&ProgramSpec{ + Type: SocketFilter, + Instructions: asm.Instructions{ + asm.LoadImm(asm.R0, 0, asm.DWord), + asm.Return(), + }, + License: "MIT", + }) + if err != nil { + // This may be because we lack sufficient permissions, etc. + return err + } + defer prog.Close() + + // Programs require at least 14 bytes input + in := make([]byte, 14) + attr := bpfProgTestRunAttr{ + fd: uint32(prog.FD()), + dataSizeIn: uint32(len(in)), + dataIn: internal.NewSlicePointer(in), + } + + err = bpfProgTestRun(&attr) + if errors.Is(err, unix.EINVAL) { + // Check for EINVAL specifically, rather than err != nil since we + // otherwise misdetect due to insufficient permissions. + return internal.ErrNotSupported + } + if errors.Is(err, unix.EINTR) { + // We know that PROG_TEST_RUN is supported if we get EINTR. + return nil + } + return err +}) + +func (p *Program) testRun(in []byte, repeat int, reset func()) (uint32, []byte, time.Duration, error) { + if uint(repeat) > math.MaxUint32 { + return 0, nil, 0, fmt.Errorf("repeat is too high") + } + + if len(in) == 0 { + return 0, nil, 0, fmt.Errorf("missing input") + } + + if uint(len(in)) > math.MaxUint32 { + return 0, nil, 0, fmt.Errorf("input is too long") + } + + if err := haveProgTestRun(); err != nil { + return 0, nil, 0, err + } + + // Older kernels ignore the dataSizeOut argument when copying to user space. + // Combined with things like bpf_xdp_adjust_head() we don't really know what the final + // size will be. Hence we allocate an output buffer which we hope will always be large + // enough, and panic if the kernel wrote past the end of the allocation. + // See https://patchwork.ozlabs.org/cover/1006822/ + out := make([]byte, len(in)+outputPad) + + fd, err := p.fd.Value() + if err != nil { + return 0, nil, 0, err + } + + attr := bpfProgTestRunAttr{ + fd: fd, + dataSizeIn: uint32(len(in)), + dataSizeOut: uint32(len(out)), + dataIn: internal.NewSlicePointer(in), + dataOut: internal.NewSlicePointer(out), + repeat: uint32(repeat), + } + + for { + err = bpfProgTestRun(&attr) + if err == nil { + break + } + + if errors.Is(err, unix.EINTR) { + if reset != nil { + reset() + } + continue + } + + return 0, nil, 0, fmt.Errorf("can't run test: %w", err) + } + + if int(attr.dataSizeOut) > cap(out) { + // Houston, we have a problem. The program created more data than we allocated, + // and the kernel wrote past the end of our buffer. + panic("kernel wrote past end of output buffer") + } + out = out[:int(attr.dataSizeOut)] + + total := time.Duration(attr.duration) * time.Nanosecond + return attr.retval, out, total, nil +} + +func unmarshalProgram(buf []byte) (*Program, error) { + if len(buf) != 4 { + return nil, errors.New("program id requires 4 byte value") + } + + // Looking up an entry in a nested map or prog array returns an id, + // not an fd. + id := internal.NativeEndian.Uint32(buf) + return NewProgramFromID(ProgramID(id)) +} + +func marshalProgram(p *Program, length int) ([]byte, error) { + if length != 4 { + return nil, fmt.Errorf("can't marshal program to %d bytes", length) + } + + value, err := p.fd.Value() + if err != nil { + return nil, err + } + + buf := make([]byte, 4) + internal.NativeEndian.PutUint32(buf, value) + return buf, nil +} + +// Attach a Program. +// +// Deprecated: use link.RawAttachProgram instead. +func (p *Program) Attach(fd int, typ AttachType, flags AttachFlags) error { + if fd < 0 { + return errors.New("invalid fd") + } + + pfd, err := p.fd.Value() + if err != nil { + return err + } + + attr := internal.BPFProgAttachAttr{ + TargetFd: uint32(fd), + AttachBpfFd: pfd, + AttachType: uint32(typ), + AttachFlags: uint32(flags), + } + + return internal.BPFProgAttach(&attr) +} + +// Detach a Program. +// +// Deprecated: use link.RawDetachProgram instead. +func (p *Program) Detach(fd int, typ AttachType, flags AttachFlags) error { + if fd < 0 { + return errors.New("invalid fd") + } + + if flags != 0 { + return errors.New("flags must be zero") + } + + pfd, err := p.fd.Value() + if err != nil { + return err + } + + attr := internal.BPFProgDetachAttr{ + TargetFd: uint32(fd), + AttachBpfFd: pfd, + AttachType: uint32(typ), + } + + return internal.BPFProgDetach(&attr) +} + +// LoadPinnedProgram loads a Program from a BPF file. +// +// Requires at least Linux 4.11. +func LoadPinnedProgram(fileName string) (*Program, error) { + fd, err := internal.BPFObjGet(fileName) + if err != nil { + return nil, err + } + + info, err := newProgramInfoFromFd(fd) + if err != nil { + _ = fd.Close() + return nil, fmt.Errorf("info for %s: %w", fileName, err) + } + + return &Program{"", fd, filepath.Base(fileName), "", info.Type}, nil +} + +// SanitizeName replaces all invalid characters in name with replacement. +// Passing a negative value for replacement will delete characters instead +// of replacing them. Use this to automatically generate valid names for maps +// and programs at runtime. +// +// The set of allowed characters depends on the running kernel version. +// Dots are only allowed as of kernel 5.2. +func SanitizeName(name string, replacement rune) string { + return strings.Map(func(char rune) rune { + if invalidBPFObjNameChar(char) { + return replacement + } + return char + }, name) +} + +// ProgramGetNextID returns the ID of the next eBPF program. +// +// Returns ErrNotExist, if there is no next eBPF program. +func ProgramGetNextID(startID ProgramID) (ProgramID, error) { + id, err := objGetNextID(internal.BPF_PROG_GET_NEXT_ID, uint32(startID)) + return ProgramID(id), err +} + +// ID returns the systemwide unique ID of the program. +// +// Deprecated: use ProgramInfo.ID() instead. +func (p *Program) ID() (ProgramID, error) { + info, err := bpfGetProgInfoByFD(p.fd) + if err != nil { + return ProgramID(0), err + } + return ProgramID(info.id), nil +} + +func findKernelType(name string, typ btf.Type) error { + kernel, err := btf.LoadKernelSpec() + if err != nil { + return fmt.Errorf("can't load kernel spec: %w", err) + } + + return kernel.FindType(name, typ) +} + +func resolveBTFType(name string, progType ProgramType, attachType AttachType) (btf.Type, error) { + type match struct { + p ProgramType + a AttachType + } + + target := match{progType, attachType} + switch target { + case match{LSM, AttachLSMMac}: + var target btf.Func + err := findKernelType("bpf_lsm_"+name, &target) + if errors.Is(err, btf.ErrNotFound) { + return nil, &internal.UnsupportedFeatureError{ + Name: name + " LSM hook", + } + } + if err != nil { + return nil, fmt.Errorf("resolve BTF for LSM hook %s: %w", name, err) + } + + return &target, nil + + case match{Tracing, AttachTraceIter}: + var target btf.Func + err := findKernelType("bpf_iter_"+name, &target) + if errors.Is(err, btf.ErrNotFound) { + return nil, &internal.UnsupportedFeatureError{ + Name: name + " iterator", + } + } + if err != nil { + return nil, fmt.Errorf("resolve BTF for iterator %s: %w", name, err) + } + + return &target, nil + + default: + return nil, nil + } +} diff --git a/agent/vendor/github.com/cilium/ebpf/run-tests.sh b/agent/vendor/github.com/cilium/ebpf/run-tests.sh new file mode 100644 index 00000000000..647a61aab0b --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/run-tests.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Test the current package under a different kernel. +# Requires virtme and qemu to be installed. + +set -eu +set -o pipefail + +if [[ "${1:-}" = "--in-vm" ]]; then + shift + + mount -t bpf bpf /sys/fs/bpf + export CGO_ENABLED=0 + export GOFLAGS=-mod=readonly + export GOPATH=/run/go-path + export GOPROXY=file:///run/go-path/pkg/mod/cache/download + export GOSUMDB=off + export GOCACHE=/run/go-cache + + if [[ -d "/run/input/bpf" ]]; then + export KERNEL_SELFTESTS="/run/input/bpf" + fi + + readonly output="${1}" + shift + + echo Running tests... + go test -v -coverpkg=./... -coverprofile="$output/coverage.txt" -count 1 ./... + touch "$output/success" + exit 0 +fi + +# Pull all dependencies, so that we can run tests without the +# vm having network access. +go mod download + +# Use sudo if /dev/kvm isn't accessible by the current user. +sudo="" +if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then + sudo="sudo" +fi +readonly sudo + +readonly kernel_version="${1:-}" +if [[ -z "${kernel_version}" ]]; then + echo "Expecting kernel version as first argument" + exit 1 +fi + +readonly kernel="linux-${kernel_version}.bz" +readonly selftests="linux-${kernel_version}-selftests-bpf.bz" +readonly input="$(mktemp -d)" +readonly output="$(mktemp -d)" +readonly tmp_dir="${TMPDIR:-/tmp}" +readonly branch="${BRANCH:-master}" + +fetch() { + echo Fetching "${1}" + wget -nv -N -P "${tmp_dir}" "https://github.com/cilium/ci-kernels/raw/${branch}/${1}" +} + +fetch "${kernel}" + +if fetch "${selftests}"; then + mkdir "${input}/bpf" + tar --strip-components=4 -xjf "${tmp_dir}/${selftests}" -C "${input}/bpf" +else + echo "No selftests found, disabling" +fi + +echo Testing on "${kernel_version}" +$sudo virtme-run --kimg "${tmp_dir}/${kernel}" --memory 512M --pwd \ + --rw \ + --rwdir=/run/input="${input}" \ + --rwdir=/run/output="${output}" \ + --rodir=/run/go-path="$(go env GOPATH)" \ + --rwdir=/run/go-cache="$(go env GOCACHE)" \ + --script-sh "PATH=\"$PATH\" $(realpath "$0") --in-vm /run/output" \ + --qemu-opts -smp 2 # need at least two CPUs for some tests + +if [[ ! -e "${output}/success" ]]; then + echo "Test failed on ${kernel_version}" + exit 1 +else + echo "Test successful on ${kernel_version}" + if [[ -v COVERALLS_TOKEN ]]; then + goveralls -coverprofile="${output}/coverage.txt" -service=semaphore -repotoken "$COVERALLS_TOKEN" + fi +fi + +$sudo rm -r "${input}" +$sudo rm -r "${output}" diff --git a/agent/vendor/github.com/cilium/ebpf/syscalls.go b/agent/vendor/github.com/cilium/ebpf/syscalls.go new file mode 100644 index 00000000000..1cba1d747a4 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/syscalls.go @@ -0,0 +1,491 @@ +package ebpf + +import ( + "errors" + "fmt" + "unsafe" + + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" + "github.com/cilium/ebpf/internal/unix" +) + +// Generic errors returned by BPF syscalls. +var ErrNotExist = errors.New("requested object does not exist") + +// bpfObjName is a null-terminated string made up of +// 'A-Za-z0-9_' characters. +type bpfObjName [unix.BPF_OBJ_NAME_LEN]byte + +// newBPFObjName truncates the result if it is too long. +func newBPFObjName(name string) bpfObjName { + var result bpfObjName + copy(result[:unix.BPF_OBJ_NAME_LEN-1], name) + return result +} + +// invalidBPFObjNameChar returns true if char may not appear in +// a BPF object name. +func invalidBPFObjNameChar(char rune) bool { + dotAllowed := objNameAllowsDot() == nil + + switch { + case char >= 'A' && char <= 'Z': + return false + case char >= 'a' && char <= 'z': + return false + case char >= '0' && char <= '9': + return false + case dotAllowed && char == '.': + return false + case char == '_': + return false + default: + return true + } +} + +type bpfMapCreateAttr struct { + mapType MapType + keySize uint32 + valueSize uint32 + maxEntries uint32 + flags uint32 + innerMapFd uint32 // since 4.12 56f668dfe00d + numaNode uint32 // since 4.14 96eabe7a40aa + mapName bpfObjName // since 4.15 ad5b177bd73f + mapIfIndex uint32 + btfFd uint32 + btfKeyTypeID btf.TypeID + btfValueTypeID btf.TypeID +} + +type bpfMapOpAttr struct { + mapFd uint32 + padding uint32 + key internal.Pointer + value internal.Pointer + flags uint64 +} + +type bpfBatchMapOpAttr struct { + inBatch internal.Pointer + outBatch internal.Pointer + keys internal.Pointer + values internal.Pointer + count uint32 + mapFd uint32 + elemFlags uint64 + flags uint64 +} + +type bpfMapInfo struct { + map_type uint32 // since 4.12 1e2709769086 + id uint32 + key_size uint32 + value_size uint32 + max_entries uint32 + map_flags uint32 + name bpfObjName // since 4.15 ad5b177bd73f + ifindex uint32 // since 4.16 52775b33bb50 + btf_vmlinux_value_type_id uint32 // since 5.6 85d33df357b6 + netns_dev uint64 // since 4.16 52775b33bb50 + netns_ino uint64 + btf_id uint32 // since 4.18 78958fca7ead + btf_key_type_id uint32 // since 4.18 9b2cf328b2ec + btf_value_type_id uint32 +} + +type bpfProgLoadAttr struct { + progType ProgramType + insCount uint32 + instructions internal.Pointer + license internal.Pointer + logLevel uint32 + logSize uint32 + logBuf internal.Pointer + kernelVersion uint32 // since 4.1 2541517c32be + progFlags uint32 // since 4.11 e07b98d9bffe + progName bpfObjName // since 4.15 067cae47771c + progIfIndex uint32 // since 4.15 1f6f4cb7ba21 + expectedAttachType AttachType // since 4.17 5e43f899b03a + progBTFFd uint32 + funcInfoRecSize uint32 + funcInfo internal.Pointer + funcInfoCnt uint32 + lineInfoRecSize uint32 + lineInfo internal.Pointer + lineInfoCnt uint32 + attachBTFID btf.TypeID + attachProgFd uint32 +} + +type bpfProgInfo struct { + prog_type uint32 + id uint32 + tag [unix.BPF_TAG_SIZE]byte + jited_prog_len uint32 + xlated_prog_len uint32 + jited_prog_insns internal.Pointer + xlated_prog_insns internal.Pointer + load_time uint64 // since 4.15 cb4d2b3f03d8 + created_by_uid uint32 + nr_map_ids uint32 + map_ids internal.Pointer + name bpfObjName // since 4.15 067cae47771c + ifindex uint32 + gpl_compatible uint32 + netns_dev uint64 + netns_ino uint64 + nr_jited_ksyms uint32 + nr_jited_func_lens uint32 + jited_ksyms internal.Pointer + jited_func_lens internal.Pointer + btf_id uint32 + func_info_rec_size uint32 + func_info internal.Pointer + nr_func_info uint32 + nr_line_info uint32 + line_info internal.Pointer + jited_line_info internal.Pointer + nr_jited_line_info uint32 + line_info_rec_size uint32 + jited_line_info_rec_size uint32 + nr_prog_tags uint32 + prog_tags internal.Pointer + run_time_ns uint64 + run_cnt uint64 +} + +type bpfProgTestRunAttr struct { + fd uint32 + retval uint32 + dataSizeIn uint32 + dataSizeOut uint32 + dataIn internal.Pointer + dataOut internal.Pointer + repeat uint32 + duration uint32 +} + +type bpfGetFDByIDAttr struct { + id uint32 + next uint32 +} + +type bpfMapFreezeAttr struct { + mapFd uint32 +} + +type bpfObjGetNextIDAttr struct { + startID uint32 + nextID uint32 + openFlags uint32 +} + +func bpfProgLoad(attr *bpfProgLoadAttr) (*internal.FD, error) { + for { + fd, err := internal.BPF(internal.BPF_PROG_LOAD, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + // As of ~4.20 the verifier can be interrupted by a signal, + // and returns EAGAIN in that case. + if err == unix.EAGAIN { + continue + } + + if err != nil { + return nil, err + } + + return internal.NewFD(uint32(fd)), nil + } +} + +func bpfProgTestRun(attr *bpfProgTestRunAttr) error { + _, err := internal.BPF(internal.BPF_PROG_TEST_RUN, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + return err +} + +func bpfMapCreate(attr *bpfMapCreateAttr) (*internal.FD, error) { + fd, err := internal.BPF(internal.BPF_MAP_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr)) + if err != nil { + return nil, err + } + + return internal.NewFD(uint32(fd)), nil +} + +var haveNestedMaps = internal.FeatureTest("nested maps", "4.12", func() error { + _, err := bpfMapCreate(&bpfMapCreateAttr{ + mapType: ArrayOfMaps, + keySize: 4, + valueSize: 4, + maxEntries: 1, + // Invalid file descriptor. + innerMapFd: ^uint32(0), + }) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +var haveMapMutabilityModifiers = internal.FeatureTest("read- and write-only maps", "5.2", func() error { + // This checks BPF_F_RDONLY_PROG and BPF_F_WRONLY_PROG. Since + // BPF_MAP_FREEZE appeared in 5.2 as well we don't do a separate check. + m, err := bpfMapCreate(&bpfMapCreateAttr{ + mapType: Array, + keySize: 4, + valueSize: 4, + maxEntries: 1, + flags: unix.BPF_F_RDONLY_PROG, + }) + if err != nil { + return internal.ErrNotSupported + } + _ = m.Close() + return nil +}) + +func bpfMapLookupElem(m *internal.FD, key, valueOut internal.Pointer) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapOpAttr{ + mapFd: fd, + key: key, + value: valueOut, + } + _, err = internal.BPF(internal.BPF_MAP_LOOKUP_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return wrapMapError(err) +} + +func bpfMapLookupAndDelete(m *internal.FD, key, valueOut internal.Pointer) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapOpAttr{ + mapFd: fd, + key: key, + value: valueOut, + } + _, err = internal.BPF(internal.BPF_MAP_LOOKUP_AND_DELETE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return wrapMapError(err) +} + +func bpfMapUpdateElem(m *internal.FD, key, valueOut internal.Pointer, flags uint64) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapOpAttr{ + mapFd: fd, + key: key, + value: valueOut, + flags: flags, + } + _, err = internal.BPF(internal.BPF_MAP_UPDATE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return wrapMapError(err) +} + +func bpfMapDeleteElem(m *internal.FD, key internal.Pointer) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapOpAttr{ + mapFd: fd, + key: key, + } + _, err = internal.BPF(internal.BPF_MAP_DELETE_ELEM, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return wrapMapError(err) +} + +func bpfMapGetNextKey(m *internal.FD, key, nextKeyOut internal.Pointer) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapOpAttr{ + mapFd: fd, + key: key, + value: nextKeyOut, + } + _, err = internal.BPF(internal.BPF_MAP_GET_NEXT_KEY, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return wrapMapError(err) +} + +func objGetNextID(cmd internal.BPFCmd, start uint32) (uint32, error) { + attr := bpfObjGetNextIDAttr{ + startID: start, + } + _, err := internal.BPF(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return attr.nextID, wrapObjError(err) +} + +func bpfMapBatch(cmd internal.BPFCmd, m *internal.FD, inBatch, outBatch, keys, values internal.Pointer, count uint32, opts *BatchOptions) (uint32, error) { + fd, err := m.Value() + if err != nil { + return 0, err + } + + attr := bpfBatchMapOpAttr{ + inBatch: inBatch, + outBatch: outBatch, + keys: keys, + values: values, + count: count, + mapFd: fd, + } + if opts != nil { + attr.elemFlags = opts.ElemFlags + attr.flags = opts.Flags + } + _, err = internal.BPF(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + // always return count even on an error, as things like update might partially be fulfilled. + return attr.count, wrapMapError(err) +} + +func wrapObjError(err error) error { + if err == nil { + return nil + } + if errors.Is(err, unix.ENOENT) { + return fmt.Errorf("%w", ErrNotExist) + } + + return errors.New(err.Error()) +} + +func wrapMapError(err error) error { + if err == nil { + return nil + } + + if errors.Is(err, unix.ENOENT) { + return ErrKeyNotExist + } + + if errors.Is(err, unix.EEXIST) { + return ErrKeyExist + } + + if errors.Is(err, unix.ENOTSUPP) { + return ErrNotSupported + } + + return errors.New(err.Error()) +} + +func bpfMapFreeze(m *internal.FD) error { + fd, err := m.Value() + if err != nil { + return err + } + + attr := bpfMapFreezeAttr{ + mapFd: fd, + } + _, err = internal.BPF(internal.BPF_MAP_FREEZE, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return err +} + +func bpfGetProgInfoByFD(fd *internal.FD) (*bpfProgInfo, error) { + var info bpfProgInfo + if err := internal.BPFObjGetInfoByFD(fd, unsafe.Pointer(&info), unsafe.Sizeof(info)); err != nil { + return nil, fmt.Errorf("can't get program info: %w", err) + } + return &info, nil +} + +func bpfGetMapInfoByFD(fd *internal.FD) (*bpfMapInfo, error) { + var info bpfMapInfo + err := internal.BPFObjGetInfoByFD(fd, unsafe.Pointer(&info), unsafe.Sizeof(info)) + if err != nil { + return nil, fmt.Errorf("can't get map info: %w", err) + } + return &info, nil +} + +var haveObjName = internal.FeatureTest("object names", "4.15", func() error { + attr := bpfMapCreateAttr{ + mapType: Array, + keySize: 4, + valueSize: 4, + maxEntries: 1, + mapName: newBPFObjName("feature_test"), + } + + fd, err := bpfMapCreate(&attr) + if err != nil { + return internal.ErrNotSupported + } + + _ = fd.Close() + return nil +}) + +var objNameAllowsDot = internal.FeatureTest("dot in object names", "5.2", func() error { + if err := haveObjName(); err != nil { + return err + } + + attr := bpfMapCreateAttr{ + mapType: Array, + keySize: 4, + valueSize: 4, + maxEntries: 1, + mapName: newBPFObjName(".test"), + } + + fd, err := bpfMapCreate(&attr) + if err != nil { + return internal.ErrNotSupported + } + + _ = fd.Close() + return nil +}) + +var haveBatchAPI = internal.FeatureTest("map batch api", "5.6", func() error { + var maxEntries uint32 = 2 + attr := bpfMapCreateAttr{ + mapType: Hash, + keySize: 4, + valueSize: 4, + maxEntries: maxEntries, + } + + fd, err := bpfMapCreate(&attr) + if err != nil { + return internal.ErrNotSupported + } + defer fd.Close() + keys := []uint32{1, 2} + values := []uint32{3, 4} + kp, _ := marshalPtr(keys, 8) + vp, _ := marshalPtr(values, 8) + nilPtr := internal.NewPointer(nil) + _, err = bpfMapBatch(internal.BPF_MAP_UPDATE_BATCH, fd, nilPtr, nilPtr, kp, vp, maxEntries, nil) + if err != nil { + return internal.ErrNotSupported + } + return nil +}) + +func bpfObjGetFDByID(cmd internal.BPFCmd, id uint32) (*internal.FD, error) { + attr := bpfGetFDByIDAttr{ + id: id, + } + ptr, err := internal.BPF(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return internal.NewFD(uint32(ptr)), wrapObjError(err) +} diff --git a/agent/vendor/github.com/cilium/ebpf/types.go b/agent/vendor/github.com/cilium/ebpf/types.go new file mode 100644 index 00000000000..3191ba1e027 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/types.go @@ -0,0 +1,213 @@ +package ebpf + +//go:generate stringer -output types_string.go -type=MapType,ProgramType,AttachType,PinType + +// MapType indicates the type map structure +// that will be initialized in the kernel. +type MapType uint32 + +// All the various map types that can be created +const ( + UnspecifiedMap MapType = iota + // Hash is a hash map + Hash + // Array is an array map + Array + // ProgramArray - A program array map is a special kind of array map whose map + // values contain only file descriptors referring to other eBPF + // programs. Thus, both the key_size and value_size must be + // exactly four bytes. This map is used in conjunction with the + // TailCall helper. + ProgramArray + // PerfEventArray - A perf event array is used in conjunction with PerfEventRead + // and PerfEventOutput calls, to read the raw bpf_perf_data from the registers. + PerfEventArray + // PerCPUHash - This data structure is useful for people who have high performance + // network needs and can reconcile adds at the end of some cycle, so that + // hashes can be lock free without the use of XAdd, which can be costly. + PerCPUHash + // PerCPUArray - This data structure is useful for people who have high performance + // network needs and can reconcile adds at the end of some cycle, so that + // hashes can be lock free without the use of XAdd, which can be costly. + // Each CPU gets a copy of this hash, the contents of all of which can be reconciled + // later. + PerCPUArray + // StackTrace - This holds whole user and kernel stack traces, it can be retrieved with + // GetStackID + StackTrace + // CGroupArray - This is a very niche structure used to help SKBInCGroup determine + // if an skb is from a socket belonging to a specific cgroup + CGroupArray + // LRUHash - This allows you to create a small hash structure that will purge the + // least recently used items rather than thow an error when you run out of memory + LRUHash + // LRUCPUHash - This is NOT like PerCPUHash, this structure is shared among the CPUs, + // it has more to do with including the CPU id with the LRU calculation so that if a + // particular CPU is using a value over-and-over again, then it will be saved, but if + // a value is being retrieved a lot but sparsely across CPUs it is not as important, basically + // giving weight to CPU locality over overall usage. + LRUCPUHash + // LPMTrie - This is an implementation of Longest-Prefix-Match Trie structure. It is useful, + // for storing things like IP addresses which can be bit masked allowing for keys of differing + // values to refer to the same reference based on their masks. See wikipedia for more details. + LPMTrie + // ArrayOfMaps - Each item in the array is another map. The inner map mustn't be a map of maps + // itself. + ArrayOfMaps + // HashOfMaps - Each item in the hash map is another map. The inner map mustn't be a map of maps + // itself. + HashOfMaps + // DevMap - Specialized map to store references to network devices. + DevMap + // SockMap - Specialized map to store references to sockets. + SockMap + // CPUMap - Specialized map to store references to CPUs. + CPUMap + // XSKMap - Specialized map for XDP programs to store references to open sockets. + XSKMap + // SockHash - Specialized hash to store references to sockets. + SockHash + // CGroupStorage - Special map for CGroups. + CGroupStorage + // ReusePortSockArray - Specialized map to store references to sockets that can be reused. + ReusePortSockArray + // PerCPUCGroupStorage - Special per CPU map for CGroups. + PerCPUCGroupStorage + // Queue - FIFO storage for BPF programs. + Queue + // Stack - LIFO storage for BPF programs. + Stack + // SkStorage - Specialized map for local storage at SK for BPF programs. + SkStorage + // DevMapHash - Hash-based indexing scheme for references to network devices. + DevMapHash +) + +// hasPerCPUValue returns true if the Map stores a value per CPU. +func (mt MapType) hasPerCPUValue() bool { + return mt == PerCPUHash || mt == PerCPUArray || mt == LRUCPUHash +} + +// canStoreMap returns true if the map type accepts a map fd +// for update and returns a map id for lookup. +func (mt MapType) canStoreMap() bool { + return mt == ArrayOfMaps || mt == HashOfMaps +} + +// canStoreProgram returns true if the map type accepts a program fd +// for update and returns a program id for lookup. +func (mt MapType) canStoreProgram() bool { + return mt == ProgramArray +} + +// ProgramType of the eBPF program +type ProgramType uint32 + +// eBPF program types +const ( + UnspecifiedProgram ProgramType = iota + SocketFilter + Kprobe + SchedCLS + SchedACT + TracePoint + XDP + PerfEvent + CGroupSKB + CGroupSock + LWTIn + LWTOut + LWTXmit + SockOps + SkSKB + CGroupDevice + SkMsg + RawTracepoint + CGroupSockAddr + LWTSeg6Local + LircMode2 + SkReuseport + FlowDissector + CGroupSysctl + RawTracepointWritable + CGroupSockopt + Tracing + StructOps + Extension + LSM + SkLookup +) + +// AttachType of the eBPF program, needed to differentiate allowed context accesses in +// some newer program types like CGroupSockAddr. Should be set to AttachNone if not required. +// Will cause invalid argument (EINVAL) at program load time if set incorrectly. +type AttachType uint32 + +// AttachNone is an alias for AttachCGroupInetIngress for readability reasons. +const AttachNone AttachType = 0 + +const ( + AttachCGroupInetIngress AttachType = iota + AttachCGroupInetEgress + AttachCGroupInetSockCreate + AttachCGroupSockOps + AttachSkSKBStreamParser + AttachSkSKBStreamVerdict + AttachCGroupDevice + AttachSkMsgVerdict + AttachCGroupInet4Bind + AttachCGroupInet6Bind + AttachCGroupInet4Connect + AttachCGroupInet6Connect + AttachCGroupInet4PostBind + AttachCGroupInet6PostBind + AttachCGroupUDP4Sendmsg + AttachCGroupUDP6Sendmsg + AttachLircMode2 + AttachFlowDissector + AttachCGroupSysctl + AttachCGroupUDP4Recvmsg + AttachCGroupUDP6Recvmsg + AttachCGroupGetsockopt + AttachCGroupSetsockopt + AttachTraceRawTp + AttachTraceFEntry + AttachTraceFExit + AttachModifyReturn + AttachLSMMac + AttachTraceIter + AttachCgroupInet4GetPeername + AttachCgroupInet6GetPeername + AttachCgroupInet4GetSockname + AttachCgroupInet6GetSockname + AttachXDPDevMap + AttachCgroupInetSockRelease + AttachXDPCPUMap + AttachSkLookup + AttachXDP +) + +// AttachFlags of the eBPF program used in BPF_PROG_ATTACH command +type AttachFlags uint32 + +// PinType determines whether a map is pinned into a BPFFS. +type PinType int + +// Valid pin types. +// +// Mirrors enum libbpf_pin_type. +const ( + PinNone PinType = iota + // Pin an object by using its name as the filename. + PinByName +) + +// BatchOptions batch map operations options +// +// Mirrors libbpf struct bpf_map_batch_opts +// Currently BPF_F_FLAG is the only supported +// flag (for ElemFlags). +type BatchOptions struct { + ElemFlags uint64 + Flags uint64 +} diff --git a/agent/vendor/github.com/cilium/ebpf/types_string.go b/agent/vendor/github.com/cilium/ebpf/types_string.go new file mode 100644 index 00000000000..976bd76be01 --- /dev/null +++ b/agent/vendor/github.com/cilium/ebpf/types_string.go @@ -0,0 +1,168 @@ +// Code generated by "stringer -output types_string.go -type=MapType,ProgramType,AttachType,PinType"; DO NOT EDIT. + +package ebpf + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[UnspecifiedMap-0] + _ = x[Hash-1] + _ = x[Array-2] + _ = x[ProgramArray-3] + _ = x[PerfEventArray-4] + _ = x[PerCPUHash-5] + _ = x[PerCPUArray-6] + _ = x[StackTrace-7] + _ = x[CGroupArray-8] + _ = x[LRUHash-9] + _ = x[LRUCPUHash-10] + _ = x[LPMTrie-11] + _ = x[ArrayOfMaps-12] + _ = x[HashOfMaps-13] + _ = x[DevMap-14] + _ = x[SockMap-15] + _ = x[CPUMap-16] + _ = x[XSKMap-17] + _ = x[SockHash-18] + _ = x[CGroupStorage-19] + _ = x[ReusePortSockArray-20] + _ = x[PerCPUCGroupStorage-21] + _ = x[Queue-22] + _ = x[Stack-23] + _ = x[SkStorage-24] + _ = x[DevMapHash-25] +} + +const _MapType_name = "UnspecifiedMapHashArrayProgramArrayPerfEventArrayPerCPUHashPerCPUArrayStackTraceCGroupArrayLRUHashLRUCPUHashLPMTrieArrayOfMapsHashOfMapsDevMapSockMapCPUMapXSKMapSockHashCGroupStorageReusePortSockArrayPerCPUCGroupStorageQueueStackSkStorageDevMapHash" + +var _MapType_index = [...]uint8{0, 14, 18, 23, 35, 49, 59, 70, 80, 91, 98, 108, 115, 126, 136, 142, 149, 155, 161, 169, 182, 200, 219, 224, 229, 238, 248} + +func (i MapType) String() string { + if i >= MapType(len(_MapType_index)-1) { + return "MapType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _MapType_name[_MapType_index[i]:_MapType_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[UnspecifiedProgram-0] + _ = x[SocketFilter-1] + _ = x[Kprobe-2] + _ = x[SchedCLS-3] + _ = x[SchedACT-4] + _ = x[TracePoint-5] + _ = x[XDP-6] + _ = x[PerfEvent-7] + _ = x[CGroupSKB-8] + _ = x[CGroupSock-9] + _ = x[LWTIn-10] + _ = x[LWTOut-11] + _ = x[LWTXmit-12] + _ = x[SockOps-13] + _ = x[SkSKB-14] + _ = x[CGroupDevice-15] + _ = x[SkMsg-16] + _ = x[RawTracepoint-17] + _ = x[CGroupSockAddr-18] + _ = x[LWTSeg6Local-19] + _ = x[LircMode2-20] + _ = x[SkReuseport-21] + _ = x[FlowDissector-22] + _ = x[CGroupSysctl-23] + _ = x[RawTracepointWritable-24] + _ = x[CGroupSockopt-25] + _ = x[Tracing-26] + _ = x[StructOps-27] + _ = x[Extension-28] + _ = x[LSM-29] + _ = x[SkLookup-30] +} + +const _ProgramType_name = "UnspecifiedProgramSocketFilterKprobeSchedCLSSchedACTTracePointXDPPerfEventCGroupSKBCGroupSockLWTInLWTOutLWTXmitSockOpsSkSKBCGroupDeviceSkMsgRawTracepointCGroupSockAddrLWTSeg6LocalLircMode2SkReuseportFlowDissectorCGroupSysctlRawTracepointWritableCGroupSockoptTracingStructOpsExtensionLSMSkLookup" + +var _ProgramType_index = [...]uint16{0, 18, 30, 36, 44, 52, 62, 65, 74, 83, 93, 98, 104, 111, 118, 123, 135, 140, 153, 167, 179, 188, 199, 212, 224, 245, 258, 265, 274, 283, 286, 294} + +func (i ProgramType) String() string { + if i >= ProgramType(len(_ProgramType_index)-1) { + return "ProgramType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ProgramType_name[_ProgramType_index[i]:_ProgramType_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[AttachNone-0] + _ = x[AttachCGroupInetIngress-0] + _ = x[AttachCGroupInetEgress-1] + _ = x[AttachCGroupInetSockCreate-2] + _ = x[AttachCGroupSockOps-3] + _ = x[AttachSkSKBStreamParser-4] + _ = x[AttachSkSKBStreamVerdict-5] + _ = x[AttachCGroupDevice-6] + _ = x[AttachSkMsgVerdict-7] + _ = x[AttachCGroupInet4Bind-8] + _ = x[AttachCGroupInet6Bind-9] + _ = x[AttachCGroupInet4Connect-10] + _ = x[AttachCGroupInet6Connect-11] + _ = x[AttachCGroupInet4PostBind-12] + _ = x[AttachCGroupInet6PostBind-13] + _ = x[AttachCGroupUDP4Sendmsg-14] + _ = x[AttachCGroupUDP6Sendmsg-15] + _ = x[AttachLircMode2-16] + _ = x[AttachFlowDissector-17] + _ = x[AttachCGroupSysctl-18] + _ = x[AttachCGroupUDP4Recvmsg-19] + _ = x[AttachCGroupUDP6Recvmsg-20] + _ = x[AttachCGroupGetsockopt-21] + _ = x[AttachCGroupSetsockopt-22] + _ = x[AttachTraceRawTp-23] + _ = x[AttachTraceFEntry-24] + _ = x[AttachTraceFExit-25] + _ = x[AttachModifyReturn-26] + _ = x[AttachLSMMac-27] + _ = x[AttachTraceIter-28] + _ = x[AttachCgroupInet4GetPeername-29] + _ = x[AttachCgroupInet6GetPeername-30] + _ = x[AttachCgroupInet4GetSockname-31] + _ = x[AttachCgroupInet6GetSockname-32] + _ = x[AttachXDPDevMap-33] + _ = x[AttachCgroupInetSockRelease-34] + _ = x[AttachXDPCPUMap-35] + _ = x[AttachSkLookup-36] + _ = x[AttachXDP-37] +} + +const _AttachType_name = "AttachNoneAttachCGroupInetEgressAttachCGroupInetSockCreateAttachCGroupSockOpsAttachSkSKBStreamParserAttachSkSKBStreamVerdictAttachCGroupDeviceAttachSkMsgVerdictAttachCGroupInet4BindAttachCGroupInet6BindAttachCGroupInet4ConnectAttachCGroupInet6ConnectAttachCGroupInet4PostBindAttachCGroupInet6PostBindAttachCGroupUDP4SendmsgAttachCGroupUDP6SendmsgAttachLircMode2AttachFlowDissectorAttachCGroupSysctlAttachCGroupUDP4RecvmsgAttachCGroupUDP6RecvmsgAttachCGroupGetsockoptAttachCGroupSetsockoptAttachTraceRawTpAttachTraceFEntryAttachTraceFExitAttachModifyReturnAttachLSMMacAttachTraceIterAttachCgroupInet4GetPeernameAttachCgroupInet6GetPeernameAttachCgroupInet4GetSocknameAttachCgroupInet6GetSocknameAttachXDPDevMapAttachCgroupInetSockReleaseAttachXDPCPUMapAttachSkLookupAttachXDP" + +var _AttachType_index = [...]uint16{0, 10, 32, 58, 77, 100, 124, 142, 160, 181, 202, 226, 250, 275, 300, 323, 346, 361, 380, 398, 421, 444, 466, 488, 504, 521, 537, 555, 567, 582, 610, 638, 666, 694, 709, 736, 751, 765, 774} + +func (i AttachType) String() string { + if i >= AttachType(len(_AttachType_index)-1) { + return "AttachType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _AttachType_name[_AttachType_index[i]:_AttachType_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PinNone-0] + _ = x[PinByName-1] +} + +const _PinType_name = "PinNonePinByName" + +var _PinType_index = [...]uint8{0, 7, 16} + +func (i PinType) String() string { + if i < 0 || i >= PinType(len(_PinType_index)-1) { + return "PinType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PinType_name[_PinType_index[i]:_PinType_index[i+1]] +} diff --git a/agent/vendor/github.com/containerd/cgroups/.travis.yml b/agent/vendor/github.com/containerd/cgroups/.travis.yml deleted file mode 100644 index 2b22c37aa88..00000000000 --- a/agent/vendor/github.com/containerd/cgroups/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -go: - - 1.8.x - -install: - - mkdir -p $GOPATH/src/github.com/prometheus $GOPATH/src/github.com/opencontainers - - cd $GOPATH/src/github.com/opencontainers && git clone https://github.com/opencontainers/runtime-spec && cd runtime-spec && git checkout 198f23f827eea397d4331d7eb048d9d4c7ff7bee - - cd $GOPATH/src/github.com/containerd/cgroups - - go get -t ./... - -script: - - go test -race -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/agent/vendor/github.com/containerd/cgroups/Vagrantfile b/agent/vendor/github.com/containerd/cgroups/Vagrantfile index 4596ad8a7d4..9a4aac8cb34 100644 --- a/agent/vendor/github.com/containerd/cgroups/Vagrantfile +++ b/agent/vendor/github.com/containerd/cgroups/Vagrantfile @@ -3,19 +3,19 @@ Vagrant.configure("2") do |config| # Fedora box is used for testing cgroup v2 support - config.vm.box = "fedora/32-cloud-base" + config.vm.box = "fedora/35-cloud-base" config.vm.provider :virtualbox do |v| - v.memory = 2048 + v.memory = 4096 v.cpus = 2 end config.vm.provider :libvirt do |v| - v.memory = 2048 + v.memory = 4096 v.cpus = 2 end config.vm.provision "shell", inline: <<-SHELL set -eux -o pipefail # configuration - GO_VERSION="1.15" + GO_VERSION="1.17.7" # install gcc and Golang dnf -y install gcc diff --git a/agent/vendor/github.com/containerd/cgroups/stats.go b/agent/vendor/github.com/containerd/cgroups/stats.go deleted file mode 100644 index 47fbfa96b5d..00000000000 --- a/agent/vendor/github.com/containerd/cgroups/stats.go +++ /dev/null @@ -1,109 +0,0 @@ -package cgroups - -import "sync" - -type Stats struct { - cpuMu sync.Mutex - - Hugetlb map[string]HugetlbStat - Pids *PidsStat - Cpu *CpuStat - Memory *MemoryStat - Blkio *BlkioStat -} - -type HugetlbStat struct { - Usage uint64 - Max uint64 - Failcnt uint64 -} - -type PidsStat struct { - Current uint64 - Limit uint64 -} - -type CpuStat struct { - Usage CpuUsage - Throttling Throttle -} - -type CpuUsage struct { - // Units: nanoseconds. - Total uint64 - PerCpu []uint64 - Kernel uint64 - User uint64 -} - -type Throttle struct { - Periods uint64 - ThrottledPeriods uint64 - ThrottledTime uint64 -} - -type MemoryStat struct { - Cache uint64 - RSS uint64 - RSSHuge uint64 - MappedFile uint64 - Dirty uint64 - Writeback uint64 - PgPgIn uint64 - PgPgOut uint64 - PgFault uint64 - PgMajFault uint64 - InactiveAnon uint64 - ActiveAnon uint64 - InactiveFile uint64 - ActiveFile uint64 - Unevictable uint64 - HierarchicalMemoryLimit uint64 - HierarchicalSwapLimit uint64 - TotalCache uint64 - TotalRSS uint64 - TotalRSSHuge uint64 - TotalMappedFile uint64 - TotalDirty uint64 - TotalWriteback uint64 - TotalPgPgIn uint64 - TotalPgPgOut uint64 - TotalPgFault uint64 - TotalPgMajFault uint64 - TotalInactiveAnon uint64 - TotalActiveAnon uint64 - TotalInactiveFile uint64 - TotalActiveFile uint64 - TotalUnevictable uint64 - - Usage MemoryEntry - Swap MemoryEntry - Kernel MemoryEntry - KernelTCP MemoryEntry -} - -type MemoryEntry struct { - Limit uint64 - Usage uint64 - Max uint64 - Failcnt uint64 -} - -type BlkioStat struct { - IoServiceBytesRecursive []BlkioEntry - IoServicedRecursive []BlkioEntry - IoQueuedRecursive []BlkioEntry - IoServiceTimeRecursive []BlkioEntry - IoWaitTimeRecursive []BlkioEntry - IoMergedRecursive []BlkioEntry - IoTimeRecursive []BlkioEntry - SectorsRecursive []BlkioEntry -} - -type BlkioEntry struct { - Op string - Device string - Major uint64 - Minor uint64 - Value uint64 -} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/cpu.go b/agent/vendor/github.com/containerd/cgroups/v2/cpu.go new file mode 100644 index 00000000000..65282ff082d --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/cpu.go @@ -0,0 +1,83 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "math" + "strconv" + "strings" +) + +type CPUMax string + +func NewCPUMax(quota *int64, period *uint64) CPUMax { + max := "max" + if quota != nil { + max = strconv.FormatInt(*quota, 10) + } + return CPUMax(strings.Join([]string{max, strconv.FormatUint(*period, 10)}, " ")) +} + +type CPU struct { + Weight *uint64 + Max CPUMax + Cpus string + Mems string +} + +func (c CPUMax) extractQuotaAndPeriod() (int64, uint64) { + var ( + quota int64 + period uint64 + ) + values := strings.Split(string(c), " ") + if values[0] == "max" { + quota = math.MaxInt64 + } else { + quota, _ = strconv.ParseInt(values[0], 10, 64) + } + period, _ = strconv.ParseUint(values[1], 10, 64) + return quota, period +} + +func (r *CPU) Values() (o []Value) { + if r.Weight != nil { + o = append(o, Value{ + filename: "cpu.weight", + value: *r.Weight, + }) + } + if r.Max != "" { + o = append(o, Value{ + filename: "cpu.max", + value: r.Max, + }) + } + if r.Cpus != "" { + o = append(o, Value{ + filename: "cpuset.cpus", + value: r.Cpus, + }) + } + if r.Mems != "" { + o = append(o, Value{ + filename: "cpuset.mems", + value: r.Mems, + }) + } + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/devicefilter.go b/agent/vendor/github.com/containerd/cgroups/v2/devicefilter.go new file mode 100644 index 00000000000..0882036c2dc --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/devicefilter.go @@ -0,0 +1,200 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Devicefilter containes eBPF device filter program +// +// The implementation is based on https://github.com/containers/crun/blob/0.10.2/src/libcrun/ebpf.c +// +// Although ebpf.c is originally licensed under LGPL-3.0-or-later, the author (Giuseppe Scrivano) +// agreed to relicense the file in Apache License 2.0: https://github.com/opencontainers/runc/issues/2144#issuecomment-543116397 +// +// This particular Go implementation based on runc version +// https://github.com/opencontainers/runc/blob/master/libcontainer/cgroups/ebpf/devicefilter/devicefilter.go + +package v2 + +import ( + "errors" + "fmt" + "math" + + "github.com/cilium/ebpf/asm" + "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) + +const ( + // license string format is same as kernel MODULE_LICENSE macro + license = "Apache" +) + +// DeviceFilter returns eBPF device filter program and its license string +func DeviceFilter(devices []specs.LinuxDeviceCgroup) (asm.Instructions, string, error) { + p := &program{} + p.init() + for i := len(devices) - 1; i >= 0; i-- { + if err := p.appendDevice(devices[i]); err != nil { + return nil, "", err + } + } + insts, err := p.finalize() + return insts, license, err +} + +type program struct { + insts asm.Instructions + hasWildCard bool + blockID int +} + +func (p *program) init() { + // struct bpf_cgroup_dev_ctx: https://elixir.bootlin.com/linux/v5.3.6/source/include/uapi/linux/bpf.h#L3423 + /* + u32 access_type + u32 major + u32 minor + */ + // R2 <- type (lower 16 bit of u32 access_type at R1[0]) + p.insts = append(p.insts, + asm.LoadMem(asm.R2, asm.R1, 0, asm.Half)) + + // R3 <- access (upper 16 bit of u32 access_type at R1[0]) + p.insts = append(p.insts, + asm.LoadMem(asm.R3, asm.R1, 0, asm.Word), + // RSh: bitwise shift right + asm.RSh.Imm32(asm.R3, 16)) + + // R4 <- major (u32 major at R1[4]) + p.insts = append(p.insts, + asm.LoadMem(asm.R4, asm.R1, 4, asm.Word)) + + // R5 <- minor (u32 minor at R1[8]) + p.insts = append(p.insts, + asm.LoadMem(asm.R5, asm.R1, 8, asm.Word)) +} + +// appendDevice needs to be called from the last element of OCI linux.resources.devices to the head element. +func (p *program) appendDevice(dev specs.LinuxDeviceCgroup) error { + if p.blockID < 0 { + return errors.New("the program is finalized") + } + if p.hasWildCard { + // All entries after wildcard entry are ignored + return nil + } + + bpfType := int32(-1) + hasType := true + switch dev.Type { + case string('c'): + bpfType = int32(unix.BPF_DEVCG_DEV_CHAR) + case string('b'): + bpfType = int32(unix.BPF_DEVCG_DEV_BLOCK) + case string('a'): + hasType = false + default: + // if not specified in OCI json, typ is set to DeviceTypeAll + return fmt.Errorf("invalid DeviceType %q", dev.Type) + } + if *dev.Major > math.MaxUint32 { + return fmt.Errorf("invalid major %d", *dev.Major) + } + if *dev.Minor > math.MaxUint32 { + return fmt.Errorf("invalid minor %d", *dev.Major) + } + hasMajor := *dev.Major >= 0 // if not specified in OCI json, major is set to -1 + hasMinor := *dev.Minor >= 0 + bpfAccess := int32(0) + for _, r := range dev.Access { + switch r { + case 'r': + bpfAccess |= unix.BPF_DEVCG_ACC_READ + case 'w': + bpfAccess |= unix.BPF_DEVCG_ACC_WRITE + case 'm': + bpfAccess |= unix.BPF_DEVCG_ACC_MKNOD + default: + return fmt.Errorf("unknown device access %v", r) + } + } + // If the access is rwm, skip the check. + hasAccess := bpfAccess != (unix.BPF_DEVCG_ACC_READ | unix.BPF_DEVCG_ACC_WRITE | unix.BPF_DEVCG_ACC_MKNOD) + + blockSym := fmt.Sprintf("block-%d", p.blockID) + nextBlockSym := fmt.Sprintf("block-%d", p.blockID+1) + prevBlockLastIdx := len(p.insts) - 1 + if hasType { + p.insts = append(p.insts, + // if (R2 != bpfType) goto next + asm.JNE.Imm(asm.R2, bpfType, nextBlockSym), + ) + } + if hasAccess { + p.insts = append(p.insts, + // if (R3 & bpfAccess == 0 /* use R1 as a temp var */) goto next + asm.Mov.Reg32(asm.R1, asm.R3), + asm.And.Imm32(asm.R1, bpfAccess), + asm.JEq.Imm(asm.R1, 0, nextBlockSym), + ) + } + if hasMajor { + p.insts = append(p.insts, + // if (R4 != major) goto next + asm.JNE.Imm(asm.R4, int32(*dev.Major), nextBlockSym), + ) + } + if hasMinor { + p.insts = append(p.insts, + // if (R5 != minor) goto next + asm.JNE.Imm(asm.R5, int32(*dev.Minor), nextBlockSym), + ) + } + if !hasType && !hasAccess && !hasMajor && !hasMinor { + p.hasWildCard = true + } + p.insts = append(p.insts, acceptBlock(dev.Allow)...) + // set blockSym to the first instruction we added in this iteration + p.insts[prevBlockLastIdx+1] = p.insts[prevBlockLastIdx+1].Sym(blockSym) + p.blockID++ + return nil +} + +func (p *program) finalize() (asm.Instructions, error) { + if p.hasWildCard { + // acceptBlock with asm.Return() is already inserted + return p.insts, nil + } + blockSym := fmt.Sprintf("block-%d", p.blockID) + p.insts = append(p.insts, + // R0 <- 0 + asm.Mov.Imm32(asm.R0, 0).Sym(blockSym), + asm.Return(), + ) + p.blockID = -1 + return p.insts, nil +} + +func acceptBlock(accept bool) asm.Instructions { + v := int32(0) + if accept { + v = 1 + } + return []asm.Instruction{ + // R0 <- v + asm.Mov.Imm32(asm.R0, v), + asm.Return(), + } +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/ebpf.go b/agent/vendor/github.com/containerd/cgroups/v2/ebpf.go new file mode 100644 index 00000000000..45bf5f99e37 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/ebpf.go @@ -0,0 +1,96 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/link" + "github.com/opencontainers/runtime-spec/specs-go" + "golang.org/x/sys/unix" +) + +// LoadAttachCgroupDeviceFilter installs eBPF device filter program to /sys/fs/cgroup/ directory. +// +// Requires the system to be running in cgroup2 unified-mode with kernel >= 4.15 . +// +// https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92 +func LoadAttachCgroupDeviceFilter(insts asm.Instructions, license string, dirFD int) (func() error, error) { + nilCloser := func() error { + return nil + } + spec := &ebpf.ProgramSpec{ + Type: ebpf.CGroupDevice, + Instructions: insts, + License: license, + } + prog, err := ebpf.NewProgram(spec) + if err != nil { + return nilCloser, err + } + err = link.RawAttachProgram(link.RawAttachProgramOptions{ + Target: dirFD, + Program: prog, + Attach: ebpf.AttachCGroupDevice, + Flags: unix.BPF_F_ALLOW_MULTI, + }) + if err != nil { + return nilCloser, fmt.Errorf("failed to call BPF_PROG_ATTACH (BPF_CGROUP_DEVICE, BPF_F_ALLOW_MULTI): %w", err) + } + closer := func() error { + err = link.RawDetachProgram(link.RawDetachProgramOptions{ + Target: dirFD, + Program: prog, + Attach: ebpf.AttachCGroupDevice, + }) + if err != nil { + return fmt.Errorf("failed to call BPF_PROG_DETACH (BPF_CGROUP_DEVICE): %w", err) + } + return nil + } + return closer, nil +} + +func isRWM(cgroupPermissions string) bool { + r := false + w := false + m := false + for _, rn := range cgroupPermissions { + switch rn { + case 'r': + r = true + case 'w': + w = true + case 'm': + m = true + } + } + return r && w && m +} + +// the logic is from runc +// https://github.com/opencontainers/runc/blob/master/libcontainer/cgroups/fs/devices_v2.go#L44 +func canSkipEBPFError(devices []specs.LinuxDeviceCgroup) bool { + for _, dev := range devices { + if dev.Allow || !isRWM(dev.Access) { + return false + } + } + return true +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/errors.go b/agent/vendor/github.com/containerd/cgroups/v2/errors.go new file mode 100644 index 00000000000..eeae362b279 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/errors.go @@ -0,0 +1,26 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "errors" +) + +var ( + ErrInvalidFormat = errors.New("cgroups: parsing file with invalid format failed") + ErrInvalidGroupPath = errors.New("cgroups: invalid group path") +) diff --git a/agent/vendor/github.com/containerd/cgroups/v2/hugetlb.go b/agent/vendor/github.com/containerd/cgroups/v2/hugetlb.go new file mode 100644 index 00000000000..16b35bd780b --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/hugetlb.go @@ -0,0 +1,37 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import "strings" + +type HugeTlb []HugeTlbEntry + +type HugeTlbEntry struct { + HugePageSize string + Limit uint64 +} + +func (r *HugeTlb) Values() (o []Value) { + for _, e := range *r { + o = append(o, Value{ + filename: strings.Join([]string{"hugetlb", e.HugePageSize, "max"}, "."), + value: e.Limit, + }) + } + + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/io.go b/agent/vendor/github.com/containerd/cgroups/v2/io.go new file mode 100644 index 00000000000..70078d576ec --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/io.go @@ -0,0 +1,64 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import "fmt" + +type IOType string + +const ( + ReadBPS IOType = "rbps" + WriteBPS IOType = "wbps" + ReadIOPS IOType = "riops" + WriteIOPS IOType = "wiops" +) + +type BFQ struct { + Weight uint16 +} + +type Entry struct { + Type IOType + Major int64 + Minor int64 + Rate uint64 +} + +func (e Entry) String() string { + return fmt.Sprintf("%d:%d %s=%d", e.Major, e.Minor, e.Type, e.Rate) +} + +type IO struct { + BFQ BFQ + Max []Entry +} + +func (i *IO) Values() (o []Value) { + if i.BFQ.Weight != 0 { + o = append(o, Value{ + filename: "io.bfq.weight", + value: i.BFQ.Weight, + }) + } + for _, e := range i.Max { + o = append(o, Value{ + filename: "io.max", + value: e.String(), + }) + } + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/manager.go b/agent/vendor/github.com/containerd/cgroups/v2/manager.go new file mode 100644 index 00000000000..c08d9a7db25 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/manager.go @@ -0,0 +1,854 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "bufio" + "context" + "errors" + "fmt" + "io/ioutil" + "math" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/containerd/cgroups/v2/stats" + + systemdDbus "github.com/coreos/go-systemd/v22/dbus" + "github.com/godbus/dbus/v5" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +const ( + subtreeControl = "cgroup.subtree_control" + controllersFile = "cgroup.controllers" + defaultCgroup2Path = "/sys/fs/cgroup" + defaultSlice = "system.slice" +) + +var ( + canDelegate bool +) + +type Event struct { + Low uint64 + High uint64 + Max uint64 + OOM uint64 + OOMKill uint64 +} + +// Resources for a cgroups v2 unified hierarchy +type Resources struct { + CPU *CPU + Memory *Memory + Pids *Pids + IO *IO + RDMA *RDMA + HugeTlb *HugeTlb + // When len(Devices) is zero, devices are not controlled + Devices []specs.LinuxDeviceCgroup +} + +// Values returns the raw filenames and values that +// can be written to the unified hierarchy +func (r *Resources) Values() (o []Value) { + if r.CPU != nil { + o = append(o, r.CPU.Values()...) + } + if r.Memory != nil { + o = append(o, r.Memory.Values()...) + } + if r.Pids != nil { + o = append(o, r.Pids.Values()...) + } + if r.IO != nil { + o = append(o, r.IO.Values()...) + } + if r.RDMA != nil { + o = append(o, r.RDMA.Values()...) + } + if r.HugeTlb != nil { + o = append(o, r.HugeTlb.Values()...) + } + return o +} + +// EnabledControllers returns the list of all not nil resource controllers +func (r *Resources) EnabledControllers() (c []string) { + if r.CPU != nil { + c = append(c, "cpu") + c = append(c, "cpuset") + } + if r.Memory != nil { + c = append(c, "memory") + } + if r.Pids != nil { + c = append(c, "pids") + } + if r.IO != nil { + c = append(c, "io") + } + if r.RDMA != nil { + c = append(c, "rdma") + } + if r.HugeTlb != nil { + c = append(c, "hugetlb") + } + return +} + +// Value of a cgroup setting +type Value struct { + filename string + value interface{} +} + +// write the value to the full, absolute path, of a unified hierarchy +func (c *Value) write(path string, perm os.FileMode) error { + var data []byte + switch t := c.value.(type) { + case uint64: + data = []byte(strconv.FormatUint(t, 10)) + case uint16: + data = []byte(strconv.FormatUint(uint64(t), 10)) + case int64: + data = []byte(strconv.FormatInt(t, 10)) + case []byte: + data = t + case string: + data = []byte(t) + case CPUMax: + data = []byte(t) + default: + return ErrInvalidFormat + } + + // Retry writes on EINTR; see: + // https://github.com/golang/go/issues/38033 + for { + err := ioutil.WriteFile( + filepath.Join(path, c.filename), + data, + perm, + ) + if err == nil { + return nil + } else if !errors.Is(err, syscall.EINTR) { + return err + } + } +} + +func writeValues(path string, values []Value) error { + for _, o := range values { + if err := o.write(path, defaultFilePerm); err != nil { + return err + } + } + return nil +} + +func NewManager(mountpoint string, group string, resources *Resources) (*Manager, error) { + if resources == nil { + return nil, errors.New("resources reference is nil") + } + if err := VerifyGroupPath(group); err != nil { + return nil, err + } + path := filepath.Join(mountpoint, group) + if err := os.MkdirAll(path, defaultDirPerm); err != nil { + return nil, err + } + m := Manager{ + unifiedMountpoint: mountpoint, + path: path, + } + if err := m.ToggleControllers(resources.EnabledControllers(), Enable); err != nil { + // clean up cgroup dir on failure + os.Remove(path) + return nil, err + } + if err := setResources(path, resources); err != nil { + os.Remove(path) + return nil, err + } + return &m, nil +} + +func LoadManager(mountpoint string, group string) (*Manager, error) { + if err := VerifyGroupPath(group); err != nil { + return nil, err + } + path := filepath.Join(mountpoint, group) + return &Manager{ + unifiedMountpoint: mountpoint, + path: path, + }, nil +} + +type Manager struct { + unifiedMountpoint string + path string +} + +func setResources(path string, resources *Resources) error { + if resources != nil { + if err := writeValues(path, resources.Values()); err != nil { + return err + } + if err := setDevices(path, resources.Devices); err != nil { + return err + } + } + return nil +} + +func (c *Manager) RootControllers() ([]string, error) { + b, err := ioutil.ReadFile(filepath.Join(c.unifiedMountpoint, controllersFile)) + if err != nil { + return nil, err + } + return strings.Fields(string(b)), nil +} + +func (c *Manager) Controllers() ([]string, error) { + b, err := ioutil.ReadFile(filepath.Join(c.path, controllersFile)) + if err != nil { + return nil, err + } + return strings.Fields(string(b)), nil +} + +type ControllerToggle int + +const ( + Enable ControllerToggle = iota + 1 + Disable +) + +func toggleFunc(controllers []string, prefix string) []string { + out := make([]string, len(controllers)) + for i, c := range controllers { + out[i] = prefix + c + } + return out +} + +func (c *Manager) ToggleControllers(controllers []string, t ControllerToggle) error { + // when c.path is like /foo/bar/baz, the following files need to be written: + // * /sys/fs/cgroup/cgroup.subtree_control + // * /sys/fs/cgroup/foo/cgroup.subtree_control + // * /sys/fs/cgroup/foo/bar/cgroup.subtree_control + // Note that /sys/fs/cgroup/foo/bar/baz/cgroup.subtree_control does not need to be written. + split := strings.Split(c.path, "/") + var lastErr error + for i := range split { + f := strings.Join(split[:i], "/") + if !strings.HasPrefix(f, c.unifiedMountpoint) || f == c.path { + continue + } + filePath := filepath.Join(f, subtreeControl) + if err := c.writeSubtreeControl(filePath, controllers, t); err != nil { + // When running as rootless, the user may face EPERM on parent groups, but it is neglible when the + // controller is already written. + // So we only return the last error. + lastErr = fmt.Errorf("failed to write subtree controllers %+v to %q: %w", controllers, filePath, err) + } else { + lastErr = nil + } + } + return lastErr +} + +func (c *Manager) writeSubtreeControl(filePath string, controllers []string, t ControllerToggle) error { + f, err := os.OpenFile(filePath, os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + switch t { + case Enable: + controllers = toggleFunc(controllers, "+") + case Disable: + controllers = toggleFunc(controllers, "-") + } + _, err = f.WriteString(strings.Join(controllers, " ")) + return err +} + +func (c *Manager) NewChild(name string, resources *Resources) (*Manager, error) { + if strings.HasPrefix(name, "/") { + return nil, errors.New("name must be relative") + } + path := filepath.Join(c.path, name) + if err := os.MkdirAll(path, defaultDirPerm); err != nil { + return nil, err + } + m := Manager{ + unifiedMountpoint: c.unifiedMountpoint, + path: path, + } + if resources != nil { + if err := m.ToggleControllers(resources.EnabledControllers(), Enable); err != nil { + // clean up cgroup dir on failure + os.Remove(path) + return nil, err + } + } + if err := setResources(path, resources); err != nil { + // clean up cgroup dir on failure + os.Remove(path) + return nil, err + } + return &m, nil +} + +func (c *Manager) AddProc(pid uint64) error { + v := Value{ + filename: cgroupProcs, + value: pid, + } + return writeValues(c.path, []Value{v}) +} + +func (c *Manager) Delete() error { + return remove(c.path) +} + +func (c *Manager) Procs(recursive bool) ([]uint64, error) { + var processes []uint64 + err := filepath.Walk(c.path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !recursive && info.IsDir() { + if p == c.path { + return nil + } + return filepath.SkipDir + } + _, name := filepath.Split(p) + if name != cgroupProcs { + return nil + } + procs, err := parseCgroupProcsFile(p) + if err != nil { + return err + } + processes = append(processes, procs...) + return nil + }) + return processes, err +} + +var singleValueFiles = []string{ + "pids.current", + "pids.max", +} + +func (c *Manager) Stat() (*stats.Metrics, error) { + controllers, err := c.Controllers() + if err != nil { + return nil, err + } + out := make(map[string]interface{}) + for _, controller := range controllers { + switch controller { + case "cpu", "memory": + if err := readKVStatsFile(c.path, controller+".stat", out); err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + } + } + for _, name := range singleValueFiles { + if err := readSingleFile(c.path, name, out); err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + } + memoryEvents := make(map[string]interface{}) + if err := readKVStatsFile(c.path, "memory.events", memoryEvents); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + } + var metrics stats.Metrics + + metrics.Pids = &stats.PidsStat{ + Current: getPidValue("pids.current", out), + Limit: getPidValue("pids.max", out), + } + metrics.CPU = &stats.CPUStat{ + UsageUsec: getUint64Value("usage_usec", out), + UserUsec: getUint64Value("user_usec", out), + SystemUsec: getUint64Value("system_usec", out), + NrPeriods: getUint64Value("nr_periods", out), + NrThrottled: getUint64Value("nr_throttled", out), + ThrottledUsec: getUint64Value("throttled_usec", out), + } + metrics.Memory = &stats.MemoryStat{ + Anon: getUint64Value("anon", out), + File: getUint64Value("file", out), + KernelStack: getUint64Value("kernel_stack", out), + Slab: getUint64Value("slab", out), + Sock: getUint64Value("sock", out), + Shmem: getUint64Value("shmem", out), + FileMapped: getUint64Value("file_mapped", out), + FileDirty: getUint64Value("file_dirty", out), + FileWriteback: getUint64Value("file_writeback", out), + AnonThp: getUint64Value("anon_thp", out), + InactiveAnon: getUint64Value("inactive_anon", out), + ActiveAnon: getUint64Value("active_anon", out), + InactiveFile: getUint64Value("inactive_file", out), + ActiveFile: getUint64Value("active_file", out), + Unevictable: getUint64Value("unevictable", out), + SlabReclaimable: getUint64Value("slab_reclaimable", out), + SlabUnreclaimable: getUint64Value("slab_unreclaimable", out), + Pgfault: getUint64Value("pgfault", out), + Pgmajfault: getUint64Value("pgmajfault", out), + WorkingsetRefault: getUint64Value("workingset_refault", out), + WorkingsetActivate: getUint64Value("workingset_activate", out), + WorkingsetNodereclaim: getUint64Value("workingset_nodereclaim", out), + Pgrefill: getUint64Value("pgrefill", out), + Pgscan: getUint64Value("pgscan", out), + Pgsteal: getUint64Value("pgsteal", out), + Pgactivate: getUint64Value("pgactivate", out), + Pgdeactivate: getUint64Value("pgdeactivate", out), + Pglazyfree: getUint64Value("pglazyfree", out), + Pglazyfreed: getUint64Value("pglazyfreed", out), + ThpFaultAlloc: getUint64Value("thp_fault_alloc", out), + ThpCollapseAlloc: getUint64Value("thp_collapse_alloc", out), + Usage: getStatFileContentUint64(filepath.Join(c.path, "memory.current")), + UsageLimit: getStatFileContentUint64(filepath.Join(c.path, "memory.max")), + SwapUsage: getStatFileContentUint64(filepath.Join(c.path, "memory.swap.current")), + SwapLimit: getStatFileContentUint64(filepath.Join(c.path, "memory.swap.max")), + } + if len(memoryEvents) > 0 { + metrics.MemoryEvents = &stats.MemoryEvents{ + Low: getUint64Value("low", memoryEvents), + High: getUint64Value("high", memoryEvents), + Max: getUint64Value("max", memoryEvents), + Oom: getUint64Value("oom", memoryEvents), + OomKill: getUint64Value("oom_kill", memoryEvents), + } + } + metrics.Io = &stats.IOStat{Usage: readIoStats(c.path)} + metrics.Rdma = &stats.RdmaStat{ + Current: rdmaStats(filepath.Join(c.path, "rdma.current")), + Limit: rdmaStats(filepath.Join(c.path, "rdma.max")), + } + metrics.Hugetlb = readHugeTlbStats(c.path) + + return &metrics, nil +} + +func getUint64Value(key string, out map[string]interface{}) uint64 { + v, ok := out[key] + if !ok { + return 0 + } + switch t := v.(type) { + case uint64: + return t + } + return 0 +} + +func getPidValue(key string, out map[string]interface{}) uint64 { + v, ok := out[key] + if !ok { + return 0 + } + switch t := v.(type) { + case uint64: + return t + case string: + if t == "max" { + return math.MaxUint64 + } + } + return 0 +} + +func readSingleFile(path string, file string, out map[string]interface{}) error { + f, err := os.Open(filepath.Join(path, file)) + if err != nil { + return err + } + defer f.Close() + data, err := ioutil.ReadAll(f) + if err != nil { + return err + } + s := strings.TrimSpace(string(data)) + v, err := parseUint(s, 10, 64) + if err != nil { + // if we cannot parse as a uint, parse as a string + out[file] = s + return nil + } + out[file] = v + return nil +} + +func readKVStatsFile(path string, file string, out map[string]interface{}) error { + f, err := os.Open(filepath.Join(path, file)) + if err != nil { + return err + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + name, value, err := parseKV(s.Text()) + if err != nil { + return fmt.Errorf("error while parsing %s (line=%q): %w", filepath.Join(path, file), s.Text(), err) + } + out[name] = value + } + return s.Err() +} + +func (c *Manager) Freeze() error { + return c.freeze(c.path, Frozen) +} + +func (c *Manager) Thaw() error { + return c.freeze(c.path, Thawed) +} + +func (c *Manager) freeze(path string, state State) error { + values := state.Values() + for { + if err := writeValues(path, values); err != nil { + return err + } + current, err := fetchState(path) + if err != nil { + return err + } + if current == state { + return nil + } + time.Sleep(1 * time.Millisecond) + } +} + +func (c *Manager) isCgroupEmpty() bool { + // In case of any error we return true so that we exit and don't leak resources + out := make(map[string]interface{}) + if err := readKVStatsFile(c.path, "cgroup.events", out); err != nil { + return true + } + if v, ok := out["populated"]; ok { + populated, ok := v.(uint64) + if !ok { + return true + } + return populated == 0 + } + return true +} + +// MemoryEventFD returns inotify file descriptor and 'memory.events' inotify watch descriptor +func (c *Manager) MemoryEventFD() (int, uint32, error) { + fpath := filepath.Join(c.path, "memory.events") + fd, err := syscall.InotifyInit() + if err != nil { + return 0, 0, errors.New("failed to create inotify fd") + } + wd, err := syscall.InotifyAddWatch(fd, fpath, unix.IN_MODIFY) + if err != nil { + syscall.Close(fd) + return 0, 0, fmt.Errorf("failed to add inotify watch for %q: %w", fpath, err) + } + // monitor to detect process exit/cgroup deletion + evpath := filepath.Join(c.path, "cgroup.events") + if _, err = syscall.InotifyAddWatch(fd, evpath, unix.IN_MODIFY); err != nil { + syscall.Close(fd) + return 0, 0, fmt.Errorf("failed to add inotify watch for %q: %w", evpath, err) + } + + return fd, uint32(wd), nil +} + +func (c *Manager) EventChan() (<-chan Event, <-chan error) { + ec := make(chan Event) + errCh := make(chan error, 1) + go c.waitForEvents(ec, errCh) + + return ec, errCh +} + +func parseMemoryEvents(out map[string]interface{}) (Event, error) { + e := Event{} + if v, ok := out["high"]; ok { + e.High, ok = v.(uint64) + if !ok { + return Event{}, fmt.Errorf("cannot convert high to uint64: %+v", v) + } + } + if v, ok := out["low"]; ok { + e.Low, ok = v.(uint64) + if !ok { + return Event{}, fmt.Errorf("cannot convert low to uint64: %+v", v) + } + } + if v, ok := out["max"]; ok { + e.Max, ok = v.(uint64) + if !ok { + return Event{}, fmt.Errorf("cannot convert max to uint64: %+v", v) + } + } + if v, ok := out["oom"]; ok { + e.OOM, ok = v.(uint64) + if !ok { + return Event{}, fmt.Errorf("cannot convert oom to uint64: %+v", v) + } + } + if v, ok := out["oom_kill"]; ok { + e.OOMKill, ok = v.(uint64) + if !ok { + return Event{}, fmt.Errorf("cannot convert oom_kill to uint64: %+v", v) + } + } + return e, nil +} + +func (c *Manager) waitForEvents(ec chan<- Event, errCh chan<- error) { + defer close(errCh) + + fd, _, err := c.MemoryEventFD() + if err != nil { + errCh <- err + return + } + defer syscall.Close(fd) + + for { + buffer := make([]byte, syscall.SizeofInotifyEvent*10) + bytesRead, err := syscall.Read(fd, buffer) + if err != nil { + errCh <- err + return + } + if bytesRead >= syscall.SizeofInotifyEvent { + out := make(map[string]interface{}) + if err := readKVStatsFile(c.path, "memory.events", out); err != nil { + // When cgroup is deleted read may return -ENODEV instead of -ENOENT from open. + if _, statErr := os.Lstat(filepath.Join(c.path, "memory.events")); !os.IsNotExist(statErr) { + errCh <- err + } + return + } + e, err := parseMemoryEvents(out) + if err != nil { + errCh <- err + return + } + ec <- e + if c.isCgroupEmpty() { + return + } + } + } +} + +func setDevices(path string, devices []specs.LinuxDeviceCgroup) error { + if len(devices) == 0 { + return nil + } + insts, license, err := DeviceFilter(devices) + if err != nil { + return err + } + dirFD, err := unix.Open(path, unix.O_DIRECTORY|unix.O_RDONLY|unix.O_CLOEXEC, 0600) + if err != nil { + return fmt.Errorf("cannot get dir FD for %s", path) + } + defer unix.Close(dirFD) + if _, err := LoadAttachCgroupDeviceFilter(insts, license, dirFD); err != nil { + if !canSkipEBPFError(devices) { + return err + } + } + return nil +} + +// getSystemdFullPath returns the full systemd path when creating a systemd slice group. +// the reason this is necessary is because the "-" character has a special meaning in +// systemd slice. For example, when creating a slice called "my-group-112233.slice", +// systemd will create a hierarchy like this: +// /sys/fs/cgroup/my.slice/my-group.slice/my-group-112233.slice +func getSystemdFullPath(slice, group string) string { + return filepath.Join(defaultCgroup2Path, dashesToPath(slice), dashesToPath(group)) +} + +// dashesToPath converts a slice name with dashes to it's corresponding systemd filesystem path. +func dashesToPath(in string) string { + path := "" + if strings.HasSuffix(in, ".slice") && strings.Contains(in, "-") { + parts := strings.Split(in, "-") + for i := range parts { + s := strings.Join(parts[0:i+1], "-") + if !strings.HasSuffix(s, ".slice") { + s += ".slice" + } + path = filepath.Join(path, s) + } + } else { + path = filepath.Join(path, in) + } + return path +} + +func NewSystemd(slice, group string, pid int, resources *Resources) (*Manager, error) { + if slice == "" { + slice = defaultSlice + } + ctx := context.TODO() + path := getSystemdFullPath(slice, group) + conn, err := systemdDbus.NewWithContext(ctx) + if err != nil { + return &Manager{}, err + } + defer conn.Close() + + properties := []systemdDbus.Property{ + systemdDbus.PropDescription("cgroup " + group), + newSystemdProperty("DefaultDependencies", false), + newSystemdProperty("MemoryAccounting", true), + newSystemdProperty("CPUAccounting", true), + newSystemdProperty("IOAccounting", true), + } + + // if we create a slice, the parent is defined via a Wants= + if strings.HasSuffix(group, ".slice") { + properties = append(properties, systemdDbus.PropWants(defaultSlice)) + } else { + // otherwise, we use Slice= + properties = append(properties, systemdDbus.PropSlice(defaultSlice)) + } + + // only add pid if its valid, -1 is used w/ general slice creation. + if pid != -1 { + properties = append(properties, newSystemdProperty("PIDs", []uint32{uint32(pid)})) + } + + if resources.Memory != nil && resources.Memory.Max != nil && *resources.Memory.Max != 0 { + properties = append(properties, + newSystemdProperty("MemoryMax", uint64(*resources.Memory.Max))) + } + + if resources.CPU != nil && resources.CPU.Weight != nil && *resources.CPU.Weight != 0 { + properties = append(properties, + newSystemdProperty("CPUWeight", *resources.CPU.Weight)) + } + + if resources.CPU != nil && resources.CPU.Max != "" { + quota, period := resources.CPU.Max.extractQuotaAndPeriod() + // cpu.cfs_quota_us and cpu.cfs_period_us are controlled by systemd. + // corresponds to USEC_INFINITY in systemd + // if USEC_INFINITY is provided, CPUQuota is left unbound by systemd + // always setting a property value ensures we can apply a quota and remove it later + cpuQuotaPerSecUSec := uint64(math.MaxUint64) + if quota > 0 { + // systemd converts CPUQuotaPerSecUSec (microseconds per CPU second) to CPUQuota + // (integer percentage of CPU) internally. This means that if a fractional percent of + // CPU is indicated by Resources.CpuQuota, we need to round up to the nearest + // 10ms (1% of a second) such that child cgroups can set the cpu.cfs_quota_us they expect. + cpuQuotaPerSecUSec = uint64(quota*1000000) / period + if cpuQuotaPerSecUSec%10000 != 0 { + cpuQuotaPerSecUSec = ((cpuQuotaPerSecUSec / 10000) + 1) * 10000 + } + } + properties = append(properties, + newSystemdProperty("CPUQuotaPerSecUSec", cpuQuotaPerSecUSec)) + } + + // If we can delegate, we add the property back in + if canDelegate { + properties = append(properties, newSystemdProperty("Delegate", true)) + } + + if resources.Pids != nil && resources.Pids.Max > 0 { + properties = append(properties, + newSystemdProperty("TasksAccounting", true), + newSystemdProperty("TasksMax", uint64(resources.Pids.Max))) + } + + statusChan := make(chan string, 1) + if _, err := conn.StartTransientUnitContext(ctx, group, "replace", properties, statusChan); err == nil { + select { + case <-statusChan: + case <-time.After(time.Second): + logrus.Warnf("Timed out while waiting for StartTransientUnit(%s) completion signal from dbus. Continuing...", group) + } + } else if !isUnitExists(err) { + return &Manager{}, err + } + + return &Manager{ + path: path, + }, nil +} + +func LoadSystemd(slice, group string) (*Manager, error) { + if slice == "" { + slice = defaultSlice + } + path := getSystemdFullPath(slice, group) + return &Manager{ + path: path, + }, nil +} + +func (c *Manager) DeleteSystemd() error { + ctx := context.TODO() + conn, err := systemdDbus.NewWithContext(ctx) + if err != nil { + return err + } + defer conn.Close() + group := systemdUnitFromPath(c.path) + ch := make(chan string) + _, err = conn.StopUnitContext(ctx, group, "replace", ch) + if err != nil { + return err + } + <-ch + return nil +} + +func newSystemdProperty(name string, units interface{}) systemdDbus.Property { + return systemdDbus.Property{ + Name: name, + Value: dbus.MakeVariant(units), + } +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/memory.go b/agent/vendor/github.com/containerd/cgroups/v2/memory.go new file mode 100644 index 00000000000..72f94b738b8 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/memory.go @@ -0,0 +1,52 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +type Memory struct { + Swap *int64 + Max *int64 + Low *int64 + High *int64 +} + +func (r *Memory) Values() (o []Value) { + if r.Swap != nil { + o = append(o, Value{ + filename: "memory.swap.max", + value: *r.Swap, + }) + } + if r.Max != nil { + o = append(o, Value{ + filename: "memory.max", + value: *r.Max, + }) + } + if r.Low != nil { + o = append(o, Value{ + filename: "memory.low", + value: *r.Low, + }) + } + if r.High != nil { + o = append(o, Value{ + filename: "memory.high", + value: *r.High, + }) + } + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/paths.go b/agent/vendor/github.com/containerd/cgroups/v2/paths.go new file mode 100644 index 00000000000..c4778c14244 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/paths.go @@ -0,0 +1,60 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "fmt" + "path/filepath" + "strings" +) + +// NestedGroupPath will nest the cgroups based on the calling processes cgroup +// placing its child processes inside its own path +func NestedGroupPath(suffix string) (string, error) { + path, err := parseCgroupFile("/proc/self/cgroup") + if err != nil { + return "", err + } + return filepath.Join(path, suffix), nil +} + +// PidGroupPath will return the correct cgroup paths for an existing process running inside a cgroup +// This is commonly used for the Load function to restore an existing container +func PidGroupPath(pid int) (string, error) { + p := fmt.Sprintf("/proc/%d/cgroup", pid) + return parseCgroupFile(p) +} + +// VerifyGroupPath verifies the format of group path string g. +// The format is same as the third field in /proc/PID/cgroup. +// e.g. "/user.slice/user-1001.slice/session-1.scope" +// +// g must be a "clean" absolute path starts with "/", and must not contain "/sys/fs/cgroup" prefix. +// +// VerifyGroupPath doesn't verify whether g actually exists on the system. +func VerifyGroupPath(g string) error { + if !strings.HasPrefix(g, "/") { + return ErrInvalidGroupPath + } + if filepath.Clean(g) != g { + return ErrInvalidGroupPath + } + if strings.HasPrefix(g, "/sys/fs/cgroup") { + return ErrInvalidGroupPath + } + return nil +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/pids.go b/agent/vendor/github.com/containerd/cgroups/v2/pids.go new file mode 100644 index 00000000000..0b5aa0c3bf7 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/pids.go @@ -0,0 +1,37 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import "strconv" + +type Pids struct { + Max int64 +} + +func (r *Pids) Values() (o []Value) { + if r.Max != 0 { + limit := "max" + if r.Max > 0 { + limit = strconv.FormatInt(r.Max, 10) + } + o = append(o, Value{ + filename: "pids.max", + value: limit, + }) + } + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/rdma.go b/agent/vendor/github.com/containerd/cgroups/v2/rdma.go new file mode 100644 index 00000000000..44caa4f57a3 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/rdma.go @@ -0,0 +1,46 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "fmt" +) + +type RDMA struct { + Limit []RDMAEntry +} + +type RDMAEntry struct { + Device string + HcaHandles uint32 + HcaObjects uint32 +} + +func (r RDMAEntry) String() string { + return fmt.Sprintf("%s hca_handle=%d hca_object=%d", r.Device, r.HcaHandles, r.HcaObjects) +} + +func (r *RDMA) Values() (o []Value) { + for _, e := range r.Limit { + o = append(o, Value{ + filename: "rdma.max", + value: e.String(), + }) + } + + return o +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/state.go b/agent/vendor/github.com/containerd/cgroups/v2/state.go new file mode 100644 index 00000000000..09b75b6c3dd --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/state.go @@ -0,0 +1,65 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "io/ioutil" + "path/filepath" + "strings" +) + +// State is a type that represents the state of the current cgroup +type State string + +const ( + Unknown State = "" + Thawed State = "thawed" + Frozen State = "frozen" + Deleted State = "deleted" + + cgroupFreeze = "cgroup.freeze" +) + +func (s State) Values() []Value { + v := Value{ + filename: cgroupFreeze, + } + switch s { + case Frozen: + v.value = "1" + case Thawed: + v.value = "0" + } + return []Value{ + v, + } +} + +func fetchState(path string) (State, error) { + current, err := ioutil.ReadFile(filepath.Join(path, cgroupFreeze)) + if err != nil { + return Unknown, err + } + switch strings.TrimSpace(string(current)) { + case "1": + return Frozen, nil + case "0": + return Thawed, nil + default: + return Unknown, nil + } +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/stats/doc.go b/agent/vendor/github.com/containerd/cgroups/v2/stats/doc.go new file mode 100644 index 00000000000..e51e12f8004 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/stats/doc.go @@ -0,0 +1,17 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package stats diff --git a/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.go b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.go new file mode 100644 index 00000000000..0bd493998f7 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.go @@ -0,0 +1,3992 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: github.com/containerd/cgroups/v2/stats/metrics.proto + +package stats + +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" + reflect "reflect" + strings "strings" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Metrics struct { + Pids *PidsStat `protobuf:"bytes,1,opt,name=pids,proto3" json:"pids,omitempty"` + CPU *CPUStat `protobuf:"bytes,2,opt,name=cpu,proto3" json:"cpu,omitempty"` + Memory *MemoryStat `protobuf:"bytes,4,opt,name=memory,proto3" json:"memory,omitempty"` + Rdma *RdmaStat `protobuf:"bytes,5,opt,name=rdma,proto3" json:"rdma,omitempty"` + Io *IOStat `protobuf:"bytes,6,opt,name=io,proto3" json:"io,omitempty"` + Hugetlb []*HugeTlbStat `protobuf:"bytes,7,rep,name=hugetlb,proto3" json:"hugetlb,omitempty"` + MemoryEvents *MemoryEvents `protobuf:"bytes,8,opt,name=memory_events,json=memoryEvents,proto3" json:"memory_events,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Metrics) Reset() { *m = Metrics{} } +func (*Metrics) ProtoMessage() {} +func (*Metrics) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{0} +} +func (m *Metrics) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Metrics) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Metrics.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Metrics) XXX_Merge(src proto.Message) { + xxx_messageInfo_Metrics.Merge(m, src) +} +func (m *Metrics) XXX_Size() int { + return m.Size() +} +func (m *Metrics) XXX_DiscardUnknown() { + xxx_messageInfo_Metrics.DiscardUnknown(m) +} + +var xxx_messageInfo_Metrics proto.InternalMessageInfo + +type PidsStat struct { + Current uint64 `protobuf:"varint,1,opt,name=current,proto3" json:"current,omitempty"` + Limit uint64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PidsStat) Reset() { *m = PidsStat{} } +func (*PidsStat) ProtoMessage() {} +func (*PidsStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{1} +} +func (m *PidsStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *PidsStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_PidsStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *PidsStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_PidsStat.Merge(m, src) +} +func (m *PidsStat) XXX_Size() int { + return m.Size() +} +func (m *PidsStat) XXX_DiscardUnknown() { + xxx_messageInfo_PidsStat.DiscardUnknown(m) +} + +var xxx_messageInfo_PidsStat proto.InternalMessageInfo + +type CPUStat struct { + UsageUsec uint64 `protobuf:"varint,1,opt,name=usage_usec,json=usageUsec,proto3" json:"usage_usec,omitempty"` + UserUsec uint64 `protobuf:"varint,2,opt,name=user_usec,json=userUsec,proto3" json:"user_usec,omitempty"` + SystemUsec uint64 `protobuf:"varint,3,opt,name=system_usec,json=systemUsec,proto3" json:"system_usec,omitempty"` + NrPeriods uint64 `protobuf:"varint,4,opt,name=nr_periods,json=nrPeriods,proto3" json:"nr_periods,omitempty"` + NrThrottled uint64 `protobuf:"varint,5,opt,name=nr_throttled,json=nrThrottled,proto3" json:"nr_throttled,omitempty"` + ThrottledUsec uint64 `protobuf:"varint,6,opt,name=throttled_usec,json=throttledUsec,proto3" json:"throttled_usec,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CPUStat) Reset() { *m = CPUStat{} } +func (*CPUStat) ProtoMessage() {} +func (*CPUStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{2} +} +func (m *CPUStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CPUStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CPUStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CPUStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_CPUStat.Merge(m, src) +} +func (m *CPUStat) XXX_Size() int { + return m.Size() +} +func (m *CPUStat) XXX_DiscardUnknown() { + xxx_messageInfo_CPUStat.DiscardUnknown(m) +} + +var xxx_messageInfo_CPUStat proto.InternalMessageInfo + +type MemoryStat struct { + Anon uint64 `protobuf:"varint,1,opt,name=anon,proto3" json:"anon,omitempty"` + File uint64 `protobuf:"varint,2,opt,name=file,proto3" json:"file,omitempty"` + KernelStack uint64 `protobuf:"varint,3,opt,name=kernel_stack,json=kernelStack,proto3" json:"kernel_stack,omitempty"` + Slab uint64 `protobuf:"varint,4,opt,name=slab,proto3" json:"slab,omitempty"` + Sock uint64 `protobuf:"varint,5,opt,name=sock,proto3" json:"sock,omitempty"` + Shmem uint64 `protobuf:"varint,6,opt,name=shmem,proto3" json:"shmem,omitempty"` + FileMapped uint64 `protobuf:"varint,7,opt,name=file_mapped,json=fileMapped,proto3" json:"file_mapped,omitempty"` + FileDirty uint64 `protobuf:"varint,8,opt,name=file_dirty,json=fileDirty,proto3" json:"file_dirty,omitempty"` + FileWriteback uint64 `protobuf:"varint,9,opt,name=file_writeback,json=fileWriteback,proto3" json:"file_writeback,omitempty"` + AnonThp uint64 `protobuf:"varint,10,opt,name=anon_thp,json=anonThp,proto3" json:"anon_thp,omitempty"` + InactiveAnon uint64 `protobuf:"varint,11,opt,name=inactive_anon,json=inactiveAnon,proto3" json:"inactive_anon,omitempty"` + ActiveAnon uint64 `protobuf:"varint,12,opt,name=active_anon,json=activeAnon,proto3" json:"active_anon,omitempty"` + InactiveFile uint64 `protobuf:"varint,13,opt,name=inactive_file,json=inactiveFile,proto3" json:"inactive_file,omitempty"` + ActiveFile uint64 `protobuf:"varint,14,opt,name=active_file,json=activeFile,proto3" json:"active_file,omitempty"` + Unevictable uint64 `protobuf:"varint,15,opt,name=unevictable,proto3" json:"unevictable,omitempty"` + SlabReclaimable uint64 `protobuf:"varint,16,opt,name=slab_reclaimable,json=slabReclaimable,proto3" json:"slab_reclaimable,omitempty"` + SlabUnreclaimable uint64 `protobuf:"varint,17,opt,name=slab_unreclaimable,json=slabUnreclaimable,proto3" json:"slab_unreclaimable,omitempty"` + Pgfault uint64 `protobuf:"varint,18,opt,name=pgfault,proto3" json:"pgfault,omitempty"` + Pgmajfault uint64 `protobuf:"varint,19,opt,name=pgmajfault,proto3" json:"pgmajfault,omitempty"` + WorkingsetRefault uint64 `protobuf:"varint,20,opt,name=workingset_refault,json=workingsetRefault,proto3" json:"workingset_refault,omitempty"` + WorkingsetActivate uint64 `protobuf:"varint,21,opt,name=workingset_activate,json=workingsetActivate,proto3" json:"workingset_activate,omitempty"` + WorkingsetNodereclaim uint64 `protobuf:"varint,22,opt,name=workingset_nodereclaim,json=workingsetNodereclaim,proto3" json:"workingset_nodereclaim,omitempty"` + Pgrefill uint64 `protobuf:"varint,23,opt,name=pgrefill,proto3" json:"pgrefill,omitempty"` + Pgscan uint64 `protobuf:"varint,24,opt,name=pgscan,proto3" json:"pgscan,omitempty"` + Pgsteal uint64 `protobuf:"varint,25,opt,name=pgsteal,proto3" json:"pgsteal,omitempty"` + Pgactivate uint64 `protobuf:"varint,26,opt,name=pgactivate,proto3" json:"pgactivate,omitempty"` + Pgdeactivate uint64 `protobuf:"varint,27,opt,name=pgdeactivate,proto3" json:"pgdeactivate,omitempty"` + Pglazyfree uint64 `protobuf:"varint,28,opt,name=pglazyfree,proto3" json:"pglazyfree,omitempty"` + Pglazyfreed uint64 `protobuf:"varint,29,opt,name=pglazyfreed,proto3" json:"pglazyfreed,omitempty"` + ThpFaultAlloc uint64 `protobuf:"varint,30,opt,name=thp_fault_alloc,json=thpFaultAlloc,proto3" json:"thp_fault_alloc,omitempty"` + ThpCollapseAlloc uint64 `protobuf:"varint,31,opt,name=thp_collapse_alloc,json=thpCollapseAlloc,proto3" json:"thp_collapse_alloc,omitempty"` + Usage uint64 `protobuf:"varint,32,opt,name=usage,proto3" json:"usage,omitempty"` + UsageLimit uint64 `protobuf:"varint,33,opt,name=usage_limit,json=usageLimit,proto3" json:"usage_limit,omitempty"` + SwapUsage uint64 `protobuf:"varint,34,opt,name=swap_usage,json=swapUsage,proto3" json:"swap_usage,omitempty"` + SwapLimit uint64 `protobuf:"varint,35,opt,name=swap_limit,json=swapLimit,proto3" json:"swap_limit,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MemoryStat) Reset() { *m = MemoryStat{} } +func (*MemoryStat) ProtoMessage() {} +func (*MemoryStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{3} +} +func (m *MemoryStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MemoryStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MemoryStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MemoryStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_MemoryStat.Merge(m, src) +} +func (m *MemoryStat) XXX_Size() int { + return m.Size() +} +func (m *MemoryStat) XXX_DiscardUnknown() { + xxx_messageInfo_MemoryStat.DiscardUnknown(m) +} + +var xxx_messageInfo_MemoryStat proto.InternalMessageInfo + +type MemoryEvents struct { + Low uint64 `protobuf:"varint,1,opt,name=low,proto3" json:"low,omitempty"` + High uint64 `protobuf:"varint,2,opt,name=high,proto3" json:"high,omitempty"` + Max uint64 `protobuf:"varint,3,opt,name=max,proto3" json:"max,omitempty"` + Oom uint64 `protobuf:"varint,4,opt,name=oom,proto3" json:"oom,omitempty"` + OomKill uint64 `protobuf:"varint,5,opt,name=oom_kill,json=oomKill,proto3" json:"oom_kill,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MemoryEvents) Reset() { *m = MemoryEvents{} } +func (*MemoryEvents) ProtoMessage() {} +func (*MemoryEvents) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{4} +} +func (m *MemoryEvents) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MemoryEvents) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MemoryEvents.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MemoryEvents) XXX_Merge(src proto.Message) { + xxx_messageInfo_MemoryEvents.Merge(m, src) +} +func (m *MemoryEvents) XXX_Size() int { + return m.Size() +} +func (m *MemoryEvents) XXX_DiscardUnknown() { + xxx_messageInfo_MemoryEvents.DiscardUnknown(m) +} + +var xxx_messageInfo_MemoryEvents proto.InternalMessageInfo + +type RdmaStat struct { + Current []*RdmaEntry `protobuf:"bytes,1,rep,name=current,proto3" json:"current,omitempty"` + Limit []*RdmaEntry `protobuf:"bytes,2,rep,name=limit,proto3" json:"limit,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RdmaStat) Reset() { *m = RdmaStat{} } +func (*RdmaStat) ProtoMessage() {} +func (*RdmaStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{5} +} +func (m *RdmaStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RdmaStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RdmaStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RdmaStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_RdmaStat.Merge(m, src) +} +func (m *RdmaStat) XXX_Size() int { + return m.Size() +} +func (m *RdmaStat) XXX_DiscardUnknown() { + xxx_messageInfo_RdmaStat.DiscardUnknown(m) +} + +var xxx_messageInfo_RdmaStat proto.InternalMessageInfo + +type RdmaEntry struct { + Device string `protobuf:"bytes,1,opt,name=device,proto3" json:"device,omitempty"` + HcaHandles uint32 `protobuf:"varint,2,opt,name=hca_handles,json=hcaHandles,proto3" json:"hca_handles,omitempty"` + HcaObjects uint32 `protobuf:"varint,3,opt,name=hca_objects,json=hcaObjects,proto3" json:"hca_objects,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RdmaEntry) Reset() { *m = RdmaEntry{} } +func (*RdmaEntry) ProtoMessage() {} +func (*RdmaEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{6} +} +func (m *RdmaEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *RdmaEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_RdmaEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *RdmaEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_RdmaEntry.Merge(m, src) +} +func (m *RdmaEntry) XXX_Size() int { + return m.Size() +} +func (m *RdmaEntry) XXX_DiscardUnknown() { + xxx_messageInfo_RdmaEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_RdmaEntry proto.InternalMessageInfo + +type IOStat struct { + Usage []*IOEntry `protobuf:"bytes,1,rep,name=usage,proto3" json:"usage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *IOStat) Reset() { *m = IOStat{} } +func (*IOStat) ProtoMessage() {} +func (*IOStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{7} +} +func (m *IOStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *IOStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_IOStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *IOStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_IOStat.Merge(m, src) +} +func (m *IOStat) XXX_Size() int { + return m.Size() +} +func (m *IOStat) XXX_DiscardUnknown() { + xxx_messageInfo_IOStat.DiscardUnknown(m) +} + +var xxx_messageInfo_IOStat proto.InternalMessageInfo + +type IOEntry struct { + Major uint64 `protobuf:"varint,1,opt,name=major,proto3" json:"major,omitempty"` + Minor uint64 `protobuf:"varint,2,opt,name=minor,proto3" json:"minor,omitempty"` + Rbytes uint64 `protobuf:"varint,3,opt,name=rbytes,proto3" json:"rbytes,omitempty"` + Wbytes uint64 `protobuf:"varint,4,opt,name=wbytes,proto3" json:"wbytes,omitempty"` + Rios uint64 `protobuf:"varint,5,opt,name=rios,proto3" json:"rios,omitempty"` + Wios uint64 `protobuf:"varint,6,opt,name=wios,proto3" json:"wios,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *IOEntry) Reset() { *m = IOEntry{} } +func (*IOEntry) ProtoMessage() {} +func (*IOEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{8} +} +func (m *IOEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *IOEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_IOEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *IOEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_IOEntry.Merge(m, src) +} +func (m *IOEntry) XXX_Size() int { + return m.Size() +} +func (m *IOEntry) XXX_DiscardUnknown() { + xxx_messageInfo_IOEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_IOEntry proto.InternalMessageInfo + +type HugeTlbStat struct { + Current uint64 `protobuf:"varint,1,opt,name=current,proto3" json:"current,omitempty"` + Max uint64 `protobuf:"varint,2,opt,name=max,proto3" json:"max,omitempty"` + Pagesize string `protobuf:"bytes,3,opt,name=pagesize,proto3" json:"pagesize,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HugeTlbStat) Reset() { *m = HugeTlbStat{} } +func (*HugeTlbStat) ProtoMessage() {} +func (*HugeTlbStat) Descriptor() ([]byte, []int) { + return fileDescriptor_2fc6005842049e6b, []int{9} +} +func (m *HugeTlbStat) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *HugeTlbStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_HugeTlbStat.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *HugeTlbStat) XXX_Merge(src proto.Message) { + xxx_messageInfo_HugeTlbStat.Merge(m, src) +} +func (m *HugeTlbStat) XXX_Size() int { + return m.Size() +} +func (m *HugeTlbStat) XXX_DiscardUnknown() { + xxx_messageInfo_HugeTlbStat.DiscardUnknown(m) +} + +var xxx_messageInfo_HugeTlbStat proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Metrics)(nil), "io.containerd.cgroups.v2.Metrics") + proto.RegisterType((*PidsStat)(nil), "io.containerd.cgroups.v2.PidsStat") + proto.RegisterType((*CPUStat)(nil), "io.containerd.cgroups.v2.CPUStat") + proto.RegisterType((*MemoryStat)(nil), "io.containerd.cgroups.v2.MemoryStat") + proto.RegisterType((*MemoryEvents)(nil), "io.containerd.cgroups.v2.MemoryEvents") + proto.RegisterType((*RdmaStat)(nil), "io.containerd.cgroups.v2.RdmaStat") + proto.RegisterType((*RdmaEntry)(nil), "io.containerd.cgroups.v2.RdmaEntry") + proto.RegisterType((*IOStat)(nil), "io.containerd.cgroups.v2.IOStat") + proto.RegisterType((*IOEntry)(nil), "io.containerd.cgroups.v2.IOEntry") + proto.RegisterType((*HugeTlbStat)(nil), "io.containerd.cgroups.v2.HugeTlbStat") +} + +func init() { + proto.RegisterFile("github.com/containerd/cgroups/v2/stats/metrics.proto", fileDescriptor_2fc6005842049e6b) +} + +var fileDescriptor_2fc6005842049e6b = []byte{ + // 1198 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0x4d, 0x73, 0xd4, 0x46, + 0x13, 0x66, 0xed, 0xc5, 0xeb, 0xed, 0xb5, 0xc1, 0x0c, 0x86, 0x57, 0xc0, 0xcb, 0xda, 0x5e, 0x02, + 0x45, 0xaa, 0x92, 0xdd, 0x94, 0xf3, 0x55, 0x49, 0x91, 0x4a, 0x19, 0x02, 0x45, 0x8a, 0x10, 0x5c, + 0x02, 0x57, 0x8e, 0xaa, 0x59, 0x69, 0x2c, 0x0d, 0x96, 0x34, 0xaa, 0x99, 0x91, 0x1d, 0x73, 0xca, + 0x21, 0xd7, 0x54, 0x7e, 0x4d, 0xfe, 0x03, 0xb7, 0xe4, 0x98, 0x53, 0x2a, 0xf8, 0x97, 0xa4, 0xba, + 0x67, 0x64, 0x29, 0x07, 0x43, 0x6e, 0xd3, 0x4f, 0x3f, 0xdd, 0xea, 0x8f, 0x99, 0x6e, 0xc1, 0x27, + 0xa9, 0xb4, 0x59, 0x3d, 0x9f, 0xc6, 0xaa, 0x98, 0xc5, 0xaa, 0xb4, 0x5c, 0x96, 0x42, 0x27, 0xb3, + 0x38, 0xd5, 0xaa, 0xae, 0xcc, 0xec, 0x70, 0x7b, 0x66, 0x2c, 0xb7, 0x66, 0x56, 0x08, 0xab, 0x65, + 0x6c, 0xa6, 0x95, 0x56, 0x56, 0xb1, 0x40, 0xaa, 0x69, 0xcb, 0x9e, 0x7a, 0xf6, 0xf4, 0x70, 0xfb, + 0xfa, 0x7a, 0xaa, 0x52, 0x45, 0xa4, 0x19, 0x9e, 0x1c, 0x7f, 0xf2, 0xdb, 0x22, 0x0c, 0x9e, 0x3a, + 0x0f, 0xec, 0x33, 0xe8, 0x57, 0x32, 0x31, 0x41, 0x6f, 0xb3, 0x77, 0x77, 0xb4, 0x3d, 0x99, 0x9e, + 0xe5, 0x6a, 0xba, 0x2b, 0x13, 0xf3, 0xdc, 0x72, 0x1b, 0x12, 0x9f, 0xdd, 0x83, 0xc5, 0xb8, 0xaa, + 0x83, 0x05, 0x32, 0xdb, 0x3a, 0xdb, 0xec, 0xc1, 0xee, 0x1e, 0x5a, 0xdd, 0x1f, 0x9c, 0xfc, 0xb5, + 0xb1, 0xf8, 0x60, 0x77, 0x2f, 0x44, 0x33, 0x76, 0x0f, 0x96, 0x0a, 0x51, 0x28, 0x7d, 0x1c, 0xf4, + 0xc9, 0xc1, 0x7b, 0x67, 0x3b, 0x78, 0x4a, 0x3c, 0xfa, 0xb2, 0xb7, 0xc1, 0x98, 0x75, 0x52, 0xf0, + 0xe0, 0xfc, 0xbb, 0x62, 0x0e, 0x93, 0x82, 0xbb, 0x98, 0x91, 0xcf, 0x3e, 0x82, 0x05, 0xa9, 0x82, + 0x25, 0xb2, 0xda, 0x3c, 0xdb, 0xea, 0xdb, 0x67, 0x64, 0xb3, 0x20, 0x15, 0xfb, 0x1a, 0x06, 0x59, + 0x9d, 0x0a, 0x9b, 0xcf, 0x83, 0xc1, 0xe6, 0xe2, 0xdd, 0xd1, 0xf6, 0xed, 0xb3, 0xcd, 0x1e, 0xd7, + 0xa9, 0x78, 0x91, 0xcf, 0xc9, 0xb6, 0xb1, 0x62, 0x4f, 0x60, 0xd5, 0x05, 0x1d, 0x89, 0x43, 0x51, + 0x5a, 0x13, 0x2c, 0xd3, 0xd7, 0xef, 0xbc, 0x2b, 0xdf, 0x87, 0xc4, 0x0e, 0x57, 0x8a, 0x8e, 0x34, + 0xf9, 0x12, 0x96, 0x9b, 0x2e, 0xb0, 0x00, 0x06, 0x71, 0xad, 0xb5, 0x28, 0x2d, 0xb5, 0xae, 0x1f, + 0x36, 0x22, 0x5b, 0x87, 0xf3, 0xb9, 0x2c, 0xa4, 0xa5, 0xde, 0xf4, 0x43, 0x27, 0x4c, 0x7e, 0xef, + 0xc1, 0xc0, 0xf7, 0x82, 0xdd, 0x04, 0xa8, 0x0d, 0x4f, 0x45, 0x54, 0x1b, 0x11, 0x7b, 0xf3, 0x21, + 0x21, 0x7b, 0x46, 0xc4, 0xec, 0x06, 0x0c, 0x6b, 0x23, 0xb4, 0xd3, 0x3a, 0x27, 0xcb, 0x08, 0x90, + 0x72, 0x03, 0x46, 0xe6, 0xd8, 0x58, 0x51, 0x38, 0xf5, 0x22, 0xa9, 0xc1, 0x41, 0x44, 0xb8, 0x09, + 0x50, 0xea, 0xa8, 0x12, 0x5a, 0xaa, 0xc4, 0x50, 0x7b, 0xfb, 0xe1, 0xb0, 0xd4, 0xbb, 0x0e, 0x60, + 0x5b, 0xb0, 0x52, 0xea, 0xc8, 0x66, 0x5a, 0x59, 0x9b, 0x8b, 0x84, 0x7a, 0xd8, 0x0f, 0x47, 0xa5, + 0x7e, 0xd1, 0x40, 0xec, 0x36, 0x5c, 0x38, 0xd5, 0xbb, 0xaf, 0x2c, 0x11, 0x69, 0xf5, 0x14, 0xc5, + 0x0f, 0x4d, 0x7e, 0x1d, 0x02, 0xb4, 0x97, 0x83, 0x31, 0xe8, 0xf3, 0x52, 0x95, 0x3e, 0x1d, 0x3a, + 0x23, 0xb6, 0x2f, 0x73, 0xe1, 0x93, 0xa0, 0x33, 0x06, 0x70, 0x20, 0x74, 0x29, 0xf2, 0xc8, 0x58, + 0x1e, 0x1f, 0xf8, 0x0c, 0x46, 0x0e, 0x7b, 0x8e, 0x10, 0x9a, 0x99, 0x9c, 0xcf, 0x7d, 0xf0, 0x74, + 0x26, 0x4c, 0xc5, 0x07, 0x3e, 0x5e, 0x3a, 0x63, 0xa5, 0x4d, 0x56, 0x88, 0xc2, 0xc7, 0xe7, 0x04, + 0xac, 0x10, 0x7e, 0x28, 0x2a, 0x78, 0x55, 0x89, 0x24, 0x18, 0xb8, 0x0a, 0x21, 0xf4, 0x94, 0x10, + 0xac, 0x10, 0x11, 0x12, 0xa9, 0xed, 0x31, 0x5d, 0x88, 0x7e, 0x38, 0x44, 0xe4, 0x1b, 0x04, 0x30, + 0x7d, 0x52, 0x1f, 0x69, 0x69, 0xc5, 0x1c, 0x43, 0x1c, 0xba, 0xf4, 0x11, 0xfd, 0xa1, 0x01, 0xd9, + 0x35, 0x58, 0xc6, 0x1c, 0x23, 0x9b, 0x55, 0x01, 0xb8, 0x1b, 0x80, 0xf2, 0x8b, 0xac, 0x62, 0xb7, + 0x60, 0x55, 0x96, 0x3c, 0xb6, 0xf2, 0x50, 0x44, 0x54, 0x93, 0x11, 0xe9, 0x57, 0x1a, 0x70, 0x07, + 0x6b, 0xb3, 0x01, 0xa3, 0x2e, 0x65, 0xc5, 0x85, 0xd9, 0x21, 0x74, 0xbd, 0x50, 0x15, 0x57, 0xff, + 0xed, 0xe5, 0x11, 0x56, 0xb3, 0xf5, 0x42, 0x94, 0x0b, 0x5d, 0x2f, 0x44, 0xd8, 0x84, 0x51, 0x5d, + 0x8a, 0x43, 0x19, 0x5b, 0x3e, 0xcf, 0x45, 0x70, 0xd1, 0x55, 0xbb, 0x03, 0xb1, 0xf7, 0x61, 0x0d, + 0x2b, 0x1c, 0x69, 0x11, 0xe7, 0x5c, 0x16, 0x44, 0x5b, 0x23, 0xda, 0x45, 0xc4, 0xc3, 0x16, 0x66, + 0x1f, 0x02, 0x23, 0x6a, 0x5d, 0x76, 0xc9, 0x97, 0x88, 0x7c, 0x09, 0x35, 0x7b, 0x5d, 0x05, 0xbe, + 0x91, 0x2a, 0xdd, 0xe7, 0x75, 0x6e, 0x03, 0xe6, 0x2a, 0xe4, 0x45, 0x36, 0x06, 0xa8, 0xd2, 0x82, + 0xbf, 0x74, 0xca, 0xcb, 0x2e, 0xea, 0x16, 0xc1, 0x0f, 0x1d, 0x29, 0x7d, 0x20, 0xcb, 0xd4, 0x08, + 0x1b, 0x69, 0xe1, 0x78, 0xeb, 0xee, 0x43, 0xad, 0x26, 0x74, 0x0a, 0x36, 0x83, 0xcb, 0x1d, 0x3a, + 0x65, 0xcf, 0xad, 0x08, 0xae, 0x10, 0xbf, 0xe3, 0x69, 0xc7, 0x6b, 0xd8, 0xa7, 0x70, 0xb5, 0x63, + 0x50, 0xaa, 0x44, 0xf8, 0xb8, 0x83, 0xab, 0x64, 0x73, 0xa5, 0xd5, 0x7e, 0xdf, 0x2a, 0xd9, 0x75, + 0x58, 0xae, 0x52, 0x2d, 0xf6, 0x65, 0x9e, 0x07, 0xff, 0x73, 0x0f, 0xb3, 0x91, 0xd9, 0x55, 0x58, + 0xaa, 0x52, 0x13, 0xf3, 0x32, 0x08, 0x48, 0xe3, 0x25, 0x57, 0x04, 0x63, 0x05, 0xcf, 0x83, 0x6b, + 0x4d, 0x11, 0x48, 0x74, 0x45, 0x38, 0x0d, 0xf6, 0x7a, 0x53, 0x84, 0x06, 0x61, 0x13, 0x58, 0xa9, + 0xd2, 0x44, 0x9c, 0x32, 0x6e, 0xb8, 0xfe, 0x77, 0x31, 0xe7, 0x23, 0xe7, 0xaf, 0x8e, 0xf7, 0xb5, + 0x10, 0xc1, 0xff, 0x1b, 0x1f, 0x0d, 0x82, 0xed, 0x6f, 0xa5, 0x24, 0xb8, 0xe9, 0xda, 0xdf, 0x81, + 0xd8, 0x1d, 0xb8, 0x68, 0xb3, 0x2a, 0xa2, 0x42, 0x46, 0x3c, 0xcf, 0x55, 0x1c, 0x8c, 0x9b, 0xe7, + 0x5e, 0x3d, 0x42, 0x74, 0x07, 0x41, 0xf6, 0x01, 0x30, 0xe4, 0xc5, 0x2a, 0xcf, 0x79, 0x65, 0x84, + 0xa7, 0x6e, 0x10, 0x75, 0xcd, 0x66, 0xd5, 0x03, 0xaf, 0x70, 0xec, 0x75, 0x38, 0x4f, 0x03, 0x2d, + 0xd8, 0x74, 0x4f, 0x93, 0x04, 0xbc, 0xad, 0x6e, 0xf0, 0xb9, 0x01, 0xb9, 0xe5, 0xc2, 0x25, 0xe8, + 0x3b, 0x44, 0xf0, 0x69, 0x9a, 0x23, 0x5e, 0x45, 0xce, 0x76, 0xe2, 0x9e, 0x26, 0x22, 0x7b, 0x64, + 0xdf, 0xa8, 0x9d, 0xf9, 0xad, 0x56, 0x4d, 0xd6, 0x13, 0x03, 0x2b, 0xdd, 0xe9, 0xcd, 0xd6, 0x60, + 0x31, 0x57, 0x47, 0x7e, 0x22, 0xe1, 0x11, 0xa7, 0x48, 0x26, 0xd3, 0xac, 0x19, 0x48, 0x78, 0x46, + 0x56, 0xc1, 0x7f, 0xf4, 0x73, 0x08, 0x8f, 0x88, 0x28, 0x55, 0xf8, 0xf1, 0x83, 0x47, 0x7c, 0xec, + 0x4a, 0x15, 0xd1, 0x01, 0x36, 0xde, 0x4d, 0xa0, 0x81, 0x52, 0xc5, 0x13, 0x99, 0xe7, 0x93, 0x9f, + 0x7b, 0xb0, 0xdc, 0xec, 0x39, 0xf6, 0x55, 0x77, 0x2b, 0xe0, 0xbe, 0xba, 0xf5, 0xf6, 0xe5, 0xf8, + 0xb0, 0xb4, 0xfa, 0xb8, 0x5d, 0x1d, 0x5f, 0xb4, 0xab, 0xe3, 0x3f, 0x1b, 0xfb, 0xfd, 0x22, 0x60, + 0x78, 0x8a, 0xe1, 0x5d, 0x4c, 0xf0, 0x81, 0x0b, 0xca, 0x7d, 0x18, 0x7a, 0x09, 0xeb, 0x9f, 0xc5, + 0x3c, 0xca, 0x78, 0x99, 0xe4, 0xc2, 0x50, 0x15, 0x56, 0x43, 0xc8, 0x62, 0xfe, 0xd8, 0x21, 0x0d, + 0x41, 0xcd, 0x5f, 0x8a, 0xd8, 0x1a, 0xaa, 0x89, 0x23, 0x3c, 0x73, 0xc8, 0x64, 0x07, 0x96, 0xdc, + 0x7a, 0x66, 0x9f, 0x37, 0x1d, 0x76, 0x89, 0x6e, 0xbd, 0x6d, 0x9f, 0xfb, 0x48, 0x89, 0x3f, 0xf9, + 0xa5, 0x07, 0x03, 0x0f, 0xe1, 0x35, 0x29, 0xf8, 0x4b, 0xa5, 0x7d, 0x8f, 0x9c, 0x40, 0xa8, 0x2c, + 0x95, 0x6e, 0x36, 0x28, 0x09, 0x98, 0x94, 0x9e, 0x1f, 0x5b, 0x61, 0x7c, 0xab, 0xbc, 0x84, 0xf8, + 0x91, 0xc3, 0x5d, 0xc3, 0xbc, 0x84, 0xbd, 0xd6, 0x52, 0x99, 0x66, 0x63, 0xe0, 0x19, 0xb1, 0x23, + 0xc4, 0xdc, 0xc2, 0xa0, 0xf3, 0x64, 0x0f, 0x46, 0x9d, 0x5f, 0x87, 0xb7, 0x2c, 0x76, 0x7f, 0x51, + 0x16, 0xda, 0x8b, 0x82, 0xf3, 0x80, 0xa7, 0xc2, 0xc8, 0x57, 0x82, 0x82, 0x1a, 0x86, 0xa7, 0xf2, + 0xfd, 0xe0, 0xf5, 0x9b, 0xf1, 0xb9, 0x3f, 0xdf, 0x8c, 0xcf, 0xfd, 0x74, 0x32, 0xee, 0xbd, 0x3e, + 0x19, 0xf7, 0xfe, 0x38, 0x19, 0xf7, 0xfe, 0x3e, 0x19, 0xf7, 0xe6, 0x4b, 0xf4, 0x17, 0xf8, 0xf1, + 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x4f, 0x2b, 0x30, 0xd6, 0x6d, 0x0a, 0x00, 0x00, +} + +func (m *Metrics) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Metrics) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Metrics) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.MemoryEvents != nil { + { + size, err := m.MemoryEvents.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x42 + } + if len(m.Hugetlb) > 0 { + for iNdEx := len(m.Hugetlb) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Hugetlb[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x3a + } + } + if m.Io != nil { + { + size, err := m.Io.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 + } + if m.Rdma != nil { + { + size, err := m.Rdma.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + } + if m.Memory != nil { + { + size, err := m.Memory.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + if m.CPU != nil { + { + size, err := m.CPU.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if m.Pids != nil { + { + size, err := m.Pids.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *PidsStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PidsStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *PidsStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Limit != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Limit)) + i-- + dAtA[i] = 0x10 + } + if m.Current != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Current)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *CPUStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CPUStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CPUStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.ThrottledUsec != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.ThrottledUsec)) + i-- + dAtA[i] = 0x30 + } + if m.NrThrottled != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.NrThrottled)) + i-- + dAtA[i] = 0x28 + } + if m.NrPeriods != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.NrPeriods)) + i-- + dAtA[i] = 0x20 + } + if m.SystemUsec != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.SystemUsec)) + i-- + dAtA[i] = 0x18 + } + if m.UserUsec != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.UserUsec)) + i-- + dAtA[i] = 0x10 + } + if m.UsageUsec != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.UsageUsec)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *MemoryStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MemoryStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MemoryStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.SwapLimit != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.SwapLimit)) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x98 + } + if m.SwapUsage != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.SwapUsage)) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x90 + } + if m.UsageLimit != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.UsageLimit)) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x88 + } + if m.Usage != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Usage)) + i-- + dAtA[i] = 0x2 + i-- + dAtA[i] = 0x80 + } + if m.ThpCollapseAlloc != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.ThpCollapseAlloc)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xf8 + } + if m.ThpFaultAlloc != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.ThpFaultAlloc)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xf0 + } + if m.Pglazyfreed != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pglazyfreed)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xe8 + } + if m.Pglazyfree != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pglazyfree)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xe0 + } + if m.Pgdeactivate != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgdeactivate)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xd8 + } + if m.Pgactivate != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgactivate)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xd0 + } + if m.Pgsteal != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgsteal)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xc8 + } + if m.Pgscan != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgscan)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xc0 + } + if m.Pgrefill != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgrefill)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb8 + } + if m.WorkingsetNodereclaim != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.WorkingsetNodereclaim)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb0 + } + if m.WorkingsetActivate != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.WorkingsetActivate)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa8 + } + if m.WorkingsetRefault != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.WorkingsetRefault)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa0 + } + if m.Pgmajfault != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgmajfault)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x98 + } + if m.Pgfault != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Pgfault)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x90 + } + if m.SlabUnreclaimable != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.SlabUnreclaimable)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x88 + } + if m.SlabReclaimable != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.SlabReclaimable)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x80 + } + if m.Unevictable != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Unevictable)) + i-- + dAtA[i] = 0x78 + } + if m.ActiveFile != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.ActiveFile)) + i-- + dAtA[i] = 0x70 + } + if m.InactiveFile != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.InactiveFile)) + i-- + dAtA[i] = 0x68 + } + if m.ActiveAnon != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.ActiveAnon)) + i-- + dAtA[i] = 0x60 + } + if m.InactiveAnon != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.InactiveAnon)) + i-- + dAtA[i] = 0x58 + } + if m.AnonThp != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.AnonThp)) + i-- + dAtA[i] = 0x50 + } + if m.FileWriteback != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.FileWriteback)) + i-- + dAtA[i] = 0x48 + } + if m.FileDirty != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.FileDirty)) + i-- + dAtA[i] = 0x40 + } + if m.FileMapped != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.FileMapped)) + i-- + dAtA[i] = 0x38 + } + if m.Shmem != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Shmem)) + i-- + dAtA[i] = 0x30 + } + if m.Sock != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Sock)) + i-- + dAtA[i] = 0x28 + } + if m.Slab != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Slab)) + i-- + dAtA[i] = 0x20 + } + if m.KernelStack != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.KernelStack)) + i-- + dAtA[i] = 0x18 + } + if m.File != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.File)) + i-- + dAtA[i] = 0x10 + } + if m.Anon != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Anon)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *MemoryEvents) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MemoryEvents) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MemoryEvents) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.OomKill != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.OomKill)) + i-- + dAtA[i] = 0x28 + } + if m.Oom != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Oom)) + i-- + dAtA[i] = 0x20 + } + if m.Max != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Max)) + i-- + dAtA[i] = 0x18 + } + if m.High != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.High)) + i-- + dAtA[i] = 0x10 + } + if m.Low != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Low)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *RdmaStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RdmaStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RdmaStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Limit) > 0 { + for iNdEx := len(m.Limit) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Limit[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.Current) > 0 { + for iNdEx := len(m.Current) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Current[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *RdmaEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RdmaEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *RdmaEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.HcaObjects != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.HcaObjects)) + i-- + dAtA[i] = 0x18 + } + if m.HcaHandles != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.HcaHandles)) + i-- + dAtA[i] = 0x10 + } + if len(m.Device) > 0 { + i -= len(m.Device) + copy(dAtA[i:], m.Device) + i = encodeVarintMetrics(dAtA, i, uint64(len(m.Device))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *IOStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *IOStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *IOStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Usage) > 0 { + for iNdEx := len(m.Usage) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Usage[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintMetrics(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *IOEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *IOEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *IOEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if m.Wios != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Wios)) + i-- + dAtA[i] = 0x30 + } + if m.Rios != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Rios)) + i-- + dAtA[i] = 0x28 + } + if m.Wbytes != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Wbytes)) + i-- + dAtA[i] = 0x20 + } + if m.Rbytes != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Rbytes)) + i-- + dAtA[i] = 0x18 + } + if m.Minor != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Minor)) + i-- + dAtA[i] = 0x10 + } + if m.Major != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Major)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *HugeTlbStat) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *HugeTlbStat) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *HugeTlbStat) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Pagesize) > 0 { + i -= len(m.Pagesize) + copy(dAtA[i:], m.Pagesize) + i = encodeVarintMetrics(dAtA, i, uint64(len(m.Pagesize))) + i-- + dAtA[i] = 0x1a + } + if m.Max != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Max)) + i-- + dAtA[i] = 0x10 + } + if m.Current != 0 { + i = encodeVarintMetrics(dAtA, i, uint64(m.Current)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintMetrics(dAtA []byte, offset int, v uint64) int { + offset -= sovMetrics(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Metrics) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Pids != nil { + l = m.Pids.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if m.CPU != nil { + l = m.CPU.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if m.Memory != nil { + l = m.Memory.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if m.Rdma != nil { + l = m.Rdma.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if m.Io != nil { + l = m.Io.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if len(m.Hugetlb) > 0 { + for _, e := range m.Hugetlb { + l = e.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + } + if m.MemoryEvents != nil { + l = m.MemoryEvents.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *PidsStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Current != 0 { + n += 1 + sovMetrics(uint64(m.Current)) + } + if m.Limit != 0 { + n += 1 + sovMetrics(uint64(m.Limit)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *CPUStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.UsageUsec != 0 { + n += 1 + sovMetrics(uint64(m.UsageUsec)) + } + if m.UserUsec != 0 { + n += 1 + sovMetrics(uint64(m.UserUsec)) + } + if m.SystemUsec != 0 { + n += 1 + sovMetrics(uint64(m.SystemUsec)) + } + if m.NrPeriods != 0 { + n += 1 + sovMetrics(uint64(m.NrPeriods)) + } + if m.NrThrottled != 0 { + n += 1 + sovMetrics(uint64(m.NrThrottled)) + } + if m.ThrottledUsec != 0 { + n += 1 + sovMetrics(uint64(m.ThrottledUsec)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *MemoryStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Anon != 0 { + n += 1 + sovMetrics(uint64(m.Anon)) + } + if m.File != 0 { + n += 1 + sovMetrics(uint64(m.File)) + } + if m.KernelStack != 0 { + n += 1 + sovMetrics(uint64(m.KernelStack)) + } + if m.Slab != 0 { + n += 1 + sovMetrics(uint64(m.Slab)) + } + if m.Sock != 0 { + n += 1 + sovMetrics(uint64(m.Sock)) + } + if m.Shmem != 0 { + n += 1 + sovMetrics(uint64(m.Shmem)) + } + if m.FileMapped != 0 { + n += 1 + sovMetrics(uint64(m.FileMapped)) + } + if m.FileDirty != 0 { + n += 1 + sovMetrics(uint64(m.FileDirty)) + } + if m.FileWriteback != 0 { + n += 1 + sovMetrics(uint64(m.FileWriteback)) + } + if m.AnonThp != 0 { + n += 1 + sovMetrics(uint64(m.AnonThp)) + } + if m.InactiveAnon != 0 { + n += 1 + sovMetrics(uint64(m.InactiveAnon)) + } + if m.ActiveAnon != 0 { + n += 1 + sovMetrics(uint64(m.ActiveAnon)) + } + if m.InactiveFile != 0 { + n += 1 + sovMetrics(uint64(m.InactiveFile)) + } + if m.ActiveFile != 0 { + n += 1 + sovMetrics(uint64(m.ActiveFile)) + } + if m.Unevictable != 0 { + n += 1 + sovMetrics(uint64(m.Unevictable)) + } + if m.SlabReclaimable != 0 { + n += 2 + sovMetrics(uint64(m.SlabReclaimable)) + } + if m.SlabUnreclaimable != 0 { + n += 2 + sovMetrics(uint64(m.SlabUnreclaimable)) + } + if m.Pgfault != 0 { + n += 2 + sovMetrics(uint64(m.Pgfault)) + } + if m.Pgmajfault != 0 { + n += 2 + sovMetrics(uint64(m.Pgmajfault)) + } + if m.WorkingsetRefault != 0 { + n += 2 + sovMetrics(uint64(m.WorkingsetRefault)) + } + if m.WorkingsetActivate != 0 { + n += 2 + sovMetrics(uint64(m.WorkingsetActivate)) + } + if m.WorkingsetNodereclaim != 0 { + n += 2 + sovMetrics(uint64(m.WorkingsetNodereclaim)) + } + if m.Pgrefill != 0 { + n += 2 + sovMetrics(uint64(m.Pgrefill)) + } + if m.Pgscan != 0 { + n += 2 + sovMetrics(uint64(m.Pgscan)) + } + if m.Pgsteal != 0 { + n += 2 + sovMetrics(uint64(m.Pgsteal)) + } + if m.Pgactivate != 0 { + n += 2 + sovMetrics(uint64(m.Pgactivate)) + } + if m.Pgdeactivate != 0 { + n += 2 + sovMetrics(uint64(m.Pgdeactivate)) + } + if m.Pglazyfree != 0 { + n += 2 + sovMetrics(uint64(m.Pglazyfree)) + } + if m.Pglazyfreed != 0 { + n += 2 + sovMetrics(uint64(m.Pglazyfreed)) + } + if m.ThpFaultAlloc != 0 { + n += 2 + sovMetrics(uint64(m.ThpFaultAlloc)) + } + if m.ThpCollapseAlloc != 0 { + n += 2 + sovMetrics(uint64(m.ThpCollapseAlloc)) + } + if m.Usage != 0 { + n += 2 + sovMetrics(uint64(m.Usage)) + } + if m.UsageLimit != 0 { + n += 2 + sovMetrics(uint64(m.UsageLimit)) + } + if m.SwapUsage != 0 { + n += 2 + sovMetrics(uint64(m.SwapUsage)) + } + if m.SwapLimit != 0 { + n += 2 + sovMetrics(uint64(m.SwapLimit)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *MemoryEvents) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Low != 0 { + n += 1 + sovMetrics(uint64(m.Low)) + } + if m.High != 0 { + n += 1 + sovMetrics(uint64(m.High)) + } + if m.Max != 0 { + n += 1 + sovMetrics(uint64(m.Max)) + } + if m.Oom != 0 { + n += 1 + sovMetrics(uint64(m.Oom)) + } + if m.OomKill != 0 { + n += 1 + sovMetrics(uint64(m.OomKill)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *RdmaStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Current) > 0 { + for _, e := range m.Current { + l = e.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + } + if len(m.Limit) > 0 { + for _, e := range m.Limit { + l = e.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *RdmaEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Device) + if l > 0 { + n += 1 + l + sovMetrics(uint64(l)) + } + if m.HcaHandles != 0 { + n += 1 + sovMetrics(uint64(m.HcaHandles)) + } + if m.HcaObjects != 0 { + n += 1 + sovMetrics(uint64(m.HcaObjects)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *IOStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Usage) > 0 { + for _, e := range m.Usage { + l = e.Size() + n += 1 + l + sovMetrics(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *IOEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Major != 0 { + n += 1 + sovMetrics(uint64(m.Major)) + } + if m.Minor != 0 { + n += 1 + sovMetrics(uint64(m.Minor)) + } + if m.Rbytes != 0 { + n += 1 + sovMetrics(uint64(m.Rbytes)) + } + if m.Wbytes != 0 { + n += 1 + sovMetrics(uint64(m.Wbytes)) + } + if m.Rios != 0 { + n += 1 + sovMetrics(uint64(m.Rios)) + } + if m.Wios != 0 { + n += 1 + sovMetrics(uint64(m.Wios)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *HugeTlbStat) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Current != 0 { + n += 1 + sovMetrics(uint64(m.Current)) + } + if m.Max != 0 { + n += 1 + sovMetrics(uint64(m.Max)) + } + l = len(m.Pagesize) + if l > 0 { + n += 1 + l + sovMetrics(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovMetrics(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozMetrics(x uint64) (n int) { + return sovMetrics(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *Metrics) String() string { + if this == nil { + return "nil" + } + repeatedStringForHugetlb := "[]*HugeTlbStat{" + for _, f := range this.Hugetlb { + repeatedStringForHugetlb += strings.Replace(f.String(), "HugeTlbStat", "HugeTlbStat", 1) + "," + } + repeatedStringForHugetlb += "}" + s := strings.Join([]string{`&Metrics{`, + `Pids:` + strings.Replace(this.Pids.String(), "PidsStat", "PidsStat", 1) + `,`, + `CPU:` + strings.Replace(this.CPU.String(), "CPUStat", "CPUStat", 1) + `,`, + `Memory:` + strings.Replace(this.Memory.String(), "MemoryStat", "MemoryStat", 1) + `,`, + `Rdma:` + strings.Replace(this.Rdma.String(), "RdmaStat", "RdmaStat", 1) + `,`, + `Io:` + strings.Replace(this.Io.String(), "IOStat", "IOStat", 1) + `,`, + `Hugetlb:` + repeatedStringForHugetlb + `,`, + `MemoryEvents:` + strings.Replace(this.MemoryEvents.String(), "MemoryEvents", "MemoryEvents", 1) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *PidsStat) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&PidsStat{`, + `Current:` + fmt.Sprintf("%v", this.Current) + `,`, + `Limit:` + fmt.Sprintf("%v", this.Limit) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *CPUStat) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&CPUStat{`, + `UsageUsec:` + fmt.Sprintf("%v", this.UsageUsec) + `,`, + `UserUsec:` + fmt.Sprintf("%v", this.UserUsec) + `,`, + `SystemUsec:` + fmt.Sprintf("%v", this.SystemUsec) + `,`, + `NrPeriods:` + fmt.Sprintf("%v", this.NrPeriods) + `,`, + `NrThrottled:` + fmt.Sprintf("%v", this.NrThrottled) + `,`, + `ThrottledUsec:` + fmt.Sprintf("%v", this.ThrottledUsec) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *MemoryStat) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MemoryStat{`, + `Anon:` + fmt.Sprintf("%v", this.Anon) + `,`, + `File:` + fmt.Sprintf("%v", this.File) + `,`, + `KernelStack:` + fmt.Sprintf("%v", this.KernelStack) + `,`, + `Slab:` + fmt.Sprintf("%v", this.Slab) + `,`, + `Sock:` + fmt.Sprintf("%v", this.Sock) + `,`, + `Shmem:` + fmt.Sprintf("%v", this.Shmem) + `,`, + `FileMapped:` + fmt.Sprintf("%v", this.FileMapped) + `,`, + `FileDirty:` + fmt.Sprintf("%v", this.FileDirty) + `,`, + `FileWriteback:` + fmt.Sprintf("%v", this.FileWriteback) + `,`, + `AnonThp:` + fmt.Sprintf("%v", this.AnonThp) + `,`, + `InactiveAnon:` + fmt.Sprintf("%v", this.InactiveAnon) + `,`, + `ActiveAnon:` + fmt.Sprintf("%v", this.ActiveAnon) + `,`, + `InactiveFile:` + fmt.Sprintf("%v", this.InactiveFile) + `,`, + `ActiveFile:` + fmt.Sprintf("%v", this.ActiveFile) + `,`, + `Unevictable:` + fmt.Sprintf("%v", this.Unevictable) + `,`, + `SlabReclaimable:` + fmt.Sprintf("%v", this.SlabReclaimable) + `,`, + `SlabUnreclaimable:` + fmt.Sprintf("%v", this.SlabUnreclaimable) + `,`, + `Pgfault:` + fmt.Sprintf("%v", this.Pgfault) + `,`, + `Pgmajfault:` + fmt.Sprintf("%v", this.Pgmajfault) + `,`, + `WorkingsetRefault:` + fmt.Sprintf("%v", this.WorkingsetRefault) + `,`, + `WorkingsetActivate:` + fmt.Sprintf("%v", this.WorkingsetActivate) + `,`, + `WorkingsetNodereclaim:` + fmt.Sprintf("%v", this.WorkingsetNodereclaim) + `,`, + `Pgrefill:` + fmt.Sprintf("%v", this.Pgrefill) + `,`, + `Pgscan:` + fmt.Sprintf("%v", this.Pgscan) + `,`, + `Pgsteal:` + fmt.Sprintf("%v", this.Pgsteal) + `,`, + `Pgactivate:` + fmt.Sprintf("%v", this.Pgactivate) + `,`, + `Pgdeactivate:` + fmt.Sprintf("%v", this.Pgdeactivate) + `,`, + `Pglazyfree:` + fmt.Sprintf("%v", this.Pglazyfree) + `,`, + `Pglazyfreed:` + fmt.Sprintf("%v", this.Pglazyfreed) + `,`, + `ThpFaultAlloc:` + fmt.Sprintf("%v", this.ThpFaultAlloc) + `,`, + `ThpCollapseAlloc:` + fmt.Sprintf("%v", this.ThpCollapseAlloc) + `,`, + `Usage:` + fmt.Sprintf("%v", this.Usage) + `,`, + `UsageLimit:` + fmt.Sprintf("%v", this.UsageLimit) + `,`, + `SwapUsage:` + fmt.Sprintf("%v", this.SwapUsage) + `,`, + `SwapLimit:` + fmt.Sprintf("%v", this.SwapLimit) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *MemoryEvents) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&MemoryEvents{`, + `Low:` + fmt.Sprintf("%v", this.Low) + `,`, + `High:` + fmt.Sprintf("%v", this.High) + `,`, + `Max:` + fmt.Sprintf("%v", this.Max) + `,`, + `Oom:` + fmt.Sprintf("%v", this.Oom) + `,`, + `OomKill:` + fmt.Sprintf("%v", this.OomKill) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *RdmaStat) String() string { + if this == nil { + return "nil" + } + repeatedStringForCurrent := "[]*RdmaEntry{" + for _, f := range this.Current { + repeatedStringForCurrent += strings.Replace(f.String(), "RdmaEntry", "RdmaEntry", 1) + "," + } + repeatedStringForCurrent += "}" + repeatedStringForLimit := "[]*RdmaEntry{" + for _, f := range this.Limit { + repeatedStringForLimit += strings.Replace(f.String(), "RdmaEntry", "RdmaEntry", 1) + "," + } + repeatedStringForLimit += "}" + s := strings.Join([]string{`&RdmaStat{`, + `Current:` + repeatedStringForCurrent + `,`, + `Limit:` + repeatedStringForLimit + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *RdmaEntry) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&RdmaEntry{`, + `Device:` + fmt.Sprintf("%v", this.Device) + `,`, + `HcaHandles:` + fmt.Sprintf("%v", this.HcaHandles) + `,`, + `HcaObjects:` + fmt.Sprintf("%v", this.HcaObjects) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *IOStat) String() string { + if this == nil { + return "nil" + } + repeatedStringForUsage := "[]*IOEntry{" + for _, f := range this.Usage { + repeatedStringForUsage += strings.Replace(f.String(), "IOEntry", "IOEntry", 1) + "," + } + repeatedStringForUsage += "}" + s := strings.Join([]string{`&IOStat{`, + `Usage:` + repeatedStringForUsage + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *IOEntry) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&IOEntry{`, + `Major:` + fmt.Sprintf("%v", this.Major) + `,`, + `Minor:` + fmt.Sprintf("%v", this.Minor) + `,`, + `Rbytes:` + fmt.Sprintf("%v", this.Rbytes) + `,`, + `Wbytes:` + fmt.Sprintf("%v", this.Wbytes) + `,`, + `Rios:` + fmt.Sprintf("%v", this.Rios) + `,`, + `Wios:` + fmt.Sprintf("%v", this.Wios) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func (this *HugeTlbStat) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&HugeTlbStat{`, + `Current:` + fmt.Sprintf("%v", this.Current) + `,`, + `Max:` + fmt.Sprintf("%v", this.Max) + `,`, + `Pagesize:` + fmt.Sprintf("%v", this.Pagesize) + `,`, + `XXX_unrecognized:` + fmt.Sprintf("%v", this.XXX_unrecognized) + `,`, + `}`, + }, "") + return s +} +func valueToStringMetrics(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *Metrics) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Metrics: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Metrics: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pids", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Pids == nil { + m.Pids = &PidsStat{} + } + if err := m.Pids.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CPU", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.CPU == nil { + m.CPU = &CPUStat{} + } + if err := m.CPU.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Memory", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Memory == nil { + m.Memory = &MemoryStat{} + } + if err := m.Memory.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Rdma", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Rdma == nil { + m.Rdma = &RdmaStat{} + } + if err := m.Rdma.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Io", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Io == nil { + m.Io = &IOStat{} + } + if err := m.Io.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hugetlb", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Hugetlb = append(m.Hugetlb, &HugeTlbStat{}) + if err := m.Hugetlb[len(m.Hugetlb)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MemoryEvents", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MemoryEvents == nil { + m.MemoryEvents = &MemoryEvents{} + } + if err := m.MemoryEvents.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *PidsStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PidsStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PidsStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Current", wireType) + } + m.Current = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Current |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) + } + m.Limit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Limit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CPUStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CPUStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CPUStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UsageUsec", wireType) + } + m.UsageUsec = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UsageUsec |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UserUsec", wireType) + } + m.UserUsec = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UserUsec |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SystemUsec", wireType) + } + m.SystemUsec = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SystemUsec |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NrPeriods", wireType) + } + m.NrPeriods = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NrPeriods |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NrThrottled", wireType) + } + m.NrThrottled = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.NrThrottled |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ThrottledUsec", wireType) + } + m.ThrottledUsec = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ThrottledUsec |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MemoryStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MemoryStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MemoryStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Anon", wireType) + } + m.Anon = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Anon |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field File", wireType) + } + m.File = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.File |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field KernelStack", wireType) + } + m.KernelStack = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.KernelStack |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Slab", wireType) + } + m.Slab = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Slab |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Sock", wireType) + } + m.Sock = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Sock |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Shmem", wireType) + } + m.Shmem = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Shmem |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field FileMapped", wireType) + } + m.FileMapped = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.FileMapped |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field FileDirty", wireType) + } + m.FileDirty = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.FileDirty |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 9: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field FileWriteback", wireType) + } + m.FileWriteback = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.FileWriteback |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field AnonThp", wireType) + } + m.AnonThp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.AnonThp |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 11: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field InactiveAnon", wireType) + } + m.InactiveAnon = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.InactiveAnon |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 12: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ActiveAnon", wireType) + } + m.ActiveAnon = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ActiveAnon |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 13: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field InactiveFile", wireType) + } + m.InactiveFile = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.InactiveFile |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 14: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ActiveFile", wireType) + } + m.ActiveFile = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ActiveFile |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 15: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Unevictable", wireType) + } + m.Unevictable = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Unevictable |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 16: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SlabReclaimable", wireType) + } + m.SlabReclaimable = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SlabReclaimable |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 17: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SlabUnreclaimable", wireType) + } + m.SlabUnreclaimable = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SlabUnreclaimable |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 18: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgfault", wireType) + } + m.Pgfault = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgfault |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 19: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgmajfault", wireType) + } + m.Pgmajfault = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgmajfault |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 20: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field WorkingsetRefault", wireType) + } + m.WorkingsetRefault = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.WorkingsetRefault |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 21: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field WorkingsetActivate", wireType) + } + m.WorkingsetActivate = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.WorkingsetActivate |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 22: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field WorkingsetNodereclaim", wireType) + } + m.WorkingsetNodereclaim = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.WorkingsetNodereclaim |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 23: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgrefill", wireType) + } + m.Pgrefill = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgrefill |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 24: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgscan", wireType) + } + m.Pgscan = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgscan |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 25: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgsteal", wireType) + } + m.Pgsteal = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgsteal |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 26: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgactivate", wireType) + } + m.Pgactivate = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgactivate |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 27: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pgdeactivate", wireType) + } + m.Pgdeactivate = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pgdeactivate |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 28: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pglazyfree", wireType) + } + m.Pglazyfree = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pglazyfree |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 29: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Pglazyfreed", wireType) + } + m.Pglazyfreed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Pglazyfreed |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 30: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ThpFaultAlloc", wireType) + } + m.ThpFaultAlloc = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ThpFaultAlloc |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 31: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ThpCollapseAlloc", wireType) + } + m.ThpCollapseAlloc = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ThpCollapseAlloc |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 32: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Usage", wireType) + } + m.Usage = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Usage |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 33: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UsageLimit", wireType) + } + m.UsageLimit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UsageLimit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 34: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SwapUsage", wireType) + } + m.SwapUsage = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SwapUsage |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 35: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SwapLimit", wireType) + } + m.SwapLimit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SwapLimit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MemoryEvents) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MemoryEvents: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MemoryEvents: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Low", wireType) + } + m.Low = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Low |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field High", wireType) + } + m.High = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.High |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Max", wireType) + } + m.Max = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Max |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Oom", wireType) + } + m.Oom = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Oom |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field OomKill", wireType) + } + m.OomKill = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.OomKill |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RdmaStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RdmaStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RdmaStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Current", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Current = append(m.Current, &RdmaEntry{}) + if err := m.Current[len(m.Current)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Limit", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Limit = append(m.Limit, &RdmaEntry{}) + if err := m.Limit[len(m.Limit)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RdmaEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RdmaEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RdmaEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Device", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Device = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HcaHandles", wireType) + } + m.HcaHandles = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HcaHandles |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field HcaObjects", wireType) + } + m.HcaObjects = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.HcaObjects |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *IOStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: IOStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: IOStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Usage", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Usage = append(m.Usage, &IOEntry{}) + if err := m.Usage[len(m.Usage)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *IOEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: IOEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: IOEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Major", wireType) + } + m.Major = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Major |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Minor", wireType) + } + m.Minor = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Minor |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Rbytes", wireType) + } + m.Rbytes = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Rbytes |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Wbytes", wireType) + } + m.Wbytes = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Wbytes |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Rios", wireType) + } + m.Rios = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Rios |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Wios", wireType) + } + m.Wios = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Wios |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *HugeTlbStat) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: HugeTlbStat: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: HugeTlbStat: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Current", wireType) + } + m.Current = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Current |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Max", wireType) + } + m.Max = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Max |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pagesize", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowMetrics + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthMetrics + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthMetrics + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Pagesize = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipMetrics(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthMetrics + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipMetrics(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMetrics + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMetrics + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowMetrics + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthMetrics + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupMetrics + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthMetrics + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthMetrics = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowMetrics = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupMetrics = fmt.Errorf("proto: unexpected end of group") +) diff --git a/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.txt b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.txt new file mode 100644 index 00000000000..59fe27cbffb --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.pb.txt @@ -0,0 +1,539 @@ +file { + name: "github.com/containerd/cgroups/v2/stats/metrics.proto" + package: "io.containerd.cgroups.v2" + dependency: "gogoproto/gogo.proto" + message_type { + name: "Metrics" + field { + name: "pids" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.PidsStat" + json_name: "pids" + } + field { + name: "cpu" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.CPUStat" + options { + 65004: "CPU" + } + json_name: "cpu" + } + field { + name: "memory" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.MemoryStat" + json_name: "memory" + } + field { + name: "rdma" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.RdmaStat" + json_name: "rdma" + } + field { + name: "io" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.IOStat" + json_name: "io" + } + field { + name: "hugetlb" + number: 7 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.HugeTlbStat" + json_name: "hugetlb" + } + field { + name: "memory_events" + number: 8 + label: LABEL_OPTIONAL + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.MemoryEvents" + json_name: "memoryEvents" + } + } + message_type { + name: "PidsStat" + field { + name: "current" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "current" + } + field { + name: "limit" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "limit" + } + } + message_type { + name: "CPUStat" + field { + name: "usage_usec" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "usageUsec" + } + field { + name: "user_usec" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "userUsec" + } + field { + name: "system_usec" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "systemUsec" + } + field { + name: "nr_periods" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "nrPeriods" + } + field { + name: "nr_throttled" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "nrThrottled" + } + field { + name: "throttled_usec" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "throttledUsec" + } + } + message_type { + name: "MemoryStat" + field { + name: "anon" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "anon" + } + field { + name: "file" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "file" + } + field { + name: "kernel_stack" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "kernelStack" + } + field { + name: "slab" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "slab" + } + field { + name: "sock" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "sock" + } + field { + name: "shmem" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "shmem" + } + field { + name: "file_mapped" + number: 7 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "fileMapped" + } + field { + name: "file_dirty" + number: 8 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "fileDirty" + } + field { + name: "file_writeback" + number: 9 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "fileWriteback" + } + field { + name: "anon_thp" + number: 10 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "anonThp" + } + field { + name: "inactive_anon" + number: 11 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "inactiveAnon" + } + field { + name: "active_anon" + number: 12 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "activeAnon" + } + field { + name: "inactive_file" + number: 13 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "inactiveFile" + } + field { + name: "active_file" + number: 14 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "activeFile" + } + field { + name: "unevictable" + number: 15 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "unevictable" + } + field { + name: "slab_reclaimable" + number: 16 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "slabReclaimable" + } + field { + name: "slab_unreclaimable" + number: 17 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "slabUnreclaimable" + } + field { + name: "pgfault" + number: 18 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgfault" + } + field { + name: "pgmajfault" + number: 19 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgmajfault" + } + field { + name: "workingset_refault" + number: 20 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "workingsetRefault" + } + field { + name: "workingset_activate" + number: 21 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "workingsetActivate" + } + field { + name: "workingset_nodereclaim" + number: 22 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "workingsetNodereclaim" + } + field { + name: "pgrefill" + number: 23 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgrefill" + } + field { + name: "pgscan" + number: 24 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgscan" + } + field { + name: "pgsteal" + number: 25 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgsteal" + } + field { + name: "pgactivate" + number: 26 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgactivate" + } + field { + name: "pgdeactivate" + number: 27 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pgdeactivate" + } + field { + name: "pglazyfree" + number: 28 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pglazyfree" + } + field { + name: "pglazyfreed" + number: 29 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "pglazyfreed" + } + field { + name: "thp_fault_alloc" + number: 30 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "thpFaultAlloc" + } + field { + name: "thp_collapse_alloc" + number: 31 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "thpCollapseAlloc" + } + field { + name: "usage" + number: 32 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "usage" + } + field { + name: "usage_limit" + number: 33 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "usageLimit" + } + field { + name: "swap_usage" + number: 34 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "swapUsage" + } + field { + name: "swap_limit" + number: 35 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "swapLimit" + } + } + message_type { + name: "MemoryEvents" + field { + name: "low" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "low" + } + field { + name: "high" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "high" + } + field { + name: "max" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "max" + } + field { + name: "oom" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "oom" + } + field { + name: "oom_kill" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "oomKill" + } + } + message_type { + name: "RdmaStat" + field { + name: "current" + number: 1 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.RdmaEntry" + json_name: "current" + } + field { + name: "limit" + number: 2 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.RdmaEntry" + json_name: "limit" + } + } + message_type { + name: "RdmaEntry" + field { + name: "device" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "device" + } + field { + name: "hca_handles" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "hcaHandles" + } + field { + name: "hca_objects" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT32 + json_name: "hcaObjects" + } + } + message_type { + name: "IOStat" + field { + name: "usage" + number: 1 + label: LABEL_REPEATED + type: TYPE_MESSAGE + type_name: ".io.containerd.cgroups.v2.IOEntry" + json_name: "usage" + } + } + message_type { + name: "IOEntry" + field { + name: "major" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "major" + } + field { + name: "minor" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "minor" + } + field { + name: "rbytes" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "rbytes" + } + field { + name: "wbytes" + number: 4 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "wbytes" + } + field { + name: "rios" + number: 5 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "rios" + } + field { + name: "wios" + number: 6 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "wios" + } + } + message_type { + name: "HugeTlbStat" + field { + name: "current" + number: 1 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "current" + } + field { + name: "max" + number: 2 + label: LABEL_OPTIONAL + type: TYPE_UINT64 + json_name: "max" + } + field { + name: "pagesize" + number: 3 + label: LABEL_OPTIONAL + type: TYPE_STRING + json_name: "pagesize" + } + } + syntax: "proto3" +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.proto b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.proto new file mode 100644 index 00000000000..8ac472e4645 --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/stats/metrics.proto @@ -0,0 +1,105 @@ +syntax = "proto3"; + +package io.containerd.cgroups.v2; + + import "gogoproto/gogo.proto"; + +message Metrics { + PidsStat pids = 1; + CPUStat cpu = 2 [(gogoproto.customname) = "CPU"]; + MemoryStat memory = 4; + RdmaStat rdma = 5; + IOStat io = 6; + repeated HugeTlbStat hugetlb = 7; + MemoryEvents memory_events = 8; +} + +message PidsStat { + uint64 current = 1; + uint64 limit = 2; +} + +message CPUStat { + uint64 usage_usec = 1; + uint64 user_usec = 2; + uint64 system_usec = 3; + uint64 nr_periods = 4; + uint64 nr_throttled = 5; + uint64 throttled_usec = 6; +} + +message MemoryStat { + uint64 anon = 1; + uint64 file = 2; + uint64 kernel_stack = 3; + uint64 slab = 4; + uint64 sock = 5; + uint64 shmem = 6; + uint64 file_mapped = 7; + uint64 file_dirty = 8; + uint64 file_writeback = 9; + uint64 anon_thp = 10; + uint64 inactive_anon = 11; + uint64 active_anon = 12; + uint64 inactive_file = 13; + uint64 active_file = 14; + uint64 unevictable = 15; + uint64 slab_reclaimable = 16; + uint64 slab_unreclaimable = 17; + uint64 pgfault = 18; + uint64 pgmajfault = 19; + uint64 workingset_refault = 20; + uint64 workingset_activate = 21; + uint64 workingset_nodereclaim = 22; + uint64 pgrefill = 23; + uint64 pgscan = 24; + uint64 pgsteal = 25; + uint64 pgactivate = 26; + uint64 pgdeactivate = 27; + uint64 pglazyfree = 28; + uint64 pglazyfreed = 29; + uint64 thp_fault_alloc = 30; + uint64 thp_collapse_alloc = 31; + uint64 usage = 32; + uint64 usage_limit = 33; + uint64 swap_usage = 34; + uint64 swap_limit = 35; +} + +message MemoryEvents { + uint64 low = 1; + uint64 high = 2; + uint64 max = 3; + uint64 oom = 4; + uint64 oom_kill = 5; +} + +message RdmaStat { + repeated RdmaEntry current = 1; + repeated RdmaEntry limit = 2; +} + +message RdmaEntry { + string device = 1; + uint32 hca_handles = 2; + uint32 hca_objects = 3; +} + +message IOStat { + repeated IOEntry usage = 1; +} + +message IOEntry { + uint64 major = 1; + uint64 minor = 2; + uint64 rbytes = 3; + uint64 wbytes = 4; + uint64 rios = 5; + uint64 wios = 6; +} + +message HugeTlbStat { + uint64 current = 1; + uint64 max = 2; + string pagesize = 3; +} diff --git a/agent/vendor/github.com/containerd/cgroups/v2/utils.go b/agent/vendor/github.com/containerd/cgroups/v2/utils.go new file mode 100644 index 00000000000..902466f51be --- /dev/null +++ b/agent/vendor/github.com/containerd/cgroups/v2/utils.go @@ -0,0 +1,436 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "math" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containerd/cgroups/v2/stats" + + "github.com/godbus/dbus/v5" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" +) + +const ( + cgroupProcs = "cgroup.procs" + defaultDirPerm = 0755 +) + +// defaultFilePerm is a var so that the test framework can change the filemode +// of all files created when the tests are running. The difference between the +// tests and real world use is that files like "cgroup.procs" will exist when writing +// to a read cgroup filesystem and do not exist prior when running in the tests. +// this is set to a non 0 value in the test code +var defaultFilePerm = os.FileMode(0) + +// remove will remove a cgroup path handling EAGAIN and EBUSY errors and +// retrying the remove after a exp timeout +func remove(path string) error { + var err error + delay := 10 * time.Millisecond + for i := 0; i < 5; i++ { + if i != 0 { + time.Sleep(delay) + delay *= 2 + } + if err = os.RemoveAll(path); err == nil { + return nil + } + } + return fmt.Errorf("cgroups: unable to remove path %q: %w", path, err) +} + +// parseCgroupProcsFile parses /sys/fs/cgroup/$GROUPPATH/cgroup.procs +func parseCgroupProcsFile(path string) ([]uint64, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var ( + out []uint64 + s = bufio.NewScanner(f) + ) + for s.Scan() { + if t := s.Text(); t != "" { + pid, err := strconv.ParseUint(t, 10, 0) + if err != nil { + return nil, err + } + out = append(out, pid) + } + } + if err := s.Err(); err != nil { + return nil, err + } + return out, nil +} + +func parseKV(raw string) (string, interface{}, error) { + parts := strings.Fields(raw) + switch len(parts) { + case 2: + v, err := parseUint(parts[1], 10, 64) + if err != nil { + // if we cannot parse as a uint, parse as a string + return parts[0], parts[1], nil + } + return parts[0], v, nil + default: + return "", 0, ErrInvalidFormat + } +} + +func parseUint(s string, base, bitSize int) (uint64, error) { + v, err := strconv.ParseUint(s, base, bitSize) + if err != nil { + intValue, intErr := strconv.ParseInt(s, base, bitSize) + // 1. Handle negative values greater than MinInt64 (and) + // 2. Handle negative values lesser than MinInt64 + if intErr == nil && intValue < 0 { + return 0, nil + } else if intErr != nil && + intErr.(*strconv.NumError).Err == strconv.ErrRange && + intValue < 0 { + return 0, nil + } + return 0, err + } + return v, nil +} + +// parseCgroupFile parses /proc/PID/cgroup file and return string +func parseCgroupFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + return parseCgroupFromReader(f) +} + +func parseCgroupFromReader(r io.Reader) (string, error) { + var ( + s = bufio.NewScanner(r) + ) + for s.Scan() { + var ( + text = s.Text() + parts = strings.SplitN(text, ":", 3) + ) + if len(parts) < 3 { + return "", fmt.Errorf("invalid cgroup entry: %q", text) + } + // text is like "0::/user.slice/user-1001.slice/session-1.scope" + if parts[0] == "0" && parts[1] == "" { + return parts[2], nil + } + } + if err := s.Err(); err != nil { + return "", err + } + return "", fmt.Errorf("cgroup path not found") +} + +// ToResources converts the oci LinuxResources struct into a +// v2 Resources type for use with this package. +// +// converting cgroups configuration from v1 to v2 +// ref: https://github.com/containers/crun/blob/master/crun.1.md#cgroup-v2 +func ToResources(spec *specs.LinuxResources) *Resources { + var resources Resources + if cpu := spec.CPU; cpu != nil { + resources.CPU = &CPU{ + Cpus: cpu.Cpus, + Mems: cpu.Mems, + } + if shares := cpu.Shares; shares != nil { + convertedWeight := 1 + ((*shares-2)*9999)/262142 + resources.CPU.Weight = &convertedWeight + } + if period := cpu.Period; period != nil { + resources.CPU.Max = NewCPUMax(cpu.Quota, period) + } + } + if mem := spec.Memory; mem != nil { + resources.Memory = &Memory{} + if swap := mem.Swap; swap != nil { + resources.Memory.Swap = swap + } + if l := mem.Limit; l != nil { + resources.Memory.Max = l + } + if l := mem.Reservation; l != nil { + resources.Memory.Low = l + } + } + if hugetlbs := spec.HugepageLimits; hugetlbs != nil { + hugeTlbUsage := HugeTlb{} + for _, hugetlb := range hugetlbs { + hugeTlbUsage = append(hugeTlbUsage, HugeTlbEntry{ + HugePageSize: hugetlb.Pagesize, + Limit: hugetlb.Limit, + }) + } + resources.HugeTlb = &hugeTlbUsage + } + if pids := spec.Pids; pids != nil { + resources.Pids = &Pids{ + Max: pids.Limit, + } + } + if i := spec.BlockIO; i != nil { + resources.IO = &IO{} + if i.Weight != nil { + resources.IO.BFQ.Weight = 1 + (*i.Weight-10)*9999/990 + } + for t, devices := range map[IOType][]specs.LinuxThrottleDevice{ + ReadBPS: i.ThrottleReadBpsDevice, + WriteBPS: i.ThrottleWriteBpsDevice, + ReadIOPS: i.ThrottleReadIOPSDevice, + WriteIOPS: i.ThrottleWriteIOPSDevice, + } { + for _, d := range devices { + resources.IO.Max = append(resources.IO.Max, Entry{ + Type: t, + Major: d.Major, + Minor: d.Minor, + Rate: d.Rate, + }) + } + } + } + if i := spec.Rdma; i != nil { + resources.RDMA = &RDMA{} + for device, value := range spec.Rdma { + if device != "" && (value.HcaHandles != nil || value.HcaObjects != nil) { + resources.RDMA.Limit = append(resources.RDMA.Limit, RDMAEntry{ + Device: device, + HcaHandles: *value.HcaHandles, + HcaObjects: *value.HcaObjects, + }) + } + } + } + + return &resources +} + +// Gets uint64 parsed content of single value cgroup stat file +func getStatFileContentUint64(filePath string) uint64 { + contents, err := ioutil.ReadFile(filePath) + if err != nil { + return 0 + } + trimmed := strings.TrimSpace(string(contents)) + if trimmed == "max" { + return math.MaxUint64 + } + + res, err := parseUint(trimmed, 10, 64) + if err != nil { + logrus.Errorf("unable to parse %q as a uint from Cgroup file %q", string(contents), filePath) + return res + } + + return res +} + +func readIoStats(path string) []*stats.IOEntry { + // more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt + var usage []*stats.IOEntry + fpath := filepath.Join(path, "io.stat") + currentData, err := ioutil.ReadFile(fpath) + if err != nil { + return usage + } + entries := strings.Split(string(currentData), "\n") + + for _, entry := range entries { + parts := strings.Split(entry, " ") + if len(parts) < 2 { + continue + } + majmin := strings.Split(parts[0], ":") + if len(majmin) != 2 { + continue + } + major, err := strconv.ParseUint(majmin[0], 10, 0) + if err != nil { + return usage + } + minor, err := strconv.ParseUint(majmin[1], 10, 0) + if err != nil { + return usage + } + parts = parts[1:] + ioEntry := stats.IOEntry{ + Major: major, + Minor: minor, + } + for _, s := range parts { + keyPairValue := strings.Split(s, "=") + if len(keyPairValue) != 2 { + continue + } + v, err := strconv.ParseUint(keyPairValue[1], 10, 0) + if err != nil { + continue + } + switch keyPairValue[0] { + case "rbytes": + ioEntry.Rbytes = v + case "wbytes": + ioEntry.Wbytes = v + case "rios": + ioEntry.Rios = v + case "wios": + ioEntry.Wios = v + } + } + usage = append(usage, &ioEntry) + } + return usage +} + +func rdmaStats(filepath string) []*stats.RdmaEntry { + currentData, err := ioutil.ReadFile(filepath) + if err != nil { + return []*stats.RdmaEntry{} + } + return toRdmaEntry(strings.Split(string(currentData), "\n")) +} + +func parseRdmaKV(raw string, entry *stats.RdmaEntry) { + var value uint64 + var err error + + parts := strings.Split(raw, "=") + switch len(parts) { + case 2: + if parts[1] == "max" { + value = math.MaxUint32 + } else { + value, err = parseUint(parts[1], 10, 32) + if err != nil { + return + } + } + if parts[0] == "hca_handle" { + entry.HcaHandles = uint32(value) + } else if parts[0] == "hca_object" { + entry.HcaObjects = uint32(value) + } + } +} + +func toRdmaEntry(strEntries []string) []*stats.RdmaEntry { + var rdmaEntries []*stats.RdmaEntry + for i := range strEntries { + parts := strings.Fields(strEntries[i]) + switch len(parts) { + case 3: + entry := new(stats.RdmaEntry) + entry.Device = parts[0] + parseRdmaKV(parts[1], entry) + parseRdmaKV(parts[2], entry) + + rdmaEntries = append(rdmaEntries, entry) + default: + continue + } + } + return rdmaEntries +} + +// isUnitExists returns true if the error is that a systemd unit already exists. +func isUnitExists(err error) bool { + if err != nil { + if dbusError, ok := err.(dbus.Error); ok { + return strings.Contains(dbusError.Name, "org.freedesktop.systemd1.UnitExists") + } + } + return false +} + +func systemdUnitFromPath(path string) string { + _, unit := filepath.Split(path) + return unit +} + +func readHugeTlbStats(path string) []*stats.HugeTlbStat { + var usage = []*stats.HugeTlbStat{} + var keyUsage = make(map[string]*stats.HugeTlbStat) + f, err := os.Open(path) + if err != nil { + return usage + } + files, err := f.Readdir(-1) + f.Close() + if err != nil { + return usage + } + + for _, file := range files { + if strings.Contains(file.Name(), "hugetlb") && + (strings.HasSuffix(file.Name(), "max") || strings.HasSuffix(file.Name(), "current")) { + var hugeTlb *stats.HugeTlbStat + var ok bool + fileName := strings.Split(file.Name(), ".") + pageSize := fileName[1] + if hugeTlb, ok = keyUsage[pageSize]; !ok { + hugeTlb = &stats.HugeTlbStat{} + } + hugeTlb.Pagesize = pageSize + out, err := ioutil.ReadFile(filepath.Join(path, file.Name())) + if err != nil { + continue + } + var value uint64 + stringVal := strings.TrimSpace(string(out)) + if stringVal == "max" { + value = math.MaxUint64 + } else { + value, err = strconv.ParseUint(stringVal, 10, 64) + } + if err != nil { + continue + } + switch fileName[2] { + case "max": + hugeTlb.Max = value + case "current": + hugeTlb.Current = value + } + keyUsage[pageSize] = hugeTlb + } + } + for _, entry := range keyUsage { + usage = append(usage, entry) + } + return usage +} diff --git a/agent/vendor/github.com/containernetworking/cni/libcni/api.go b/agent/vendor/github.com/containernetworking/cni/libcni/api.go index 0f14d3427e9..7e52bd83873 100644 --- a/agent/vendor/github.com/containernetworking/cni/libcni/api.go +++ b/agent/vendor/github.com/containernetworking/cni/libcni/api.go @@ -25,6 +25,7 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" ) @@ -32,6 +33,10 @@ var ( CacheDir = "/var/lib/cni" ) +const ( + CNICacheV1 = "cniCacheV1" +) + // A RuntimeConf holds the arguments to one invocation of a CNI plugin // excepting the network configuration, with the nested exception that // the `runtimeConfig` from the network configuration is included @@ -48,7 +53,7 @@ type RuntimeConf struct { // to the plugin CapabilityArgs map[string]interface{} - // A cache directory in which to library data. Defaults to CacheDir + // DEPRECATED. Will be removed in a future release. CacheDir string } @@ -70,19 +75,22 @@ type CNI interface { CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) + GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) + GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) } type CNIConfig struct { - Path []string - exec invoke.Exec + Path []string + exec invoke.Exec + cacheDir string } // CNIConfig implements the CNI interface @@ -92,9 +100,18 @@ var _ CNI = &CNIConfig{} // in the given paths and use the given exec interface to run those plugins, // or if the exec interface is not given, will use a default exec handler. func NewCNIConfig(path []string, exec invoke.Exec) *CNIConfig { + return NewCNIConfigWithCacheDir(path, "", exec) +} + +// NewCNIConfigWithCacheDir returns a new CNIConfig object that will search for plugins +// in the given paths use the given exec interface to run those plugins, +// or if the exec interface is not given, will use a default exec handler. +// The given cache directory will be used for temporary data storage when needed. +func NewCNIConfigWithCacheDir(path []string, cacheDir string, exec invoke.Exec) *CNIConfig { return &CNIConfig{ - Path: path, - exec: exec, + Path: path, + cacheDir: cacheDir, + exec: exec, } } @@ -165,33 +182,122 @@ func (c *CNIConfig) ensureExec() invoke.Exec { return c.exec } -func getResultCacheFilePath(netName string, rt *RuntimeConf) string { - cacheDir := rt.CacheDir - if cacheDir == "" { - cacheDir = CacheDir +type cachedInfo struct { + Kind string `json:"kind"` + ContainerID string `json:"containerId"` + Config []byte `json:"config"` + IfName string `json:"ifName"` + NetworkName string `json:"networkName"` + CniArgs [][2]string `json:"cniArgs,omitempty"` + CapabilityArgs map[string]interface{} `json:"capabilityArgs,omitempty"` + RawResult map[string]interface{} `json:"result,omitempty"` + Result types.Result `json:"-"` +} + +// getCacheDir returns the cache directory in this order: +// 1) global cacheDir from CNIConfig object +// 2) deprecated cacheDir from RuntimeConf object +// 3) fall back to default cache directory +func (c *CNIConfig) getCacheDir(rt *RuntimeConf) string { + if c.cacheDir != "" { + return c.cacheDir + } + if rt.CacheDir != "" { + return rt.CacheDir + } + return CacheDir +} + +func (c *CNIConfig) getCacheFilePath(netName string, rt *RuntimeConf) (string, error) { + if netName == "" || rt.ContainerID == "" || rt.IfName == "" { + return "", fmt.Errorf("cache file path requires network name (%q), container ID (%q), and interface name (%q)", netName, rt.ContainerID, rt.IfName) } - return filepath.Join(cacheDir, "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName)) + return filepath.Join(c.getCacheDir(rt), "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName)), nil } -func setCachedResult(result types.Result, netName string, rt *RuntimeConf) error { +func (c *CNIConfig) cacheAdd(result types.Result, config []byte, netName string, rt *RuntimeConf) error { + cached := cachedInfo{ + Kind: CNICacheV1, + ContainerID: rt.ContainerID, + Config: config, + IfName: rt.IfName, + NetworkName: netName, + CniArgs: rt.Args, + CapabilityArgs: rt.CapabilityArgs, + } + + // We need to get type.Result into cachedInfo as JSON map + // Marshal to []byte, then Unmarshal into cached.RawResult data, err := json.Marshal(result) if err != nil { return err } - fname := getResultCacheFilePath(netName, rt) + + err = json.Unmarshal(data, &cached.RawResult) + if err != nil { + return err + } + + newBytes, err := json.Marshal(&cached) + if err != nil { + return err + } + + fname, err := c.getCacheFilePath(netName, rt) + if err != nil { + return err + } if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil { return err } - return ioutil.WriteFile(fname, data, 0600) + + return ioutil.WriteFile(fname, newBytes, 0600) } -func delCachedResult(netName string, rt *RuntimeConf) error { - fname := getResultCacheFilePath(netName, rt) +func (c *CNIConfig) cacheDel(netName string, rt *RuntimeConf) error { + fname, err := c.getCacheFilePath(netName, rt) + if err != nil { + // Ignore error + return nil + } return os.Remove(fname) } -func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) { - fname := getResultCacheFilePath(netName, rt) +func (c *CNIConfig) getCachedConfig(netName string, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { + var bytes []byte + + fname, err := c.getCacheFilePath(netName, rt) + if err != nil { + return nil, nil, err + } + bytes, err = ioutil.ReadFile(fname) + if err != nil { + // Ignore read errors; the cached result may not exist on-disk + return nil, nil, nil + } + + unmarshaled := cachedInfo{} + if err := json.Unmarshal(bytes, &unmarshaled); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal cached network %q config: %v", netName, err) + } + if unmarshaled.Kind != CNICacheV1 { + return nil, nil, fmt.Errorf("read cached network %q config has wrong kind: %v", netName, unmarshaled.Kind) + } + + newRt := *rt + if unmarshaled.CniArgs != nil { + newRt.Args = unmarshaled.CniArgs + } + newRt.CapabilityArgs = unmarshaled.CapabilityArgs + + return unmarshaled.Config, &newRt, nil +} + +func (c *CNIConfig) getLegacyCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) { + fname, err := c.getCacheFilePath(netName, rt) + if err != nil { + return nil, err + } data, err := ioutil.ReadFile(fname) if err != nil { // Ignore read errors; the cached result may not exist on-disk @@ -222,16 +328,73 @@ func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, return result, err } +func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) { + fname, err := c.getCacheFilePath(netName, rt) + if err != nil { + return nil, err + } + fdata, err := ioutil.ReadFile(fname) + if err != nil { + // Ignore read errors; the cached result may not exist on-disk + return nil, nil + } + + cachedInfo := cachedInfo{} + if err := json.Unmarshal(fdata, &cachedInfo); err != nil || cachedInfo.Kind != CNICacheV1 { + return c.getLegacyCachedResult(netName, cniVersion, rt) + } + + newBytes, err := json.Marshal(&cachedInfo.RawResult) + if err != nil { + return nil, fmt.Errorf("failed to marshal cached network %q config: %v", netName, err) + } + + // Read the version of the cached result + decoder := version.ConfigDecoder{} + resultCniVersion, err := decoder.Decode(newBytes) + if err != nil { + return nil, err + } + + // Ensure we can understand the result + result, err := version.NewResult(resultCniVersion, newBytes) + if err != nil { + return nil, err + } + + // Convert to the config version to ensure plugins get prevResult + // in the same version as the config. The cached result version + // should match the config version unless the config was changed + // while the container was running. + result, err = result.GetAsVersion(cniVersion) + if err != nil && resultCniVersion != cniVersion { + return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err) + } + return result, err +} + // GetNetworkListCachedResult returns the cached Result of the previous -// previous AddNetworkList() operation for a network list, or an error. +// AddNetworkList() operation for a network list, or an error. func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) { - return getCachedResult(list.Name, list.CNIVersion, rt) + return c.getCachedResult(list.Name, list.CNIVersion, rt) } // GetNetworkCachedResult returns the cached Result of the previous -// previous AddNetwork() operation for a network, or an error. +// AddNetwork() operation for a network, or an error. func (c *CNIConfig) GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) { - return getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) + return c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) +} + +// GetNetworkListCachedConfig copies the input RuntimeConf to output +// RuntimeConf with fields updated with info from the cached Config. +func (c *CNIConfig) GetNetworkListCachedConfig(list *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { + return c.getCachedConfig(list.Name, rt) +} + +// GetNetworkCachedConfig copies the input RuntimeConf to output +// RuntimeConf with fields updated with info from the cached Config. +func (c *CNIConfig) GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { + return c.getCachedConfig(net.Network.Name, rt) } func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) { @@ -240,6 +403,15 @@ func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net if err != nil { return nil, err } + if err := utils.ValidateContainerID(rt.ContainerID); err != nil { + return nil, err + } + if err := utils.ValidateNetworkName(name); err != nil { + return nil, err + } + if err := utils.ValidateInterfaceName(rt.IfName); err != nil { + return nil, err + } newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt) if err != nil { @@ -260,7 +432,7 @@ func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, } } - if err = setCachedResult(result, list.Name, rt); err != nil { + if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil { return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err) } @@ -295,7 +467,7 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis return nil } - cachedResult, err := getCachedResult(list.Name, list.CNIVersion, rt) + cachedResult, err := c.getCachedResult(list.Name, list.CNIVersion, rt) if err != nil { return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err) } @@ -332,7 +504,7 @@ func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil { return err } else if gtet { - cachedResult, err = getCachedResult(list.Name, list.CNIVersion, rt) + cachedResult, err = c.getCachedResult(list.Name, list.CNIVersion, rt) if err != nil { return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err) } @@ -344,7 +516,7 @@ func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, return err } } - _ = delCachedResult(list.Name, rt) + _ = c.cacheDel(list.Name, rt) return nil } @@ -356,7 +528,7 @@ func (c *CNIConfig) AddNetwork(ctx context.Context, net *NetworkConfig, rt *Runt return nil, err } - if err = setCachedResult(result, net.Network.Name, rt); err != nil { + if err = c.cacheAdd(result, net.Bytes, net.Network.Name, rt); err != nil { return nil, fmt.Errorf("failed to set network %q cached result: %v", net.Network.Name, err) } @@ -372,7 +544,7 @@ func (c *CNIConfig) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *Ru return fmt.Errorf("configuration version %q does not support the CHECK command", net.Network.CNIVersion) } - cachedResult, err := getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) + cachedResult, err := c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) if err != nil { return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err) } @@ -387,7 +559,7 @@ func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *Runt if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil { return err } else if gtet { - cachedResult, err = getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) + cachedResult, err = c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) if err != nil { return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err) } @@ -396,7 +568,7 @@ func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *Runt if err := c.delNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt); err != nil { return err } - _ = delCachedResult(net.Network.Name, rt) + _ = c.cacheDel(net.Network.Name, rt) return nil } @@ -455,10 +627,14 @@ func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([] // validatePlugin checks that an individual plugin's configuration is sane func (c *CNIConfig) validatePlugin(ctx context.Context, pluginName, expectedVersion string) error { - pluginPath, err := invoke.FindInPath(pluginName, c.Path) + c.ensureExec() + pluginPath, err := c.exec.FindInPath(pluginName, c.Path) if err != nil { return err } + if expectedVersion == "" { + expectedVersion = "0.1.0" + } vi, err := invoke.GetVersionInfo(ctx, pluginPath, c.exec) if err != nil { diff --git a/agent/vendor/github.com/containernetworking/cni/libcni/conf.go b/agent/vendor/github.com/containernetworking/cni/libcni/conf.go index ea56c509d01..d8920cf8cd5 100644 --- a/agent/vendor/github.com/containernetworking/cni/libcni/conf.go +++ b/agent/vendor/github.com/containernetworking/cni/libcni/conf.go @@ -114,11 +114,11 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { for i, conf := range plugins { newBytes, err := json.Marshal(conf) if err != nil { - return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err) + return nil, fmt.Errorf("failed to marshal plugin config %d: %v", i, err) } netConf, err := ConfFromBytes(newBytes) if err != nil { - return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err) + return nil, fmt.Errorf("failed to parse plugin config %d: %v", i, err) } list.Plugins = append(list.Plugins, netConf) } diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/args.go b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/args.go index 913528c1d59..3cdb4bc8dad 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/args.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/args.go @@ -32,7 +32,7 @@ type inherited struct{} var inheritArgsFromEnv inherited -func (_ *inherited) AsEnv() []string { +func (*inherited) AsEnv() []string { return nil } @@ -60,8 +60,8 @@ func (args *Args) AsEnv() []string { pluginArgsStr = stringify(args.PluginArgs) } - // Duplicated values which come first will be overrided, so we must put the - // custom values in the end to avoid being overrided by the process environments. + // Duplicated values which come first will be overridden, so we must put the + // custom values in the end to avoid being overridden by the process environments. env = append(env, "CNI_COMMAND="+args.Command, "CNI_CONTAINERID="+args.ContainerID, diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/find.go b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/find.go index e815404c859..e62029eb788 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/find.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/find.go @@ -18,6 +18,7 @@ import ( "fmt" "os" "path/filepath" + "strings" ) // FindInPath returns the full path of the plugin by searching in the provided path @@ -26,6 +27,10 @@ func FindInPath(plugin string, paths []string) (string, error) { return "", fmt.Errorf("no plugin name provided") } + if strings.ContainsRune(plugin, os.PathSeparator) { + return "", fmt.Errorf("invalid plugin name: %s", plugin) + } + if len(paths) == 0 { return "", fmt.Errorf("no paths provided") } diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec.go b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec.go index ad8498ba27d..5ab5cc88576 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/invoke/raw_exec.go @@ -21,6 +21,8 @@ import ( "fmt" "io" "os/exec" + "strings" + "time" "github.com/containernetworking/cni/pkg/types" ) @@ -31,30 +33,54 @@ type RawExec struct { func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) { stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} c := exec.CommandContext(ctx, pluginPath) c.Env = environ c.Stdin = bytes.NewBuffer(stdinData) c.Stdout = stdout - c.Stderr = e.Stderr - if err := c.Run(); err != nil { - return nil, pluginErr(err, stdout.Bytes()) + c.Stderr = stderr + + // Retry the command on "text file busy" errors + for i := 0; i <= 5; i++ { + err := c.Run() + + // Command succeeded + if err == nil { + break + } + + // If the plugin is currently about to be written, then we wait a + // second and try it again + if strings.Contains(err.Error(), "text file busy") { + time.Sleep(time.Second) + continue + } + + // All other errors except than the busy text file + return nil, e.pluginErr(err, stdout.Bytes(), stderr.Bytes()) } + // Copy stderr to caller's buffer in case plugin printed to both + // stdout and stderr for some reason. Ignore failures as stderr is + // only informational. + if e.Stderr != nil && stderr.Len() > 0 { + _, _ = stderr.WriteTo(e.Stderr) + } return stdout.Bytes(), nil } -func pluginErr(err error, output []byte) error { - if _, ok := err.(*exec.ExitError); ok { - emsg := types.Error{} - if len(output) == 0 { - emsg.Msg = "netplugin failed with no error message" - } else if perr := json.Unmarshal(output, &emsg); perr != nil { - emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) +func (e *RawExec) pluginErr(err error, stdout, stderr []byte) error { + emsg := types.Error{} + if len(stdout) == 0 { + if len(stderr) == 0 { + emsg.Msg = fmt.Sprintf("netplugin failed with no error message: %v", err) + } else { + emsg.Msg = fmt.Sprintf("netplugin failed: %q", string(stderr)) } - return &emsg + } else if perr := json.Unmarshal(stdout, &emsg); perr != nil { + emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(stdout), perr) } - - return err + return &emsg } func (e *RawExec) FindInPath(plugin string, paths []string) (string, error) { diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/types/020/types.go b/agent/vendor/github.com/containernetworking/cni/pkg/types/020/types.go index 53256167fad..36f31678a8e 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/types/020/types.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/types/020/types.go @@ -86,20 +86,6 @@ func (r *Result) PrintTo(writer io.Writer) error { return err } -// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where -// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the -// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. -func (r *Result) String() string { - var str string - if r.IP4 != nil { - str = fmt.Sprintf("IP4:%+v, ", *r.IP4) - } - if r.IP6 != nil { - str += fmt.Sprintf("IP6:%+v, ", *r.IP6) - } - return fmt.Sprintf("%sDNS:%+v", str, r.DNS) -} - // IPConfig contains values necessary to configure an interface type IPConfig struct { IP net.IPNet diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/types/args.go b/agent/vendor/github.com/containernetworking/cni/pkg/types/args.go index bd8640fc969..4eac6489947 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/types/args.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/types/args.go @@ -36,7 +36,7 @@ func (b *UnmarshallableBool) UnmarshalText(data []byte) error { case "0", "false": *b = false default: - return fmt.Errorf("Boolean unmarshal error: invalid input %s", s) + return fmt.Errorf("boolean unmarshal error: invalid input %s", s) } return nil } diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/types/current/types.go b/agent/vendor/github.com/containernetworking/cni/pkg/types/current/types.go index 7267a2e6d1f..754cc6e722e 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/types/current/types.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/types/current/types.go @@ -207,23 +207,6 @@ func (r *Result) PrintTo(writer io.Writer) error { return err } -// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where -// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the -// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. -func (r *Result) String() string { - var str string - if len(r.Interfaces) > 0 { - str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces) - } - if len(r.IPs) > 0 { - str += fmt.Sprintf("IP:%+v, ", r.IPs) - } - if len(r.Routes) > 0 { - str += fmt.Sprintf("Routes:%+v, ", r.Routes) - } - return fmt.Sprintf("%sDNS:%+v", str, r.DNS) -} - // Convert this old version result to the current CNI version result func (r *Result) Convert() (*Result, error) { return r, nil diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/types/types.go b/agent/vendor/github.com/containernetworking/cni/pkg/types/types.go index d0d11006a05..3fa757a5d22 100644 --- a/agent/vendor/github.com/containernetworking/cni/pkg/types/types.go +++ b/agent/vendor/github.com/containernetworking/cni/pkg/types/types.go @@ -16,7 +16,6 @@ package types import ( "encoding/json" - "errors" "fmt" "io" "net" @@ -101,9 +100,6 @@ type Result interface { // Prints the result in JSON format to provided writer PrintTo(writer io.Writer) error - - // Returns a JSON string representation of the result - String() string } func PrintResult(result Result, version string) error { @@ -134,9 +130,16 @@ func (r *Route) String() string { // Well known error codes // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes const ( - ErrUnknown uint = iota // 0 - ErrIncompatibleCNIVersion // 1 - ErrUnsupportedField // 2 + ErrUnknown uint = iota // 0 + ErrIncompatibleCNIVersion // 1 + ErrUnsupportedField // 2 + ErrUnknownContainer // 3 + ErrInvalidEnvironmentVariables // 4 + ErrIOFailure // 5 + ErrDecodingFailure // 6 + ErrInvalidNetworkConfig // 7 + ErrTryAgainLater uint = 11 + ErrInternal uint = 999 ) type Error struct { @@ -145,6 +148,14 @@ type Error struct { Details string `json:"details,omitempty"` } +func NewError(code uint, msg, details string) *Error { + return &Error{ + Code: code, + Msg: msg, + Details: details, + } +} + func (e *Error) Error() string { details := "" if e.Details != "" { @@ -194,6 +205,3 @@ func prettyPrint(obj interface{}) error { _, err = os.Stdout.Write(data) return err } - -// NotImplementedError is used to indicate that a method is not implemented for the given platform -var NotImplementedError = errors.New("Not Implemented") diff --git a/agent/vendor/github.com/containernetworking/cni/pkg/utils/utils.go b/agent/vendor/github.com/containernetworking/cni/pkg/utils/utils.go new file mode 100644 index 00000000000..b8ec3887459 --- /dev/null +++ b/agent/vendor/github.com/containernetworking/cni/pkg/utils/utils.go @@ -0,0 +1,84 @@ +// Copyright 2019 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "bytes" + "fmt" + "regexp" + "unicode" + + "github.com/containernetworking/cni/pkg/types" +) + +const ( + // cniValidNameChars is the regexp used to validate valid characters in + // containerID and networkName + cniValidNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.\-]` + + // maxInterfaceNameLength is the length max of a valid interface name + maxInterfaceNameLength = 15 +) + +var cniReg = regexp.MustCompile(`^` + cniValidNameChars + `*$`) + +// ValidateContainerID will validate that the supplied containerID is not empty does not contain invalid characters +func ValidateContainerID(containerID string) *types.Error { + + if containerID == "" { + return types.NewError(types.ErrUnknownContainer, "missing containerID", "") + } + if !cniReg.MatchString(containerID) { + return types.NewError(types.ErrInvalidEnvironmentVariables, "invalid characters in containerID", containerID) + } + return nil +} + +// ValidateNetworkName will validate that the supplied networkName does not contain invalid characters +func ValidateNetworkName(networkName string) *types.Error { + + if networkName == "" { + return types.NewError(types.ErrInvalidNetworkConfig, "missing network name:", "") + } + if !cniReg.MatchString(networkName) { + return types.NewError(types.ErrInvalidNetworkConfig, "invalid characters found in network name", networkName) + } + return nil +} + +// ValidateInterfaceName will validate the interface name based on the three rules below +// 1. The name must not be empty +// 2. The name must be less than 16 characters +// 3. The name must not be "." or ".." +// 3. The name must not contain / or : or any whitespace characters +// ref to https://github.com/torvalds/linux/blob/master/net/core/dev.c#L1024 +func ValidateInterfaceName(ifName string) *types.Error { + if len(ifName) == 0 { + return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is empty", "") + } + if len(ifName) > maxInterfaceNameLength { + return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is too long", fmt.Sprintf("interface name should be less than %d characters", maxInterfaceNameLength+1)) + } + if ifName == "." || ifName == ".." { + return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is . or ..", "") + } + for _, r := range bytes.Runes([]byte(ifName)) { + if r == '/' || r == ':' || unicode.IsSpace(r) { + return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name contains / or : or whitespace characters", "") + } + } + + return nil +} diff --git a/agent/vendor/github.com/coreos/go-systemd/LICENSE b/agent/vendor/github.com/coreos/go-systemd/LICENSE deleted file mode 100644 index 37ec93a14fd..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/dbus.go b/agent/vendor/github.com/coreos/go-systemd/dbus/dbus.go deleted file mode 100644 index c1694fb522e..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/dbus.go +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Integration with the systemd D-Bus API. See http://www.freedesktop.org/wiki/Software/systemd/dbus/ -package dbus - -import ( - "fmt" - "os" - "strconv" - "strings" - "sync" - - "github.com/godbus/dbus" -) - -const ( - alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` - num = `0123456789` - alphanum = alpha + num - signalBuffer = 100 -) - -// needsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped -func needsEscape(i int, b byte) bool { - // Escape everything that is not a-z-A-Z-0-9 - // Also escape 0-9 if it's the first character - return strings.IndexByte(alphanum, b) == -1 || - (i == 0 && strings.IndexByte(num, b) != -1) -} - -// PathBusEscape sanitizes a constituent string of a dbus ObjectPath using the -// rules that systemd uses for serializing special characters. -func PathBusEscape(path string) string { - // Special case the empty string - if len(path) == 0 { - return "_" - } - n := []byte{} - for i := 0; i < len(path); i++ { - c := path[i] - if needsEscape(i, c) { - e := fmt.Sprintf("_%x", c) - n = append(n, []byte(e)...) - } else { - n = append(n, c) - } - } - return string(n) -} - -// Conn is a connection to systemd's dbus endpoint. -type Conn struct { - // sysconn/sysobj are only used to call dbus methods - sysconn *dbus.Conn - sysobj dbus.BusObject - - // sigconn/sigobj are only used to receive dbus signals - sigconn *dbus.Conn - sigobj dbus.BusObject - - jobListener struct { - jobs map[dbus.ObjectPath]chan<- string - sync.Mutex - } - subscriber struct { - updateCh chan<- *SubStateUpdate - errCh chan<- error - sync.Mutex - ignore map[dbus.ObjectPath]int64 - cleanIgnore int64 - } -} - -// New establishes a connection to any available bus and authenticates. -// Callers should call Close() when done with the connection. -func New() (*Conn, error) { - conn, err := NewSystemConnection() - if err != nil && os.Geteuid() == 0 { - return NewSystemdConnection() - } - return conn, err -} - -// NewSystemConnection establishes a connection to the system bus and authenticates. -// Callers should call Close() when done with the connection -func NewSystemConnection() (*Conn, error) { - return NewConnection(func() (*dbus.Conn, error) { - return dbusAuthHelloConnection(dbus.SystemBusPrivate) - }) -} - -// NewUserConnection establishes a connection to the session bus and -// authenticates. This can be used to connect to systemd user instances. -// Callers should call Close() when done with the connection. -func NewUserConnection() (*Conn, error) { - return NewConnection(func() (*dbus.Conn, error) { - return dbusAuthHelloConnection(dbus.SessionBusPrivate) - }) -} - -// NewSystemdConnection establishes a private, direct connection to systemd. -// This can be used for communicating with systemd without a dbus daemon. -// Callers should call Close() when done with the connection. -func NewSystemdConnection() (*Conn, error) { - return NewConnection(func() (*dbus.Conn, error) { - // We skip Hello when talking directly to systemd. - return dbusAuthConnection(func() (*dbus.Conn, error) { - return dbus.Dial("unix:path=/run/systemd/private") - }) - }) -} - -// Close closes an established connection -func (c *Conn) Close() { - c.sysconn.Close() - c.sigconn.Close() -} - -// NewConnection establishes a connection to a bus using a caller-supplied function. -// This allows connecting to remote buses through a user-supplied mechanism. -// The supplied function may be called multiple times, and should return independent connections. -// The returned connection must be fully initialised: the org.freedesktop.DBus.Hello call must have succeeded, -// and any authentication should be handled by the function. -func NewConnection(dialBus func() (*dbus.Conn, error)) (*Conn, error) { - sysconn, err := dialBus() - if err != nil { - return nil, err - } - - sigconn, err := dialBus() - if err != nil { - sysconn.Close() - return nil, err - } - - c := &Conn{ - sysconn: sysconn, - sysobj: systemdObject(sysconn), - sigconn: sigconn, - sigobj: systemdObject(sigconn), - } - - c.subscriber.ignore = make(map[dbus.ObjectPath]int64) - c.jobListener.jobs = make(map[dbus.ObjectPath]chan<- string) - - // Setup the listeners on jobs so that we can get completions - c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal', interface='org.freedesktop.systemd1.Manager', member='JobRemoved'") - - c.dispatch() - return c, nil -} - -// GetManagerProperty returns the value of a property on the org.freedesktop.systemd1.Manager -// interface. The value is returned in its string representation, as defined at -// https://developer.gnome.org/glib/unstable/gvariant-text.html -func (c *Conn) GetManagerProperty(prop string) (string, error) { - variant, err := c.sysobj.GetProperty("org.freedesktop.systemd1.Manager." + prop) - if err != nil { - return "", err - } - return variant.String(), nil -} - -func dbusAuthConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) { - conn, err := createBus() - if err != nil { - return nil, err - } - - // Only use EXTERNAL method, and hardcode the uid (not username) - // to avoid a username lookup (which requires a dynamically linked - // libc) - methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - - err = conn.Auth(methods) - if err != nil { - conn.Close() - return nil, err - } - - return conn, nil -} - -func dbusAuthHelloConnection(createBus func() (*dbus.Conn, error)) (*dbus.Conn, error) { - conn, err := dbusAuthConnection(createBus) - if err != nil { - return nil, err - } - - if err = conn.Hello(); err != nil { - conn.Close() - return nil, err - } - - return conn, nil -} - -func systemdObject(conn *dbus.Conn) dbus.BusObject { - return conn.Object("org.freedesktop.systemd1", dbus.ObjectPath("/org/freedesktop/systemd1")) -} diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/methods.go b/agent/vendor/github.com/coreos/go-systemd/dbus/methods.go deleted file mode 100644 index ab17f7cc75a..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/methods.go +++ /dev/null @@ -1,565 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dbus - -import ( - "errors" - "path" - "strconv" - - "github.com/godbus/dbus" -) - -func (c *Conn) jobComplete(signal *dbus.Signal) { - var id uint32 - var job dbus.ObjectPath - var unit string - var result string - dbus.Store(signal.Body, &id, &job, &unit, &result) - c.jobListener.Lock() - out, ok := c.jobListener.jobs[job] - if ok { - out <- result - delete(c.jobListener.jobs, job) - } - c.jobListener.Unlock() -} - -func (c *Conn) startJob(ch chan<- string, job string, args ...interface{}) (int, error) { - if ch != nil { - c.jobListener.Lock() - defer c.jobListener.Unlock() - } - - var p dbus.ObjectPath - err := c.sysobj.Call(job, 0, args...).Store(&p) - if err != nil { - return 0, err - } - - if ch != nil { - c.jobListener.jobs[p] = ch - } - - // ignore error since 0 is fine if conversion fails - jobID, _ := strconv.Atoi(path.Base(string(p))) - - return jobID, nil -} - -// StartUnit enqueues a start job and depending jobs, if any (unless otherwise -// specified by the mode string). -// -// Takes the unit to activate, plus a mode string. The mode needs to be one of -// replace, fail, isolate, ignore-dependencies, ignore-requirements. If -// "replace" the call will start the unit and its dependencies, possibly -// replacing already queued jobs that conflict with this. If "fail" the call -// will start the unit and its dependencies, but will fail if this would change -// an already queued job. If "isolate" the call will start the unit in question -// and terminate all units that aren't dependencies of it. If -// "ignore-dependencies" it will start a unit but ignore all its dependencies. -// If "ignore-requirements" it will start a unit but only ignore the -// requirement dependencies. It is not recommended to make use of the latter -// two options. -// -// If the provided channel is non-nil, a result string will be sent to it upon -// job completion: one of done, canceled, timeout, failed, dependency, skipped. -// done indicates successful execution of a job. canceled indicates that a job -// has been canceled before it finished execution. timeout indicates that the -// job timeout was reached. failed indicates that the job failed. dependency -// indicates that a job this job has been depending on failed and the job hence -// has been removed too. skipped indicates that a job was skipped because it -// didn't apply to the units current state. -// -// If no error occurs, the ID of the underlying systemd job will be returned. There -// does exist the possibility for no error to be returned, but for the returned job -// ID to be 0. In this case, the actual underlying ID is not 0 and this datapoint -// should not be considered authoritative. -// -// If an error does occur, it will be returned to the user alongside a job ID of 0. -func (c *Conn) StartUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.StartUnit", name, mode) -} - -// StopUnit is similar to StartUnit but stops the specified unit rather -// than starting it. -func (c *Conn) StopUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.StopUnit", name, mode) -} - -// ReloadUnit reloads a unit. Reloading is done only if the unit is already running and fails otherwise. -func (c *Conn) ReloadUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadUnit", name, mode) -} - -// RestartUnit restarts a service. If a service is restarted that isn't -// running it will be started. -func (c *Conn) RestartUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.RestartUnit", name, mode) -} - -// TryRestartUnit is like RestartUnit, except that a service that isn't running -// is not affected by the restart. -func (c *Conn) TryRestartUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.TryRestartUnit", name, mode) -} - -// ReloadOrRestart attempts a reload if the unit supports it and use a restart -// otherwise. -func (c *Conn) ReloadOrRestartUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadOrRestartUnit", name, mode) -} - -// ReloadOrTryRestart attempts a reload if the unit supports it and use a "Try" -// flavored restart otherwise. -func (c *Conn) ReloadOrTryRestartUnit(name string, mode string, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.ReloadOrTryRestartUnit", name, mode) -} - -// StartTransientUnit() may be used to create and start a transient unit, which -// will be released as soon as it is not running or referenced anymore or the -// system is rebooted. name is the unit name including suffix, and must be -// unique. mode is the same as in StartUnit(), properties contains properties -// of the unit. -func (c *Conn) StartTransientUnit(name string, mode string, properties []Property, ch chan<- string) (int, error) { - return c.startJob(ch, "org.freedesktop.systemd1.Manager.StartTransientUnit", name, mode, properties, make([]PropertyCollection, 0)) -} - -// KillUnit takes the unit name and a UNIX signal number to send. All of the unit's -// processes are killed. -func (c *Conn) KillUnit(name string, signal int32) { - c.sysobj.Call("org.freedesktop.systemd1.Manager.KillUnit", 0, name, "all", signal).Store() -} - -// ResetFailedUnit resets the "failed" state of a specific unit. -func (c *Conn) ResetFailedUnit(name string) error { - return c.sysobj.Call("org.freedesktop.systemd1.Manager.ResetFailedUnit", 0, name).Store() -} - -// getProperties takes the unit name and returns all of its dbus object properties, for the given dbus interface -func (c *Conn) getProperties(unit string, dbusInterface string) (map[string]interface{}, error) { - var err error - var props map[string]dbus.Variant - - path := unitPath(unit) - if !path.IsValid() { - return nil, errors.New("invalid unit name: " + unit) - } - - obj := c.sysconn.Object("org.freedesktop.systemd1", path) - err = obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, dbusInterface).Store(&props) - if err != nil { - return nil, err - } - - out := make(map[string]interface{}, len(props)) - for k, v := range props { - out[k] = v.Value() - } - - return out, nil -} - -// GetUnitProperties takes the unit name and returns all of its dbus object properties. -func (c *Conn) GetUnitProperties(unit string) (map[string]interface{}, error) { - return c.getProperties(unit, "org.freedesktop.systemd1.Unit") -} - -func (c *Conn) getProperty(unit string, dbusInterface string, propertyName string) (*Property, error) { - var err error - var prop dbus.Variant - - path := unitPath(unit) - if !path.IsValid() { - return nil, errors.New("invalid unit name: " + unit) - } - - obj := c.sysconn.Object("org.freedesktop.systemd1", path) - err = obj.Call("org.freedesktop.DBus.Properties.Get", 0, dbusInterface, propertyName).Store(&prop) - if err != nil { - return nil, err - } - - return &Property{Name: propertyName, Value: prop}, nil -} - -func (c *Conn) GetUnitProperty(unit string, propertyName string) (*Property, error) { - return c.getProperty(unit, "org.freedesktop.systemd1.Unit", propertyName) -} - -// GetServiceProperty returns property for given service name and property name -func (c *Conn) GetServiceProperty(service string, propertyName string) (*Property, error) { - return c.getProperty(service, "org.freedesktop.systemd1.Service", propertyName) -} - -// GetUnitTypeProperties returns the extra properties for a unit, specific to the unit type. -// Valid values for unitType: Service, Socket, Target, Device, Mount, Automount, Snapshot, Timer, Swap, Path, Slice, Scope -// return "dbus.Error: Unknown interface" if the unitType is not the correct type of the unit -func (c *Conn) GetUnitTypeProperties(unit string, unitType string) (map[string]interface{}, error) { - return c.getProperties(unit, "org.freedesktop.systemd1."+unitType) -} - -// SetUnitProperties() may be used to modify certain unit properties at runtime. -// Not all properties may be changed at runtime, but many resource management -// settings (primarily those in systemd.cgroup(5)) may. The changes are applied -// instantly, and stored on disk for future boots, unless runtime is true, in which -// case the settings only apply until the next reboot. name is the name of the unit -// to modify. properties are the settings to set, encoded as an array of property -// name and value pairs. -func (c *Conn) SetUnitProperties(name string, runtime bool, properties ...Property) error { - return c.sysobj.Call("org.freedesktop.systemd1.Manager.SetUnitProperties", 0, name, runtime, properties).Store() -} - -func (c *Conn) GetUnitTypeProperty(unit string, unitType string, propertyName string) (*Property, error) { - return c.getProperty(unit, "org.freedesktop.systemd1."+unitType, propertyName) -} - -type UnitStatus struct { - Name string // The primary unit name as string - Description string // The human readable description string - LoadState string // The load state (i.e. whether the unit file has been loaded successfully) - ActiveState string // The active state (i.e. whether the unit is currently started or not) - SubState string // The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not) - Followed string // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string. - Path dbus.ObjectPath // The unit object path - JobId uint32 // If there is a job queued for the job unit the numeric job id, 0 otherwise - JobType string // The job type as string - JobPath dbus.ObjectPath // The job object path -} - -type storeFunc func(retvalues ...interface{}) error - -func (c *Conn) listUnitsInternal(f storeFunc) ([]UnitStatus, error) { - result := make([][]interface{}, 0) - err := f(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - status := make([]UnitStatus, len(result)) - statusInterface := make([]interface{}, len(status)) - for i := range status { - statusInterface[i] = &status[i] - } - - err = dbus.Store(resultInterface, statusInterface...) - if err != nil { - return nil, err - } - - return status, nil -} - -// ListUnits returns an array with all currently loaded units. Note that -// units may be known by multiple names at the same time, and hence there might -// be more unit names loaded than actual units behind them. -func (c *Conn) ListUnits() ([]UnitStatus, error) { - return c.listUnitsInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnits", 0).Store) -} - -// ListUnitsFiltered returns an array with units filtered by state. -// It takes a list of units' statuses to filter. -func (c *Conn) ListUnitsFiltered(states []string) ([]UnitStatus, error) { - return c.listUnitsInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnitsFiltered", 0, states).Store) -} - -// ListUnitsByPatterns returns an array with units. -// It takes a list of units' statuses and names to filter. -// Note that units may be known by multiple names at the same time, -// and hence there might be more unit names loaded than actual units behind them. -func (c *Conn) ListUnitsByPatterns(states []string, patterns []string) ([]UnitStatus, error) { - return c.listUnitsInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnitsByPatterns", 0, states, patterns).Store) -} - -// ListUnitsByNames returns an array with units. It takes a list of units' -// names and returns an UnitStatus array. Comparing to ListUnitsByPatterns -// method, this method returns statuses even for inactive or non-existing -// units. Input array should contain exact unit names, but not patterns. -func (c *Conn) ListUnitsByNames(units []string) ([]UnitStatus, error) { - return c.listUnitsInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnitsByNames", 0, units).Store) -} - -type UnitFile struct { - Path string - Type string -} - -func (c *Conn) listUnitFilesInternal(f storeFunc) ([]UnitFile, error) { - result := make([][]interface{}, 0) - err := f(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - files := make([]UnitFile, len(result)) - fileInterface := make([]interface{}, len(files)) - for i := range files { - fileInterface[i] = &files[i] - } - - err = dbus.Store(resultInterface, fileInterface...) - if err != nil { - return nil, err - } - - return files, nil -} - -// ListUnitFiles returns an array of all available units on disk. -func (c *Conn) ListUnitFiles() ([]UnitFile, error) { - return c.listUnitFilesInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnitFiles", 0).Store) -} - -// ListUnitFilesByPatterns returns an array of all available units on disk matched the patterns. -func (c *Conn) ListUnitFilesByPatterns(states []string, patterns []string) ([]UnitFile, error) { - return c.listUnitFilesInternal(c.sysobj.Call("org.freedesktop.systemd1.Manager.ListUnitFilesByPatterns", 0, states, patterns).Store) -} - -type LinkUnitFileChange EnableUnitFileChange - -// LinkUnitFiles() links unit files (that are located outside of the -// usual unit search paths) into the unit search path. -// -// It takes a list of absolute paths to unit files to link and two -// booleans. The first boolean controls whether the unit shall be -// enabled for runtime only (true, /run), or persistently (false, -// /etc). -// The second controls whether symlinks pointing to other units shall -// be replaced if necessary. -// -// This call returns a list of the changes made. The list consists of -// structures with three strings: the type of the change (one of symlink -// or unlink), the file name of the symlink and the destination of the -// symlink. -func (c *Conn) LinkUnitFiles(files []string, runtime bool, force bool) ([]LinkUnitFileChange, error) { - result := make([][]interface{}, 0) - err := c.sysobj.Call("org.freedesktop.systemd1.Manager.LinkUnitFiles", 0, files, runtime, force).Store(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - changes := make([]LinkUnitFileChange, len(result)) - changesInterface := make([]interface{}, len(changes)) - for i := range changes { - changesInterface[i] = &changes[i] - } - - err = dbus.Store(resultInterface, changesInterface...) - if err != nil { - return nil, err - } - - return changes, nil -} - -// EnableUnitFiles() may be used to enable one or more units in the system (by -// creating symlinks to them in /etc or /run). -// -// It takes a list of unit files to enable (either just file names or full -// absolute paths if the unit files are residing outside the usual unit -// search paths), and two booleans: the first controls whether the unit shall -// be enabled for runtime only (true, /run), or persistently (false, /etc). -// The second one controls whether symlinks pointing to other units shall -// be replaced if necessary. -// -// This call returns one boolean and an array with the changes made. The -// boolean signals whether the unit files contained any enablement -// information (i.e. an [Install]) section. The changes list consists of -// structures with three strings: the type of the change (one of symlink -// or unlink), the file name of the symlink and the destination of the -// symlink. -func (c *Conn) EnableUnitFiles(files []string, runtime bool, force bool) (bool, []EnableUnitFileChange, error) { - var carries_install_info bool - - result := make([][]interface{}, 0) - err := c.sysobj.Call("org.freedesktop.systemd1.Manager.EnableUnitFiles", 0, files, runtime, force).Store(&carries_install_info, &result) - if err != nil { - return false, nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - changes := make([]EnableUnitFileChange, len(result)) - changesInterface := make([]interface{}, len(changes)) - for i := range changes { - changesInterface[i] = &changes[i] - } - - err = dbus.Store(resultInterface, changesInterface...) - if err != nil { - return false, nil, err - } - - return carries_install_info, changes, nil -} - -type EnableUnitFileChange struct { - Type string // Type of the change (one of symlink or unlink) - Filename string // File name of the symlink - Destination string // Destination of the symlink -} - -// DisableUnitFiles() may be used to disable one or more units in the system (by -// removing symlinks to them from /etc or /run). -// -// It takes a list of unit files to disable (either just file names or full -// absolute paths if the unit files are residing outside the usual unit -// search paths), and one boolean: whether the unit was enabled for runtime -// only (true, /run), or persistently (false, /etc). -// -// This call returns an array with the changes made. The changes list -// consists of structures with three strings: the type of the change (one of -// symlink or unlink), the file name of the symlink and the destination of the -// symlink. -func (c *Conn) DisableUnitFiles(files []string, runtime bool) ([]DisableUnitFileChange, error) { - result := make([][]interface{}, 0) - err := c.sysobj.Call("org.freedesktop.systemd1.Manager.DisableUnitFiles", 0, files, runtime).Store(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - changes := make([]DisableUnitFileChange, len(result)) - changesInterface := make([]interface{}, len(changes)) - for i := range changes { - changesInterface[i] = &changes[i] - } - - err = dbus.Store(resultInterface, changesInterface...) - if err != nil { - return nil, err - } - - return changes, nil -} - -type DisableUnitFileChange struct { - Type string // Type of the change (one of symlink or unlink) - Filename string // File name of the symlink - Destination string // Destination of the symlink -} - -// MaskUnitFiles masks one or more units in the system -// -// It takes three arguments: -// * list of units to mask (either just file names or full -// absolute paths if the unit files are residing outside -// the usual unit search paths) -// * runtime to specify whether the unit was enabled for runtime -// only (true, /run/systemd/..), or persistently (false, /etc/systemd/..) -// * force flag -func (c *Conn) MaskUnitFiles(files []string, runtime bool, force bool) ([]MaskUnitFileChange, error) { - result := make([][]interface{}, 0) - err := c.sysobj.Call("org.freedesktop.systemd1.Manager.MaskUnitFiles", 0, files, runtime, force).Store(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - changes := make([]MaskUnitFileChange, len(result)) - changesInterface := make([]interface{}, len(changes)) - for i := range changes { - changesInterface[i] = &changes[i] - } - - err = dbus.Store(resultInterface, changesInterface...) - if err != nil { - return nil, err - } - - return changes, nil -} - -type MaskUnitFileChange struct { - Type string // Type of the change (one of symlink or unlink) - Filename string // File name of the symlink - Destination string // Destination of the symlink -} - -// UnmaskUnitFiles unmasks one or more units in the system -// -// It takes two arguments: -// * list of unit files to mask (either just file names or full -// absolute paths if the unit files are residing outside -// the usual unit search paths) -// * runtime to specify whether the unit was enabled for runtime -// only (true, /run/systemd/..), or persistently (false, /etc/systemd/..) -func (c *Conn) UnmaskUnitFiles(files []string, runtime bool) ([]UnmaskUnitFileChange, error) { - result := make([][]interface{}, 0) - err := c.sysobj.Call("org.freedesktop.systemd1.Manager.UnmaskUnitFiles", 0, files, runtime).Store(&result) - if err != nil { - return nil, err - } - - resultInterface := make([]interface{}, len(result)) - for i := range result { - resultInterface[i] = result[i] - } - - changes := make([]UnmaskUnitFileChange, len(result)) - changesInterface := make([]interface{}, len(changes)) - for i := range changes { - changesInterface[i] = &changes[i] - } - - err = dbus.Store(resultInterface, changesInterface...) - if err != nil { - return nil, err - } - - return changes, nil -} - -type UnmaskUnitFileChange struct { - Type string // Type of the change (one of symlink or unlink) - Filename string // File name of the symlink - Destination string // Destination of the symlink -} - -// Reload instructs systemd to scan for and reload unit files. This is -// equivalent to a 'systemctl daemon-reload'. -func (c *Conn) Reload() error { - return c.sysobj.Call("org.freedesktop.systemd1.Manager.Reload", 0).Store() -} - -func unitPath(name string) dbus.ObjectPath { - return dbus.ObjectPath("/org/freedesktop/systemd1/unit/" + PathBusEscape(name)) -} diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/properties.go b/agent/vendor/github.com/coreos/go-systemd/dbus/properties.go deleted file mode 100644 index 6c818958763..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/properties.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dbus - -import ( - "github.com/godbus/dbus" -) - -// From the systemd docs: -// -// The properties array of StartTransientUnit() may take many of the settings -// that may also be configured in unit files. Not all parameters are currently -// accepted though, but we plan to cover more properties with future release. -// Currently you may set the Description, Slice and all dependency types of -// units, as well as RemainAfterExit, ExecStart for service units, -// TimeoutStopUSec and PIDs for scope units, and CPUAccounting, CPUShares, -// BlockIOAccounting, BlockIOWeight, BlockIOReadBandwidth, -// BlockIOWriteBandwidth, BlockIODeviceWeight, MemoryAccounting, MemoryLimit, -// DevicePolicy, DeviceAllow for services/scopes/slices. These fields map -// directly to their counterparts in unit files and as normal D-Bus object -// properties. The exception here is the PIDs field of scope units which is -// used for construction of the scope only and specifies the initial PIDs to -// add to the scope object. - -type Property struct { - Name string - Value dbus.Variant -} - -type PropertyCollection struct { - Name string - Properties []Property -} - -type execStart struct { - Path string // the binary path to execute - Args []string // an array with all arguments to pass to the executed command, starting with argument 0 - UncleanIsFailure bool // a boolean whether it should be considered a failure if the process exits uncleanly -} - -// PropExecStart sets the ExecStart service property. The first argument is a -// slice with the binary path to execute followed by the arguments to pass to -// the executed command. See -// http://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= -func PropExecStart(command []string, uncleanIsFailure bool) Property { - execStarts := []execStart{ - execStart{ - Path: command[0], - Args: command, - UncleanIsFailure: uncleanIsFailure, - }, - } - - return Property{ - Name: "ExecStart", - Value: dbus.MakeVariant(execStarts), - } -} - -// PropRemainAfterExit sets the RemainAfterExit service property. See -// http://www.freedesktop.org/software/systemd/man/systemd.service.html#RemainAfterExit= -func PropRemainAfterExit(b bool) Property { - return Property{ - Name: "RemainAfterExit", - Value: dbus.MakeVariant(b), - } -} - -// PropType sets the Type service property. See -// http://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= -func PropType(t string) Property { - return Property{ - Name: "Type", - Value: dbus.MakeVariant(t), - } -} - -// PropDescription sets the Description unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit#Description= -func PropDescription(desc string) Property { - return Property{ - Name: "Description", - Value: dbus.MakeVariant(desc), - } -} - -func propDependency(name string, units []string) Property { - return Property{ - Name: name, - Value: dbus.MakeVariant(units), - } -} - -// PropRequires sets the Requires unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requires= -func PropRequires(units ...string) Property { - return propDependency("Requires", units) -} - -// PropRequiresOverridable sets the RequiresOverridable unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresOverridable= -func PropRequiresOverridable(units ...string) Property { - return propDependency("RequiresOverridable", units) -} - -// PropRequisite sets the Requisite unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Requisite= -func PropRequisite(units ...string) Property { - return propDependency("Requisite", units) -} - -// PropRequisiteOverridable sets the RequisiteOverridable unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequisiteOverridable= -func PropRequisiteOverridable(units ...string) Property { - return propDependency("RequisiteOverridable", units) -} - -// PropWants sets the Wants unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Wants= -func PropWants(units ...string) Property { - return propDependency("Wants", units) -} - -// PropBindsTo sets the BindsTo unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#BindsTo= -func PropBindsTo(units ...string) Property { - return propDependency("BindsTo", units) -} - -// PropRequiredBy sets the RequiredBy unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredBy= -func PropRequiredBy(units ...string) Property { - return propDependency("RequiredBy", units) -} - -// PropRequiredByOverridable sets the RequiredByOverridable unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiredByOverridable= -func PropRequiredByOverridable(units ...string) Property { - return propDependency("RequiredByOverridable", units) -} - -// PropWantedBy sets the WantedBy unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#WantedBy= -func PropWantedBy(units ...string) Property { - return propDependency("WantedBy", units) -} - -// PropBoundBy sets the BoundBy unit property. See -// http://www.freedesktop.org/software/systemd/main/systemd.unit.html#BoundBy= -func PropBoundBy(units ...string) Property { - return propDependency("BoundBy", units) -} - -// PropConflicts sets the Conflicts unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Conflicts= -func PropConflicts(units ...string) Property { - return propDependency("Conflicts", units) -} - -// PropConflictedBy sets the ConflictedBy unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#ConflictedBy= -func PropConflictedBy(units ...string) Property { - return propDependency("ConflictedBy", units) -} - -// PropBefore sets the Before unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Before= -func PropBefore(units ...string) Property { - return propDependency("Before", units) -} - -// PropAfter sets the After unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#After= -func PropAfter(units ...string) Property { - return propDependency("After", units) -} - -// PropOnFailure sets the OnFailure unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#OnFailure= -func PropOnFailure(units ...string) Property { - return propDependency("OnFailure", units) -} - -// PropTriggers sets the Triggers unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#Triggers= -func PropTriggers(units ...string) Property { - return propDependency("Triggers", units) -} - -// PropTriggeredBy sets the TriggeredBy unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#TriggeredBy= -func PropTriggeredBy(units ...string) Property { - return propDependency("TriggeredBy", units) -} - -// PropPropagatesReloadTo sets the PropagatesReloadTo unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#PropagatesReloadTo= -func PropPropagatesReloadTo(units ...string) Property { - return propDependency("PropagatesReloadTo", units) -} - -// PropRequiresMountsFor sets the RequiresMountsFor unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.unit.html#RequiresMountsFor= -func PropRequiresMountsFor(units ...string) Property { - return propDependency("RequiresMountsFor", units) -} - -// PropSlice sets the Slice unit property. See -// http://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Slice= -func PropSlice(slice string) Property { - return Property{ - Name: "Slice", - Value: dbus.MakeVariant(slice), - } -} - -// PropPids sets the PIDs field of scope units used in the initial construction -// of the scope only and specifies the initial PIDs to add to the scope object. -// See https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/#properties -func PropPids(pids ...uint32) Property { - return Property{ - Name: "PIDs", - Value: dbus.MakeVariant(pids), - } -} diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/set.go b/agent/vendor/github.com/coreos/go-systemd/dbus/set.go deleted file mode 100644 index f92e6fbed1e..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/set.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dbus - -type set struct { - data map[string]bool -} - -func (s *set) Add(value string) { - s.data[value] = true -} - -func (s *set) Remove(value string) { - delete(s.data, value) -} - -func (s *set) Contains(value string) (exists bool) { - _, exists = s.data[value] - return -} - -func (s *set) Length() int { - return len(s.data) -} - -func (s *set) Values() (values []string) { - for val, _ := range s.data { - values = append(values, val) - } - return -} - -func newSet() *set { - return &set{make(map[string]bool)} -} diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/subscription.go b/agent/vendor/github.com/coreos/go-systemd/dbus/subscription.go deleted file mode 100644 index 996451445c0..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/subscription.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dbus - -import ( - "errors" - "time" - - "github.com/godbus/dbus" -) - -const ( - cleanIgnoreInterval = int64(10 * time.Second) - ignoreInterval = int64(30 * time.Millisecond) -) - -// Subscribe sets up this connection to subscribe to all systemd dbus events. -// This is required before calling SubscribeUnits. When the connection closes -// systemd will automatically stop sending signals so there is no need to -// explicitly call Unsubscribe(). -func (c *Conn) Subscribe() error { - c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='org.freedesktop.systemd1.Manager',member='UnitNew'") - c.sigconn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'") - - err := c.sigobj.Call("org.freedesktop.systemd1.Manager.Subscribe", 0).Store() - if err != nil { - return err - } - - return nil -} - -// Unsubscribe this connection from systemd dbus events. -func (c *Conn) Unsubscribe() error { - err := c.sigobj.Call("org.freedesktop.systemd1.Manager.Unsubscribe", 0).Store() - if err != nil { - return err - } - - return nil -} - -func (c *Conn) dispatch() { - ch := make(chan *dbus.Signal, signalBuffer) - - c.sigconn.Signal(ch) - - go func() { - for { - signal, ok := <-ch - if !ok { - return - } - - if signal.Name == "org.freedesktop.systemd1.Manager.JobRemoved" { - c.jobComplete(signal) - } - - if c.subscriber.updateCh == nil { - continue - } - - var unitPath dbus.ObjectPath - switch signal.Name { - case "org.freedesktop.systemd1.Manager.JobRemoved": - unitName := signal.Body[2].(string) - c.sysobj.Call("org.freedesktop.systemd1.Manager.GetUnit", 0, unitName).Store(&unitPath) - case "org.freedesktop.systemd1.Manager.UnitNew": - unitPath = signal.Body[1].(dbus.ObjectPath) - case "org.freedesktop.DBus.Properties.PropertiesChanged": - if signal.Body[0].(string) == "org.freedesktop.systemd1.Unit" { - unitPath = signal.Path - } - } - - if unitPath == dbus.ObjectPath("") { - continue - } - - c.sendSubStateUpdate(unitPath) - } - }() -} - -// Returns two unbuffered channels which will receive all changed units every -// interval. Deleted units are sent as nil. -func (c *Conn) SubscribeUnits(interval time.Duration) (<-chan map[string]*UnitStatus, <-chan error) { - return c.SubscribeUnitsCustom(interval, 0, func(u1, u2 *UnitStatus) bool { return *u1 != *u2 }, nil) -} - -// SubscribeUnitsCustom is like SubscribeUnits but lets you specify the buffer -// size of the channels, the comparison function for detecting changes and a filter -// function for cutting down on the noise that your channel receives. -func (c *Conn) SubscribeUnitsCustom(interval time.Duration, buffer int, isChanged func(*UnitStatus, *UnitStatus) bool, filterUnit func(string) bool) (<-chan map[string]*UnitStatus, <-chan error) { - old := make(map[string]*UnitStatus) - statusChan := make(chan map[string]*UnitStatus, buffer) - errChan := make(chan error, buffer) - - go func() { - for { - timerChan := time.After(interval) - - units, err := c.ListUnits() - if err == nil { - cur := make(map[string]*UnitStatus) - for i := range units { - if filterUnit != nil && filterUnit(units[i].Name) { - continue - } - cur[units[i].Name] = &units[i] - } - - // add all new or changed units - changed := make(map[string]*UnitStatus) - for n, u := range cur { - if oldU, ok := old[n]; !ok || isChanged(oldU, u) { - changed[n] = u - } - delete(old, n) - } - - // add all deleted units - for oldN := range old { - changed[oldN] = nil - } - - old = cur - - if len(changed) != 0 { - statusChan <- changed - } - } else { - errChan <- err - } - - <-timerChan - } - }() - - return statusChan, errChan -} - -type SubStateUpdate struct { - UnitName string - SubState string -} - -// SetSubStateSubscriber writes to updateCh when any unit's substate changes. -// Although this writes to updateCh on every state change, the reported state -// may be more recent than the change that generated it (due to an unavoidable -// race in the systemd dbus interface). That is, this method provides a good -// way to keep a current view of all units' states, but is not guaranteed to -// show every state transition they go through. Furthermore, state changes -// will only be written to the channel with non-blocking writes. If updateCh -// is full, it attempts to write an error to errCh; if errCh is full, the error -// passes silently. -func (c *Conn) SetSubStateSubscriber(updateCh chan<- *SubStateUpdate, errCh chan<- error) { - c.subscriber.Lock() - defer c.subscriber.Unlock() - c.subscriber.updateCh = updateCh - c.subscriber.errCh = errCh -} - -func (c *Conn) sendSubStateUpdate(path dbus.ObjectPath) { - c.subscriber.Lock() - defer c.subscriber.Unlock() - - if c.shouldIgnore(path) { - return - } - - info, err := c.GetUnitProperties(string(path)) - if err != nil { - select { - case c.subscriber.errCh <- err: - default: - } - } - - name := info["Id"].(string) - substate := info["SubState"].(string) - - update := &SubStateUpdate{name, substate} - select { - case c.subscriber.updateCh <- update: - default: - select { - case c.subscriber.errCh <- errors.New("update channel full!"): - default: - } - } - - c.updateIgnore(path, info) -} - -// The ignore functions work around a wart in the systemd dbus interface. -// Requesting the properties of an unloaded unit will cause systemd to send a -// pair of UnitNew/UnitRemoved signals. Because we need to get a unit's -// properties on UnitNew (as that's the only indication of a new unit coming up -// for the first time), we would enter an infinite loop if we did not attempt -// to detect and ignore these spurious signals. The signal themselves are -// indistinguishable from relevant ones, so we (somewhat hackishly) ignore an -// unloaded unit's signals for a short time after requesting its properties. -// This means that we will miss e.g. a transient unit being restarted -// *immediately* upon failure and also a transient unit being started -// immediately after requesting its status (with systemctl status, for example, -// because this causes a UnitNew signal to be sent which then causes us to fetch -// the properties). - -func (c *Conn) shouldIgnore(path dbus.ObjectPath) bool { - t, ok := c.subscriber.ignore[path] - return ok && t >= time.Now().UnixNano() -} - -func (c *Conn) updateIgnore(path dbus.ObjectPath, info map[string]interface{}) { - c.cleanIgnore() - - // unit is unloaded - it will trigger bad systemd dbus behavior - if info["LoadState"].(string) == "not-found" { - c.subscriber.ignore[path] = time.Now().UnixNano() + ignoreInterval - } -} - -// without this, ignore would grow unboundedly over time -func (c *Conn) cleanIgnore() { - now := time.Now().UnixNano() - if c.subscriber.cleanIgnore < now { - c.subscriber.cleanIgnore = now + cleanIgnoreInterval - - for p, t := range c.subscriber.ignore { - if t < now { - delete(c.subscriber.ignore, p) - } - } - } -} diff --git a/agent/vendor/github.com/coreos/go-systemd/dbus/subscription_set.go b/agent/vendor/github.com/coreos/go-systemd/dbus/subscription_set.go deleted file mode 100644 index 5b408d5847a..00000000000 --- a/agent/vendor/github.com/coreos/go-systemd/dbus/subscription_set.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dbus - -import ( - "time" -) - -// SubscriptionSet returns a subscription set which is like conn.Subscribe but -// can filter to only return events for a set of units. -type SubscriptionSet struct { - *set - conn *Conn -} - -func (s *SubscriptionSet) filter(unit string) bool { - return !s.Contains(unit) -} - -// Subscribe starts listening for dbus events for all of the units in the set. -// Returns channels identical to conn.SubscribeUnits. -func (s *SubscriptionSet) Subscribe() (<-chan map[string]*UnitStatus, <-chan error) { - // TODO: Make fully evented by using systemd 209 with properties changed values - return s.conn.SubscribeUnitsCustom(time.Second, 0, - mismatchUnitStatus, - func(unit string) bool { return s.filter(unit) }, - ) -} - -// NewSubscriptionSet returns a new subscription set. -func (conn *Conn) NewSubscriptionSet() *SubscriptionSet { - return &SubscriptionSet{newSet(), conn} -} - -// mismatchUnitStatus returns true if the provided UnitStatus objects -// are not equivalent. false is returned if the objects are equivalent. -// Only the Name, Description and state-related fields are used in -// the comparison. -func mismatchUnitStatus(u1, u2 *UnitStatus) bool { - return u1.Name != u2.Name || - u1.Description != u2.Description || - u1.LoadState != u2.LoadState || - u1.ActiveState != u2.ActiveState || - u1.SubState != u2.SubState -} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/.editorconfig b/agent/vendor/github.com/fsnotify/fsnotify/.editorconfig new file mode 100644 index 00000000000..fad895851e5 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*.go] +indent_style = tab +indent_size = 4 +insert_final_newline = true + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/agent/vendor/github.com/fsnotify/fsnotify/.gitattributes b/agent/vendor/github.com/fsnotify/fsnotify/.gitattributes new file mode 100644 index 00000000000..32f1001be0a --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/.gitattributes @@ -0,0 +1 @@ +go.sum linguist-generated diff --git a/agent/vendor/github.com/fsnotify/fsnotify/.gitignore b/agent/vendor/github.com/fsnotify/fsnotify/.gitignore new file mode 100644 index 00000000000..4cd0cbaf432 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/.gitignore @@ -0,0 +1,6 @@ +# Setup a Global .gitignore for OS and editor generated files: +# https://help.github.com/articles/ignoring-files +# git config --global core.excludesfile ~/.gitignore_global + +.vagrant +*.sublime-project diff --git a/agent/vendor/github.com/fsnotify/fsnotify/.mailmap b/agent/vendor/github.com/fsnotify/fsnotify/.mailmap new file mode 100644 index 00000000000..a04f2907fed --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/.mailmap @@ -0,0 +1,2 @@ +Chris Howey +Nathan Youngman <4566+nathany@users.noreply.github.com> diff --git a/agent/vendor/github.com/fsnotify/fsnotify/AUTHORS b/agent/vendor/github.com/fsnotify/fsnotify/AUTHORS new file mode 100644 index 00000000000..6cbabe5ef50 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/AUTHORS @@ -0,0 +1,62 @@ +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# You can update this list using the following command: +# +# $ (head -n10 AUTHORS && git shortlog -se | sed -E 's/^\s+[0-9]+\t//') | tee AUTHORS + +# Please keep the list sorted. + +Aaron L +Adrien Bustany +Alexey Kazakov +Amit Krishnan +Anmol Sethi +Bjørn Erik Pedersen +Brian Goff +Bruno Bigras +Caleb Spare +Case Nelson +Chris Howey +Christoffer Buchholz +Daniel Wagner-Hall +Dave Cheney +Eric Lin +Evan Phoenix +Francisco Souza +Gautam Dey +Hari haran +Ichinose Shogo +Johannes Ebke +John C Barstow +Kelvin Fo +Ken-ichirou MATSUZAWA +Matt Layher +Matthias Stone +Nathan Youngman +Nickolai Zeldovich +Oliver Bristow +Patrick +Paul Hammond +Pawel Knap +Pieter Droogendijk +Pratik Shinde +Pursuit92 +Riku Voipio +Rob Figueiredo +Rodrigo Chiossi +Slawek Ligus +Soge Zhang +Tiffany Jernigan +Tilak Sharma +Tobias Klauser +Tom Payne +Travis Cline +Tudor Golubenco +Vahe Khachikyan +Yukang +bronze1man +debrando +henrikedwards +铁哥 diff --git a/agent/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md b/agent/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md new file mode 100644 index 00000000000..cc01c08f56d --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md @@ -0,0 +1,357 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.5.4] - 2022-04-25 + +* Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447) +* go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444) +* Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443) + +## [1.5.3] - 2022-04-22 + +* This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445) + +## [1.5.2] - 2022-04-21 + +* Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374) +* Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361) +* Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424) +* Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406) +* fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416) + +## [1.5.1] - 2021-08-24 + +* Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394) + +## [1.5.0] - 2021-08-20 + +* Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381) +* Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298) +* Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289) +* CI: Use GitHub Actions for CI and cover go 1.12-1.17 + [#378](https://github.com/fsnotify/fsnotify/pull/378) + [#381](https://github.com/fsnotify/fsnotify/pull/381) + [#385](https://github.com/fsnotify/fsnotify/pull/385) +* Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325) + +## [1.4.7] - 2018-01-09 + +* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine) +* Tests: Fix missing verb on format string (thanks @rchiossi) +* Linux: Fix deadlock in Remove (thanks @aarondl) +* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne) +* Docs: Moved FAQ into the README (thanks @vahe) +* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich) +* Docs: replace references to OS X with macOS + +## [1.4.2] - 2016-10-10 + +* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack) + +## [1.4.1] - 2016-10-04 + +* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack) + +## [1.4.0] - 2016-10-01 + +* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie) + +## [1.3.1] - 2016-06-28 + +* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc) + +## [1.3.0] - 2016-04-19 + +* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135) + +## [1.2.10] - 2016-03-02 + +* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj) + +## [1.2.9] - 2016-01-13 + +kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep) + +## [1.2.8] - 2015-12-17 + +* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test) +* inotify: fix race in test +* enable race detection for continuous integration (Linux, Mac, Windows) + +## [1.2.5] - 2015-10-17 + +* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki) +* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken) +* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie) +* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion) + +## [1.2.1] - 2015-10-14 + +* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx) + +## [1.2.0] - 2015-02-08 + +* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD) +* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD) +* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59) + +## [1.1.1] - 2015-02-05 + +* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD) + +## [1.1.0] - 2014-12-12 + +* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43) + * add low-level functions + * only need to store flags on directories + * less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13) + * done can be an unbuffered channel + * remove calls to os.NewSyscallError +* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher) +* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48) +* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) + +## [1.0.4] - 2014-09-07 + +* kqueue: add dragonfly to the build tags. +* Rename source code files, rearrange code so exported APIs are at the top. +* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang) + +## [1.0.3] - 2014-08-19 + +* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36) + +## [1.0.2] - 2014-08-17 + +* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) +* [Fix] Make ./path and path equivalent. (thanks @zhsso) + +## [1.0.0] - 2014-08-15 + +* [API] Remove AddWatch on Windows, use Add. +* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30) +* Minor updates based on feedback from golint. + +## dev / 2014-07-09 + +* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify). +* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) + +## dev / 2014-07-04 + +* kqueue: fix incorrect mutex used in Close() +* Update example to demonstrate usage of Op. + +## dev / 2014-06-28 + +* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4) +* Fix for String() method on Event (thanks Alex Brainman) +* Don't build on Plan 9 or Solaris (thanks @4ad) + +## dev / 2014-06-21 + +* Events channel of type Event rather than *Event. +* [internal] use syscall constants directly for inotify and kqueue. +* [internal] kqueue: rename events to kevents and fileEvent to event. + +## dev / 2014-06-19 + +* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). +* [internal] remove cookie from Event struct (unused). +* [internal] Event struct has the same definition across every OS. +* [internal] remove internal watch and removeWatch methods. + +## dev / 2014-06-12 + +* [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). +* [API] Pluralized channel names: Events and Errors. +* [API] Renamed FileEvent struct to Event. +* [API] Op constants replace methods like IsCreate(). + +## dev / 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## dev / 2014-05-23 + +* [API] Remove current implementation of WatchFlags. + * current implementation doesn't take advantage of OS for efficiency + * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes + * no tests for the current implementation + * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) + +## [0.9.3] - 2014-12-31 + +* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51) + +## [0.9.2] - 2014-08-17 + +* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso) + +## [0.9.1] - 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## [0.9.0] - 2014-01-17 + +* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) +* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) +* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. + +## [0.8.12] - 2013-11-13 + +* [API] Remove FD_SET and friends from Linux adapter + +## [0.8.11] - 2013-11-02 + +* [Doc] Add Changelog [#72][] (thanks @nathany) +* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond) + +## [0.8.10] - 2013-10-19 + +* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) +* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) +* [Doc] specify OS-specific limits in README (thanks @debrando) + +## [0.8.9] - 2013-09-08 + +* [Doc] Contributing (thanks @nathany) +* [Doc] update package path in example code [#63][] (thanks @paulhammond) +* [Doc] GoCI badge in README (Linux only) [#60][] +* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) + +## [0.8.8] - 2013-06-17 + +* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) + +## [0.8.7] - 2013-06-03 + +* [API] Make syscall flags internal +* [Fix] inotify: ignore event changes +* [Fix] race in symlink test [#45][] (reported by @srid) +* [Fix] tests on Windows +* lower case error messages + +## [0.8.6] - 2013-05-23 + +* kqueue: Use EVT_ONLY flag on Darwin +* [Doc] Update README with full example + +## [0.8.5] - 2013-05-09 + +* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) + +## [0.8.4] - 2013-04-07 + +* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) + +## [0.8.3] - 2013-03-13 + +* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) +* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) + +## [0.8.2] - 2013-02-07 + +* [Doc] add Authors +* [Fix] fix data races for map access [#29][] (thanks @fsouza) + +## [0.8.1] - 2013-01-09 + +* [Fix] Windows path separators +* [Doc] BSD License + +## [0.8.0] - 2012-11-09 + +* kqueue: directory watching improvements (thanks @vmirage) +* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) +* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) + +## [0.7.4] - 2012-10-09 + +* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) +* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) +* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) +* [Fix] kqueue: modify after recreation of file + +## [0.7.3] - 2012-09-27 + +* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) +* [Fix] kqueue: no longer get duplicate CREATE events + +## [0.7.2] - 2012-09-01 + +* kqueue: events for created directories + +## [0.7.1] - 2012-07-14 + +* [Fix] for renaming files + +## [0.7.0] - 2012-07-02 + +* [Feature] FSNotify flags +* [Fix] inotify: Added file name back to event path + +## [0.6.0] - 2012-06-06 + +* kqueue: watch files after directory created (thanks @tmc) + +## [0.5.1] - 2012-05-22 + +* [Fix] inotify: remove all watches before Close() + +## [0.5.0] - 2012-05-03 + +* [API] kqueue: return errors during watch instead of sending over channel +* kqueue: match symlink behavior on Linux +* inotify: add `DELETE_SELF` (requested by @taralx) +* [Fix] kqueue: handle EINTR (reported by @robfig) +* [Doc] Godoc example [#1][] (thanks @davecheney) + +## [0.4.0] - 2012-03-30 + +* Go 1 released: build with go tool +* [Feature] Windows support using winfsnotify +* Windows does not have attribute change notifications +* Roll attribute notifications into IsModify + +## [0.3.0] - 2012-02-19 + +* kqueue: add files when watch directory + +## [0.2.0] - 2011-12-30 + +* update to latest Go weekly code + +## [0.1.0] - 2011-10-19 + +* kqueue: add watch on file creation to match inotify +* kqueue: create file event +* inotify: ignore `IN_IGNORED` events +* event String() +* linux: common FileEvent functions +* initial commit + +[#79]: https://github.com/howeyc/fsnotify/pull/79 +[#77]: https://github.com/howeyc/fsnotify/pull/77 +[#72]: https://github.com/howeyc/fsnotify/issues/72 +[#71]: https://github.com/howeyc/fsnotify/issues/71 +[#70]: https://github.com/howeyc/fsnotify/issues/70 +[#63]: https://github.com/howeyc/fsnotify/issues/63 +[#62]: https://github.com/howeyc/fsnotify/issues/62 +[#60]: https://github.com/howeyc/fsnotify/issues/60 +[#59]: https://github.com/howeyc/fsnotify/issues/59 +[#49]: https://github.com/howeyc/fsnotify/issues/49 +[#45]: https://github.com/howeyc/fsnotify/issues/45 +[#40]: https://github.com/howeyc/fsnotify/issues/40 +[#36]: https://github.com/howeyc/fsnotify/issues/36 +[#33]: https://github.com/howeyc/fsnotify/issues/33 +[#29]: https://github.com/howeyc/fsnotify/issues/29 +[#25]: https://github.com/howeyc/fsnotify/issues/25 +[#24]: https://github.com/howeyc/fsnotify/issues/24 +[#21]: https://github.com/howeyc/fsnotify/issues/21 diff --git a/agent/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md b/agent/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md new file mode 100644 index 00000000000..8a642563d71 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing + +## Issues + +* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsnotify/issues). +* Please indicate the platform you are using fsnotify on. +* A code example to reproduce the problem is appreciated. + +## Pull Requests + +### Contributor License Agreement + +fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). + +Please indicate that you have signed the CLA in your pull request. + +### How fsnotify is Developed + +* Development is done on feature branches. +* Tests are run on BSD, Linux, macOS and Windows. +* Pull requests are reviewed and [applied to master][am] using [hub][]. + * Maintainers may modify or squash commits rather than asking contributors to. +* To issue a new release, the maintainers will: + * Update the CHANGELOG + * Tag a version, which will become available through gopkg.in. + +### How to Fork + +For smooth sailing, always use the original import path. Installing with `go get` makes this easy. + +1. Install from GitHub (`go get -u github.com/fsnotify/fsnotify`) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Ensure everything works and the tests pass (see below) +4. Commit your changes (`git commit -am 'Add some feature'`) + +Contribute upstream: + +1. Fork fsnotify on GitHub +2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) +3. Push to the branch (`git push fork my-new-feature`) +4. Create a new Pull Request on GitHub + +This workflow is [thoroughly explained by Katrina Owen](https://splice.com/blog/contributing-open-source-git-repositories-go/). + +### Testing + +fsnotify uses build tags to compile different code on Linux, BSD, macOS, and Windows. + +Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. + +### Maintainers + +Help maintaining fsnotify is welcome. To be a maintainer: + +* Submit a pull request and sign the CLA as above. +* You must be able to run the test suite on Mac, Windows, Linux and BSD. + +All code changes should be internal pull requests. + +Releases are tagged using [Semantic Versioning](http://semver.org/). diff --git a/agent/vendor/github.com/fsnotify/fsnotify/LICENSE b/agent/vendor/github.com/fsnotify/fsnotify/LICENSE new file mode 100644 index 00000000000..e180c8fb059 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/agent/vendor/github.com/fsnotify/fsnotify/README.md b/agent/vendor/github.com/fsnotify/fsnotify/README.md new file mode 100644 index 00000000000..0731c5ef8ad --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/README.md @@ -0,0 +1,120 @@ +# File system notifications for Go + +[![Go Reference](https://pkg.go.dev/badge/github.com/fsnotify/fsnotify.svg)](https://pkg.go.dev/github.com/fsnotify/fsnotify) [![Go Report Card](https://goreportcard.com/badge/github.com/fsnotify/fsnotify)](https://goreportcard.com/report/github.com/fsnotify/fsnotify) [![Maintainers Wanted](https://img.shields.io/badge/maintainers-wanted-red.svg)](https://github.com/fsnotify/fsnotify/issues/413) + +fsnotify utilizes [`golang.org/x/sys`](https://pkg.go.dev/golang.org/x/sys) rather than [`syscall`](https://pkg.go.dev/syscall) from the standard library. + +Cross platform: Windows, Linux, BSD and macOS. + +| Adapter | OS | Status | +| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| inotify | Linux 2.6.27 or later, Android\* | Supported | +| kqueue | BSD, macOS, iOS\* | Supported | +| ReadDirectoryChangesW | Windows | Supported | +| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) | +| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) | +| fanotify | Linux 2.6.37+ | [Maybe](https://github.com/fsnotify/fsnotify/issues/114) | +| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) | +| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) | + +\* Android and iOS are untested. + +Please see [the documentation](https://pkg.go.dev/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information. + +## API stability + +fsnotify is a fork of [howeyc/fsnotify](https://github.com/howeyc/fsnotify) with a new API as of v1.0. The API is based on [this design document](http://goo.gl/MrYxyA). + +All [releases](https://github.com/fsnotify/fsnotify/releases) are tagged based on [Semantic Versioning](http://semver.org/). + +## Usage + +```go +package main + +import ( + "log" + + "github.com/fsnotify/fsnotify" +) + +func main() { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + log.Println("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("modified file:", event.Name) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + err = watcher.Add("/tmp/foo") + if err != nil { + log.Fatal(err) + } + <-done +} +``` + +## Contributing + +Please refer to [CONTRIBUTING][] before opening an issue or pull request. + +## FAQ + +**When a file is moved to another directory is it still being watched?** + +No (it shouldn't be, unless you are watching where it was moved to). + +**When I watch a directory, are all subdirectories watched as well?** + +No, you must add watches for any directory you want to watch (a recursive watcher is on the roadmap [#18][]). + +**Do I have to watch the Error and Event channels in a separate goroutine?** + +As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7]) + +**Why am I receiving multiple events for the same file on OS X?** + +Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]). + +**How many files can be watched at once?** + +There are OS-specific limits as to how many watches can be created: +* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error. +* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error. + +**Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?** + +fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications. + +[#62]: https://github.com/howeyc/fsnotify/issues/62 +[#18]: https://github.com/fsnotify/fsnotify/issues/18 +[#11]: https://github.com/fsnotify/fsnotify/issues/11 +[#7]: https://github.com/howeyc/fsnotify/issues/7 + +[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md + +## Related Projects + +* [notify](https://github.com/rjeczalik/notify) +* [fsevents](https://github.com/fsnotify/fsevents) + diff --git a/agent/vendor/github.com/fsnotify/fsnotify/fen.go b/agent/vendor/github.com/fsnotify/fsnotify/fen.go new file mode 100644 index 00000000000..b3ac3d8f55f --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/fen.go @@ -0,0 +1,38 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build solaris +// +build solaris + +package fsnotify + +import ( + "errors" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + return nil +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + return nil +} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/fsnotify.go b/agent/vendor/github.com/fsnotify/fsnotify/fsnotify.go new file mode 100644 index 00000000000..0f4ee52e8aa --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/fsnotify.go @@ -0,0 +1,69 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !plan9 +// +build !plan9 + +// Package fsnotify provides a platform-independent interface for file system notifications. +package fsnotify + +import ( + "bytes" + "errors" + "fmt" +) + +// Event represents a single file system notification. +type Event struct { + Name string // Relative path to the file or directory. + Op Op // File operation that triggered the event. +} + +// Op describes a set of file operations. +type Op uint32 + +// These are the generalized file operations that can trigger a notification. +const ( + Create Op = 1 << iota + Write + Remove + Rename + Chmod +) + +func (op Op) String() string { + // Use a buffer for efficient string concatenation + var buffer bytes.Buffer + + if op&Create == Create { + buffer.WriteString("|CREATE") + } + if op&Remove == Remove { + buffer.WriteString("|REMOVE") + } + if op&Write == Write { + buffer.WriteString("|WRITE") + } + if op&Rename == Rename { + buffer.WriteString("|RENAME") + } + if op&Chmod == Chmod { + buffer.WriteString("|CHMOD") + } + if buffer.Len() == 0 { + return "" + } + return buffer.String()[1:] // Strip leading pipe +} + +// String returns a string representation of the event in the form +// "file: REMOVE|WRITE|..." +func (e Event) String() string { + return fmt.Sprintf("%q: %s", e.Name, e.Op.String()) +} + +// Common errors that can be reported by a watcher +var ( + ErrEventOverflow = errors.New("fsnotify queue overflow") +) diff --git a/agent/vendor/github.com/fsnotify/fsnotify/fsnotify_unsupported.go b/agent/vendor/github.com/fsnotify/fsnotify/fsnotify_unsupported.go new file mode 100644 index 00000000000..59688559836 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/fsnotify_unsupported.go @@ -0,0 +1,36 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows +// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows + +package fsnotify + +import ( + "fmt" + "runtime" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct{} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS) +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + return nil +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + return nil +} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/go.mod b/agent/vendor/github.com/fsnotify/fsnotify/go.mod new file mode 100644 index 00000000000..48cfd07fe23 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/go.mod @@ -0,0 +1,10 @@ +module github.com/fsnotify/fsnotify + +go 1.16 + +require golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + +retract ( + v1.5.3 // Published an incorrect branch accidentally https://github.com/fsnotify/fsnotify/issues/445 + v1.5.0 // Contains symlink regression https://github.com/fsnotify/fsnotify/pull/394 +) diff --git a/agent/vendor/github.com/fsnotify/fsnotify/go.sum b/agent/vendor/github.com/fsnotify/fsnotify/go.sum new file mode 100644 index 00000000000..7f2d82d5c1b --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/agent/vendor/github.com/fsnotify/fsnotify/inotify.go b/agent/vendor/github.com/fsnotify/fsnotify/inotify.go new file mode 100644 index 00000000000..a6d0e0ec8c1 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/inotify.go @@ -0,0 +1,351 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +package fsnotify + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + "unsafe" + + "golang.org/x/sys/unix" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + mu sync.Mutex // Map access + fd int + poller *fdPoller + watches map[string]*watch // Map of inotify watches (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + done chan struct{} // Channel for sending a "quit message" to the reader goroutine + doneResp chan struct{} // Channel to respond to Close +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + // Create inotify fd + fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC) + if fd == -1 { + return nil, errno + } + // Create epoll + poller, err := newFdPoller(fd) + if err != nil { + unix.Close(fd) + return nil, err + } + w := &Watcher{ + fd: fd, + poller: poller, + watches: make(map[string]*watch), + paths: make(map[int]string), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan struct{}), + doneResp: make(chan struct{}), + } + + go w.readEvents() + return w, nil +} + +func (w *Watcher) isClosed() bool { + select { + case <-w.done: + return true + default: + return false + } +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed() { + return nil + } + + // Send 'close' signal to goroutine, and set the Watcher to closed. + close(w.done) + + // Wake up goroutine + w.poller.wake() + + // Wait for goroutine to close + <-w.doneResp + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + name = filepath.Clean(name) + if w.isClosed() { + return errors.New("inotify instance already closed") + } + + const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | + unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | + unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF + + var flags uint32 = agnosticEvents + + w.mu.Lock() + defer w.mu.Unlock() + watchEntry := w.watches[name] + if watchEntry != nil { + flags |= watchEntry.flags | unix.IN_MASK_ADD + } + wd, errno := unix.InotifyAddWatch(w.fd, name, flags) + if wd == -1 { + return errno + } + + if watchEntry == nil { + w.watches[name] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = name + } else { + watchEntry.wd = uint32(wd) + watchEntry.flags = flags + } + + return nil +} + +// Remove stops watching the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + + // Fetch the watch. + w.mu.Lock() + defer w.mu.Unlock() + watch, ok := w.watches[name] + + // Remove it from inotify. + if !ok { + return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) + } + + // We successfully removed the watch if InotifyRmWatch doesn't return an + // error, we need to clean up our internal state to ensure it matches + // inotify's kernel state. + delete(w.paths, int(watch.wd)) + delete(w.watches, name) + + // inotify_rm_watch will return EINVAL if the file has been deleted; + // the inotify will already have been removed. + // watches and pathes are deleted in ignoreLinux() implicitly and asynchronously + // by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE + // so that EINVAL means that the wd is being rm_watch()ed or its file removed + // by another thread and we have not received IN_IGNORE event. + success, errno := unix.InotifyRmWatch(w.fd, watch.wd) + if success == -1 { + // TODO: Perhaps it's not helpful to return an error here in every case. + // the only two possible errors are: + // EBADF, which happens when w.fd is not a valid file descriptor of any kind. + // EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor. + // Watch descriptors are invalidated when they are removed explicitly or implicitly; + // explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted. + return errno + } + + return nil +} + +// WatchList returns the directories and files that are being monitered. +func (w *Watcher) WatchList() []string { + w.mu.Lock() + defer w.mu.Unlock() + + entries := make([]string, 0, len(w.watches)) + for pathname := range w.watches { + entries = append(entries, pathname) + } + + return entries +} + +type watch struct { + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) +} + +// readEvents reads from the inotify file descriptor, converts the +// received events into Event objects and sends them via the Events channel +func (w *Watcher) readEvents() { + var ( + buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events + n int // Number of bytes read with read() + errno error // Syscall errno + ok bool // For poller.wait + ) + + defer close(w.doneResp) + defer close(w.Errors) + defer close(w.Events) + defer unix.Close(w.fd) + defer w.poller.close() + + for { + // See if we have been closed. + if w.isClosed() { + return + } + + ok, errno = w.poller.wait() + if errno != nil { + select { + case w.Errors <- errno: + case <-w.done: + return + } + continue + } + + if !ok { + continue + } + + n, errno = unix.Read(w.fd, buf[:]) + // If a signal interrupted execution, see if we've been asked to close, and try again. + // http://man7.org/linux/man-pages/man7/signal.7.html : + // "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable" + if errno == unix.EINTR { + continue + } + + // unix.Read might have been woken up by Close. If so, we're done. + if w.isClosed() { + return + } + + if n < unix.SizeofInotifyEvent { + var err error + if n == 0 { + // If EOF is received. This should really never happen. + err = io.EOF + } else if n < 0 { + // If an error occurred while reading. + err = errno + } else { + // Read was too short. + err = errors.New("notify: short read in readEvents()") + } + select { + case w.Errors <- err: + case <-w.done: + return + } + continue + } + + var offset uint32 + // We don't know how many events we just read into the buffer + // While the offset points to at least one whole event... + for offset <= uint32(n-unix.SizeofInotifyEvent) { + // Point "raw" to the event in the buffer + raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) + + mask := uint32(raw.Mask) + nameLen := uint32(raw.Len) + + if mask&unix.IN_Q_OVERFLOW != 0 { + select { + case w.Errors <- ErrEventOverflow: + case <-w.done: + return + } + } + + // If the event happened to the watched directory or the watched file, the kernel + // doesn't append the filename to the event, but we would like to always fill the + // the "Name" field with a valid filename. We retrieve the path of the watch from + // the "paths" map. + w.mu.Lock() + name, ok := w.paths[int(raw.Wd)] + // IN_DELETE_SELF occurs when the file/directory being watched is removed. + // This is a sign to clean up the maps, otherwise we are no longer in sync + // with the inotify kernel state which has already deleted the watch + // automatically. + if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { + delete(w.paths, int(raw.Wd)) + delete(w.watches, name) + } + w.mu.Unlock() + + if nameLen > 0 { + // Point "bytes" at the first byte of the filename + bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] + // The filename is padded with NULL bytes. TrimRight() gets rid of those. + name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + } + + event := newEvent(name, mask) + + // Send the events that are not ignored on the events channel + if !event.ignoreLinux(mask) { + select { + case w.Events <- event: + case <-w.done: + return + } + } + + // Move to the next event in the buffer + offset += unix.SizeofInotifyEvent + nameLen + } + } +} + +// Certain types of events can be "ignored" and not sent over the Events +// channel. Such as events marked ignore by the kernel, or MODIFY events +// against files that do not exist. +func (e *Event) ignoreLinux(mask uint32) bool { + // Ignore anything the inotify API says to ignore + if mask&unix.IN_IGNORED == unix.IN_IGNORED { + return true + } + + // If the event is not a DELETE or RENAME, the file must exist. + // Otherwise the event is ignored. + // *Note*: this was put in place because it was seen that a MODIFY + // event was sent after the DELETE. This ignores that MODIFY and + // assumes a DELETE will come or has come if the file doesn't exist. + if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { + _, statErr := os.Lstat(e.Name) + return os.IsNotExist(statErr) + } + return false +} + +// newEvent returns an platform-independent Event based on an inotify mask. +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { + e.Op |= Create + } + if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { + e.Op |= Remove + } + if mask&unix.IN_MODIFY == unix.IN_MODIFY { + e.Op |= Write + } + if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { + e.Op |= Rename + } + if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { + e.Op |= Chmod + } + return e +} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/inotify_poller.go b/agent/vendor/github.com/fsnotify/fsnotify/inotify_poller.go new file mode 100644 index 00000000000..b572a37c3f1 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/inotify_poller.go @@ -0,0 +1,187 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +package fsnotify + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +type fdPoller struct { + fd int // File descriptor (as returned by the inotify_init() syscall) + epfd int // Epoll file descriptor + pipe [2]int // Pipe for waking up +} + +func emptyPoller(fd int) *fdPoller { + poller := new(fdPoller) + poller.fd = fd + poller.epfd = -1 + poller.pipe[0] = -1 + poller.pipe[1] = -1 + return poller +} + +// Create a new inotify poller. +// This creates an inotify handler, and an epoll handler. +func newFdPoller(fd int) (*fdPoller, error) { + var errno error + poller := emptyPoller(fd) + defer func() { + if errno != nil { + poller.close() + } + }() + + // Create epoll fd + poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC) + if poller.epfd == -1 { + return nil, errno + } + // Create pipe; pipe[0] is the read end, pipe[1] the write end. + errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC) + if errno != nil { + return nil, errno + } + + // Register inotify fd with epoll + event := unix.EpollEvent{ + Fd: int32(poller.fd), + Events: unix.EPOLLIN, + } + errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event) + if errno != nil { + return nil, errno + } + + // Register pipe fd with epoll + event = unix.EpollEvent{ + Fd: int32(poller.pipe[0]), + Events: unix.EPOLLIN, + } + errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event) + if errno != nil { + return nil, errno + } + + return poller, nil +} + +// Wait using epoll. +// Returns true if something is ready to be read, +// false if there is not. +func (poller *fdPoller) wait() (bool, error) { + // 3 possible events per fd, and 2 fds, makes a maximum of 6 events. + // I don't know whether epoll_wait returns the number of events returned, + // or the total number of events ready. + // I decided to catch both by making the buffer one larger than the maximum. + events := make([]unix.EpollEvent, 7) + for { + n, errno := unix.EpollWait(poller.epfd, events, -1) + if n == -1 { + if errno == unix.EINTR { + continue + } + return false, errno + } + if n == 0 { + // If there are no events, try again. + continue + } + if n > 6 { + // This should never happen. More events were returned than should be possible. + return false, errors.New("epoll_wait returned more events than I know what to do with") + } + ready := events[:n] + epollhup := false + epollerr := false + epollin := false + for _, event := range ready { + if event.Fd == int32(poller.fd) { + if event.Events&unix.EPOLLHUP != 0 { + // This should not happen, but if it does, treat it as a wakeup. + epollhup = true + } + if event.Events&unix.EPOLLERR != 0 { + // If an error is waiting on the file descriptor, we should pretend + // something is ready to read, and let unix.Read pick up the error. + epollerr = true + } + if event.Events&unix.EPOLLIN != 0 { + // There is data to read. + epollin = true + } + } + if event.Fd == int32(poller.pipe[0]) { + if event.Events&unix.EPOLLHUP != 0 { + // Write pipe descriptor was closed, by us. This means we're closing down the + // watcher, and we should wake up. + } + if event.Events&unix.EPOLLERR != 0 { + // If an error is waiting on the pipe file descriptor. + // This is an absolute mystery, and should never ever happen. + return false, errors.New("Error on the pipe descriptor.") + } + if event.Events&unix.EPOLLIN != 0 { + // This is a regular wakeup, so we have to clear the buffer. + err := poller.clearWake() + if err != nil { + return false, err + } + } + } + } + + if epollhup || epollerr || epollin { + return true, nil + } + return false, nil + } +} + +// Close the write end of the poller. +func (poller *fdPoller) wake() error { + buf := make([]byte, 1) + n, errno := unix.Write(poller.pipe[1], buf) + if n == -1 { + if errno == unix.EAGAIN { + // Buffer is full, poller will wake. + return nil + } + return errno + } + return nil +} + +func (poller *fdPoller) clearWake() error { + // You have to be woken up a LOT in order to get to 100! + buf := make([]byte, 100) + n, errno := unix.Read(poller.pipe[0], buf) + if n == -1 { + if errno == unix.EAGAIN { + // Buffer is empty, someone else cleared our wake. + return nil + } + return errno + } + return nil +} + +// Close all poller file descriptors, but not the one passed to it. +func (poller *fdPoller) close() { + if poller.pipe[1] != -1 { + unix.Close(poller.pipe[1]) + } + if poller.pipe[0] != -1 { + unix.Close(poller.pipe[0]) + } + if poller.epfd != -1 { + unix.Close(poller.epfd) + } +} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/kqueue.go b/agent/vendor/github.com/fsnotify/fsnotify/kqueue.go new file mode 100644 index 00000000000..6fb8d8532e7 --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/kqueue.go @@ -0,0 +1,535 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build freebsd || openbsd || netbsd || dragonfly || darwin +// +build freebsd openbsd netbsd dragonfly darwin + +package fsnotify + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" + + "golang.org/x/sys/unix" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + done chan struct{} // Channel for sending a "quit message" to the reader goroutine + + kq int // File descriptor (as returned by the kqueue() syscall). + + mu sync.Mutex // Protects access to watcher data + watches map[string]int // Map of watched file descriptors (key: path). + externalWatches map[string]bool // Map of watches added by user of the library. + dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue. + paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events. + fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). + isClosed bool // Set to true when Close() is first called +} + +type pathInfo struct { + name string + isDir bool +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + kq, err := kqueue() + if err != nil { + return nil, err + } + + w := &Watcher{ + kq: kq, + watches: make(map[string]int), + dirFlags: make(map[string]uint32), + paths: make(map[int]pathInfo), + fileExists: make(map[string]bool), + externalWatches: make(map[string]bool), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan struct{}), + } + + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return nil + } + w.isClosed = true + + // copy paths to remove while locked + var pathsToRemove = make([]string, 0, len(w.watches)) + for name := range w.watches { + pathsToRemove = append(pathsToRemove, name) + } + w.mu.Unlock() + // unlock before calling Remove, which also locks + + for _, name := range pathsToRemove { + w.Remove(name) + } + + // send a "quit" message to the reader goroutine + close(w.done) + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + w.mu.Lock() + w.externalWatches[name] = true + w.mu.Unlock() + _, err := w.addWatch(name, noteAllEvents) + return err +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + w.mu.Lock() + watchfd, ok := w.watches[name] + w.mu.Unlock() + if !ok { + return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) + } + + const registerRemove = unix.EV_DELETE + if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil { + return err + } + + unix.Close(watchfd) + + w.mu.Lock() + isDir := w.paths[watchfd].isDir + delete(w.watches, name) + delete(w.paths, watchfd) + delete(w.dirFlags, name) + w.mu.Unlock() + + // Find all watched paths that are in this directory that are not external. + if isDir { + var pathsToRemove []string + w.mu.Lock() + for _, path := range w.paths { + wdir, _ := filepath.Split(path.name) + if filepath.Clean(wdir) == name { + if !w.externalWatches[path.name] { + pathsToRemove = append(pathsToRemove, path.name) + } + } + } + w.mu.Unlock() + for _, name := range pathsToRemove { + // Since these are internal, not much sense in propagating error + // to the user, as that will just confuse them with an error about + // a path they did not explicitly watch themselves. + w.Remove(name) + } + } + + return nil +} + +// WatchList returns the directories and files that are being monitered. +func (w *Watcher) WatchList() []string { + w.mu.Lock() + defer w.mu.Unlock() + + entries := make([]string, 0, len(w.watches)) + for pathname := range w.watches { + entries = append(entries, pathname) + } + + return entries +} + +// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) +const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME + +// keventWaitTime to block on each read from kevent +var keventWaitTime = durationToTimespec(100 * time.Millisecond) + +// addWatch adds name to the watched file set. +// The flags are interpreted as described in kevent(2). +// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks. +func (w *Watcher) addWatch(name string, flags uint32) (string, error) { + var isDir bool + // Make ./name and name equivalent + name = filepath.Clean(name) + + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return "", errors.New("kevent instance already closed") + } + watchfd, alreadyWatching := w.watches[name] + // We already have a watch, but we can still override flags. + if alreadyWatching { + isDir = w.paths[watchfd].isDir + } + w.mu.Unlock() + + if !alreadyWatching { + fi, err := os.Lstat(name) + if err != nil { + return "", err + } + + // Don't watch sockets. + if fi.Mode()&os.ModeSocket == os.ModeSocket { + return "", nil + } + + // Don't watch named pipes. + if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe { + return "", nil + } + + // Follow Symlinks + // Unfortunately, Linux can add bogus symlinks to watch list without + // issue, and Windows can't do symlinks period (AFAIK). To maintain + // consistency, we will act like everything is fine. There will simply + // be no file events for broken symlinks. + // Hence the returns of nil on errors. + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + name, err = filepath.EvalSymlinks(name) + if err != nil { + return "", nil + } + + w.mu.Lock() + _, alreadyWatching = w.watches[name] + w.mu.Unlock() + + if alreadyWatching { + return name, nil + } + + fi, err = os.Lstat(name) + if err != nil { + return "", nil + } + } + + watchfd, err = unix.Open(name, openMode, 0700) + if watchfd == -1 { + return "", err + } + + isDir = fi.IsDir() + } + + const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE + if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil { + unix.Close(watchfd) + return "", err + } + + if !alreadyWatching { + w.mu.Lock() + w.watches[name] = watchfd + w.paths[watchfd] = pathInfo{name: name, isDir: isDir} + w.mu.Unlock() + } + + if isDir { + // Watch the directory if it has not been watched before, + // or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) + w.mu.Lock() + + watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && + (!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE) + // Store flags so this watch can be updated later + w.dirFlags[name] = flags + w.mu.Unlock() + + if watchDir { + if err := w.watchDirectoryFiles(name); err != nil { + return "", err + } + } + } + return name, nil +} + +// readEvents reads from kqueue and converts the received kevents into +// Event values that it sends down the Events channel. +func (w *Watcher) readEvents() { + eventBuffer := make([]unix.Kevent_t, 10) + +loop: + for { + // See if there is a message on the "done" channel + select { + case <-w.done: + break loop + default: + } + + // Get new events + kevents, err := read(w.kq, eventBuffer, &keventWaitTime) + // EINTR is okay, the syscall was interrupted before timeout expired. + if err != nil && err != unix.EINTR { + select { + case w.Errors <- err: + case <-w.done: + break loop + } + continue + } + + // Flush the events we received to the Events channel + for len(kevents) > 0 { + kevent := &kevents[0] + watchfd := int(kevent.Ident) + mask := uint32(kevent.Fflags) + w.mu.Lock() + path := w.paths[watchfd] + w.mu.Unlock() + event := newEvent(path.name, mask) + + if path.isDir && !(event.Op&Remove == Remove) { + // Double check to make sure the directory exists. This can happen when + // we do a rm -fr on a recursively watched folders and we receive a + // modification event first but the folder has been deleted and later + // receive the delete event + if _, err := os.Lstat(event.Name); os.IsNotExist(err) { + // mark is as delete event + event.Op |= Remove + } + } + + if event.Op&Rename == Rename || event.Op&Remove == Remove { + w.Remove(event.Name) + w.mu.Lock() + delete(w.fileExists, event.Name) + w.mu.Unlock() + } + + if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { + w.sendDirectoryChangeEvents(event.Name) + } else { + // Send the event on the Events channel. + select { + case w.Events <- event: + case <-w.done: + break loop + } + } + + if event.Op&Remove == Remove { + // Look for a file that may have overwritten this. + // For example, mv f1 f2 will delete f2, then create f2. + if path.isDir { + fileDir := filepath.Clean(event.Name) + w.mu.Lock() + _, found := w.watches[fileDir] + w.mu.Unlock() + if found { + // make sure the directory exists before we watch for changes. When we + // do a recursive watch and perform rm -fr, the parent directory might + // have gone missing, ignore the missing directory and let the + // upcoming delete event remove the watch from the parent directory. + if _, err := os.Lstat(fileDir); err == nil { + w.sendDirectoryChangeEvents(fileDir) + } + } + } else { + filePath := filepath.Clean(event.Name) + if fileInfo, err := os.Lstat(filePath); err == nil { + w.sendFileCreatedEventIfNew(filePath, fileInfo) + } + } + } + + // Move to next event + kevents = kevents[1:] + } + } + + // cleanup + err := unix.Close(w.kq) + if err != nil { + // only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors. + select { + case w.Errors <- err: + default: + } + } + close(w.Events) + close(w.Errors) +} + +// newEvent returns an platform-independent Event based on kqueue Fflags. +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&unix.NOTE_DELETE == unix.NOTE_DELETE { + e.Op |= Remove + } + if mask&unix.NOTE_WRITE == unix.NOTE_WRITE { + e.Op |= Write + } + if mask&unix.NOTE_RENAME == unix.NOTE_RENAME { + e.Op |= Rename + } + if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { + e.Op |= Chmod + } + return e +} + +func newCreateEvent(name string) Event { + return Event{Name: name, Op: Create} +} + +// watchDirectoryFiles to mimic inotify when adding a watch on a directory +func (w *Watcher) watchDirectoryFiles(dirPath string) error { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + return err + } + + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + filePath, err = w.internalWatch(filePath, fileInfo) + if err != nil { + return err + } + + w.mu.Lock() + w.fileExists[filePath] = true + w.mu.Unlock() + } + + return nil +} + +// sendDirectoryEvents searches the directory for newly created files +// and sends them over the event channel. This functionality is to have +// the BSD version of fsnotify match Linux inotify which provides a +// create event for files created in a watched directory. +func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + select { + case w.Errors <- err: + case <-w.done: + return + } + } + + // Search for new files + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + err := w.sendFileCreatedEventIfNew(filePath, fileInfo) + + if err != nil { + return + } + } +} + +// sendFileCreatedEvent sends a create event if the file isn't already being tracked. +func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { + w.mu.Lock() + _, doesExist := w.fileExists[filePath] + w.mu.Unlock() + if !doesExist { + // Send create event + select { + case w.Events <- newCreateEvent(filePath): + case <-w.done: + return + } + } + + // like watchDirectoryFiles (but without doing another ReadDir) + filePath, err = w.internalWatch(filePath, fileInfo) + if err != nil { + return err + } + + w.mu.Lock() + w.fileExists[filePath] = true + w.mu.Unlock() + + return nil +} + +func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { + if fileInfo.IsDir() { + // mimic Linux providing delete events for subdirectories + // but preserve the flags used if currently watching subdirectory + w.mu.Lock() + flags := w.dirFlags[name] + w.mu.Unlock() + + flags |= unix.NOTE_DELETE | unix.NOTE_RENAME + return w.addWatch(name, flags) + } + + // watch file to mimic Linux inotify + return w.addWatch(name, noteAllEvents) +} + +// kqueue creates a new kernel event queue and returns a descriptor. +func kqueue() (kq int, err error) { + kq, err = unix.Kqueue() + if kq == -1 { + return kq, err + } + return kq, nil +} + +// register events with the queue +func register(kq int, fds []int, flags int, fflags uint32) error { + changes := make([]unix.Kevent_t, len(fds)) + + for i, fd := range fds { + // SetKevent converts int to the platform-specific types: + unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags) + changes[i].Fflags = fflags + } + + // register the events + success, err := unix.Kevent(kq, changes, nil, nil) + if success == -1 { + return err + } + return nil +} + +// read retrieves pending events, or waits until an event occurs. +// A timeout of nil blocks indefinitely, while 0 polls the queue. +func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) { + n, err := unix.Kevent(kq, nil, events, timeout) + if err != nil { + return nil, err + } + return events[0:n], nil +} + +// durationToTimespec prepares a timeout value +func durationToTimespec(d time.Duration) unix.Timespec { + return unix.NsecToTimespec(d.Nanoseconds()) +} diff --git a/agent/vendor/github.com/fsnotify/fsnotify/open_mode_bsd.go b/agent/vendor/github.com/fsnotify/fsnotify/open_mode_bsd.go new file mode 100644 index 00000000000..36cc3845b6e --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/open_mode_bsd.go @@ -0,0 +1,12 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build freebsd || openbsd || netbsd || dragonfly +// +build freebsd openbsd netbsd dragonfly + +package fsnotify + +import "golang.org/x/sys/unix" + +const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC diff --git a/agent/vendor/github.com/fsnotify/fsnotify/open_mode_darwin.go b/agent/vendor/github.com/fsnotify/fsnotify/open_mode_darwin.go new file mode 100644 index 00000000000..98cd8476ffb --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/open_mode_darwin.go @@ -0,0 +1,13 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin +// +build darwin + +package fsnotify + +import "golang.org/x/sys/unix" + +// note: this constant is not defined on BSD +const openMode = unix.O_EVTONLY | unix.O_CLOEXEC diff --git a/agent/vendor/github.com/fsnotify/fsnotify/windows.go b/agent/vendor/github.com/fsnotify/fsnotify/windows.go new file mode 100644 index 00000000000..02ce7deb0bb --- /dev/null +++ b/agent/vendor/github.com/fsnotify/fsnotify/windows.go @@ -0,0 +1,586 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package fsnotify + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "sync" + "syscall" + "unsafe" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + isClosed bool // Set to true when Close() is first called + mu sync.Mutex // Map access + port syscall.Handle // Handle to completion port + watches watchMap // Map of watches (key: i-number) + input chan *input // Inputs to the reader are sent on this channel + quit chan chan<- error +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) + if e != nil { + return nil, os.NewSyscallError("CreateIoCompletionPort", e) + } + w := &Watcher{ + port: port, + watches: make(watchMap), + input: make(chan *input, 1), + Events: make(chan Event, 50), + Errors: make(chan error), + quit: make(chan chan<- error, 1), + } + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + ch := make(chan error) + w.quit <- ch + if err := w.wakeupReader(); err != nil { + return err + } + return <-ch +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + if w.isClosed { + return errors.New("watcher already closed") + } + in := &input{ + op: opAddWatch, + path: filepath.Clean(name), + flags: sysFSALLEVENTS, + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + in := &input{ + op: opRemoveWatch, + path: filepath.Clean(name), + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +// WatchList returns the directories and files that are being monitered. +func (w *Watcher) WatchList() []string { + w.mu.Lock() + defer w.mu.Unlock() + + entries := make([]string, 0, len(w.watches)) + for _, entry := range w.watches { + for _, watchEntry := range entry { + entries = append(entries, watchEntry.path) + } + } + + return entries +} + +const ( + // Options for AddWatch + sysFSONESHOT = 0x80000000 + sysFSONLYDIR = 0x1000000 + + // Events + sysFSACCESS = 0x1 + sysFSALLEVENTS = 0xfff + sysFSATTRIB = 0x4 + sysFSCLOSE = 0x18 + sysFSCREATE = 0x100 + sysFSDELETE = 0x200 + sysFSDELETESELF = 0x400 + sysFSMODIFY = 0x2 + sysFSMOVE = 0xc0 + sysFSMOVEDFROM = 0x40 + sysFSMOVEDTO = 0x80 + sysFSMOVESELF = 0x800 + + // Special events + sysFSIGNORED = 0x8000 + sysFSQOVERFLOW = 0x4000 +) + +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO { + e.Op |= Create + } + if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF { + e.Op |= Remove + } + if mask&sysFSMODIFY == sysFSMODIFY { + e.Op |= Write + } + if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { + e.Op |= Rename + } + if mask&sysFSATTRIB == sysFSATTRIB { + e.Op |= Chmod + } + return e +} + +const ( + opAddWatch = iota + opRemoveWatch +) + +const ( + provisional uint64 = 1 << (32 + iota) +) + +type input struct { + op int + path string + flags uint32 + reply chan error +} + +type inode struct { + handle syscall.Handle + volume uint32 + index uint64 +} + +type watch struct { + ov syscall.Overlapped + ino *inode // i-number + path string // Directory path + mask uint64 // Directory itself is being watched with these notify flags + names map[string]uint64 // Map of names being watched and their notify flags + rename string // Remembers the old name while renaming a file + buf [4096]byte +} + +type indexMap map[uint64]*watch +type watchMap map[uint32]indexMap + +func (w *Watcher) wakeupReader() error { + e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) + if e != nil { + return os.NewSyscallError("PostQueuedCompletionStatus", e) + } + return nil +} + +func getDir(pathname string) (dir string, err error) { + attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) + if e != nil { + return "", os.NewSyscallError("GetFileAttributes", e) + } + if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + dir = pathname + } else { + dir, _ = filepath.Split(pathname) + dir = filepath.Clean(dir) + } + return +} + +func getIno(path string) (ino *inode, err error) { + h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), + syscall.FILE_LIST_DIRECTORY, + syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, + nil, syscall.OPEN_EXISTING, + syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) + if e != nil { + return nil, os.NewSyscallError("CreateFile", e) + } + var fi syscall.ByHandleFileInformation + if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { + syscall.CloseHandle(h) + return nil, os.NewSyscallError("GetFileInformationByHandle", e) + } + ino = &inode{ + handle: h, + volume: fi.VolumeSerialNumber, + index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), + } + return ino, nil +} + +// Must run within the I/O thread. +func (m watchMap) get(ino *inode) *watch { + if i := m[ino.volume]; i != nil { + return i[ino.index] + } + return nil +} + +// Must run within the I/O thread. +func (m watchMap) set(ino *inode, watch *watch) { + i := m[ino.volume] + if i == nil { + i = make(indexMap) + m[ino.volume] = i + } + i[ino.index] = watch +} + +// Must run within the I/O thread. +func (w *Watcher) addWatch(pathname string, flags uint64) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + if flags&sysFSONLYDIR != 0 && pathname != dir { + return nil + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watchEntry := w.watches.get(ino) + w.mu.Unlock() + if watchEntry == nil { + if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { + syscall.CloseHandle(ino.handle) + return os.NewSyscallError("CreateIoCompletionPort", e) + } + watchEntry = &watch{ + ino: ino, + path: dir, + names: make(map[string]uint64), + } + w.mu.Lock() + w.watches.set(ino, watchEntry) + w.mu.Unlock() + flags |= provisional + } else { + syscall.CloseHandle(ino.handle) + } + if pathname == dir { + watchEntry.mask |= flags + } else { + watchEntry.names[filepath.Base(pathname)] |= flags + } + if err = w.startRead(watchEntry); err != nil { + return err + } + if pathname == dir { + watchEntry.mask &= ^provisional + } else { + watchEntry.names[filepath.Base(pathname)] &= ^provisional + } + return nil +} + +// Must run within the I/O thread. +func (w *Watcher) remWatch(pathname string) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watch := w.watches.get(ino) + w.mu.Unlock() + if watch == nil { + return fmt.Errorf("can't remove non-existent watch for: %s", pathname) + } + if pathname == dir { + w.sendEvent(watch.path, watch.mask&sysFSIGNORED) + watch.mask = 0 + } else { + name := filepath.Base(pathname) + w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED) + delete(watch.names, name) + } + return w.startRead(watch) +} + +// Must run within the I/O thread. +func (w *Watcher) deleteWatch(watch *watch) { + for name, mask := range watch.names { + if mask&provisional == 0 { + w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED) + } + delete(watch.names, name) + } + if watch.mask != 0 { + if watch.mask&provisional == 0 { + w.sendEvent(watch.path, watch.mask&sysFSIGNORED) + } + watch.mask = 0 + } +} + +// Must run within the I/O thread. +func (w *Watcher) startRead(watch *watch) error { + if e := syscall.CancelIo(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CancelIo", e) + w.deleteWatch(watch) + } + mask := toWindowsFlags(watch.mask) + for _, m := range watch.names { + mask |= toWindowsFlags(m) + } + if mask == 0 { + if e := syscall.CloseHandle(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CloseHandle", e) + } + w.mu.Lock() + delete(w.watches[watch.ino.volume], watch.ino.index) + w.mu.Unlock() + return nil + } + e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], + uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) + if e != nil { + err := os.NewSyscallError("ReadDirectoryChanges", e) + if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { + // Watched directory was probably removed + if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) { + if watch.mask&sysFSONESHOT != 0 { + watch.mask = 0 + } + } + err = nil + } + w.deleteWatch(watch) + w.startRead(watch) + return err + } + return nil +} + +// readEvents reads from the I/O completion port, converts the +// received events into Event objects and sends them via the Events channel. +// Entry point to the I/O thread. +func (w *Watcher) readEvents() { + var ( + n, key uint32 + ov *syscall.Overlapped + ) + runtime.LockOSThread() + + for { + e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) + watch := (*watch)(unsafe.Pointer(ov)) + + if watch == nil { + select { + case ch := <-w.quit: + w.mu.Lock() + var indexes []indexMap + for _, index := range w.watches { + indexes = append(indexes, index) + } + w.mu.Unlock() + for _, index := range indexes { + for _, watch := range index { + w.deleteWatch(watch) + w.startRead(watch) + } + } + var err error + if e := syscall.CloseHandle(w.port); e != nil { + err = os.NewSyscallError("CloseHandle", e) + } + close(w.Events) + close(w.Errors) + ch <- err + return + case in := <-w.input: + switch in.op { + case opAddWatch: + in.reply <- w.addWatch(in.path, uint64(in.flags)) + case opRemoveWatch: + in.reply <- w.remWatch(in.path) + } + default: + } + continue + } + + switch e { + case syscall.ERROR_MORE_DATA: + if watch == nil { + w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") + } else { + // The i/o succeeded but the buffer is full. + // In theory we should be building up a full packet. + // In practice we can get away with just carrying on. + n = uint32(unsafe.Sizeof(watch.buf)) + } + case syscall.ERROR_ACCESS_DENIED: + // Watched directory was probably removed + w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) + w.deleteWatch(watch) + w.startRead(watch) + continue + case syscall.ERROR_OPERATION_ABORTED: + // CancelIo was called on this handle + continue + default: + w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) + continue + case nil: + } + + var offset uint32 + for { + if n == 0 { + w.Events <- newEvent("", sysFSQOVERFLOW) + w.Errors <- errors.New("short read in readEvents()") + break + } + + // Point "raw" to the event in the buffer + raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) + // TODO: Consider using unsafe.Slice that is available from go1.17 + // https://stackoverflow.com/questions/51187973/how-to-create-an-array-or-a-slice-from-an-array-unsafe-pointer-in-golang + // instead of using a fixed syscall.MAX_PATH buf, we create a buf that is the size of the path name + size := int(raw.FileNameLength / 2) + var buf []uint16 + sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) + sh.Data = uintptr(unsafe.Pointer(&raw.FileName)) + sh.Len = size + sh.Cap = size + name := syscall.UTF16ToString(buf) + fullname := filepath.Join(watch.path, name) + + var mask uint64 + switch raw.Action { + case syscall.FILE_ACTION_REMOVED: + mask = sysFSDELETESELF + case syscall.FILE_ACTION_MODIFIED: + mask = sysFSMODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + watch.rename = name + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + if watch.names[watch.rename] != 0 { + watch.names[name] |= watch.names[watch.rename] + delete(watch.names, watch.rename) + mask = sysFSMOVESELF + } + } + + sendNameEvent := func() { + if w.sendEvent(fullname, watch.names[name]&mask) { + if watch.names[name]&sysFSONESHOT != 0 { + delete(watch.names, name) + } + } + } + if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { + sendNameEvent() + } + if raw.Action == syscall.FILE_ACTION_REMOVED { + w.sendEvent(fullname, watch.names[name]&sysFSIGNORED) + delete(watch.names, name) + } + if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { + if watch.mask&sysFSONESHOT != 0 { + watch.mask = 0 + } + } + if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { + fullname = filepath.Join(watch.path, watch.rename) + sendNameEvent() + } + + // Move to the next event in the buffer + if raw.NextEntryOffset == 0 { + break + } + offset += raw.NextEntryOffset + + // Error! + if offset >= n { + w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") + break + } + } + + if err := w.startRead(watch); err != nil { + w.Errors <- err + } + } +} + +func (w *Watcher) sendEvent(name string, mask uint64) bool { + if mask == 0 { + return false + } + event := newEvent(name, uint32(mask)) + select { + case ch := <-w.quit: + w.quit <- ch + case w.Events <- event: + } + return true +} + +func toWindowsFlags(mask uint64) uint32 { + var m uint32 + if mask&sysFSACCESS != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS + } + if mask&sysFSMODIFY != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE + } + if mask&sysFSATTRIB != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES + } + if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME + } + return m +} + +func toFSnotifyFlags(action uint32) uint64 { + switch action { + case syscall.FILE_ACTION_ADDED: + return sysFSCREATE + case syscall.FILE_ACTION_REMOVED: + return sysFSDELETE + case syscall.FILE_ACTION_MODIFIED: + return sysFSMODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + return sysFSMOVEDFROM + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + return sysFSMOVEDTO + } + return 0 +} diff --git a/agent/vendor/github.com/godbus/dbus/.travis.yml b/agent/vendor/github.com/godbus/dbus/.travis.yml deleted file mode 100644 index 2e1bbb78c39..00000000000 --- a/agent/vendor/github.com/godbus/dbus/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -dist: precise -language: go -go_import_path: github.com/godbus/dbus -sudo: true - -go: - - 1.6.3 - - 1.7.3 - - tip - -env: - global: - matrix: - - TARGET=amd64 - - TARGET=arm64 - - TARGET=arm - - TARGET=386 - - TARGET=ppc64le - -matrix: - fast_finish: true - allow_failures: - - go: tip - exclude: - - go: tip - env: TARGET=arm - - go: tip - env: TARGET=arm64 - - go: tip - env: TARGET=386 - - go: tip - env: TARGET=ppc64le - -addons: - apt: - packages: - - dbus - - dbus-x11 - -before_install: diff --git a/agent/vendor/github.com/godbus/dbus/CONTRIBUTING.md b/agent/vendor/github.com/godbus/dbus/CONTRIBUTING.md deleted file mode 100644 index c88f9b2bdd0..00000000000 --- a/agent/vendor/github.com/godbus/dbus/CONTRIBUTING.md +++ /dev/null @@ -1,50 +0,0 @@ -# How to Contribute - -## Getting Started - -- Fork the repository on GitHub -- Read the [README](README.markdown) for build and test instructions -- Play with the project, submit bugs, submit patches! - -## Contribution Flow - -This is a rough outline of what a contributor's workflow looks like: - -- Create a topic branch from where you want to base your work (usually master). -- Make commits of logical units. -- Make sure your commit messages are in the proper format (see below). -- Push your changes to a topic branch in your fork of the repository. -- Make sure the tests pass, and add any new tests as appropriate. -- Submit a pull request to the original repository. - -Thanks for your contributions! - -### Format of the Commit Message - -We follow a rough convention for commit messages that is designed to answer two -questions: what changed and why. The subject line should feature the what and -the body of the commit should describe the why. - -``` -scripts: add the test-cluster command - -this uses tmux to setup a test cluster that you can easily kill and -start for debugging. - -Fixes #38 -``` - -The format can be described more formally as follows: - -``` -: - - - -