├── .github ├── dependabot.yml └── workflows │ ├── pr_workflow.yml │ ├── release.yml │ ├── test_and_build.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── CHANGELOG.md ├── INSTALL.md ├── LICENSE ├── README.md ├── build.gradle ├── contrib └── scripts │ ├── bootstrap-via-installer │ ├── Dockerfile │ ├── go-agent │ └── log4j.properties │ └── bootstrap-without-installed-agent │ ├── Dockerfile │ ├── go-agent │ └── log4j.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── configure-job.png ├── pipeline.png ├── plugin-settings.png ├── plugins.png ├── profile.png ├── profiles_page.png └── quick-edit.png ├── settings.gradle └── src ├── main ├── java │ └── cd │ │ └── go │ │ └── contrib │ │ └── elasticagents │ │ └── dockerswarm │ │ └── elasticagent │ │ ├── Agent.java │ │ ├── AgentInstances.java │ │ ├── Agents.java │ │ ├── Clock.java │ │ ├── ClusterProfile.java │ │ ├── ClusterProfileProperties.java │ │ ├── Constants.java │ │ ├── DockerClientFactory.java │ │ ├── DockerMounts.java │ │ ├── DockerPlugin.java │ │ ├── DockerSecrets.java │ │ ├── DockerService.java │ │ ├── DockerServices.java │ │ ├── ElasticAgentProfile.java │ │ ├── Hosts.java │ │ ├── Networks.java │ │ ├── PluginRequest.java │ │ ├── PluginSettings.java │ │ ├── PluginSettingsNotConfiguredException.java │ │ ├── Request.java │ │ ├── RequestExecutor.java │ │ ├── ServerRequestFailedException.java │ │ ├── SetupSemaphore.java │ │ ├── builders │ │ └── PluginStatusReportViewBuilder.java │ │ ├── executors │ │ ├── AgentStatusReportExecutor.java │ │ ├── ClusterProfileValidateRequestExecutor.java │ │ ├── ClusterStatusReportExecutor.java │ │ ├── CreateAgentRequestExecutor.java │ │ ├── Field.java │ │ ├── GetCapabilitiesExecutor.java │ │ ├── GetClusterProfileMetadataExecutor.java │ │ ├── GetClusterProfileViewRequestExecutor.java │ │ ├── GetPluginSettingsIconExecutor.java │ │ ├── GetProfileMetadataExecutor.java │ │ ├── GetProfileViewExecutor.java │ │ ├── GoServerURLMetadata.java │ │ ├── JobCompletionRequestExecutor.java │ │ ├── MemoryMetadata.java │ │ ├── Metadata.java │ │ ├── MigrateConfigurationRequestExecutor.java │ │ ├── NonBlankField.java │ │ ├── ProfileValidateRequestExecutor.java │ │ ├── ServerPingRequestExecutor.java │ │ └── ShouldAssignWorkRequestExecutor.java │ │ ├── metadata │ │ └── HostMetadata.java │ │ ├── model │ │ ├── JobIdentifier.java │ │ ├── ValidationError.java │ │ ├── ValidationResult.java │ │ └── reports │ │ │ ├── DockerNode.java │ │ │ ├── DockerTask.java │ │ │ ├── StatusReportGenerationError.java │ │ │ ├── SwarmCluster.java │ │ │ └── agent │ │ │ ├── DockerServiceElasticAgent.java │ │ │ └── TaskStatus.java │ │ ├── reports │ │ ├── StatusReportGenerationErrorHandler.java │ │ └── StatusReportGenerationException.java │ │ ├── requests │ │ ├── AgentStatusReportRequest.java │ │ ├── ClusterProfileValidateRequest.java │ │ ├── ClusterStatusReportRequest.java │ │ ├── CreateAgentRequest.java │ │ ├── JobCompletionRequest.java │ │ ├── MigrateConfigurationRequest.java │ │ ├── ProfileValidateRequest.java │ │ ├── ServerPingRequest.java │ │ └── ShouldAssignWorkRequest.java │ │ ├── utils │ │ ├── Size.java │ │ ├── SizeUnit.java │ │ └── Util.java │ │ └── validator │ │ ├── DockerMountsValidator.java │ │ ├── DockerSecretValidator.java │ │ ├── PrivateDockerRegistrySettingsValidator.java │ │ └── Validatable.java ├── resource-templates │ ├── plugin.properties │ └── plugin.xml └── resources │ ├── agent-status-report.template.ftlh │ ├── docker-swarm.png │ ├── error.template.ftlh │ ├── plugin-settings.template.html │ ├── profile.template.html │ └── status-report.template.ftlh └── test └── java └── cd └── go └── contrib └── elasticagents └── dockerswarm └── elasticagent ├── AgentTest.java ├── BaseTest.java ├── DockerMountsTest.java ├── DockerSecretsTest.java ├── DockerServiceElasticAgentTest.java ├── DockerServicesTestElasticAgent.java ├── HostsTest.java ├── NetworksTest.java ├── SetupSemaphoreTest.java ├── executors ├── AgentStatusReportExecutorTest.java ├── ClusterProfileValidateRequestExecutorTest.java ├── ClusterStatusReportExecutorTest.java ├── CreateAgentRequestExecutorTest.java ├── GetCapabilitiesExecutorTest.java ├── GetClusterProfileMetadataExecutorTest.java ├── GetClusterProfileViewRequestExecutorTest.java ├── GetPluginSettingsIconExecutorTest.java ├── GetProfileMetadataExecutorTest.java ├── GetProfileViewExecutorTest.java ├── GoServerURLMetadataTest.java ├── JobCompletionRequestExecutorTest.java ├── MemoryMetadataTest.java ├── MigrateConfigurationRequestExecutorTest.java ├── ProfileValidateRequestExecutorTest.java ├── ServerPingRequestExecutorTest.java └── ShouldAssignWorkRequestExecutorTest.java ├── metadata └── HostMetadataTest.java ├── model ├── CapabilitiesTest.java ├── DockerNodeTest.java ├── DockerTaskTest.java ├── JobIdentifierTest.java └── SwarmClusterTest.java ├── reports └── StatusReportGenerationErrorHandlerTest.java ├── requests ├── AgentStatusReportRequestTest.java ├── ClusterProfileValidateRequestTest.java ├── ClusterStatusReportRequestTest.java ├── CreateAgentRequestTest.java ├── JobCompletionRequestTest.java ├── MigrateConfigurationRequestTest.java └── ShouldAssignWorkRequestTest.java ├── utils ├── JobIdentifierMother.java └── UtilTest.java └── validator ├── DockerMountsValidatorTest.java └── DockerSecretValidatorTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: gradle 12 | directory: / 13 | schedule: 14 | interval: monthly 15 | groups: 16 | gradle-deps: 17 | patterns: 18 | - "*" -------------------------------------------------------------------------------- /.github/workflows/pr_workflow.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Testing For PRs 5 | 6 | on: [ pull_request ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | docker-version: # See https://endoflife.date/docker-engine / https://download.docker.com/linux/static/stable/x86_64/ 14 | - "v23.0.6" # 2023-02 --> EOL ? 15 | - "v25.0.5" # 2024-01 --> EOL ? 16 | - "v27.5.1" # 2025-01 --> EOL ? 17 | - "v28.1.1" # 2025-04 --> EOL ? 18 | fail-fast: false 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: Set up JDK 22 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 23 | with: 24 | java-version: 17 25 | distribution: temurin 26 | - name: Set up Docker 27 | uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0 28 | with: 29 | version: ${{ matrix.docker-version }} 30 | set-host: 'true' 31 | - run: docker swarm init 32 | - name: Build with Gradle 33 | run: ./gradlew assemble check -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Create Stable Release 3 | 4 | # Controls when the action will run. Workflow runs when manually triggered using the UI 5 | # or API. 6 | on: 7 | workflow_dispatch: 8 | # Inputs the workflow accepts. 9 | inputs: 10 | prerelease: 11 | description: 'The release should be an experimental release' 12 | default: 'NO' 13 | required: true 14 | 15 | jobs: 16 | build_and_release: 17 | runs-on: ubuntu-latest 18 | env: 19 | GITHUB_USER: "gocd-contrib" 20 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 21 | PRERELEASE: "${{ github.event.inputs.prerelease }}" 22 | steps: 23 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | with: 25 | fetch-depth: 0 26 | - name: Set up JDK 27 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 28 | with: 29 | java-version: 17 30 | distribution: temurin 31 | - name: Release 32 | run: ./gradlew githubRelease 33 | -------------------------------------------------------------------------------- /.github/workflows/test_and_build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Test and Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | docker-version: # See https://endoflife.date/docker-engine / https://download.docker.com/linux/static/stable/x86_64/ 16 | - "v23.0.6" # 2023-02 --> EOL ? 17 | - "v25.0.5" # 2024-01 --> EOL ? 18 | - "v27.5.1" # 2025-01 --> EOL ? 19 | - "v28.1.1" # 2025-04 --> EOL ? 20 | fail-fast: false 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | - name: Set up JDK 24 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 25 | with: 26 | java-version: 17 27 | distribution: temurin 28 | - name: Set up Docker 29 | uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0 30 | with: 31 | version: ${{ matrix.docker-version }} 32 | set-host: 'true' 33 | - run: docker swarm init 34 | - name: Build with Gradle 35 | run: ./gradlew assemble check 36 | previewGithubRelease: 37 | needs: test 38 | runs-on: ubuntu-latest 39 | env: 40 | GITHUB_USER: "gocd-contrib" 41 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 42 | steps: 43 | - name: Harden the runner (Audit all outbound calls) 44 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 45 | with: 46 | egress-policy: audit 47 | 48 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | with: 50 | fetch-depth: 0 51 | - name: Set up JDK 52 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 53 | with: 54 | java-version: 17 55 | distribution: temurin 56 | - name: Test with Gradle 57 | run: ./gradlew githubRelease 58 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 1 * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Harden the runner (Audit all outbound calls) 14 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 15 | with: 16 | egress-policy: audit 17 | 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Update Gradle Wrapper 21 | uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v2.1.0 22 | with: 23 | labels: dependencies 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | build/ 4 | .gradle 5 | .gradletasknamecache 6 | .tags 7 | .tags1 8 | classes 9 | out 10 | src/main/resources-generated/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoCD Elastic agent plugin for Docker Swarm 2 | 3 | Table of Contents 4 | ================= 5 | 6 | * [Installation](#installation) 7 | * [Building the code base](#building-the-code-base) 8 | * [Using your own docker image with elastic agents](#using-your-own-docker-image-with-elastic-agents) 9 | * [Troubleshooting](#troubleshooting) 10 | * [License](#license) 11 | 12 | ## Installation 13 | 14 | Documentation for installation is available [here](INSTALL.md). 15 | 16 | ## Building the code base 17 | 18 | To build the jar, run `./gradlew clean test assemble` 19 | 20 | ## Using your own docker image with elastic agents 21 | 22 | More information to build custom GoCD agent docker image is available [here](https://github.com/gocd/docker-gocd-agent) 23 | 24 | ## Troubleshooting 25 | 26 | If you already have it running it on a mac, make sure to restart it (see https://github.com/docker/for-mac/issues/17#mobyaccess). Time drift is known to cause the plugin to not work, because the timestamps returned by the docker API has drifted from the host. 27 | 28 | A good way to know if there's a time drift is to run `docker ps` — 29 | 30 | ``` 31 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 32 | e0754c9f4cdb alpine:latest "/bin/sh" 32 minutes ago Up 17 seconds test 33 | 809f310ba1e4 ubuntu:trusty "/bin/bash" 33 minutes ago Up About a minute reverent_raman 34 | ``` 35 | 36 | Notice how the `CREATED` and `STATUS` are several minutes apart for a recently created container. 37 | 38 | ### Enabling debug level logging 39 | 40 | #### If you are on GoCD version 19.6 and above: 41 | 42 | Edit the file `wrapper-properties.conf` on your GoCD server and add the following options. The location of the `wrapper-properties.conf` can be found in the [installation documentation](https://docs.gocd.org/current/installation/installing_go_server.html) of the GoCD server. 43 | 44 | ```properties 45 | # We recommend that you begin with the index `100` and increment the index for each system property 46 | wrapper.java.additional.100=-Dplugin.cd.go.contrib.elastic-agent.docker-swarm.log.level=debug 47 | ``` 48 | 49 | If you're running with GoCD server 19.6 and above on docker using one of the supported GoCD server images, set the environment variable `GOCD_SERVER_JVM_OPTIONS`: 50 | 51 | ```shell 52 | docker run -e "GOCD_SERVER_JVM_OPTIONS=-Dplugin.cd.go.contrib.elastic-agent.docker-swarm.log.level=debug" ... 53 | ``` 54 | 55 | #### If you are on GoCD version 19.5 and lower: 56 | 57 | Enabling debug level logging can help you troubleshoot an issue with the elastic agent plugin. To enable debug level logs, edit the `/etc/default/go-server` (for Linux) to add: 58 | 59 | ```bash 60 | export GO_SERVER_SYSTEM_PROPERTIES="$GO_SERVER_SYSTEM_PROPERTIES -Dplugin.cd.go.contrib.elastic-agent.docker-swarm.log.level=debug" 61 | ``` 62 | 63 | If you're running the server via `./server.sh` script — 64 | 65 | ``` 66 | $ GO_SERVER_SYSTEM_PROPERTIES="-Dplugin.cd.go.contrib.elastic-agent.docker-swarm.log.level=debug" ./server.sh 67 | ``` 68 | 69 | ## License 70 | 71 | ```plain 72 | Copyright 2019, Thoughtworks, Inc. 73 | 74 | Licensed under the Apache License, Version 2.0 (the "License"); 75 | you may not use this file except in compliance with the License. 76 | You may obtain a copy of the License at 77 | 78 | http://www.apache.org/licenses/LICENSE-2.0 79 | 80 | Unless required by applicable law or agreed to in writing, software 81 | distributed under the License is distributed on an "AS IS" BASIS, 82 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 83 | See the License for the specific language governing permissions and 84 | limitations under the License. 85 | ``` 86 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-via-installer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ... 2 | 3 | 4 | # GoCD agent needs the jdk and git/svn/mercurial... 5 | # Uncomment one of the lines below to ensure that openjdk is installed. 6 | # apt-get install openjdk-8-jre-headless git 7 | # yum install java-1.8.0-openjdk-headless git 8 | 9 | # add steps to download and install the gocd agent 10 | # See https://docs.gocd.io/current/installation/install/agent/linux.html 11 | 12 | # download tini to ensure that an init process exists 13 | ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /tini 14 | RUN chmod +x /tini 15 | ENTRYPOINT ["/tini", "--"] 16 | 17 | # ensure that the container logs on stdout 18 | ADD log4j.properties /var/lib/go-agent/log4j.properties 19 | ADD log4j.properties /var/lib/go-agent/go-agent-log4j.properties 20 | 21 | ADD go-agent /go-agent 22 | RUN chmod 755 /go-agent 23 | 24 | # Run the bootstrapper as the `go` user 25 | USER go 26 | CMD /go-agent 27 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-via-installer/go-agent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use this script to start up your GoCD agent process. The script assumes that 4 | # the agent is installed using .deb/.rpm as the case may be with your OS. 5 | 6 | die () { 7 | echo 8 | echo "$*" 9 | echo 10 | exit 1 11 | } 12 | 13 | if ! [[ -x /etc/init.d/go-agent && -e /etc/defaults/go-agent && -d /var/lib/go-agent ]]; then 14 | echo "It looks like the agent was not installed via deb/rpm" 15 | exit -1 16 | fi 17 | 18 | if [[ "$(whoami)" != 'go' ]]; then 19 | echo "Must run this script as the `go` user" 20 | exit -1 21 | fi 22 | 23 | mkdir -p /var/lib/go-agent/config || die "Could not create /var/lib/go-agent/config" 24 | 25 | # write out autoregister.properties 26 | ( 27 | cat < /var/lib/go-agent/config/autoregister.properties 34 | 35 | # write out server url, and prevent backgrounding 36 | echo "GO_SERVER_URL=${GO_EA_SERVER_URL}" > /etc/default/go-agent 37 | 38 | # prevent environment variables from leaking into the agent 39 | unset GO_EA_AUTO_REGISTER_KEY 40 | unset GO_EA_AUTO_REGISTER_ENVIRONMENT 41 | unset GO_EA_AUTO_REGISTER_ELASTIC_AGENT_ID 42 | unset GO_EA_AUTO_REGISTER_ELASTIC_PLUGIN_ID 43 | unset GO_EA_SERVER_URL 44 | 45 | exec /usr/share/go-agent/agent.sh go-agent 46 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-via-installer/log4j.properties: -------------------------------------------------------------------------------- 1 | # default to INFO logging on stdout 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # write logs to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.conversionPattern=%d{ISO8601} [%-9t] %-5p %-16c{4}:%L %x- %m%n 8 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-without-installed-agent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ... 2 | 3 | # GoCD agent needs the jdk and git/svn/mercurial... 4 | # Uncomment one of the lines below to ensure that openjdk is installed. 5 | # apt-get install openjdk-8-jre-headless git 6 | # yum install java-1.8.0-openjdk-headless git 7 | 8 | # download tini to ensure that an init process exists 9 | ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /tini 10 | RUN chmod +x /tini 11 | ENTRYPOINT ["/tini", "--"] 12 | 13 | # Add a user to run the go agent 14 | RUN adduser go go -h /go -S -D 15 | 16 | # ensure that the container logs on stdout 17 | ADD log4j.properties /go/log4j.properties 18 | ADD log4j.properties /go/go-agent-log4j.properties 19 | 20 | ADD go-agent /go-agent 21 | RUN chmod 755 /go-agent 22 | 23 | # Run the bootstrapper as the `go` user 24 | USER go 25 | CMD /go/go-agent 26 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-without-installed-agent/go-agent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Use this script to start up your GoCD agent process. The script assumes that — 4 | # - no agent is installed via deb/rpm 5 | # - curl is available on $PATH 6 | # - user `go` exists 7 | 8 | die () { 9 | echo 10 | echo "$*" 11 | echo 12 | exit 1 13 | } 14 | 15 | download_file () { 16 | local remote_path="$1" 17 | local local_path="$2" 18 | local url="${GO_EA_SERVER_URL}${remote_path}" 19 | 20 | echo "Downloading remote file ${url} into ${local_path}." 21 | curl --fail --silent --insecure "${url}" -o "${local_path}" || die "Could not fetch ${url}." 22 | } 23 | 24 | get_checksums () { 25 | local url="${GO_EA_SERVER_URL}/admin/latest-agent.status" 26 | curl --fail --silent --insecure "${url}" -D - || die "Could not fetch ${url}" 27 | } 28 | 29 | get_checksum () { 30 | local checksums="${1}" 31 | local header="${2}" 32 | echo "${checksums}" | tr -d '\r' | grep "${header}" | sed -e "s/${header}: //g" 33 | } 34 | 35 | if [[ "$(whoami)" != 'go' ]]; then 36 | echo "Must run this script as the `go` user" 37 | exit -1 38 | fi 39 | 40 | mkdir -p /go/config || die "Could not create /go/config" 41 | cd /go || die "Could not chdir to /go" 42 | 43 | # write out autoregister.properties 44 | ( 45 | cat < ./config/autoregister.properties 52 | 53 | while true; do 54 | download_file '/admin/agent' 'agent.jar' 55 | download_file '/admin/agent-plugins.zip' 'agent-plugins.zip' 56 | download_file '/admin/tfs-impl.jar' 'tfs-impl.jar' 57 | 58 | checksums=$(get_checksums) 59 | agent_md5=$(get_checksum "${checksums}" 'Agent-Content-MD5') 60 | tfs_md5=$(get_checksum "${checksums}" 'TFS-SDK-Content-MD5') 61 | plugins_md5=$(get_checksum "${checksums}" 'Agent-Plugins-Content-MD5') 62 | agent_launcher_md5=$(get_checksum "${checksums}" 'Agent-Launcher-Content-MD5') 63 | 64 | echo "Launching the GoCD Agent, and waiting for it to exit..." 65 | RUN_CMD=(java -Dcruise.console.publish.interval=10 \ 66 | -Xms128m \ 67 | -Xmx256m \ 68 | -Djava.security.egd=file:/dev/./urandom \ 69 | -Dagent.plugins.md5="${plugins_md5}" \ 70 | -Dagent.binary.md5="${agent_md5}" \ 71 | -Dagent.launcher.md5="${agent_launcher_md5}" \ 72 | -Dagent.tfs.md5="${tfs_md5}" \ 73 | -jar \ 74 | agent.jar \ 75 | -serverUrl \ 76 | "${GO_EA_SERVER_URL}") 77 | "${RUN_CMD[@]}" 78 | echo "The GoCD Agent exited with code $?. Waiting 10 seconds before re-launching" 79 | sleep 10 80 | done 81 | -------------------------------------------------------------------------------- /contrib/scripts/bootstrap-without-installed-agent/log4j.properties: -------------------------------------------------------------------------------- 1 | # default to INFO logging on stdout 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # write logs to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.stdout.layout.conversionPattern=%d{ISO8601} [%-9t] %-5p %-16c{4}:%L %x- %m%n 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=845952a9d6afa783db70bb3b0effaae45ae5542ca2bb7929619e8af49cb634cf 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /images/configure-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/configure-job.png -------------------------------------------------------------------------------- /images/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/pipeline.png -------------------------------------------------------------------------------- /images/plugin-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/plugin-settings.png -------------------------------------------------------------------------------- /images/plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/plugins.png -------------------------------------------------------------------------------- /images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/profile.png -------------------------------------------------------------------------------- /images/profiles_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/profiles_page.png -------------------------------------------------------------------------------- /images/quick-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/images/quick-edit.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | rootProject.name = 'docker-swarm-elastic-agents' 18 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/Agents.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import com.google.common.base.Predicate; 20 | import com.google.common.collect.FluentIterable; 21 | 22 | import java.util.*; 23 | 24 | /** 25 | * Represents a map of {@link Agent#elasticAgentId()} to the {@link Agent} for easy lookups 26 | */ 27 | public class Agents { 28 | 29 | private final Map agents = new HashMap<>(); 30 | 31 | // Filter for agents that can be disabled safely 32 | private static final Predicate AGENT_IDLE_PREDICATE = new Predicate() { 33 | @Override 34 | public boolean apply(Agent metadata) { 35 | Agent.AgentState agentState = metadata.agentState(); 36 | return metadata.configState().equals(Agent.ConfigState.Enabled) && (agentState.equals(Agent.AgentState.Idle) || agentState.equals(Agent.AgentState.Missing) || agentState.equals(Agent.AgentState.LostContact)); 37 | } 38 | }; 39 | 40 | // Filter for agents that can be terminated safely 41 | private static final Predicate AGENT_DISABLED_PREDICATE = new Predicate() { 42 | @Override 43 | public boolean apply(Agent metadata) { 44 | Agent.AgentState agentState = metadata.agentState(); 45 | return metadata.configState().equals(Agent.ConfigState.Disabled) && (agentState.equals(Agent.AgentState.Idle) || agentState.equals(Agent.AgentState.Missing) || agentState.equals(Agent.AgentState.LostContact)); 46 | } 47 | }; 48 | 49 | public Agents() { 50 | } 51 | 52 | public Agents(Collection toCopy) { 53 | addAll(toCopy); 54 | } 55 | 56 | public void addAll(Collection toAdd) { 57 | for (Agent agent : toAdd) { 58 | add(agent); 59 | } 60 | } 61 | 62 | public void addAll(Agents agents) { 63 | addAll(agents.agents()); 64 | } 65 | 66 | public Collection findInstancesToDisable() { 67 | return FluentIterable.from(agents.values()).filter(AGENT_IDLE_PREDICATE).toList(); 68 | } 69 | 70 | public Collection findInstancesToTerminate() { 71 | return FluentIterable.from(agents.values()).filter(AGENT_DISABLED_PREDICATE).toList(); 72 | } 73 | 74 | public Set agentIds() { 75 | return new LinkedHashSet<>(agents.keySet()); 76 | } 77 | 78 | public boolean containsServiceWithId(String agentId) { 79 | return agents.containsKey(agentId); 80 | } 81 | 82 | public Collection agents() { 83 | return new ArrayList<>(agents.values()); 84 | } 85 | 86 | public void add(Agent agent) { 87 | agents.put(agent.elasticAgentId(), agent); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/Clock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import org.joda.time.DateTime; 20 | import org.joda.time.Period; 21 | 22 | public interface Clock { 23 | DateTime now(); 24 | 25 | Clock DEFAULT = new Clock() { 26 | @Override 27 | public DateTime now() { 28 | return new DateTime(); 29 | } 30 | }; 31 | 32 | class TestClock implements Clock { 33 | 34 | DateTime time = null; 35 | 36 | public TestClock(DateTime time) { 37 | this.time = time; 38 | } 39 | 40 | public TestClock() { 41 | this(new DateTime()); 42 | } 43 | 44 | @Override 45 | public DateTime now() { 46 | return time; 47 | } 48 | 49 | public TestClock reset() { 50 | time = new DateTime(); 51 | return this; 52 | } 53 | 54 | public TestClock set(DateTime time) { 55 | this.time = time; 56 | return this; 57 | } 58 | 59 | public TestClock rewind(Period period) { 60 | this.time = this.time.minus(period); 61 | return this; 62 | } 63 | 64 | public TestClock forward(Period period) { 65 | this.time = this.time.plus(period); 66 | return this; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/ClusterProfile.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import com.google.gson.FieldNamingPolicy; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.annotations.Expose; 7 | import com.google.gson.annotations.SerializedName; 8 | 9 | import java.util.HashMap; 10 | import java.util.Objects; 11 | 12 | public class ClusterProfile { 13 | public static final Gson GSON = new GsonBuilder() 14 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 15 | .excludeFieldsWithoutExposeAnnotation() 16 | .create(); 17 | 18 | @Expose 19 | @SerializedName("id") 20 | private String id; 21 | 22 | @Expose 23 | @SerializedName("plugin_id") 24 | private String pluginId; 25 | 26 | @Expose 27 | @SerializedName("properties") 28 | private ClusterProfileProperties clusterProfileProperties; 29 | 30 | 31 | public ClusterProfile() { 32 | } 33 | 34 | public ClusterProfile(String id, String pluginId, PluginSettings pluginSettings) { 35 | this.id = id; 36 | this.pluginId = pluginId; 37 | setClusterProfileProperties(pluginSettings); 38 | } 39 | 40 | public static ClusterProfile fromJSON(String json) { 41 | return GSON.fromJson(json, ClusterProfile.class); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | ClusterProfile that = (ClusterProfile) o; 49 | return Objects.equals(id, that.id) && 50 | Objects.equals(pluginId, that.pluginId) && 51 | Objects.equals(clusterProfileProperties, that.clusterProfileProperties); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(id, pluginId, clusterProfileProperties); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "ClusterProfile{" + 62 | "id='" + id + '\'' + 63 | ", pluginId='" + pluginId + '\'' + 64 | ", clusterProfileProperties=" + clusterProfileProperties + 65 | '}'; 66 | } 67 | 68 | public String getId() { 69 | return id; 70 | } 71 | 72 | public String getPluginId() { 73 | return pluginId; 74 | } 75 | 76 | public ClusterProfileProperties getClusterProfileProperties() { 77 | return clusterProfileProperties; 78 | } 79 | 80 | public void setId(String id) { 81 | this.id = id; 82 | } 83 | 84 | public void setPluginId(String pluginId) { 85 | this.pluginId = pluginId; 86 | } 87 | public void setClusterProfileProperties(ClusterProfileProperties clusterProfileProperties) { 88 | this.clusterProfileProperties = clusterProfileProperties; 89 | } 90 | 91 | public void setClusterProfileProperties(PluginSettings pluginSettings) { 92 | this.clusterProfileProperties = ClusterProfileProperties.fromConfiguration(GSON.fromJson(GSON.toJson(pluginSettings), HashMap.class)); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/ClusterProfileProperties.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import java.util.Map; 4 | import java.util.Objects; 5 | 6 | public class ClusterProfileProperties extends PluginSettings { 7 | public static ClusterProfileProperties fromJSON(String json) { 8 | return GSON.fromJson(json, ClusterProfileProperties.class); 9 | } 10 | 11 | public static ClusterProfileProperties fromConfiguration(Map clusterProfileProperties) { 12 | return GSON.fromJson(GSON.toJson(clusterProfileProperties), ClusterProfileProperties.class); 13 | } 14 | 15 | public String uuid() { 16 | return Integer.toHexString(Objects.hash(this)); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return super.toString(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 20 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier; 21 | 22 | import java.util.Collections; 23 | 24 | public interface Constants { 25 | String PLUGIN_ID = Util.pluginId(); 26 | 27 | // The type of this extension 28 | String EXTENSION_TYPE = "elastic-agent"; 29 | 30 | // The extension point API version that this plugin understands 31 | String PROCESSOR_API_VERSION = "1.0"; 32 | String EXTENSION_API_VERSION = "5.0"; 33 | 34 | // the identifier of this plugin 35 | GoPluginIdentifier PLUGIN_IDENTIFIER = new GoPluginIdentifier(EXTENSION_TYPE, Collections.singletonList(EXTENSION_API_VERSION)); 36 | 37 | // requests that the plugin makes to the server 38 | String REQUEST_SERVER_PREFIX = "go.processor"; 39 | String REQUEST_SERVER_DISABLE_AGENT = REQUEST_SERVER_PREFIX + ".elastic-agents.disable-agents"; 40 | String REQUEST_SERVER_DELETE_AGENT = REQUEST_SERVER_PREFIX + ".elastic-agents.delete-agents"; 41 | String REQUEST_SERVER_LIST_AGENTS = REQUEST_SERVER_PREFIX + ".elastic-agents.list-agents"; 42 | String REQUEST_SERVER_SERVER_HEALTH_ADD_MESSAGES = REQUEST_SERVER_PREFIX + ".server-health.add-messages"; 43 | 44 | // internal use only 45 | String CREATED_BY_LABEL_KEY = "Elastic-Agent-Created-By"; 46 | String JOB_IDENTIFIER_LABEL_KEY = "Elastic-Agent-Job-Identifier"; 47 | String ENVIRONMENT_LABEL_KEY = "Elastic-Agent-Environment-Name"; 48 | String CONFIGURATION_LABEL_KEY = "Elastic-Agent-Configuration"; 49 | String SWARM_SERVICE_NAME = "com.docker.swarm.service.name"; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/ElasticAgentProfile.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import com.google.gson.FieldNamingPolicy; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.annotations.Expose; 7 | import com.google.gson.annotations.SerializedName; 8 | 9 | import java.util.HashMap; 10 | import java.util.Objects; 11 | 12 | public class ElasticAgentProfile { 13 | public static final Gson GSON = new GsonBuilder() 14 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 15 | .excludeFieldsWithoutExposeAnnotation() 16 | .create(); 17 | 18 | @Expose 19 | @SerializedName("id") 20 | private String id; 21 | 22 | @Expose 23 | @SerializedName("plugin_id") 24 | private String pluginId; 25 | 26 | @Expose 27 | @SerializedName("cluster_profile_id") 28 | private String clusterProfileId; 29 | 30 | @Expose 31 | @SerializedName("properties") 32 | private HashMap properties; 33 | 34 | public static ElasticAgentProfile fromJSON(String json) { 35 | return GSON.fromJson(json, ElasticAgentProfile.class); 36 | } 37 | 38 | public String getId() { 39 | return id; 40 | } 41 | 42 | public String getPluginId() { 43 | return pluginId; 44 | } 45 | 46 | public String getClusterProfileId() { 47 | return clusterProfileId; 48 | } 49 | 50 | public HashMap getProperties() { 51 | return properties; 52 | } 53 | 54 | public void setId(String id) { 55 | this.id = id; 56 | } 57 | 58 | public void setPluginId(String pluginId) { 59 | this.pluginId = pluginId; 60 | } 61 | 62 | public void setClusterProfileId(String clusterProfileId) { 63 | this.clusterProfileId = clusterProfileId; 64 | } 65 | 66 | public void setProperties(HashMap properties) { 67 | this.properties = properties; 68 | } 69 | 70 | @Override 71 | public boolean equals(Object o) { 72 | if (this == o) return true; 73 | if (o == null || getClass() != o.getClass()) return false; 74 | ElasticAgentProfile that = (ElasticAgentProfile) o; 75 | return Objects.equals(id, that.id) && 76 | Objects.equals(pluginId, that.pluginId) && 77 | Objects.equals(clusterProfileId, that.clusterProfileId) && 78 | Objects.equals(properties, that.properties); 79 | } 80 | 81 | @Override 82 | public int hashCode() { 83 | return Objects.hash(id, pluginId, clusterProfileId, properties); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return "ElasticAgentProfile{" + 89 | "id='" + id + '\'' + 90 | ", pluginId='" + pluginId + '\'' + 91 | ", clusterProfileId='" + clusterProfileId + '\'' + 92 | ", properties=" + properties + 93 | '}'; 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/Hosts.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import com.google.common.net.InetAddresses; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.splitIntoLinesAndTrimSpaces; 11 | import static java.text.MessageFormat.format; 12 | import static org.apache.commons.lang.StringUtils.isBlank; 13 | 14 | public class Hosts { 15 | 16 | public List hosts(String hostConfig) { 17 | if (isBlank(hostConfig)) { 18 | return Collections.emptyList(); 19 | } 20 | 21 | List hostMappings = new ArrayList<>(); 22 | Collection hostEntries = splitIntoLinesAndTrimSpaces(hostConfig); 23 | 24 | hostEntries.forEach(hostEntry -> { 25 | hostMappings.addAll(toHosts(hostEntry)); 26 | }); 27 | 28 | return hostMappings; 29 | } 30 | 31 | private List toHosts(String hostEntry) { 32 | String[] parts = hostEntry.split("\\s+"); 33 | 34 | final String ipAddress = parts[0]; 35 | 36 | List hosts = new ArrayList<>(); 37 | for (int i = 1; i < parts.length; i++) { 38 | hosts.add(format("{0} {1}", ipAddress, parts[i])); 39 | } 40 | 41 | return hosts; 42 | } 43 | 44 | public List validate(String hostConfig) { 45 | if (isBlank(hostConfig)) { 46 | return Collections.emptyList(); 47 | } 48 | 49 | List errors = new ArrayList<>(); 50 | Collection hostEntries = splitIntoLinesAndTrimSpaces(hostConfig); 51 | 52 | hostEntries.forEach(hostEntry -> { 53 | String[] parts = hostEntry.split("\\s+"); 54 | if (parts.length < 2) { 55 | errors.add(format("Host entry `{0}` is invalid. Must be in `IP-ADDRESS HOST-1 HOST-2...` format.", hostEntry)); 56 | } else { 57 | validateIpAddress(errors, parts[0]); 58 | } 59 | }); 60 | 61 | return errors; 62 | } 63 | 64 | private void validateIpAddress(List errors, String part) { 65 | try { 66 | InetAddresses.forString(part); 67 | } catch (Exception e) { 68 | errors.add(e.getMessage()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/Networks.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 4 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.splitIntoLinesAndTrimSpaces; 5 | import static java.text.MessageFormat.format; 6 | import static org.apache.commons.lang.StringUtils.isBlank; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.stream.Collectors; 14 | 15 | import com.spotify.docker.client.messages.Network; 16 | import com.spotify.docker.client.messages.swarm.NetworkAttachmentConfig; 17 | 18 | public class Networks { 19 | public static List fromString(String networkConfig, List dockerNetworks) { 20 | if (isBlank(networkConfig)) { 21 | return Collections.emptyList(); 22 | } 23 | 24 | final Map availableNetworks = dockerNetworks.stream().collect(Collectors.toMap(o -> o.name(), o -> o)); 25 | 26 | final List serviceNetworks = new ArrayList<>(); 27 | final Collection networkEntries = splitIntoLinesAndTrimSpaces(networkConfig); 28 | networkEntries.forEach(networkEntry -> { 29 | final Network availableNetwork = availableNetworks.get(networkEntry); 30 | 31 | if (availableNetwork == null) { 32 | throw new RuntimeException(format("Network with name `{0}` does not exist.", networkEntry)); 33 | } 34 | 35 | LOG.debug(format("Using network `{0}` with id `{1}`.", networkEntry, availableNetwork.id())); 36 | serviceNetworks.add(NetworkAttachmentConfig.builder().target(networkEntry).build()); 37 | }); 38 | 39 | return serviceNetworks; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/PluginSettingsNotConfiguredException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | public class PluginSettingsNotConfiguredException extends RuntimeException { 20 | public PluginSettingsNotConfiguredException() { 21 | super("Plugin settings is not configured."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/RequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 20 | 21 | public interface RequestExecutor { 22 | 23 | GoPluginApiResponse execute() throws Exception; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/ServerRequestFailedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import com.thoughtworks.go.plugin.api.response.GoApiResponse; 20 | 21 | import static java.lang.String.format; 22 | 23 | public class ServerRequestFailedException extends Exception { 24 | 25 | private ServerRequestFailedException(GoApiResponse response, String request) { 26 | super(format( 27 | "The server sent an unexpected status code %d with the response body %s when it was sent a %s message", 28 | response.responseCode(), response.responseBody(), request 29 | )); 30 | } 31 | 32 | public static ServerRequestFailedException disableAgents(GoApiResponse response) { 33 | return new ServerRequestFailedException(response, "disable agents"); 34 | } 35 | 36 | public static ServerRequestFailedException deleteAgents(GoApiResponse response) { 37 | return new ServerRequestFailedException(response, "delete agents"); 38 | } 39 | 40 | public static ServerRequestFailedException listAgents(GoApiResponse response) { 41 | return new ServerRequestFailedException(response, "list agents"); 42 | } 43 | 44 | public static ServerRequestFailedException getPluginSettings(GoApiResponse response) { 45 | return new ServerRequestFailedException(response, "get plugin settings"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/SetupSemaphore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import java.util.Map; 20 | import java.util.concurrent.Semaphore; 21 | 22 | class SetupSemaphore implements Runnable { 23 | private final Integer maxAllowedContainers; 24 | private final Map instances; 25 | private final Semaphore semaphore; 26 | 27 | public SetupSemaphore(Integer maxAllowedContainers, Map instances, Semaphore semaphore) { 28 | this.maxAllowedContainers = maxAllowedContainers; 29 | this.instances = instances; 30 | this.semaphore = semaphore; 31 | } 32 | 33 | @Override 34 | public void run() { 35 | int currentContainers = instances.size(); 36 | int availablePermits = maxAllowedContainers - currentContainers; 37 | if (availablePermits <= 0) { 38 | // no more capacity available. 39 | semaphore.drainPermits(); 40 | } else { 41 | int semaphoreValueDifference = availablePermits - semaphore.availablePermits(); 42 | if (semaphoreValueDifference > 0) { 43 | semaphore.release(semaphoreValueDifference); 44 | } else if (semaphoreValueDifference < 0) { 45 | semaphore.tryAcquire(Math.abs(semaphoreValueDifference)); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/builders/PluginStatusReportViewBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.builders; 18 | 19 | import freemarker.cache.ClassTemplateLoader; 20 | import freemarker.template.Configuration; 21 | import freemarker.template.Template; 22 | import freemarker.template.TemplateException; 23 | import freemarker.template.TemplateExceptionHandler; 24 | 25 | import java.io.IOException; 26 | import java.io.StringWriter; 27 | import java.io.Writer; 28 | 29 | public class PluginStatusReportViewBuilder { 30 | private static PluginStatusReportViewBuilder builder; 31 | private final Configuration configuration; 32 | 33 | private PluginStatusReportViewBuilder() { 34 | configuration = new Configuration(Configuration.VERSION_2_3_23); 35 | configuration.setTemplateLoader(new ClassTemplateLoader(getClass(), "/")); 36 | configuration.setDefaultEncoding("UTF-8"); 37 | configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); 38 | configuration.setLogTemplateExceptions(false); 39 | configuration.setDateTimeFormat("iso"); 40 | } 41 | 42 | public Template getTemplate(String template) throws IOException { 43 | return configuration.getTemplate(template); 44 | } 45 | 46 | public String build(Template template, Object cluster) throws IOException, TemplateException { 47 | Writer writer = new StringWriter(); 48 | template.process(cluster, writer); 49 | return writer.toString(); 50 | } 51 | 52 | public static PluginStatusReportViewBuilder instance() { 53 | if (builder == null) { 54 | builder = new PluginStatusReportViewBuilder(); 55 | } 56 | return builder; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ClusterProfileValidateRequestExecutor.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationError; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 6 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ClusterProfileValidateRequest; 7 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator.PrivateDockerRegistrySettingsValidator; 8 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 9 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 10 | 11 | import java.util.*; 12 | 13 | public class ClusterProfileValidateRequestExecutor implements RequestExecutor { 14 | private ClusterProfileValidateRequest request; 15 | 16 | public ClusterProfileValidateRequestExecutor(ClusterProfileValidateRequest request) { 17 | this.request = request; 18 | } 19 | 20 | public GoPluginApiResponse execute() { 21 | final List knownFields = new ArrayList<>(); 22 | final ValidationResult validationResult = new ValidationResult(); 23 | 24 | for (Metadata field : GetClusterProfileMetadataExecutor.FIELDS) { 25 | knownFields.add(field.getKey()); 26 | validationResult.addError(field.validate(request.getProperties().get(field.getKey()))); 27 | } 28 | final Set set = new HashSet<>(request.getProperties().keySet()); 29 | set.removeAll(knownFields); 30 | 31 | if (!set.isEmpty()) { 32 | for (String key : set) { 33 | validationResult.addError(key, "Is an unknown property."); 34 | } 35 | } 36 | List> validateErrors = new PrivateDockerRegistrySettingsValidator().validate(request); 37 | validateErrors.forEach(error -> validationResult.addError(new ValidationError(error.get("key"), error.get("message")))); 38 | return DefaultGoPluginApiResponse.success(validationResult.toJSON()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ClusterStatusReportExecutor.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerClientFactory; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerServices; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.builders.PluginStatusReportViewBuilder; 6 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.SwarmCluster; 7 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.reports.StatusReportGenerationErrorHandler; 8 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ClusterStatusReportRequest; 9 | import com.google.gson.JsonObject; 10 | import com.spotify.docker.client.DockerClient; 11 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 12 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 13 | import freemarker.template.Template; 14 | 15 | import java.io.IOException; 16 | 17 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 18 | 19 | public class ClusterStatusReportExecutor { 20 | 21 | private final ClusterStatusReportRequest clusterStatusReportRequest; 22 | private final DockerServices agentInstances; 23 | private final DockerClientFactory dockerClientFactory; 24 | private PluginStatusReportViewBuilder viewBuilder; 25 | 26 | public ClusterStatusReportExecutor(ClusterStatusReportRequest clusterStatusReportRequest, DockerServices agentInstances) throws IOException { 27 | this(clusterStatusReportRequest, agentInstances, DockerClientFactory.instance(), PluginStatusReportViewBuilder.instance()); 28 | } 29 | 30 | ClusterStatusReportExecutor(ClusterStatusReportRequest clusterStatusReportRequest, DockerServices agentInstances, DockerClientFactory dockerClientFactory, PluginStatusReportViewBuilder viewBuilder) { 31 | this.clusterStatusReportRequest = clusterStatusReportRequest; 32 | this.agentInstances = agentInstances; 33 | this.dockerClientFactory = dockerClientFactory; 34 | this.viewBuilder = viewBuilder; 35 | } 36 | 37 | public GoPluginApiResponse execute() { 38 | try { 39 | LOG.debug("[status-report] Generating cluster status report."); 40 | final DockerClient dockerClient = dockerClientFactory.docker(clusterStatusReportRequest.getClusterProfile()); 41 | final SwarmCluster swarmCluster = new SwarmCluster(dockerClient); 42 | final Template template = viewBuilder.getTemplate("status-report.template.ftlh"); 43 | final String statusReportView = viewBuilder.build(template, swarmCluster); 44 | 45 | JsonObject responseJSON = new JsonObject(); 46 | responseJSON.addProperty("view", statusReportView); 47 | 48 | return DefaultGoPluginApiResponse.success(responseJSON.toString()); 49 | } catch (Exception e) { 50 | return StatusReportGenerationErrorHandler.handle(viewBuilder, e); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/CreateAgentRequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.AgentInstances; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerService; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.PluginRequest; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 23 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 24 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.CreateAgentRequest; 25 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator.DockerMountsValidator; 26 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator.DockerSecretValidator; 27 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator.Validatable; 28 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 29 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 30 | 31 | import java.util.ArrayList; 32 | import java.util.HashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 37 | import static java.text.MessageFormat.format; 38 | 39 | public class CreateAgentRequestExecutor implements RequestExecutor { 40 | private final AgentInstances agentInstances; 41 | private final PluginRequest pluginRequest; 42 | private final CreateAgentRequest request; 43 | private List validators = new ArrayList<>(); 44 | 45 | public CreateAgentRequestExecutor(CreateAgentRequest request, AgentInstances agentInstances, PluginRequest pluginRequest) { 46 | this.request = request; 47 | this.agentInstances = agentInstances; 48 | this.pluginRequest = pluginRequest; 49 | validators.add(new DockerSecretValidator(request)); 50 | validators.add(new DockerMountsValidator(request)); 51 | } 52 | 53 | @Override 54 | public GoPluginApiResponse execute() throws Exception { 55 | LOG.debug(format("[create-agent] Validating with profile: {0}", request.properties())); 56 | boolean hasError = false; 57 | List> messages = new ArrayList<>(); 58 | for (Validatable validatable : validators) { 59 | ValidationResult validationResult = validatable.validate(request.properties()); 60 | if (validationResult.hasErrors()) { 61 | hasError = true; 62 | Map messageToBeAdded = new HashMap<>(); 63 | messageToBeAdded.put("type", "warning"); 64 | messageToBeAdded.put("message", validationResult.toJSON()); 65 | messages.add(messageToBeAdded); 66 | } 67 | } 68 | if (hasError) { 69 | LOG.debug(format("[create-agent] Error in validtion: {0}", messages)); 70 | pluginRequest.addServerHealthMessage(messages); 71 | return DefaultGoPluginApiResponse.incompleteRequest(messages.toString()); 72 | } 73 | LOG.debug(format("[create-agent] Creating agent with profile: {0}", request.properties())); 74 | 75 | agentInstances.create(request, pluginRequest); 76 | 77 | LOG.debug(format("[create-agent] Done creating agent for profile: {0}", request.properties())); 78 | return new DefaultGoPluginApiResponse(200); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/Field.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import com.google.gson.annotations.Expose; 20 | import com.google.gson.annotations.SerializedName; 21 | import org.apache.commons.lang.StringUtils; 22 | 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | public class Field { 27 | protected final String key; 28 | 29 | @Expose 30 | @SerializedName("display-name") 31 | protected String displayName; 32 | 33 | @Expose 34 | @SerializedName("default-value") 35 | protected String defaultValue; 36 | 37 | @Expose 38 | @SerializedName("required") 39 | protected Boolean required; 40 | 41 | @Expose 42 | @SerializedName("secure") 43 | protected Boolean secure; 44 | 45 | @Expose 46 | @SerializedName("display-order") 47 | protected String displayOrder; 48 | 49 | public Field(String key, String displayName, String defaultValue, Boolean required, Boolean secure, String displayOrder) { 50 | this.key = key; 51 | this.displayName = displayName; 52 | this.defaultValue = defaultValue; 53 | this.required = required; 54 | this.secure = secure; 55 | this.displayOrder = displayOrder; 56 | } 57 | 58 | public Map validate(String input) { 59 | HashMap result = new HashMap<>(); 60 | String validationError = doValidate(input); 61 | if (StringUtils.isNotBlank(validationError)) { 62 | result.put("key", key); 63 | result.put("message", validationError); 64 | } 65 | return result; 66 | } 67 | 68 | protected String doValidate(String input) { 69 | return null; 70 | } 71 | 72 | public String key() { 73 | return key; 74 | } 75 | 76 | public String displayName() { 77 | return displayName; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetCapabilitiesExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import com.google.gson.Gson; 20 | import com.google.gson.GsonBuilder; 21 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 22 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 23 | 24 | import java.util.LinkedHashMap; 25 | import java.util.Map; 26 | 27 | public class GetCapabilitiesExecutor { 28 | 29 | private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); 30 | 31 | private static final Map CAPABILITIES_RESPONSE = new LinkedHashMap<>(); 32 | 33 | static { 34 | CAPABILITIES_RESPONSE.put("supports_plugin_status_report", false); 35 | CAPABILITIES_RESPONSE.put("supports_agent_status_report", true); 36 | CAPABILITIES_RESPONSE.put("supports_cluster_status_report", true); 37 | } 38 | public GoPluginApiResponse execute() { 39 | return DefaultGoPluginApiResponse.success(GSON.toJson(CAPABILITIES_RESPONSE)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetClusterProfileMetadataExecutor.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 7 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class GetClusterProfileMetadataExecutor implements RequestExecutor { 13 | private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); 14 | 15 | public static final Metadata GO_SERVER_URL = new GoServerURLMetadata(); 16 | public static final Metadata ENVIRONMENT_VARIABLES = new Metadata("environment_variables", false, false); 17 | public static final Metadata MAX_DOCKER_CONTAINERS = new Metadata("max_docker_containers", true, false); 18 | public static final Metadata DOCKER_URI = new Metadata("docker_uri", true, false); 19 | public static final Metadata AUTO_REGISTER_TIMEOUT = new Metadata("auto_register_timeout", true, false); 20 | public static final Metadata DOCKER_CA_CERT = new Metadata("docker_ca_cert", false, true); 21 | public static final Metadata DOCKER_CLIENT_KEY = new Metadata("docker_client_key", false, true); 22 | public static final Metadata DOCKER_CLIENT_CERT = new Metadata("docker_client_cert", false, true); 23 | public static final Metadata ENABLE_PRIVATE_REGISTRY_AUTHENTICATION = new Metadata("enable_private_registry_authentication", false, false); 24 | public static final Metadata PRIVATE_REGISTRY_SERVER = new Metadata("private_registry_server", false, false); 25 | public static final Metadata PRIVATE_REGISTRY_USERNAME = new Metadata("private_registry_username", false, false); 26 | public static final Metadata PRIVATE_REGISTRY_PASSWORD = new Metadata("private_registry_password", false, true); 27 | 28 | public static final List FIELDS = new ArrayList<>(); 29 | 30 | static { 31 | FIELDS.add(GO_SERVER_URL); 32 | FIELDS.add(ENVIRONMENT_VARIABLES); 33 | FIELDS.add(MAX_DOCKER_CONTAINERS); 34 | FIELDS.add(DOCKER_URI); 35 | FIELDS.add(AUTO_REGISTER_TIMEOUT); 36 | 37 | // certs 38 | FIELDS.add(DOCKER_CA_CERT); 39 | FIELDS.add(DOCKER_CLIENT_KEY); 40 | FIELDS.add(DOCKER_CLIENT_CERT); 41 | 42 | FIELDS.add(ENABLE_PRIVATE_REGISTRY_AUTHENTICATION); 43 | FIELDS.add(PRIVATE_REGISTRY_SERVER); 44 | FIELDS.add(PRIVATE_REGISTRY_USERNAME); 45 | FIELDS.add(PRIVATE_REGISTRY_PASSWORD); 46 | } 47 | 48 | @Override 49 | public GoPluginApiResponse execute() { 50 | return new DefaultGoPluginApiResponse(200, GSON.toJson(FIELDS)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetClusterProfileViewRequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 21 | import com.google.gson.Gson; 22 | import com.google.gson.JsonObject; 23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 25 | 26 | public class GetClusterProfileViewRequestExecutor implements RequestExecutor { 27 | private static final Gson GSON = new Gson(); 28 | 29 | @Override 30 | public GoPluginApiResponse execute() { 31 | JsonObject jsonObject = new JsonObject(); 32 | jsonObject.addProperty("template", Util.readResource("/plugin-settings.template.html")); 33 | DefaultGoPluginApiResponse defaultGoPluginApiResponse = new DefaultGoPluginApiResponse(200, GSON.toJson(jsonObject)); 34 | return defaultGoPluginApiResponse; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetPluginSettingsIconExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 21 | import com.google.gson.Gson; 22 | import com.google.gson.JsonObject; 23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 25 | import org.apache.commons.codec.binary.Base64; 26 | 27 | public class GetPluginSettingsIconExecutor implements RequestExecutor { 28 | private static final Gson GSON = new Gson(); 29 | 30 | @Override 31 | public GoPluginApiResponse execute() { 32 | JsonObject jsonObject = new JsonObject(); 33 | jsonObject.addProperty("content_type", "image/png"); 34 | jsonObject.addProperty("data", Base64.encodeBase64String(Util.readResourceBytes("/docker-swarm.png"))); 35 | DefaultGoPluginApiResponse defaultGoPluginApiResponse = new DefaultGoPluginApiResponse(200, GSON.toJson(jsonObject)); 36 | return defaultGoPluginApiResponse; 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetProfileMetadataExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.metadata.HostMetadata; 21 | import com.google.gson.Gson; 22 | import com.google.gson.GsonBuilder; 23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | public class GetProfileMetadataExecutor implements RequestExecutor { 30 | private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); 31 | 32 | public static final Metadata IMAGE = new Metadata("Image", true, false); 33 | public static final Metadata COMMAND = new Metadata("Command", false, false); 34 | public static final Metadata ENVIRONMENT = new Metadata("Environment", false, false); 35 | public static final Metadata MAX_MEMORY = new MemoryMetadata("MaxMemory", false); 36 | public static final Metadata RESERVED_MEMORY = new MemoryMetadata("ReservedMemory", false); 37 | public static final Metadata SECRETS = new Metadata("Secrets", false, false); 38 | public static final Metadata NETWORKS = new Metadata("Networks", false, false); 39 | public static final Metadata MOUNTS = new Metadata("Mounts", false, false); 40 | public static final Metadata HOSTS = new HostMetadata("Hosts", false, false); 41 | public static final Metadata CONSTRAINTS = new Metadata("Constraints", false, false); 42 | public static final Metadata LOG_DRIVER = new Metadata("LogDriver", false, false); 43 | public static final Metadata LOG_DRIVER_OPTIONS = new Metadata("LogDriverOptions", false, false); 44 | 45 | public static final List FIELDS = new ArrayList<>(); 46 | 47 | static { 48 | FIELDS.add(IMAGE); 49 | FIELDS.add(COMMAND); 50 | FIELDS.add(ENVIRONMENT); 51 | FIELDS.add(MAX_MEMORY); 52 | FIELDS.add(RESERVED_MEMORY); 53 | FIELDS.add(SECRETS); 54 | FIELDS.add(NETWORKS); 55 | FIELDS.add(MOUNTS); 56 | FIELDS.add(HOSTS); 57 | FIELDS.add(CONSTRAINTS); 58 | FIELDS.add(LOG_DRIVER); 59 | FIELDS.add(LOG_DRIVER_OPTIONS); 60 | } 61 | 62 | @Override 63 | 64 | public GoPluginApiResponse execute() { 65 | return new DefaultGoPluginApiResponse(200, GSON.toJson(FIELDS)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetProfileViewExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 21 | import com.google.gson.Gson; 22 | import com.google.gson.JsonObject; 23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 25 | 26 | public class GetProfileViewExecutor implements RequestExecutor { 27 | private static final Gson GSON = new Gson(); 28 | 29 | @Override 30 | public GoPluginApiResponse execute() { 31 | JsonObject jsonObject = new JsonObject(); 32 | jsonObject.addProperty("template", Util.readResource("/profile.template.html")); 33 | DefaultGoPluginApiResponse defaultGoPluginApiResponse = new DefaultGoPluginApiResponse(200, GSON.toJson(jsonObject)); 34 | return defaultGoPluginApiResponse; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GoServerURLMetadata.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import org.apache.http.client.utils.URIBuilder; 4 | 5 | import java.net.URISyntaxException; 6 | import java.util.Arrays; 7 | 8 | import static org.apache.commons.lang.StringUtils.isBlank; 9 | 10 | public class GoServerURLMetadata extends Metadata { 11 | private static String GO_SERVER_URL = "go_server_url"; 12 | private static String GO_SERVER_URL_DISPLAY_VALUE = "Go Server URL"; 13 | 14 | public GoServerURLMetadata() { 15 | super(GO_SERVER_URL, true, false); 16 | } 17 | 18 | @Override 19 | public String doValidate(String input) { 20 | if (isBlank(input)) { 21 | return GO_SERVER_URL_DISPLAY_VALUE + " must not be blank."; 22 | } 23 | 24 | URIBuilder uriBuilder = null; 25 | try { 26 | uriBuilder = new URIBuilder(input); 27 | } catch (URISyntaxException e) { 28 | return GO_SERVER_URL_DISPLAY_VALUE + " must be a valid URL (http://example.com:8153/go)"; 29 | } 30 | 31 | if (isBlank(uriBuilder.getScheme())) { 32 | return GO_SERVER_URL_DISPLAY_VALUE + " must be a valid URL (http://example.com:8153/go)"; 33 | } 34 | 35 | if (!Arrays.asList("http", "https").contains(uriBuilder.getScheme())) { 36 | return GO_SERVER_URL_DISPLAY_VALUE + " must use http or https protocol"; 37 | } 38 | 39 | if (uriBuilder.getHost().equalsIgnoreCase("localhost") || uriBuilder.getHost().equalsIgnoreCase("127.0.0.1")) { 40 | return GO_SERVER_URL_DISPLAY_VALUE + " must not be localhost, since this gets resolved on the agents"; 41 | } 42 | 43 | if (!(uriBuilder.getPath().endsWith("/go") || uriBuilder.getPath().endsWith("/go/"))) { 44 | return GO_SERVER_URL_DISPLAY_VALUE + " must be a valid URL ending with '/go' (http://example.com:8153/go)"; 45 | } 46 | 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/JobCompletionRequestExecutor.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.JobCompletionRequest; 5 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 6 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 12 | import static java.text.MessageFormat.format; 13 | 14 | public class JobCompletionRequestExecutor implements RequestExecutor { 15 | private final JobCompletionRequest jobCompletionRequest; 16 | private final AgentInstances agentInstances; 17 | private final PluginRequest pluginRequest; 18 | 19 | public JobCompletionRequestExecutor(JobCompletionRequest jobCompletionRequest, AgentInstances agentInstances, PluginRequest pluginRequest) { 20 | this.jobCompletionRequest = jobCompletionRequest; 21 | this.agentInstances = agentInstances; 22 | this.pluginRequest = pluginRequest; 23 | } 24 | 25 | @Override 26 | public GoPluginApiResponse execute() throws Exception { 27 | ClusterProfileProperties pluginSettings = jobCompletionRequest.getClusterProfileProperties(); 28 | String elasticAgentId = jobCompletionRequest.getElasticAgentId(); 29 | 30 | Agent agent = new Agent(); 31 | agent.elasticAgentId(elasticAgentId); 32 | 33 | LOG.info(format("[Job Completion] Terminating elastic agent with id {0} on job completion {1}.", agent.elasticAgentId(), jobCompletionRequest.jobIdentifier())); 34 | 35 | List agents = Collections.singletonList(agent); 36 | pluginRequest.disableAgents(agents); 37 | agentInstances.terminate(agent.elasticAgentId(), pluginSettings); 38 | pluginRequest.deleteAgents(agents); 39 | return DefaultGoPluginApiResponse.success(""); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/MemoryMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Size; 20 | import org.apache.commons.lang.StringUtils; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | public class MemoryMetadata extends Metadata { 28 | 29 | public MemoryMetadata(String key, boolean required) { 30 | super(key, required, false); 31 | } 32 | 33 | @Override 34 | protected String doValidate(String input) { 35 | List errors = new ArrayList<>(Arrays.asList(super.doValidate(input))); 36 | 37 | try { 38 | Size.parse(input); 39 | } catch (Exception e) { 40 | errors.add(e.getMessage()); 41 | } 42 | 43 | errors.removeAll(Collections.singleton(null)); 44 | 45 | if (errors.isEmpty()) { 46 | return null; 47 | } 48 | return StringUtils.join(errors, ". "); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/Metadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationError; 20 | import com.google.gson.annotations.Expose; 21 | import com.google.gson.annotations.SerializedName; 22 | import org.apache.commons.lang.StringUtils; 23 | 24 | public class Metadata { 25 | 26 | @Expose 27 | @SerializedName("key") 28 | private String key; 29 | 30 | @Expose 31 | @SerializedName("metadata") 32 | private ProfileMetadata metadata; 33 | 34 | public Metadata(String key, boolean required, boolean secure) { 35 | this(key, new ProfileMetadata(required, secure)); 36 | } 37 | 38 | public Metadata(String key) { 39 | this(key, new ProfileMetadata(false, false)); 40 | } 41 | 42 | public Metadata(String key, ProfileMetadata metadata) { 43 | this.key = key; 44 | this.metadata = metadata; 45 | } 46 | 47 | public ValidationError validate(String input) { 48 | String errorMessage = doValidate(input); 49 | if (StringUtils.isNotBlank(errorMessage)) { 50 | return new ValidationError(key, errorMessage); 51 | } 52 | return null; 53 | } 54 | 55 | protected String doValidate(String input) { 56 | if (isRequired()) { 57 | if (StringUtils.isBlank(input)) { 58 | return this.key + " must not be blank."; 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | 65 | public String getKey() { 66 | return key; 67 | } 68 | 69 | public boolean isRequired() { 70 | return metadata.required; 71 | } 72 | 73 | public static class ProfileMetadata { 74 | @Expose 75 | @SerializedName("required") 76 | private Boolean required; 77 | 78 | @Expose 79 | @SerializedName("secure") 80 | private Boolean secure; 81 | 82 | public ProfileMetadata(boolean required, boolean secure) { 83 | this.required = required; 84 | this.secure = secure; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/NonBlankField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | 20 | import org.apache.commons.lang.StringUtils; 21 | 22 | public class NonBlankField extends Field { 23 | 24 | public NonBlankField(String key, String displayName, String defaultValue, Boolean secure, String displayOrder) { 25 | super(key, displayName, defaultValue, true, secure, displayOrder); 26 | } 27 | 28 | @Override 29 | public String doValidate(String input) { 30 | if (StringUtils.isBlank(input)) { 31 | return this.displayName + " must not be blank."; 32 | } 33 | return null; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ProfileValidateRequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ProfileValidateRequest; 22 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 23 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 24 | 25 | import java.util.ArrayList; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Set; 29 | 30 | public class ProfileValidateRequestExecutor implements RequestExecutor { 31 | private final ProfileValidateRequest request; 32 | 33 | public ProfileValidateRequestExecutor(ProfileValidateRequest request) { 34 | this.request = request; 35 | } 36 | 37 | @Override 38 | public GoPluginApiResponse execute() { 39 | final List knownFields = new ArrayList<>(); 40 | final ValidationResult validationResult = new ValidationResult(); 41 | 42 | for (Metadata field : GetProfileMetadataExecutor.FIELDS) { 43 | knownFields.add(field.getKey()); 44 | validationResult.addError(field.validate(request.getProperties().get(field.getKey()))); 45 | } 46 | 47 | final Set set = new HashSet<>(request.getProperties().keySet()); 48 | set.removeAll(knownFields); 49 | 50 | if (!set.isEmpty()) { 51 | for (String key : set) { 52 | validationResult.addError(key, "Is an unknown property."); 53 | } 54 | } 55 | 56 | return DefaultGoPluginApiResponse.success(validationResult.toJSON()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ShouldAssignWorkRequestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.AgentInstances; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerService; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ShouldAssignWorkRequest; 23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 25 | 26 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 27 | import static java.text.MessageFormat.format; 28 | 29 | 30 | public class ShouldAssignWorkRequestExecutor implements RequestExecutor { 31 | private final AgentInstances agentInstances; 32 | private final ShouldAssignWorkRequest request; 33 | 34 | public ShouldAssignWorkRequestExecutor(ShouldAssignWorkRequest request, AgentInstances agentInstances) { 35 | this.request = request; 36 | this.agentInstances = agentInstances; 37 | } 38 | 39 | @Override 40 | public GoPluginApiResponse execute() { 41 | DockerService instance = agentInstances.find(request.agent().elasticAgentId()); 42 | 43 | if (instance == null) { 44 | LOG.info(format("[should-assign-work] Agent with id `{0}` not exists.", request.agent().elasticAgentId())); 45 | return DefaultGoPluginApiResponse.success("false"); 46 | } 47 | 48 | if (request.jobIdentifier() != null && request.jobIdentifier().getJobId().equals(instance.jobIdentifier().getJobId())) { 49 | LOG.info(format("[should-assign-work] Job with identifier {0} can be assigned to an agent {1}.", request.jobIdentifier(), instance.name())); 50 | return DefaultGoPluginApiResponse.success("true"); 51 | } 52 | 53 | LOG.info(format("[should-assign-work] Job with identifier {0} can not be assigned to an agent {1}.", request.jobIdentifier(), instance.name())); 54 | return DefaultGoPluginApiResponse.success("false"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/metadata/HostMetadata.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.metadata; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.Hosts; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.Metadata; 5 | import org.apache.commons.lang.StringUtils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import static org.apache.commons.lang.StringUtils.isNotBlank; 11 | 12 | public class HostMetadata extends Metadata { 13 | public HostMetadata(String key, boolean required, boolean secure) { 14 | super(key, required, secure); 15 | } 16 | 17 | @Override 18 | protected String doValidate(String input) { 19 | final List errors = new ArrayList<>(); 20 | final String validate = super.doValidate(input); 21 | 22 | if (isNotBlank(validate)) { 23 | errors.add(validate); 24 | } 25 | 26 | errors.addAll(new Hosts().validate(input)); 27 | 28 | if (errors.isEmpty()) { 29 | return null; 30 | } else { 31 | return StringUtils.join(errors, ". "); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/ValidationError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | import com.google.gson.annotations.Expose; 20 | import com.google.gson.annotations.SerializedName; 21 | 22 | import java.text.MessageFormat; 23 | 24 | public class ValidationError { 25 | @Expose 26 | @SerializedName("key") 27 | private final String key; 28 | @Expose 29 | @SerializedName("message") 30 | private final String message; 31 | 32 | public ValidationError(String key, String message) { 33 | this.key = key; 34 | this.message = message; 35 | } 36 | 37 | public String key() { 38 | return key; 39 | } 40 | 41 | public String message() { 42 | return message; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | 50 | ValidationError that = (ValidationError) o; 51 | 52 | if (key != null ? !key.equals(that.key) : that.key != null) return false; 53 | return message != null ? message.equals(that.message) : that.message == null; 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | int result = key != null ? key.hashCode() : 0; 59 | result = 31 * result + (message != null ? message.hashCode() : 0); 60 | return result; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return MessageFormat.format("ValidationError'{'key=''{0}'', message=''{1}'''}'", key, message); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/ValidationResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | import java.util.*; 20 | 21 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.GSON; 22 | import static java.util.Collections.unmodifiableList; 23 | 24 | 25 | public class ValidationResult { 26 | private final Set errors = new HashSet<>(); 27 | 28 | public ValidationResult() { 29 | } 30 | 31 | public ValidationResult(Collection errors) { 32 | this.errors.addAll(errors); 33 | } 34 | 35 | public void addError(String key, String message) { 36 | errors.add(new ValidationError(key, message)); 37 | } 38 | 39 | public void addError(ValidationError validationError) { 40 | if (validationError == null) { 41 | return; 42 | } 43 | errors.add(validationError); 44 | } 45 | 46 | public void merge(ValidationResult validationResult) { 47 | if (validationResult != null) { 48 | this.errors.addAll(validationResult.errors); 49 | } 50 | } 51 | 52 | public boolean hasErrors() { 53 | return !errors.isEmpty(); 54 | } 55 | 56 | public String toJSON() { 57 | return GSON.toJson(errors); 58 | } 59 | 60 | public boolean hasKey(String key) { 61 | return errors.stream().anyMatch(validationError -> key.equals(validationError.key())); 62 | } 63 | 64 | public List allErrors() { 65 | return unmodifiableList(new ArrayList<>(errors)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/reports/DockerTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.Constants; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 21 | import com.spotify.docker.client.messages.swarm.Service; 22 | import com.spotify.docker.client.messages.swarm.Task; 23 | 24 | import java.util.Date; 25 | 26 | import static org.apache.commons.lang.StringUtils.capitalize; 27 | 28 | public class DockerTask { 29 | private final String id; 30 | private final String image; 31 | private final Date created; 32 | private final String state; 33 | private final String nodeId; 34 | private final String serviceId; 35 | private JobIdentifier jobIdentifier; 36 | 37 | public DockerTask(Task task, Service service) { 38 | id = task.id(); 39 | image = task.spec().containerSpec().image(); 40 | nodeId = task.nodeId(); 41 | serviceId = task.serviceId(); 42 | created = task.createdAt(); 43 | state = capitalize(task.status().state()); 44 | jobIdentifier = JobIdentifier.fromJson(service.spec().labels().get(Constants.JOB_IDENTIFIER_LABEL_KEY)); 45 | } 46 | 47 | public String getId() { 48 | return id; 49 | } 50 | 51 | public String getImage() { 52 | return image; 53 | } 54 | 55 | public Date getCreated() { 56 | return created; 57 | } 58 | 59 | public String getState() { 60 | return state; 61 | } 62 | 63 | public String getNodeId() { 64 | return nodeId; 65 | } 66 | 67 | public String getServiceId() { 68 | return serviceId; 69 | } 70 | 71 | public JobIdentifier getJobIdentifier() { 72 | return jobIdentifier; 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) return true; 78 | if (!(o instanceof DockerTask)) return false; 79 | 80 | DockerTask that = (DockerTask) o; 81 | 82 | if (id != null ? !id.equals(that.id) : that.id != null) return false; 83 | if (image != null ? !image.equals(that.image) : that.image != null) return false; 84 | if (created != null ? !created.equals(that.created) : that.created != null) return false; 85 | if (state != null ? !state.equals(that.state) : that.state != null) return false; 86 | if (nodeId != null ? !nodeId.equals(that.nodeId) : that.nodeId != null) return false; 87 | if (serviceId != null ? !serviceId.equals(that.serviceId) : that.serviceId != null) return false; 88 | return jobIdentifier != null ? jobIdentifier.equals(that.jobIdentifier) : that.jobIdentifier == null; 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | int result = id != null ? id.hashCode() : 0; 94 | result = 31 * result + (image != null ? image.hashCode() : 0); 95 | result = 31 * result + (created != null ? created.hashCode() : 0); 96 | result = 31 * result + (state != null ? state.hashCode() : 0); 97 | result = 31 * result + (nodeId != null ? nodeId.hashCode() : 0); 98 | result = 31 * result + (serviceId != null ? serviceId.hashCode() : 0); 99 | result = 31 * result + (jobIdentifier != null ? jobIdentifier.hashCode() : 0); 100 | return result; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/reports/StatusReportGenerationError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.PluginSettingsNotConfiguredException; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.reports.StatusReportGenerationException; 21 | import org.apache.commons.lang.StringUtils; 22 | 23 | public class StatusReportGenerationError { 24 | private static final String DEFAULT_ERROR_MESSAGE = "We're sorry, but something went wrong."; 25 | private String message; 26 | private String description; 27 | 28 | public StatusReportGenerationError(Throwable throwable) { 29 | this.message = getOrDefaultMessage(throwable); 30 | 31 | if (throwable instanceof StatusReportGenerationException) { 32 | this.description = ((StatusReportGenerationException) throwable).getDetailedMessage(); 33 | } else if (throwable instanceof PluginSettingsNotConfiguredException) { 34 | this.description = "Configure plugin settings in order to view agent or plugin status report."; 35 | } 36 | 37 | if (StringUtils.isBlank(this.description)) { 38 | this.description = "If you are the application owner check the plugin logs for more information."; 39 | } 40 | } 41 | 42 | public String getMessage() { 43 | return message; 44 | } 45 | 46 | public String getDescription() { 47 | return description; 48 | } 49 | 50 | private String getOrDefaultMessage(Throwable throwable) { 51 | if (StringUtils.isNotBlank(throwable.getMessage())) { 52 | return throwable.getMessage(); 53 | } 54 | 55 | return DEFAULT_ERROR_MESSAGE; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/reports/agent/TaskStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.agent; 18 | 19 | import com.spotify.docker.client.messages.swarm.Task; 20 | import org.apache.commons.lang.StringUtils; 21 | 22 | public class TaskStatus { 23 | 24 | private final String id; 25 | private final String state; 26 | private final String message; 27 | private String containerId; 28 | private Long exitCode; 29 | private String pid; 30 | 31 | public TaskStatus(Task task) { 32 | id = task.id(); 33 | state = task.status().state(); 34 | if ("failed".equals(state)) { 35 | message = task.status().err(); 36 | } else { 37 | message = task.status().message(); 38 | } 39 | 40 | if (task.status().containerStatus() != null) { 41 | if (StringUtils.isNotBlank(task.status().containerStatus().containerId())) { 42 | containerId = task.status().containerStatus().containerId().substring(0, 12); 43 | } 44 | exitCode = task.status().containerStatus().exitCode(); 45 | 46 | if (task.status().containerStatus().pid() == null) { 47 | pid = "-"; 48 | } else { 49 | pid = task.status().containerStatus().pid().toString(); 50 | } 51 | } 52 | } 53 | 54 | public String getId() { 55 | return id; 56 | } 57 | 58 | public String getState() { 59 | return state; 60 | } 61 | 62 | public String getMessage() { 63 | return message; 64 | } 65 | 66 | public String getContainerId() { 67 | return containerId; 68 | } 69 | 70 | public Long getExitCode() { 71 | return exitCode; 72 | } 73 | 74 | public String getPid() { 75 | return pid; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/reports/StatusReportGenerationErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.reports; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.builders.PluginStatusReportViewBuilder; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.StatusReportGenerationError; 21 | import com.google.gson.JsonObject; 22 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; 23 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 24 | import freemarker.template.Template; 25 | 26 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerPlugin.LOG; 27 | import static java.text.MessageFormat.format; 28 | 29 | public class StatusReportGenerationErrorHandler { 30 | 31 | public static GoPluginApiResponse handle(PluginStatusReportViewBuilder builder, Exception e) { 32 | try { 33 | LOG.error(format("Error while generating status report: {0}", e.getMessage()), e); 34 | final Template template = builder.getTemplate("error.template.ftlh"); 35 | final String errorView = builder.build(template, new StatusReportGenerationError(e)); 36 | 37 | final JsonObject responseJSON = new JsonObject(); 38 | responseJSON.addProperty("view", errorView); 39 | 40 | return DefaultGoPluginApiResponse.success(responseJSON.toString()); 41 | } catch (Exception ex) { 42 | LOG.error(format("Failed to generate error report: {0}", e.getMessage()), e); 43 | return DefaultGoPluginApiResponse.error(format("Failed to generate error report: {0}.", e.toString())); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/reports/StatusReportGenerationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.reports; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 20 | 21 | import static java.lang.String.format; 22 | 23 | public class StatusReportGenerationException extends RuntimeException { 24 | private final String message; 25 | private final String detailedMessage; 26 | private static final String MISSING_SERVICE = "Can not find a running service for the provided %s '%s'"; 27 | 28 | public StatusReportGenerationException(String message) { 29 | this(message, null); 30 | } 31 | 32 | public StatusReportGenerationException(String message, String detailedMessage) { 33 | this.message = message; 34 | this.detailedMessage = detailedMessage; 35 | } 36 | 37 | public String getMessage() { 38 | return message; 39 | } 40 | 41 | public String getDetailedMessage() { 42 | return detailedMessage; 43 | } 44 | 45 | public static StatusReportGenerationException noRunningService(JobIdentifier jobIdentifier) { 46 | return new StatusReportGenerationException("Service is not running.", format(MISSING_SERVICE, "job identifier", jobIdentifier.getRepresentation())); 47 | } 48 | 49 | public static StatusReportGenerationException noRunningService(String elasticAgentId) { 50 | return new StatusReportGenerationException("Service is not running.", format(MISSING_SERVICE, "elastic agent id", elasticAgentId)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/AgentStatusReportRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.AgentInstances; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfileProperties; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerService; 6 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.PluginRequest; 7 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.AgentStatusReportExecutor; 8 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 9 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 10 | import com.google.gson.annotations.Expose; 11 | import com.google.gson.annotations.SerializedName; 12 | 13 | import java.io.IOException; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | 17 | public class AgentStatusReportRequest { 18 | @Expose 19 | @SerializedName("elastic_agent_id") 20 | private String elasticAgentId; 21 | 22 | @Expose 23 | @SerializedName("job_identifier") 24 | private JobIdentifier jobIdentifier; 25 | 26 | @Expose 27 | @SerializedName("cluster_profile_properties") 28 | private ClusterProfileProperties clusterProfileProperties; 29 | 30 | public AgentStatusReportRequest() { 31 | } 32 | 33 | public AgentStatusReportRequest(String elasticAgentId, JobIdentifier jobIdentifier, Map clusterProfileProperties) { 34 | this.elasticAgentId = elasticAgentId; 35 | this.jobIdentifier = jobIdentifier; 36 | this.clusterProfileProperties = ClusterProfileProperties.fromConfiguration(clusterProfileProperties); 37 | } 38 | 39 | public static AgentStatusReportRequest fromJSON(String json) { 40 | return Util.GSON.fromJson(json, AgentStatusReportRequest.class); 41 | } 42 | 43 | public ClusterProfileProperties getClusterProfileProperties() { 44 | return clusterProfileProperties; 45 | } 46 | 47 | public String getElasticAgentId() { 48 | return elasticAgentId; 49 | } 50 | 51 | public JobIdentifier getJobIdentifier() { 52 | return jobIdentifier; 53 | } 54 | 55 | public AgentStatusReportExecutor executor(PluginRequest pluginRequest, AgentInstances agentInstances) throws IOException { 56 | return new AgentStatusReportExecutor(this, pluginRequest, agentInstances); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "AgentStatusReportRequest{" + 62 | "elasticAgentId='" + elasticAgentId + '\'' + 63 | ", jobIdentifier=" + jobIdentifier + 64 | ", clusterProfileProperties=" + clusterProfileProperties + 65 | '}'; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | if (this == o) return true; 71 | if (o == null || getClass() != o.getClass()) return false; 72 | AgentStatusReportRequest that = (AgentStatusReportRequest) o; 73 | return Objects.equals(elasticAgentId, that.elasticAgentId) && 74 | Objects.equals(jobIdentifier, that.jobIdentifier) && 75 | Objects.equals(clusterProfileProperties, that.clusterProfileProperties); 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return Objects.hash(elasticAgentId, jobIdentifier, clusterProfileProperties); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ClusterProfileValidateRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.ClusterProfileValidateRequestExecutor; 5 | import com.google.common.reflect.TypeToken; 6 | import com.google.gson.FieldNamingPolicy; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | 10 | import java.lang.reflect.Type; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class ClusterProfileValidateRequest extends HashMap{ 15 | public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); 16 | private Map properties; 17 | 18 | public ClusterProfileValidateRequest(Map properties) { 19 | this.properties = properties; 20 | } 21 | 22 | public Map getProperties() { 23 | return properties; 24 | } 25 | 26 | public static ClusterProfileValidateRequest fromJSON(String json) { 27 | final Type type = new TypeToken>() { 28 | }.getType(); 29 | final Map properties = GSON.fromJson(json, type); 30 | return new ClusterProfileValidateRequest(properties); 31 | } 32 | 33 | public RequestExecutor executor() { 34 | return new ClusterProfileValidateRequestExecutor(this); 35 | }} 36 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ClusterStatusReportRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfileProperties; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerServices; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.ClusterStatusReportExecutor; 6 | import com.google.gson.FieldNamingPolicy; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | import com.google.gson.annotations.Expose; 10 | import com.google.gson.annotations.SerializedName; 11 | 12 | import java.io.IOException; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | 16 | public class ClusterStatusReportRequest { 17 | private static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation() 18 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 19 | .create(); 20 | 21 | @Expose 22 | @SerializedName("cluster_profile_properties") 23 | private ClusterProfileProperties clusterProfile; 24 | 25 | public ClusterStatusReportRequest() { 26 | } 27 | 28 | public ClusterStatusReportRequest(Map clusterProfileConfigurations) { 29 | this.clusterProfile = ClusterProfileProperties.fromConfiguration(clusterProfileConfigurations); 30 | } 31 | 32 | public ClusterProfileProperties getClusterProfile() { 33 | return clusterProfile; 34 | } 35 | 36 | public static ClusterStatusReportRequest fromJSON(String json) { 37 | return GSON.fromJson(json, ClusterStatusReportRequest.class); 38 | } 39 | 40 | public ClusterStatusReportExecutor executor(DockerServices dockerServices) throws IOException { 41 | return new ClusterStatusReportExecutor(this, dockerServices); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | ClusterStatusReportRequest that = (ClusterStatusReportRequest) o; 49 | return Objects.equals(clusterProfile, that.clusterProfile); 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return Objects.hash(clusterProfile); 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "ClusterStatusReportRequest{" + 60 | "clusterProfile=" + clusterProfile + 61 | '}'; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/JobCompletionRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.JobCompletionRequestExecutor; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 6 | import com.google.gson.annotations.Expose; 7 | import com.google.gson.annotations.SerializedName; 8 | 9 | import java.util.Map; 10 | 11 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.GSON; 12 | 13 | public class JobCompletionRequest { 14 | @Expose 15 | @SerializedName("elastic_agent_id") 16 | private String elasticAgentId; 17 | @Expose 18 | @SerializedName("job_identifier") 19 | private JobIdentifier jobIdentifier; 20 | 21 | @Expose 22 | @SerializedName("cluster_profile_properties") 23 | private ClusterProfileProperties clusterProfileProperties; 24 | 25 | public JobCompletionRequest() { 26 | } 27 | 28 | public JobCompletionRequest(String elasticAgentId, JobIdentifier jobIdentifier, Map clusterProfile) { 29 | this.elasticAgentId = elasticAgentId; 30 | this.jobIdentifier = jobIdentifier; 31 | this.clusterProfileProperties = ClusterProfileProperties.fromConfiguration(clusterProfile); 32 | } 33 | 34 | public JobCompletionRequest(String elasticAgentId, JobIdentifier jobIdentifier, ClusterProfileProperties profileProperties) { 35 | this.elasticAgentId = elasticAgentId; 36 | this.jobIdentifier = jobIdentifier; 37 | this.clusterProfileProperties = profileProperties; 38 | } 39 | 40 | public static JobCompletionRequest fromJSON(String json) { 41 | JobCompletionRequest jobCompletionRequest = GSON.fromJson(json, JobCompletionRequest.class); 42 | return jobCompletionRequest; 43 | } 44 | 45 | public String getElasticAgentId() { 46 | return elasticAgentId; 47 | } 48 | 49 | public JobIdentifier jobIdentifier() { 50 | return jobIdentifier; 51 | } 52 | 53 | public RequestExecutor executor(AgentInstances agentInstances, PluginRequest pluginRequest) { 54 | return new JobCompletionRequestExecutor(this, agentInstances, pluginRequest); 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "JobCompletionRequest{" + 60 | "elasticAgentId='" + elasticAgentId + '\'' + 61 | ", jobIdentifier=" + jobIdentifier + 62 | ", clusterProfileProperties=" + clusterProfileProperties + 63 | '}'; 64 | } 65 | 66 | public ClusterProfileProperties getClusterProfileProperties() { 67 | return clusterProfileProperties; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/MigrateConfigurationRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfile; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ElasticAgentProfile; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.PluginSettings; 6 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.MigrateConfigurationRequestExecutor; 7 | import com.google.gson.FieldNamingPolicy; 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | import com.google.gson.annotations.Expose; 11 | import com.google.gson.annotations.SerializedName; 12 | 13 | import java.util.List; 14 | import java.util.Objects; 15 | 16 | public class MigrateConfigurationRequest { 17 | public static final Gson GSON = new GsonBuilder() 18 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 19 | .create(); 20 | 21 | @Expose 22 | @SerializedName("plugin_settings") 23 | private PluginSettings pluginSettings; 24 | 25 | @Expose 26 | @SerializedName("cluster_profiles") 27 | private List clusterProfiles; 28 | 29 | @Expose 30 | @SerializedName("elastic_agent_profiles") 31 | private List elasticAgentProfiles; 32 | 33 | public MigrateConfigurationRequest() { 34 | } 35 | 36 | public MigrateConfigurationRequest(PluginSettings pluginSettings, List clusterProfiles, List elasticAgentProfiles) { 37 | this.pluginSettings = pluginSettings; 38 | this.clusterProfiles = clusterProfiles; 39 | this.elasticAgentProfiles = elasticAgentProfiles; 40 | } 41 | 42 | public static MigrateConfigurationRequest fromJSON(String requestBody) { 43 | MigrateConfigurationRequest request = GSON.fromJson(requestBody, MigrateConfigurationRequest.class); 44 | return request; 45 | } 46 | 47 | public String toJSON() { 48 | return GSON.toJson(this); 49 | } 50 | 51 | public MigrateConfigurationRequestExecutor executor() { 52 | return new MigrateConfigurationRequestExecutor(this); 53 | } 54 | 55 | public PluginSettings getPluginSettings() { 56 | return pluginSettings; 57 | } 58 | 59 | public void setPluginSettings(PluginSettings pluginSettings) { 60 | this.pluginSettings = pluginSettings; 61 | } 62 | 63 | public List getClusterProfiles() { 64 | return clusterProfiles; 65 | } 66 | 67 | public void setClusterProfiles(List clusterProfiles) { 68 | this.clusterProfiles = clusterProfiles; 69 | } 70 | 71 | public List getElasticAgentProfiles() { 72 | return elasticAgentProfiles; 73 | } 74 | 75 | public void setElasticAgentProfiles(List elasticAgentProfiles) { 76 | this.elasticAgentProfiles = elasticAgentProfiles; 77 | } 78 | 79 | @Override 80 | public boolean equals(Object o) { 81 | if (this == o) return true; 82 | if (o == null || getClass() != o.getClass()) return false; 83 | MigrateConfigurationRequest that = (MigrateConfigurationRequest) o; 84 | return Objects.equals(pluginSettings, that.pluginSettings) && 85 | Objects.equals(clusterProfiles, that.clusterProfiles) && 86 | Objects.equals(elasticAgentProfiles, that.elasticAgentProfiles); 87 | } 88 | 89 | @Override 90 | public int hashCode() { 91 | return Objects.hash(pluginSettings, clusterProfiles, elasticAgentProfiles); 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "MigrateConfigurationRequest{" + 97 | "pluginSettings=" + pluginSettings + 98 | ", clusterProfiles=" + clusterProfiles + 99 | ", elasticAgentProfiles=" + elasticAgentProfiles + 100 | '}'; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ProfileValidateRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.RequestExecutor; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.ProfileValidateRequestExecutor; 21 | import com.google.gson.Gson; 22 | import com.google.gson.reflect.TypeToken; 23 | 24 | import java.util.Map; 25 | 26 | public class ProfileValidateRequest { 27 | 28 | private static final Gson GSON = new Gson(); 29 | private Map properties; 30 | 31 | public ProfileValidateRequest(Map properties) { 32 | this.properties = properties; 33 | } 34 | 35 | 36 | public Map getProperties() { 37 | return properties; 38 | } 39 | 40 | public static ProfileValidateRequest fromJSON(String json) { 41 | return new ProfileValidateRequest(GSON.fromJson(json, new TypeToken>() { 42 | }.getType())); 43 | } 44 | 45 | public RequestExecutor executor() { 46 | return new ProfileValidateRequestExecutor(this); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ServerPingRequest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfileProperties; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerServices; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.PluginRequest; 6 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.ServerPingRequestExecutor; 7 | import com.google.gson.FieldNamingPolicy; 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.stream.Collectors; 15 | 16 | public class ServerPingRequest { 17 | private static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); 18 | private List allClusterProfileProperties = new ArrayList<>(); 19 | 20 | public ServerPingRequest() { 21 | } 22 | 23 | public ServerPingRequest(List> allClusterProfileProperties) { 24 | this.allClusterProfileProperties = allClusterProfileProperties.stream() 25 | .map(ClusterProfileProperties::fromConfiguration) 26 | .collect(Collectors.toList()); 27 | } 28 | 29 | public List allClusterProfileProperties() { 30 | return allClusterProfileProperties; 31 | } 32 | 33 | public static ServerPingRequest fromJSON(String json) { 34 | return GSON.fromJson(json, ServerPingRequest.class); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "ServerPingRequest{" + 40 | "allClusterProfileProperties=" + allClusterProfileProperties + 41 | '}'; 42 | } 43 | 44 | public ServerPingRequestExecutor executor(Map clusterSpecificAgentInstances, PluginRequest pluginRequest) { 45 | return new ServerPingRequestExecutor(this, clusterSpecificAgentInstances, pluginRequest); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ShouldAssignWorkRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.ShouldAssignWorkRequestExecutor; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 22 | import com.google.gson.FieldNamingPolicy; 23 | import com.google.gson.Gson; 24 | import com.google.gson.GsonBuilder; 25 | 26 | import java.util.Map; 27 | 28 | /** 29 | * Represents the {@link Request#REQUEST_SHOULD_ASSIGN_WORK} message. 30 | */ 31 | public class ShouldAssignWorkRequest { 32 | public static final Gson GSON = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); 33 | private Agent agent; 34 | private String environment; 35 | private Map properties; 36 | private JobIdentifier jobIdentifier; 37 | private ClusterProfileProperties clusterProfileProperties; 38 | 39 | public ShouldAssignWorkRequest(Agent agent, String environment, Map properties, JobIdentifier jobIdentifier, Map clusterProfileProperties) { 40 | this.agent = agent; 41 | this.environment = environment; 42 | this.properties = properties; 43 | this.jobIdentifier = jobIdentifier; 44 | this.clusterProfileProperties = ClusterProfileProperties.fromConfiguration(clusterProfileProperties); 45 | } 46 | 47 | public ShouldAssignWorkRequest() { 48 | } 49 | 50 | public Agent agent() { 51 | return agent; 52 | } 53 | 54 | public String environment() { 55 | return environment; 56 | } 57 | 58 | public Map properties() { 59 | return properties; 60 | } 61 | 62 | public JobIdentifier jobIdentifier() { 63 | return jobIdentifier; 64 | } 65 | 66 | public static ShouldAssignWorkRequest fromJSON(String json) { 67 | return GSON.fromJson(json, ShouldAssignWorkRequest.class); 68 | } 69 | 70 | public RequestExecutor executor(AgentInstances agentInstances, PluginRequest pluginRequest) { 71 | return new ShouldAssignWorkRequestExecutor(this, agentInstances); 72 | } 73 | 74 | public ClusterProfileProperties getClusterProfileProperties() { 75 | return clusterProfileProperties; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/utils/SizeUnit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2013 Coda Hale and Yammer, Inc., 2014-2016 Dropwizard Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils; 18 | 19 | public enum SizeUnit { 20 | /** 21 | * Bytes. 22 | */ 23 | BYTES(8), 24 | 25 | /** 26 | * Kilobytes. 27 | */ 28 | KILOBYTES(8L * 1024), 29 | 30 | /** 31 | * Megabytes. 32 | */ 33 | MEGABYTES(8L * 1024 * 1024), 34 | 35 | /** 36 | * Gigabytes. 37 | */ 38 | GIGABYTES(8L * 1024 * 1024 * 1024), 39 | 40 | /** 41 | * Terabytes. 42 | */ 43 | TERABYTES(8L * 1024 * 1024 * 1024 * 1024); 44 | 45 | private final long bits; 46 | 47 | SizeUnit(long bits) { 48 | this.bits = bits; 49 | } 50 | 51 | /** 52 | * Converts a size of the given unit into the current unit. 53 | * 54 | * @param size the magnitude of the size 55 | * @param unit the unit of the size 56 | * @return the given size in the current unit. 57 | */ 58 | public long convert(long size, SizeUnit unit) { 59 | return (size * unit.bits) / bits; 60 | } 61 | 62 | /** 63 | * Converts the given number of the current units into bytes. 64 | * 65 | * @param l the magnitude of the size in the current unit 66 | * @return {@code l} of the current units in bytes 67 | */ 68 | public long toBytes(long l) { 69 | return BYTES.convert(l, this); 70 | } 71 | 72 | /** 73 | * Converts the given number of the current units into kilobytes. 74 | * 75 | * @param l the magnitude of the size in the current unit 76 | * @return {@code l} of the current units in kilobytes 77 | */ 78 | public long toKilobytes(long l) { 79 | return KILOBYTES.convert(l, this); 80 | } 81 | 82 | /** 83 | * Converts the given number of the current units into megabytes. 84 | * 85 | * @param l the magnitude of the size in the current unit 86 | * @return {@code l} of the current units in megabytes 87 | */ 88 | public long toMegabytes(long l) { 89 | return MEGABYTES.convert(l, this); 90 | } 91 | 92 | /** 93 | * Converts the given number of the current units into gigabytes. 94 | * 95 | * @param l the magnitude of the size in the current unit 96 | * @return {@code l} of the current units in bytes 97 | */ 98 | public long toGigabytes(long l) { 99 | return GIGABYTES.convert(l, this); 100 | } 101 | 102 | /** 103 | * Converts the given number of the current units into terabytes. 104 | * 105 | * @param l the magnitude of the size in the current unit 106 | * @return {@code l} of the current units in terabytes 107 | */ 108 | public long toTerabytes(long l) { 109 | return TERABYTES.convert(l, this); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/validator/DockerMountsValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerClientFactory; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerMounts; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.CreateAgentRequest; 23 | import com.spotify.docker.client.DockerClient; 24 | 25 | import java.util.Map; 26 | 27 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.dockerApiVersionAtLeast; 28 | 29 | public class DockerMountsValidator implements Validatable { 30 | private final CreateAgentRequest createAgentRequest; 31 | private final DockerClientFactory dockerClientFactory; 32 | 33 | public DockerMountsValidator(CreateAgentRequest createAgentRequest) { 34 | this(createAgentRequest, DockerClientFactory.instance()); 35 | } 36 | 37 | DockerMountsValidator(CreateAgentRequest createAgentRequest, DockerClientFactory dockerClientFactory) { 38 | this.createAgentRequest = createAgentRequest; 39 | this.dockerClientFactory = dockerClientFactory; 40 | } 41 | 42 | @Override 43 | public ValidationResult validate(Map elasticProfile) { 44 | final ValidationResult validationResult = new ValidationResult(); 45 | 46 | try { 47 | final DockerMounts dockerMounts = DockerMounts.fromString(elasticProfile.get("Mounts")); 48 | 49 | if (!dockerMounts.isEmpty()) { 50 | DockerClient dockerClient = dockerClientFactory.docker(createAgentRequest.getClusterProfileProperties()); 51 | 52 | if (!dockerApiVersionAtLeast(dockerClient, "1.26")) { 53 | throw new RuntimeException("Docker volume mount requires api version 1.26 or higher."); 54 | } 55 | 56 | dockerMounts.toMount(); 57 | } 58 | } catch (Exception e) { 59 | validationResult.addError("Mounts", e.getMessage()); 60 | } 61 | 62 | return validationResult; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/validator/DockerSecretValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerClientFactory; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerSecrets; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.CreateAgentRequest; 23 | import com.spotify.docker.client.DockerClient; 24 | 25 | import java.util.Map; 26 | 27 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util.dockerApiVersionAtLeast; 28 | 29 | public class DockerSecretValidator implements Validatable { 30 | private final CreateAgentRequest createAgentRequest; 31 | private final DockerClientFactory dockerClientFactory; 32 | 33 | public DockerSecretValidator(CreateAgentRequest createAgentRequest) { 34 | this(createAgentRequest, DockerClientFactory.instance()); 35 | } 36 | 37 | DockerSecretValidator(CreateAgentRequest createAgentRequest, DockerClientFactory dockerClientFactory) { 38 | this.createAgentRequest = createAgentRequest; 39 | this.dockerClientFactory = dockerClientFactory; 40 | } 41 | 42 | @Override 43 | public ValidationResult validate(Map elasticProfile) { 44 | final ValidationResult validationResult = new ValidationResult(); 45 | try { 46 | final DockerSecrets dockerSecrets = DockerSecrets.fromString(elasticProfile.get("Secrets")); 47 | if (!dockerSecrets.isEmpty()) { 48 | DockerClient dockerClient = dockerClientFactory.docker(createAgentRequest.getClusterProfileProperties()); 49 | if (!dockerApiVersionAtLeast(dockerClient, "1.26")) { 50 | throw new RuntimeException("Docker secret requires api version 1.26 or higher."); 51 | } 52 | dockerSecrets.toSecretBind(dockerClient.listSecrets()); 53 | } 54 | } catch (Exception e) { 55 | validationResult.addError("Secrets", e.getMessage()); 56 | } 57 | 58 | return validationResult; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/validator/PrivateDockerRegistrySettingsValidator.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.Metadata; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ClusterProfileValidateRequest; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.GetClusterProfileMetadataExecutor.*; 12 | import static org.apache.commons.lang.StringUtils.isBlank; 13 | 14 | public class PrivateDockerRegistrySettingsValidator { 15 | 16 | public List> validate(ClusterProfileValidateRequest request) { 17 | final List> result = new ArrayList<>(); 18 | final boolean useDockerAuthInfo = Boolean.valueOf(request.getProperties().get(ENABLE_PRIVATE_REGISTRY_AUTHENTICATION.getKey())); 19 | if (!useDockerAuthInfo) { 20 | return result; 21 | } 22 | 23 | validate(PRIVATE_REGISTRY_SERVER, request, result); 24 | validate(PRIVATE_REGISTRY_USERNAME, request, result); 25 | validate(PRIVATE_REGISTRY_PASSWORD, request, result); 26 | return result; 27 | } 28 | 29 | private void validate(Metadata field, ClusterProfileValidateRequest request, List> errorResult) { 30 | if (isBlank(request.getProperties().get(field.getKey()))) { 31 | Map result = new HashMap<>(); 32 | result.put("key", field.getKey()); 33 | result.put("message", field.getKey()+ " must not be blank."); 34 | errorResult.add(result); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/validator/Validatable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.validator; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationResult; 20 | 21 | import java.util.Map; 22 | 23 | public interface Validatable { 24 | ValidationResult validate(Map elasticProfile); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resource-templates/plugin.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2022 Thoughtworks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | pluginId=${id} 18 | -------------------------------------------------------------------------------- /src/main/resource-templates/plugin.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | ${name} 20 | ${version} 21 | ${goCdVersion} 22 | ${description} 23 | 24 | ${vendorName} 25 | ${vendorUrl} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/resources/docker-swarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocd-contrib/docker-swarm-elastic-agent-plugin/80b301d8a2fb4c05da39c8b30f09fc08741e9a9e/src/main/resources/docker-swarm.png -------------------------------------------------------------------------------- /src/main/resources/error.template.ftlh: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |
${ message!'' }
48 |

${description!''}

49 |
50 |
51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/AgentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import org.junit.jupiter.api.Test; 20 | import org.skyscreamer.jsonassert.JSONAssert; 21 | 22 | import java.util.List; 23 | 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.hamcrest.Matchers.*; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 28 | 29 | 30 | public class AgentTest { 31 | 32 | @Test 33 | public void shouldSerializeToJSON() throws Exception { 34 | Agent agent = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 35 | String agentsJSON = Agent.toJSONArray(List.of(agent)); 36 | 37 | JSONAssert.assertEquals("[{\"agent_id\":\"eeb9e0eb-1f12-4366-a5a5-59011810273b\",\"agent_state\":\"Building\",\"build_state\":\"Cancelled\",\"config_state\":\"Disabled\"}]", agentsJSON, true); 38 | } 39 | 40 | @Test 41 | public void shouldDeserializeFromJSON() { 42 | List agents = Agent.fromJSONArray("[{\"agent_id\":\"eeb9e0eb-1f12-4366-a5a5-59011810273b\",\"agent_state\":\"Building\",\"build_state\":\"Cancelled\",\"config_state\":\"Disabled\"}]"); 43 | assertThat(agents, hasSize(1)); 44 | 45 | Agent agent = agents.get(0); 46 | 47 | assertThat(agent.elasticAgentId(), is("eeb9e0eb-1f12-4366-a5a5-59011810273b")); 48 | assertThat(agent.agentState(), is(Agent.AgentState.Building)); 49 | assertThat(agent.buildState(), is(Agent.BuildState.Cancelled)); 50 | assertThat(agent.configState(), is(Agent.ConfigState.Disabled)); 51 | } 52 | 53 | @Test 54 | public void agentsWithSameAttributesShouldBeEqual() { 55 | Agent agent1 = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 56 | Agent agent2 = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 57 | 58 | assertEquals(agent1, agent2); 59 | } 60 | 61 | @Test 62 | public void agentShouldNotEqualAnotherAgentWithDifferentAttributes() { 63 | Agent agent = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 64 | 65 | assertNotEquals(agent, new Agent()); 66 | } 67 | 68 | @Test 69 | public void agentsWithSameAttributesShareSameHashCode() { 70 | Agent agent1 = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 71 | Agent agent2 = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 72 | 73 | assertThat(agent1.hashCode(), equalTo(agent2.hashCode())); 74 | } 75 | 76 | @Test 77 | public void agentsWithDifferentAttributesDoNotShareSameHashCode() { 78 | Agent agent1 = new Agent("eeb9e0eb-1f12-4366-a5a5-59011810273b", Agent.AgentState.Building, Agent.BuildState.Cancelled, Agent.ConfigState.Disabled); 79 | Agent agent2 = new Agent(); 80 | 81 | assertThat(agent1.hashCode(), not(equalTo(agent2.hashCode()))); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/DockerMountsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | import com.spotify.docker.client.messages.Volume; 20 | import com.spotify.docker.client.messages.mount.Mount; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import java.util.List; 24 | 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.hamcrest.Matchers.hasSize; 27 | import static org.hamcrest.Matchers.is; 28 | import static org.junit.jupiter.api.Assertions.assertNotNull; 29 | import static org.mockito.Mockito.mock; 30 | import static org.mockito.Mockito.when; 31 | 32 | public class DockerMountsTest { 33 | 34 | @Test 35 | public void shouldBuildVolumeMountFromString() { 36 | final DockerMounts mounts = DockerMounts.fromString("source=namedVolume, target=/path/in/container"); 37 | 38 | assertNotNull(mounts); 39 | assertThat(mounts, hasSize(1)); 40 | 41 | assertThat(mounts.get(0).type(), is("volume")); 42 | assertThat(mounts.get(0).source(), is("namedVolume")); 43 | assertThat(mounts.get(0).target(), is("/path/in/container")); 44 | } 45 | 46 | @Test 47 | public void shouldBuildBindMountFromString() { 48 | final DockerMounts mounts = DockerMounts.fromString("type=bind, source=/path/in/host, target=/path/in/container"); 49 | 50 | assertNotNull(mounts); 51 | assertThat(mounts, hasSize(1)); 52 | 53 | assertThat(mounts.get(0).type(), is("bind")); 54 | assertThat(mounts.get(0).source(), is("/path/in/host")); 55 | assertThat(mounts.get(0).target(), is("/path/in/container")); 56 | } 57 | 58 | @Test 59 | public void shouldSkipEmptyLine() { 60 | final DockerMounts dockerMounts = DockerMounts.fromString("type=volume, source=namedVolume, target=/path/in/container\n\ntype=bind, source=/path/in/host, target=/path/in/container2"); 61 | 62 | assertNotNull(dockerMounts); 63 | assertThat(dockerMounts, hasSize(2)); 64 | 65 | assertThat(dockerMounts.get(0).type(), is("volume")); 66 | assertThat(dockerMounts.get(1).type(), is("bind")); 67 | } 68 | 69 | @Test 70 | public void shouldBuildMountFromDockerMount() { 71 | final DockerMounts dockerMounts = DockerMounts.fromString("source=namedVolume, target=/path/in/container\ntype=bind, src=/path/in/host, target=/path/in/container2, readonly"); 72 | final Volume volume = mock(Volume.class); 73 | 74 | when(volume.name()).thenReturn("namedVolume"); 75 | 76 | final List mounts = dockerMounts.toMount(); 77 | 78 | assertThat(mounts, hasSize(2)); 79 | assertThat(mounts.get(0).type(), is("volume")); 80 | assertThat(mounts.get(0).source(), is("namedVolume")); 81 | assertThat(mounts.get(0).target(), is("/path/in/container")); 82 | assertThat(mounts.get(0).readOnly(), is(false)); 83 | 84 | assertThat(mounts.get(1).type(), is("bind")); 85 | assertThat(mounts.get(1).source(), is("/path/in/host")); 86 | assertThat(mounts.get(1).target(), is("/path/in/container2")); 87 | assertThat(mounts.get(1).readOnly(), is(true)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/HostsTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.*; 10 | 11 | public class HostsTest { 12 | 13 | @Test 14 | public void shouldReturnEmptyListWhenHostConfigIsNotProvided() { 15 | assertThat(new Hosts().hosts(null), hasSize(0)); 16 | assertThat(new Hosts().hosts(""), hasSize(0)); 17 | } 18 | 19 | @Test 20 | public void shouldReturnHostMappingForOneIpToOneHostnameMapping() { 21 | final List hosts = new Hosts().hosts("10.0.0.1 foo-host"); 22 | assertThat(hosts, hasItem("10.0.0.1 foo-host")); 23 | } 24 | 25 | @Test 26 | public void shouldReturnHostMappingForOneIpToMAnyHostnameMapping() { 27 | final List hosts = new Hosts().hosts("10.0.0.1 foo-host bar-host"); 28 | assertThat(hosts, hasItem("10.0.0.1 foo-host")); 29 | assertThat(hosts, hasItem("10.0.0.1 bar-host")); 30 | } 31 | 32 | @Test 33 | public void shouldErrorOutIfHostEntryIsInvalid() { 34 | final List errors = new Hosts().validate("foo-host 10.0.0.1"); 35 | 36 | assertThat(errors, contains("'foo-host' is not an IP string literal.")); 37 | } 38 | 39 | @Test 40 | public void shouldErrorOutIfHostEntryIsNotContainsHostName() { 41 | final List errors = new Hosts().validate("10.0.0.1"); 42 | 43 | assertThat(errors, contains("Host entry `10.0.0.1` is invalid. Must be in `IP-ADDRESS HOST-1 HOST-2...` format.")); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/NetworksTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 2 | 3 | import com.spotify.docker.client.messages.Network; 4 | import com.spotify.docker.client.messages.swarm.NetworkAttachmentConfig; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import static java.util.Arrays.asList; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.hasSize; 14 | import static org.hamcrest.Matchers.is; 15 | import static org.junit.jupiter.api.Assertions.assertNotNull; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | 19 | public class NetworksTest { 20 | @Test 21 | public void shouldReturnEmptyListWhenNetworkConfigIsNotProvided() { 22 | assertThat(Networks.fromString(null, Collections.emptyList()), hasSize(0)); 23 | assertThat(Networks.fromString("", Collections.emptyList()), hasSize(0)); 24 | } 25 | 26 | @Test 27 | public void shouldReturnNetworkAttachmentConfigListFromString() { 28 | final Network swarmNetwork = mock(Network.class); 29 | when(swarmNetwork.name()).thenReturn("frontend"); 30 | 31 | final List serviceNetworks = Networks.fromString("frontend", asList(swarmNetwork)); 32 | 33 | assertNotNull(serviceNetworks); 34 | assertThat(serviceNetworks, hasSize(1)); 35 | 36 | assertThat(serviceNetworks.get(0).target(), is("frontend")); 37 | } 38 | 39 | @Test 40 | public void shouldErrorOutWhenNetworkDoesNotExist() { 41 | assertThatThrownBy(() -> Networks.fromString("frontend", Collections.emptyList())) 42 | .isInstanceOf(RuntimeException.class) 43 | .hasMessage("Network with name `frontend` does not exist."); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/SetupSemaphoreTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent; 18 | 19 | 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.LinkedHashMap; 23 | import java.util.Map; 24 | import java.util.concurrent.Semaphore; 25 | 26 | import static org.hamcrest.MatcherAssert.assertThat; 27 | import static org.hamcrest.Matchers.is; 28 | 29 | public class SetupSemaphoreTest { 30 | 31 | @Test 32 | public void shouldDrainSemaphoreCompletely_when_maxAllowedContainersIsLessThanCurrentContainerCount() { 33 | Map instances = instancesWithSize(10); 34 | Semaphore semaphore = new Semaphore(9, true); 35 | 36 | new SetupSemaphore(5, instances, semaphore).run(); 37 | 38 | assertThat(semaphore.availablePermits(), is(0)); 39 | } 40 | 41 | @Test 42 | public void shouldDecreaseSemaphore_when_maxAllowedContainersIsMoreThanCurrentContainerCount_and_availablePermitInSemaphoreIsLessThanNumberOfInstances() { 43 | Map instances = instancesWithSize(10); 44 | Semaphore semaphore = new Semaphore(11, true); 45 | 46 | new SetupSemaphore(15, instances, semaphore).run(); 47 | 48 | assertThat(semaphore.availablePermits(), is(5)); 49 | } 50 | 51 | @Test 52 | public void shouldIncreaseSemaphore_when_maxAllowedContainersIsMoreThanCurrentContainerCount_and_availablePermitInSemaphoreIsLessThanNumberOfInstances() { 53 | Map instances = instancesWithSize(10); 54 | Semaphore semaphore = new Semaphore(1, true); 55 | 56 | new SetupSemaphore(15, instances, semaphore).run(); 57 | 58 | assertThat(semaphore.availablePermits(), is(5)); 59 | } 60 | 61 | private Map instancesWithSize(int size) { 62 | Map instances = new LinkedHashMap<>(); 63 | 64 | while (size != 0) { 65 | instances.put(size, size); 66 | size--; 67 | } 68 | 69 | return instances; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ClusterStatusReportExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfileProperties; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerClientFactory; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerServices; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.builders.PluginStatusReportViewBuilder; 23 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.SwarmCluster; 24 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ClusterStatusReportRequest; 25 | import com.spotify.docker.client.DockerClient; 26 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 27 | import freemarker.template.Template; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import static org.hamcrest.MatcherAssert.assertThat; 32 | import static org.hamcrest.Matchers.is; 33 | import static org.mockito.ArgumentMatchers.any; 34 | import static org.mockito.ArgumentMatchers.eq; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.when; 37 | 38 | public class ClusterStatusReportExecutorTest { 39 | 40 | private DockerClientFactory dockerClientFactory; 41 | private DockerClient dockerClient; 42 | private DockerServices dockerServices; 43 | private ClusterStatusReportRequest clusterStatusReportRequest; 44 | private ClusterProfileProperties profileProperties; 45 | 46 | @BeforeEach 47 | public void setUp() throws Exception { 48 | dockerClientFactory = mock(DockerClientFactory.class); 49 | dockerClient = mock(DockerClient.class); 50 | dockerServices = mock(DockerServices.class); 51 | clusterStatusReportRequest = mock(ClusterStatusReportRequest.class); 52 | profileProperties = new ClusterProfileProperties(); 53 | when(clusterStatusReportRequest.getClusterProfile()).thenReturn(profileProperties); 54 | when(dockerClientFactory.docker(profileProperties)).thenReturn(dockerClient); 55 | } 56 | 57 | @Test 58 | public void shouldBuildStatusReportView() throws Exception { 59 | final PluginStatusReportViewBuilder builder = mock(PluginStatusReportViewBuilder.class); 60 | final Template template = mock(Template.class); 61 | 62 | when(builder.getTemplate("status-report.template.ftlh")).thenReturn(template); 63 | when(builder.build(eq(template), any(SwarmCluster.class))).thenReturn("status-report"); 64 | final GoPluginApiResponse response = new ClusterStatusReportExecutor(clusterStatusReportRequest, dockerServices, dockerClientFactory, builder).execute(); 65 | 66 | assertThat(response.responseCode(), is(200)); 67 | assertThat(response.responseBody(), is("{\"view\":\"status-report\"}")); 68 | } 69 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/CreateAgentRequestExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.CreateAgentRequest; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static org.mockito.Mockito.*; 24 | 25 | public class CreateAgentRequestExecutorTest { 26 | @Test 27 | public void shouldAskDockerContainersToCreateAnAgent() throws Exception { 28 | CreateAgentRequest request = mock(CreateAgentRequest.class); 29 | AgentInstances agentInstances = mock(DockerServices.class); 30 | PluginRequest pluginRequest = mock(PluginRequest.class); 31 | ClusterProfileProperties clusterProfileProperties = mock(ClusterProfileProperties.class); 32 | when(request.getClusterProfileProperties()).thenReturn(clusterProfileProperties); 33 | new CreateAgentRequestExecutor(request, agentInstances, pluginRequest).execute(); 34 | 35 | verify(agentInstances).create(request, pluginRequest); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetCapabilitiesExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 20 | import org.hamcrest.Matchers; 21 | import org.json.JSONObject; 22 | import org.junit.jupiter.api.Test; 23 | import org.skyscreamer.jsonassert.JSONAssert; 24 | 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | 27 | public class GetCapabilitiesExecutorTest { 28 | 29 | @Test 30 | public void shouldAgentAndPluginSupportStatusReport() throws Exception { 31 | GoPluginApiResponse response = new GetCapabilitiesExecutor().execute(); 32 | 33 | assertThat(response.responseCode(), Matchers.is(200)); 34 | JSONObject expected = new JSONObject().put("supports_plugin_status_report", false); 35 | expected.put("supports_agent_status_report", true); 36 | expected.put("supports_cluster_status_report", true); 37 | JSONAssert.assertEquals(expected, new JSONObject(response.responseBody()), true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetClusterProfileViewRequestExecutorTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 4 | import com.google.gson.Gson; 5 | import com.google.gson.reflect.TypeToken; 6 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.lang.reflect.Type; 10 | import java.util.Map; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.*; 14 | 15 | public class GetClusterProfileViewRequestExecutorTest { 16 | 17 | @Test 18 | public void shouldRenderTheTemplateInJSON() throws Exception { 19 | GoPluginApiResponse response = new GetClusterProfileViewRequestExecutor().execute(); 20 | assertThat(response.responseCode(), is(200)); 21 | final Type type = new TypeToken>() { 22 | }.getType(); 23 | Map hashSet = new Gson().fromJson(response.responseBody(), type); 24 | assertThat(hashSet, hasEntry("template", Util.readResource("/plugin-settings.template.html"))); 25 | } 26 | 27 | @Test 28 | public void allFieldsShouldBePresentInView() { 29 | String template = Util.readResource("/plugin-settings.template.html"); 30 | 31 | for (Metadata field : GetClusterProfileMetadataExecutor.FIELDS) { 32 | assertThat(template, containsString("ng-model=\"" + field.getKey() + "\"")); 33 | assertThat(template, containsString("{{GOINPUTNAME[" + field.getKey() + 35 | "].$error.server}}")); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetPluginSettingsIconExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 20 | import com.google.gson.Gson; 21 | import com.google.gson.reflect.TypeToken; 22 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 23 | import org.apache.commons.codec.binary.Base64; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import java.util.Map; 27 | 28 | import static org.hamcrest.MatcherAssert.assertThat; 29 | import static org.hamcrest.Matchers.is; 30 | 31 | public class GetPluginSettingsIconExecutorTest { 32 | 33 | @Test 34 | public void rendersIconInBase64() throws Exception { 35 | GoPluginApiResponse response = new GetPluginSettingsIconExecutor().execute(); 36 | Map hashMap = new Gson().fromJson(response.responseBody(), new TypeToken>() { 37 | }.getType()); 38 | assertThat(hashMap.size(), is(2)); 39 | assertThat(hashMap.get("content_type"), is("image/png")); 40 | assertThat(Util.readResourceBytes("/docker-swarm.png"), is(Base64.decodeBase64(hashMap.get("data")))); 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GetProfileViewExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.Util; 20 | import com.google.gson.Gson; 21 | import com.google.gson.reflect.TypeToken; 22 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 23 | import org.jsoup.Jsoup; 24 | import org.jsoup.nodes.Document; 25 | import org.jsoup.select.Elements; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.Map; 29 | 30 | import static org.hamcrest.MatcherAssert.assertThat; 31 | import static org.hamcrest.Matchers.*; 32 | 33 | public class GetProfileViewExecutorTest { 34 | @Test 35 | public void shouldRenderTheTemplateInJSON() throws Exception { 36 | GoPluginApiResponse response = new GetProfileViewExecutor().execute(); 37 | assertThat(response.responseCode(), is(200)); 38 | Map hashSet = new Gson().fromJson(response.responseBody(), new TypeToken>() { 39 | }.getType()); 40 | assertThat(hashSet, hasEntry("template", Util.readResource("/profile.template.html"))); 41 | } 42 | 43 | @Test 44 | public void allFieldsShouldBePresentInView() { 45 | String template = Util.readResource("/profile.template.html"); 46 | final Document document = Jsoup.parse(template); 47 | 48 | for (Metadata field : GetProfileMetadataExecutor.FIELDS) { 49 | final Elements inputFieldForKey = document.getElementsByAttributeValue("ng-model", field.getKey()); 50 | assertThat(inputFieldForKey, hasSize(1)); 51 | 52 | final Elements spanToShowError = document.getElementsByAttributeValue("ng-class", "{'is-visible': GOINPUTNAME[" + field.getKey() + "].$error.server}"); 53 | assertThat(spanToShowError, hasSize(1)); 54 | assertThat(spanToShowError.attr("ng-show"), is("GOINPUTNAME[" + field.getKey() + "].$error.server")); 55 | assertThat(spanToShowError.text(), is("{{GOINPUTNAME[" + field.getKey() + "].$error.server}}")); 56 | } 57 | 58 | final Elements inputs = document.select("textarea,input,select"); 59 | assertThat(inputs, hasSize(GetProfileMetadataExecutor.FIELDS.size())); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/GoServerURLMetadataTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.nullValue; 25 | 26 | public class GoServerURLMetadataTest { 27 | private final GoServerURLMetadata goServerURLMetadata = new GoServerURLMetadata(); 28 | 29 | @Test 30 | public void shouldCheckBlankInput() { 31 | String result = goServerURLMetadata.doValidate(""); 32 | 33 | assertThat(result, is("Go Server URL must not be blank.")); 34 | } 35 | 36 | @Test 37 | public void shouldCheckIfStringIsValidUrl() { 38 | String result = goServerURLMetadata.doValidate("foobar"); 39 | 40 | assertThat(result, is("Go Server URL must be a valid URL (http://example.com:8153/go)")); 41 | } 42 | 43 | @Test 44 | public void shouldCheckIfSchemeIsValid() { 45 | String result = goServerURLMetadata.doValidate("example.com"); 46 | 47 | assertThat(result, is("Go Server URL must be a valid URL (http://example.com:8153/go)")); 48 | } 49 | 50 | @Test 51 | public void shouldCheckIfSchemeIsHTTPS() { 52 | String result = goServerURLMetadata.doValidate("ftp://example.com"); 53 | 54 | assertThat(result, is("Go Server URL must use http or https protocol")); 55 | } 56 | 57 | @Test 58 | public void shouldCheckForLocalhost() { 59 | String result = goServerURLMetadata.doValidate("https://localhost:8154/go"); 60 | 61 | assertThat(result, is("Go Server URL must not be localhost, since this gets resolved on the agents")); 62 | 63 | result = goServerURLMetadata.doValidate("https://127.0.0.1:8154/go"); 64 | 65 | assertThat(result, is("Go Server URL must not be localhost, since this gets resolved on the agents")); 66 | } 67 | 68 | @Test 69 | public void shouldCheckIfUrlEndsWithContextGo() { 70 | String result = goServerURLMetadata.doValidate("https://example.com:8154/"); 71 | assertThat(result, is("Go Server URL must be a valid URL ending with '/go' (http://example.com:8153/go)")); 72 | 73 | result = goServerURLMetadata.doValidate("http://example.com:8153/crimemastergogo"); 74 | assertThat(result, is("Go Server URL must be a valid URL ending with '/go' (http://example.com:8153/go)")); 75 | } 76 | 77 | @Test 78 | public void shouldReturnNullForValidUrls() { 79 | String result = goServerURLMetadata.doValidate("https://example.com:8154/go"); 80 | assertThat(result, is(nullValue())); 81 | 82 | result = goServerURLMetadata.doValidate("https://example.com:8154/go/"); 83 | assertThat(result, is(nullValue())); 84 | 85 | result = goServerURLMetadata.doValidate("https://example.com:8154/foo/go/"); 86 | assertThat(result, is(nullValue())); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/JobCompletionRequestExecutorTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 4 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 5 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.JobCompletionRequest; 6 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.ArgumentCaptor; 10 | import org.mockito.Captor; 11 | import org.mockito.InOrder; 12 | import org.mockito.Mock; 13 | 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | import static org.mockito.Mockito.inOrder; 19 | import static org.mockito.MockitoAnnotations.openMocks; 20 | 21 | public class JobCompletionRequestExecutorTest { 22 | @Mock 23 | private PluginRequest mockPluginRequest; 24 | @Mock 25 | private AgentInstances mockAgentInstances; 26 | 27 | @Captor 28 | private ArgumentCaptor> agentsArgumentCaptor; 29 | 30 | @BeforeEach 31 | public void setUp() { 32 | openMocks(this); 33 | } 34 | 35 | @Test 36 | public void shouldTerminateElasticAgentOnJobCompletion() throws Exception { 37 | JobIdentifier jobIdentifier = new JobIdentifier(100L); 38 | ClusterProfileProperties profileProperties = new ClusterProfileProperties(); 39 | String elasticAgentId = "agent-1"; 40 | JobCompletionRequest request = new JobCompletionRequest(elasticAgentId, jobIdentifier, profileProperties); 41 | JobCompletionRequestExecutor executor = new JobCompletionRequestExecutor(request, mockAgentInstances, mockPluginRequest); 42 | 43 | 44 | GoPluginApiResponse response = executor.execute(); 45 | 46 | InOrder inOrder = inOrder(mockPluginRequest, mockAgentInstances); 47 | 48 | inOrder.verify(mockPluginRequest).disableAgents(agentsArgumentCaptor.capture()); 49 | List agentsToDisabled = agentsArgumentCaptor.getValue(); 50 | assertEquals(1, agentsToDisabled.size()); 51 | assertEquals(elasticAgentId, agentsToDisabled.get(0).elasticAgentId()); 52 | inOrder.verify(mockAgentInstances).terminate(elasticAgentId, profileProperties); 53 | inOrder.verify(mockPluginRequest).deleteAgents(agentsArgumentCaptor.capture()); 54 | List agentsToDelete = agentsArgumentCaptor.getValue(); 55 | 56 | assertEquals(agentsToDisabled, agentsToDelete); 57 | 58 | assertEquals(200, response.responseCode()); 59 | assertTrue(response.responseBody().isEmpty()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/MemoryMetadataTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationError; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.junit.jupiter.api.Assertions.assertNotNull; 25 | import static org.junit.jupiter.api.Assertions.assertNull; 26 | 27 | public class MemoryMetadataTest { 28 | 29 | @Test 30 | public void shouldValidateMemoryBytes() { 31 | assertNull(new MemoryMetadata("Disk", false).validate("100mb")); 32 | 33 | ValidationError validationError = new MemoryMetadata("Disk", false).validate("xxx"); 34 | 35 | assertNotNull(validationError); 36 | assertThat(validationError.key(), is("Disk")); 37 | assertThat(validationError.message(), is("Invalid size: xxx")); 38 | } 39 | 40 | @Test 41 | public void shouldValidateMemoryBytesWhenRequireField() { 42 | ValidationError validationError = new MemoryMetadata("Disk", true).validate(null); 43 | 44 | assertNotNull(validationError); 45 | assertThat(validationError.key(), is("Disk")); 46 | assertThat(validationError.message(), is("Disk must not be blank.")); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ProfileValidateRequestExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.ClusterProfileProperties; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.DockerClientFactory; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ProfileValidateRequest; 22 | import com.spotify.docker.client.DockerClient; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.skyscreamer.jsonassert.JSONAssert; 26 | import org.skyscreamer.jsonassert.JSONCompareMode; 27 | 28 | import java.util.Collections; 29 | 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | public class ProfileValidateRequestExecutorTest { 34 | 35 | private DockerClientFactory dockerClientFactory; 36 | private DockerClient dockerClient; 37 | 38 | @BeforeEach 39 | public void setUp() throws Exception { 40 | dockerClientFactory = mock(DockerClientFactory.class); 41 | dockerClient = mock(DockerClient.class); 42 | 43 | when(dockerClientFactory.docker(mock(ClusterProfileProperties.class))).thenReturn(dockerClient); 44 | } 45 | 46 | @Test 47 | public void shouldBarfWhenUnknownKeysArePassed() throws Exception { 48 | ProfileValidateRequestExecutor executor = new ProfileValidateRequestExecutor(new ProfileValidateRequest(Collections.singletonMap("foo", "bar"))); 49 | String json = executor.execute().responseBody(); 50 | JSONAssert.assertEquals("[{\"message\":\"Image must not be blank.\",\"key\":\"Image\"},{\"key\":\"foo\",\"message\":\"Is an unknown property.\"}]", json, JSONCompareMode.NON_EXTENSIBLE); 51 | } 52 | 53 | @Test 54 | public void shouldValidateMandatoryKeys() throws Exception { 55 | ProfileValidateRequestExecutor executor = new ProfileValidateRequestExecutor(new ProfileValidateRequest(Collections.emptyMap())); 56 | String json = executor.execute().responseBody(); 57 | JSONAssert.assertEquals("[{\"message\":\"Image must not be blank.\",\"key\":\"Image\"}]", json, JSONCompareMode.NON_EXTENSIBLE); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/executors/ShouldAssignWorkRequestExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.*; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 21 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.CreateAgentRequest; 22 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests.ShouldAssignWorkRequest; 23 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | import java.util.UUID; 30 | 31 | import static org.hamcrest.MatcherAssert.assertThat; 32 | import static org.hamcrest.Matchers.is; 33 | import static org.mockito.Mockito.mock; 34 | 35 | public class ShouldAssignWorkRequestExecutorTest extends BaseTest { 36 | 37 | private DockerServices agentInstances; 38 | private DockerService instance; 39 | private final String environment = "production"; 40 | private Map properties = new HashMap<>(); 41 | private JobIdentifier jobIdentifier; 42 | 43 | @BeforeEach 44 | public void setUp() throws Exception { 45 | jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 46 | agentInstances = new DockerServices(); 47 | ClusterProfileProperties clusterProfiles = createClusterProfiles(); 48 | properties.put("foo", "bar"); 49 | properties.put("Image", "alpine:latest"); 50 | instance = agentInstances.create(new CreateAgentRequest(UUID.randomUUID().toString(), properties, environment, jobIdentifier, clusterProfiles), mock(PluginRequest.class)); 51 | services.add(instance.name()); 52 | } 53 | 54 | @Test 55 | public void shouldAssignWorkToContainerWithMatchingJobId() { 56 | ShouldAssignWorkRequest request = new ShouldAssignWorkRequest(new Agent(instance.name(), null, null, null), environment, properties, jobIdentifier, null); 57 | GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); 58 | assertThat(response.responseCode(), is(200)); 59 | assertThat(response.responseBody(), is("true")); 60 | } 61 | 62 | @Test 63 | public void shouldNotAssignWorkToContainerWithNotMatchingJobId() { 64 | JobIdentifier mismatchingJobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 999999L); 65 | ShouldAssignWorkRequest request = new ShouldAssignWorkRequest(new Agent(instance.name(), null, null, null), "FooEnv", properties, mismatchingJobIdentifier, null); 66 | GoPluginApiResponse response = new ShouldAssignWorkRequestExecutor(request, agentInstances).execute(); 67 | assertThat(response.responseCode(), is(200)); 68 | assertThat(response.responseBody(), is("false")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/metadata/HostMetadataTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.metadata; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.ValidationError; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.is; 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | 11 | public class HostMetadataTest { 12 | 13 | @Test 14 | public void shouldValidateHostConfig() { 15 | assertNull(new HostMetadata("Hosts", false, false).validate("10.0.0.1 hostname")); 16 | 17 | ValidationError validationError = new HostMetadata("Hosts", false, false) 18 | .validate("some-config"); 19 | 20 | assertNotNull(validationError); 21 | assertThat(validationError.key(), is("Hosts")); 22 | assertThat(validationError.message(), is("Host entry `some-config` is invalid. Must be in `IP-ADDRESS HOST-1 HOST-2...` format.")); 23 | } 24 | 25 | @Test 26 | public void shouldValidateHostConfigWhenRequireField() { 27 | ValidationError validationError = new HostMetadata("Hosts", true, false).validate(null); 28 | 29 | assertNotNull(validationError); 30 | assertThat(validationError.key(), is("Hosts")); 31 | assertThat(validationError.message(), is("Hosts must not be blank.")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/CapabilitiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.executors.GetCapabilitiesExecutor; 20 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; 21 | import org.junit.jupiter.api.Test; 22 | import org.skyscreamer.jsonassert.JSONAssert; 23 | 24 | public class CapabilitiesTest { 25 | @Test 26 | public void shouldSupportAgentAndPluginStatusReport() throws Exception { 27 | GoPluginApiResponse response = new GetCapabilitiesExecutor().execute(); 28 | 29 | String expectedJSON = "{\n" + 30 | " \"supports_plugin_status_report\":false\n," + 31 | " \"supports_cluster_status_report\":true\n," + 32 | " \"supports_agent_status_report\":true\n" + 33 | "}"; 34 | 35 | JSONAssert.assertEquals(expectedJSON, response.responseBody(), true); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/DockerNodeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.DockerNode; 20 | import com.spotify.docker.client.messages.swarm.*; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.is; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.when; 27 | 28 | public class DockerNodeTest { 29 | 30 | @Test 31 | public void shouldCreateDockerNodeFromNodeObject() { 32 | final Node node = mock(Node.class); 33 | 34 | when(node.id()).thenReturn("node-id"); 35 | 36 | final NodeStatus nodeStatus = mock(NodeStatus.class); 37 | when(node.status()).thenReturn(nodeStatus); 38 | when(nodeStatus.state()).thenReturn("ready"); 39 | when(nodeStatus.addr()).thenReturn("192.168.65.2"); 40 | 41 | mockNodeDescription(node, "moby", "17.09.0-ce-rc1", "x86_64", "linux"); 42 | 43 | when(node.spec()).thenReturn(NodeSpec.builder().name("node-name").availability("active").role("manager").build()); 44 | 45 | final ManagerStatus managerStatus = mock(ManagerStatus.class); 46 | when(node.managerStatus()).thenReturn(managerStatus); 47 | when(managerStatus.leader()).thenReturn(true); 48 | when(managerStatus.reachability()).thenReturn("reachable"); 49 | when(managerStatus.addr()).thenReturn("192.168.65.2:2377"); 50 | 51 | final DockerNode dockerNode = new DockerNode(node); 52 | 53 | assertThat(dockerNode.getId(), is("node-id")); 54 | assertThat(dockerNode.getRole(), is("Manager")); 55 | assertThat(dockerNode.getAvailability(), is("Active")); 56 | 57 | assertThat(dockerNode.getState(), is("Ready")); 58 | assertThat(dockerNode.getNodeIP(), is("192.168.65.2")); 59 | 60 | assertThat(dockerNode.getHostname(), is("moby")); 61 | assertThat(dockerNode.getEngineVersion(), is("17.09.0-ce-rc1")); 62 | assertThat(dockerNode.getArchitecture(), is("x86_64")); 63 | assertThat(dockerNode.getOs(), is("linux")); 64 | assertThat(dockerNode.getCpus(), is(4L)); 65 | assertThat(dockerNode.getMemory(), is("1.95 GB")); 66 | } 67 | 68 | private void mockNodeDescription(Node node, String hostname, String dockerVersion, String architecture, String os) { 69 | final NodeDescription nodeDescription = mock(NodeDescription.class); 70 | when(nodeDescription.hostname()).thenReturn(hostname); 71 | 72 | final Platform platform = mock(Platform.class); 73 | when(nodeDescription.platform()).thenReturn(platform); 74 | when(platform.architecture()).thenReturn(architecture); 75 | when(platform.os()).thenReturn(os); 76 | 77 | final Resources resources = mock(Resources.class); 78 | when(nodeDescription.resources()).thenReturn(resources); 79 | when(resources.nanoCpus()).thenReturn(4000000000L); 80 | when(resources.memoryBytes()).thenReturn(2095878144L); 81 | 82 | final EngineConfig engineConfig = mock(EngineConfig.class); 83 | when(nodeDescription.engine()).thenReturn(engineConfig); 84 | when(engineConfig.engineVersion()).thenReturn(dockerVersion); 85 | 86 | when(node.description()).thenReturn(nodeDescription); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/DockerTaskTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.DockerTask; 20 | import com.spotify.docker.client.messages.swarm.*; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import java.util.Collections; 24 | import java.util.Date; 25 | 26 | import static cd.go.contrib.elasticagents.dockerswarm.elasticagent.Constants.JOB_IDENTIFIER_LABEL_KEY; 27 | import static org.hamcrest.MatcherAssert.assertThat; 28 | import static org.hamcrest.Matchers.is; 29 | import static org.mockito.Mockito.mock; 30 | import static org.mockito.Mockito.when; 31 | 32 | public class DockerTaskTest { 33 | @Test 34 | public void shouldCreateDockerContainerFromContainerObject() { 35 | final Task task = mock(Task.class); 36 | final ContainerSpec containerSpec = ContainerSpec.builder().image("gocd-agent:latest").build(); 37 | final TaskSpec taskSpec = TaskSpec.builder().containerSpec(containerSpec).build(); 38 | final TaskStatus taskStatus = mock(TaskStatus.class); 39 | final Date createdAt = new Date(); 40 | final Service service = mock(Service.class); 41 | final ServiceSpec serviceSpec = ServiceSpec.builder() 42 | .labels(Collections.singletonMap(JOB_IDENTIFIER_LABEL_KEY, new JobIdentifier().toJson())) 43 | .taskTemplate(TaskSpec.builder().build()) 44 | .build(); 45 | 46 | when(task.id()).thenReturn("task-id"); 47 | when(task.createdAt()).thenReturn(createdAt); 48 | when(task.spec()).thenReturn(taskSpec); 49 | when(task.nodeId()).thenReturn("node-id"); 50 | when(task.serviceId()).thenReturn("service-id"); 51 | when(task.status()).thenReturn(taskStatus); 52 | when(taskStatus.state()).thenReturn("running"); 53 | when(service.spec()).thenReturn(serviceSpec); 54 | 55 | final DockerTask dockerTask = new DockerTask(task, service); 56 | 57 | assertThat(dockerTask.getId(), is("task-id")); 58 | assertThat(dockerTask.getCreated(), is(createdAt)); 59 | assertThat(dockerTask.getImage(), is("gocd-agent:latest")); 60 | assertThat(dockerTask.getServiceId(), is("service-id")); 61 | assertThat(dockerTask.getState(), is("Running")); 62 | assertThat(dockerTask.getNodeId(), is("node-id")); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/model/JobIdentifierTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.model; 18 | 19 | 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.junit.jupiter.api.Assertions.assertTrue; 25 | 26 | public class JobIdentifierTest { 27 | @Test 28 | public void shouldMatchJobIdentifier() { 29 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 30 | 31 | final JobIdentifier deserializedJobIdentifier = JobIdentifier.fromJson(jobIdentifier.toJson()); 32 | 33 | assertTrue(jobIdentifier.equals(deserializedJobIdentifier)); 34 | } 35 | 36 | @Test 37 | public void shouldCreateRepresentationFromJobIdentifier() { 38 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 39 | 40 | assertThat(jobIdentifier.getRepresentation(), is("up42/98765/stage_1/30000/job_1")); 41 | } 42 | 43 | @Test 44 | public void shouldCreatePipelineHistoryPageLink() { 45 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 46 | 47 | assertThat(jobIdentifier.getPipelineHistoryPageLink(), is("/go/tab/pipeline/history/up42")); 48 | } 49 | 50 | @Test 51 | public void shouldCreateVSMPageLink() { 52 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 53 | 54 | assertThat(jobIdentifier.getVsmPageLink(), is("/go/pipelines/value_stream_map/up42/98765")); 55 | } 56 | 57 | @Test 58 | public void shouldCreateStageDetailsPageLink() { 59 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 60 | 61 | assertThat(jobIdentifier.getStageDetailsPageLink(), is("/go/pipelines/up42/98765/stage_1/30000")); 62 | } 63 | 64 | @Test 65 | public void shouldCreateJobDetailsPageLink() { 66 | final JobIdentifier jobIdentifier = new JobIdentifier("up42", 98765L, "foo", "stage_1", "30000", "job_1", 876578L); 67 | 68 | assertThat(jobIdentifier.getJobDetailsPageLink(), is("/go/tab/build/detail/up42/98765/stage_1/30000/job_1")); 69 | } 70 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/reports/StatusReportGenerationErrorHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.reports; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.reports.StatusReportGenerationError; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static org.hamcrest.MatcherAssert.assertThat; 23 | import static org.hamcrest.Matchers.is; 24 | import static org.hamcrest.Matchers.startsWith; 25 | 26 | public class StatusReportGenerationErrorHandlerTest { 27 | @Test 28 | public void shouldConvertThrowableToStatusReportGenerationErrorObject() { 29 | final StatusReportGenerationError statusReportGenerationError = new StatusReportGenerationError(StatusReportGenerationException.noRunningService("foo")); 30 | 31 | assertThat(statusReportGenerationError.getMessage(), is("Service is not running.")); 32 | assertThat(statusReportGenerationError.getDescription(), startsWith("Can not find a running service for the provided elastic agent id 'foo'")); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/AgentStatusReportRequestTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.JobIdentifierMother; 4 | import com.google.gson.JsonObject; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.is; 12 | 13 | public class AgentStatusReportRequestTest { 14 | 15 | @Test 16 | public void shouldDeserializeFromJSON() { 17 | JsonObject jobIdentifierJson = JobIdentifierMother.getJson(); 18 | JsonObject jsonObject = new JsonObject(); 19 | jsonObject.addProperty("elastic_agent_id", "some-id"); 20 | jsonObject.add("job_identifier", jobIdentifierJson); 21 | 22 | AgentStatusReportRequest agentStatusReportRequest = AgentStatusReportRequest.fromJSON(jsonObject.toString()); 23 | 24 | AgentStatusReportRequest expected = new AgentStatusReportRequest("some-id", JobIdentifierMother.get(), null); 25 | assertThat(agentStatusReportRequest, is(expected)); 26 | } 27 | 28 | @Test 29 | public void shouldDeserializeFromJSONWithClusterProfile() { 30 | JsonObject jobIdentifierJson = JobIdentifierMother.getJson(); 31 | JsonObject jsonObject = new JsonObject(); 32 | jsonObject.addProperty("elastic_agent_id", "some-id"); 33 | jsonObject.add("job_identifier", jobIdentifierJson); 34 | JsonObject clusterJSON = new JsonObject(); 35 | clusterJSON.addProperty("go_server_url", "https://foo.com/go"); 36 | clusterJSON.addProperty("docker_uri", "unix:///var/run/docker.sock"); 37 | jsonObject.add("cluster_profile_properties", clusterJSON); 38 | 39 | AgentStatusReportRequest agentStatusReportRequest = AgentStatusReportRequest.fromJSON(jsonObject.toString()); 40 | 41 | Map expectedClusterProfile = new HashMap<>(); 42 | expectedClusterProfile.put("go_server_url", "https://foo.com/go"); 43 | expectedClusterProfile.put("docker_uri", "unix:///var/run/docker.sock"); 44 | 45 | AgentStatusReportRequest expected = new AgentStatusReportRequest("some-id", JobIdentifierMother.get(), expectedClusterProfile); 46 | assertThat(agentStatusReportRequest, is(expected)); 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ClusterProfileValidateRequestTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.HashMap; 7 | 8 | import static org.hamcrest.CoreMatchers.equalTo; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | 11 | public class ClusterProfileValidateRequestTest { 12 | @Test 13 | public void shouldDeserializeFromJSON() { 14 | 15 | String json ="{" + 16 | " \"go_server_url\":\"http://localhost\"," + 17 | " \"auto_register_timeout\":\"10\"," + 18 | " \"username\":\"Bob\"," + 19 | " \"password\":\"secret\"" + 20 | "}"; 21 | ClusterProfileValidateRequest request = ClusterProfileValidateRequest.fromJSON(json); 22 | HashMap expectedSettings = new HashMap<>(); 23 | expectedSettings.put("go_server_url", "http://localhost"); 24 | expectedSettings.put("auto_register_timeout", "10"); 25 | expectedSettings.put("username", "Bob"); 26 | expectedSettings.put("password", "secret"); 27 | assertThat(request.getProperties(), equalTo(expectedSettings)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ClusterStatusReportRequestTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import com.google.gson.JsonObject; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Collections; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.is; 10 | 11 | public class ClusterStatusReportRequestTest { 12 | @Test 13 | public void shouldDeserializeFromJSON() { 14 | JsonObject jsonObject = new JsonObject(); 15 | JsonObject clusterJSON = new JsonObject(); 16 | clusterJSON.addProperty("go_server_url", "https://go-server/go"); 17 | jsonObject.add("cluster_profile_properties", clusterJSON); 18 | 19 | ClusterStatusReportRequest clusterStatusReportRequest = ClusterStatusReportRequest.fromJSON(jsonObject.toString()); 20 | 21 | ClusterStatusReportRequest expected = new ClusterStatusReportRequest(Collections.singletonMap("go_server_url", "https://go-server/go")); 22 | assertThat(clusterStatusReportRequest, is(expected)); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/CreateAgentRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 18 | 19 | import org.hamcrest.Matchers; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.HashMap; 23 | 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.hamcrest.Matchers.equalTo; 26 | 27 | public class CreateAgentRequestTest { 28 | 29 | @Test 30 | public void shouldDeserializeFromJSON() { 31 | String json = "{\n" + 32 | " \"auto_register_key\": \"secret-key\",\n" + 33 | " \"elastic_agent_profile_properties\": {\n" + 34 | " \"key1\": \"value1\",\n" + 35 | " \"key2\": \"value2\"\n" + 36 | " },\n" + 37 | " \"environment\": \"prod\"\n" + 38 | "}"; 39 | 40 | CreateAgentRequest request = CreateAgentRequest.fromJSON(json); 41 | assertThat(request.autoRegisterKey(), equalTo("secret-key")); 42 | assertThat(request.environment(), equalTo("prod")); 43 | HashMap expectedProperties = new HashMap<>(); 44 | expectedProperties.put("key1", "value1"); 45 | expectedProperties.put("key2", "value2"); 46 | assertThat(request.properties(), Matchers.equalTo(expectedProperties)); 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/JobCompletionRequestTest.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | import static org.hamcrest.Matchers.is; 8 | 9 | public class JobCompletionRequestTest { 10 | 11 | @Test 12 | public void shouldDeserializeFromJSON() { 13 | 14 | String json = "{\n" + 15 | " \"elastic_agent_id\": \"ea1\",\n" + 16 | " \"job_identifier\": {\n" + 17 | " \"pipeline_name\": \"test-pipeline\",\n" + 18 | " \"pipeline_counter\": 1,\n" + 19 | " \"pipeline_label\": \"Test Pipeline\",\n" + 20 | " \"stage_name\": \"test-stage\",\n" + 21 | " \"stage_counter\": \"1\",\n" + 22 | " \"job_name\": \"test-job\",\n" + 23 | " \"job_id\": 100\n" + 24 | " }\n" + 25 | "}"; 26 | 27 | JobCompletionRequest request = JobCompletionRequest.fromJSON(json); 28 | 29 | JobIdentifier expectedJobIdentifier = new JobIdentifier("test-pipeline", 1L, "Test Pipeline", "test-stage", "1", "test-job", 100L); 30 | JobIdentifier actualJobIdentifier = request.jobIdentifier(); 31 | 32 | assertThat(actualJobIdentifier, is(expectedJobIdentifier)); 33 | 34 | assertThat(request.getElasticAgentId(), is("ea1")); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/requests/ShouldAssignWorkRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.requests; 18 | 19 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.Agent; 20 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils.JobIdentifierMother; 21 | import com.google.gson.JsonObject; 22 | import org.hamcrest.Matchers; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.util.HashMap; 26 | 27 | import static org.hamcrest.MatcherAssert.assertThat; 28 | import static org.hamcrest.Matchers.equalTo; 29 | import static org.hamcrest.Matchers.is; 30 | 31 | public class ShouldAssignWorkRequestTest { 32 | 33 | @Test 34 | public void shouldDeserializeFromJSON() { 35 | JsonObject agentJson = new JsonObject(); 36 | agentJson.addProperty("agent_id", "42"); 37 | agentJson.addProperty("agent_state", "Idle"); 38 | agentJson.addProperty("build_state", "Idle"); 39 | agentJson.addProperty("config_state", "Enabled"); 40 | 41 | JsonObject propertiesJson = new JsonObject(); 42 | propertiesJson.addProperty("property_name", "property_value"); 43 | 44 | JsonObject json = new JsonObject(); 45 | json.addProperty("environment", "prod"); 46 | json.add("agent", agentJson); 47 | json.add("job_identifier", JobIdentifierMother.getJson()); 48 | json.add("properties", propertiesJson); 49 | 50 | ShouldAssignWorkRequest request = ShouldAssignWorkRequest.fromJSON(json.toString()); 51 | 52 | assertThat(request.environment(), equalTo("prod")); 53 | assertThat(request.agent(), equalTo(new Agent("42", Agent.AgentState.Idle, Agent.BuildState.Idle, Agent.ConfigState.Enabled))); 54 | HashMap expectedProperties = new HashMap<>(); 55 | expectedProperties.put("property_name", "property_value"); 56 | assertThat(request.properties(), Matchers.equalTo(expectedProperties)); 57 | assertThat(request.jobIdentifier(), is(JobIdentifierMother.get())); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/utils/JobIdentifierMother.java: -------------------------------------------------------------------------------- 1 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils; 2 | 3 | import cd.go.contrib.elasticagents.dockerswarm.elasticagent.model.JobIdentifier; 4 | import com.google.gson.JsonObject; 5 | 6 | public class JobIdentifierMother { 7 | public static JsonObject getJson() { 8 | JsonObject jobIdentifierJson = new JsonObject(); 9 | jobIdentifierJson.addProperty("pipeline_name", "up42"); 10 | jobIdentifierJson.addProperty("pipeline_counter", 1); 11 | jobIdentifierJson.addProperty("pipeline_label", "label"); 12 | jobIdentifierJson.addProperty("stage_name", "stage"); 13 | jobIdentifierJson.addProperty("stage_counter", "1"); 14 | jobIdentifierJson.addProperty("job_name", "job1"); 15 | jobIdentifierJson.addProperty("job_id", 1); 16 | return jobIdentifierJson; 17 | } 18 | 19 | public static JobIdentifier get() { 20 | return new JobIdentifier("up42", 1L, "label", "stage", "1", "job1", 1L); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/cd/go/contrib/elasticagents/dockerswarm/elasticagent/utils/UtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Thoughtworks, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cd.go.contrib.elasticagents.dockerswarm.elasticagent.utils; 18 | 19 | 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.Collection; 23 | 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.hamcrest.Matchers.hasItems; 26 | import static org.hamcrest.Matchers.is; 27 | 28 | public class UtilTest { 29 | 30 | @Test 31 | public void shouldSplitIntoLinesAndTrimSpaces() { 32 | Collection strings = Util.splitIntoLinesAndTrimSpaces("FOO=BAR\n" + 33 | " X=Y\n" + 34 | "\n" + 35 | " A=B\r\n" + 36 | "\n" + 37 | "W=1"); 38 | 39 | assertThat(strings.size(), is(4)); 40 | assertThat(strings, hasItems("FOO=BAR", "X=Y", "A=B", "W=1")); 41 | } 42 | } 43 | --------------------------------------------------------------------------------