├── .github └── workflows │ ├── build.yml │ └── security-submit-dependecy-graph.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── VERSION ├── app ├── build.gradle ├── conf │ ├── jni-config.json │ ├── predefined-classes-config.json │ ├── proxy-config.json │ ├── reflect-config.json │ ├── resource-config.json │ └── serialization-config.json └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── seqera │ │ │ └── wave │ │ │ └── cli │ │ │ ├── App.java │ │ │ ├── Client.java │ │ │ ├── config │ │ │ └── RetryOpts.java │ │ │ ├── exception │ │ │ ├── BadClientResponseException.java │ │ │ ├── ClientConnectionException.java │ │ │ ├── IllegalCliArgumentException.java │ │ │ └── ReadyTimeoutException.java │ │ │ ├── json │ │ │ ├── ByteArrayAdapter.java │ │ │ ├── DateTimeAdapter.java │ │ │ ├── ImageNameStrategyAdapter.java │ │ │ ├── JsonHelper.java │ │ │ ├── LayerRefAdapter.java │ │ │ └── PathAdapter.java │ │ │ ├── model │ │ │ ├── ContainerInspectResponseEx.java │ │ │ ├── ContainerSpecEx.java │ │ │ ├── LayerRef.java │ │ │ └── SubmitContainerTokenResponseEx.java │ │ │ └── util │ │ │ ├── BuildInfo.java │ │ │ ├── Checkers.java │ │ │ ├── CliVersionProvider.java │ │ │ ├── DurationConverter.java │ │ │ ├── GptHelper.java │ │ │ ├── StreamHelper.java │ │ │ └── YamlHelper.java │ └── resources │ │ ├── META-INF │ │ └── build-info.properties │ │ ├── io │ │ └── seqera │ │ │ └── wave │ │ │ └── cli │ │ │ └── usage-examples.txt │ │ └── logback.xml │ └── test │ └── groovy │ └── io │ └── seqera │ └── wave │ └── cli │ ├── AppCondaOptsTest.groovy │ ├── AppConfigOptsTest.groovy │ ├── AppTest.groovy │ ├── ClientTest.groovy │ ├── json │ └── JsonHelperTest.groovy │ └── util │ ├── BuildInfoTest.groovy │ ├── CheckersTest.groovy │ ├── GptHelperTest.groovy │ ├── StreamHelperTest.groovy │ └── YamlHelperTest.groovy ├── buildSrc ├── build.gradle ├── settings.gradle └── src │ └── main │ └── groovy │ ├── io.seqera.wave.cli.java-application-conventions.gradle │ ├── io.seqera.wave.cli.java-common-conventions.gradle │ └── io.seqera.wave.cli.java-library-conventions.gradle ├── changelog.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jreleaser.yml ├── run.sh └── settings.gradle /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Wave-cli builds 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | - '!refs/tags/.*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | types: [opened, reopened, synchronize] 11 | branches: 12 | - '*' 13 | - '!refs/tags/.*' 14 | tags-ignore: 15 | - '*' 16 | jobs: 17 | build: 18 | name: Wave on ${{ matrix.os }} 19 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest-large, macos-latest-xlarge, windows-latest] 24 | include: 25 | - os: ubuntu-latest 26 | fatjar: true 27 | musl: true 28 | - os: macos-latest-large 29 | codesign: true 30 | - os: macos-latest-xlarge 31 | codesign: true 32 | steps: 33 | - name: Environment 34 | run: env | sort 35 | 36 | - uses: actions/checkout@v4 37 | 38 | - uses: graalvm/setup-graalvm@v1 39 | if: ${{ matrix.musl }} 40 | with: 41 | java-version: '21' 42 | github-token: ${{ secrets.GITHUB_TOKEN }} 43 | native-image-job-reports: 'true' 44 | native-image-musl: 'true' 45 | 46 | - uses: graalvm/setup-graalvm@v1 47 | if: ${{ !matrix.musl }} 48 | with: 49 | java-version: '21' 50 | github-token: ${{ secrets.GITHUB_TOKEN }} 51 | native-image-job-reports: 'true' 52 | 53 | - name: Run tests 54 | run: | 55 | ./gradlew test 56 | 57 | - name: Build fat JAR 58 | if: ${{ matrix.fatjar }} 59 | run: ./gradlew shadowJar 60 | 61 | - name: Upload fat JAR artifact 62 | if: ${{ matrix.fatjar }} 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: wave-jar 66 | path: ./app/build/libs/wave.jar 67 | 68 | - name: Build static native 69 | if: ${{ matrix.musl }} 70 | run: ./gradlew app:nativeCompile 71 | env: 72 | PLATFORM: linux-x86_64 73 | 74 | - name: Build native 75 | if: ${{ !matrix.musl }} 76 | run: ./gradlew app:nativeCompile 77 | 78 | - name: Codesign binary 79 | if: ${{ matrix.codesign && contains(github.event.head_commit.message, '[release]') && github.event.ref=='refs/heads/master'}} 80 | env: 81 | MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} 82 | MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} 83 | MACOS_CERTIFICATE_NAME: ${{ secrets.MACOS_CERTIFICATE_NAME }} 84 | MACOS_CI_KEYCHAIN_PWD: ${{ secrets.MACOS_CI_KEYCHAIN_PWD }} 85 | run: | 86 | echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 87 | security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain 88 | security default-keychain -s build.keychain 89 | security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain 90 | security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign 91 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain 92 | /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime ./app/build/native/nativeCompile/wave -v 93 | 94 | - name: Notarize binary 95 | if: ${{ matrix.codesign && contains(github.event.head_commit.message, '[release]') && github.event.ref=='refs/heads/master'}} 96 | env: 97 | MACOS_AC_API_CERT: ${{ secrets.MACOS_AC_API_CERT }} 98 | MACOS_AC_API_ISSUER_ID: ${{ secrets.MACOS_AC_API_ISSUER_ID }} 99 | MACOS_AC_API_KEY_ID: ${{ secrets.MACOS_AC_API_KEY_ID }} 100 | run: | 101 | echo $MACOS_AC_API_CERT | base64 --decode > AuthKey.p8 102 | xcrun notarytool store-credentials "notarytool-profile" -k AuthKey.p8 -d "$MACOS_AC_API_KEY_ID" -i "$MACOS_AC_API_ISSUER_ID" 103 | ditto -c -k --keepParent "./app/build/native/nativeCompile/wave" "notarization.zip" 104 | xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait 105 | 106 | - name: Upload Binary 107 | uses: actions/upload-artifact@v4 108 | with: 109 | name: nativeCompile-${{ matrix.os }} 110 | path: ./app/build/native/nativeCompile 111 | 112 | - name: Publish tests report 113 | if: failure() 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: test-reports-jdk-${{ matrix.java_version }} 117 | path: | 118 | **/build/reports/tests/test 119 | release: 120 | name: Release 121 | if: "contains(github.event.head_commit.message, '[release]') && github.event.ref=='refs/heads/master'" 122 | needs: [ build ] 123 | runs-on: ubuntu-latest 124 | steps: 125 | - name: Checkout repository 126 | uses: actions/checkout@v4 127 | with: 128 | fetch-depth: 0 129 | 130 | - name: Download all build artifacts 131 | uses: actions/download-artifact@v4 132 | 133 | - name: Setup Java for JReleaser 134 | uses: actions/setup-java@v4 135 | with: 136 | java-version: '21' 137 | distribution: 'adopt' 138 | 139 | - name: Version 140 | id: version 141 | run: | 142 | VERSION=$(cat ./VERSION) 143 | echo "VERSION=$VERSION" 144 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 145 | 146 | - name: Enable Homebrew for final release 147 | run: | 148 | VERSION=${{ steps.version.outputs.VERSION }} 149 | if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 150 | echo "Homebrew release enabled." 151 | echo "JRELEASER_PACKAGERS_BREW_ACTIVE=ALWAYS" >> $GITHUB_ENV 152 | else 153 | echo "Homebrew release disabled (pre-release detected)." 154 | echo "JRELEASER_PACKAGERS_BREW_ACTIVE=NEVER" >> $GITHUB_ENV 155 | fi 156 | 157 | - name: Run JReleaser 158 | uses: jreleaser/release-action@v2 159 | env: 160 | JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 161 | JRELEASER_PROJECT_VERSION: ${{ steps.version.outputs.VERSION }} 162 | JRELEASER_PACKAGERS_BREW_ACTIVE: ${{ env.JRELEASER_PACKAGERS_BREW_ACTIVE }} 163 | 164 | - name: JReleaser release output 165 | if: always() 166 | uses: actions/upload-artifact@v4 167 | with: 168 | name: jreleaser-release 169 | path: | 170 | out/jreleaser/trace.log 171 | out/jreleaser/output.properties 172 | ... 173 | -------------------------------------------------------------------------------- /.github/workflows/security-submit-dependecy-graph.yml: -------------------------------------------------------------------------------- 1 | name: Generate and submit dependency graph for wave-cli 2 | on: 3 | push: 4 | branches: ['master'] 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | dependency-submission: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: graalvm/setup-graalvm@v1 15 | with: 16 | java-version: 21 17 | 18 | - name: Generate and submit dependency graph for wave-cli 19 | uses: gradle/actions/dependency-submission@v4 20 | with: 21 | dependency-resolution-task: ":app:dependencies" 22 | additional-arguments: "--configuration runtimeClasspath" 23 | dependency-graph: generate-and-submit 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | .idea 4 | 5 | # Ignore Gradle build output directory 6 | build 7 | *.log 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | config ?= compileClasspath 2 | 3 | ifdef module 4 | mm = :${module}: 5 | else 6 | mm = :app: 7 | endif 8 | 9 | 10 | compile: 11 | ./gradlew assemble 12 | 13 | check: 14 | ./gradlew check 15 | 16 | image: 17 | ./gradlew jibDockerBuild 18 | 19 | push: 20 | # docker login 21 | docker login -u pditommaso -p ${DOCKER_PASSWORD} 22 | ./gradlew jib 23 | 24 | # 25 | # Show dependencies try `make deps config=runtime`, `make deps config=google` 26 | # 27 | deps: 28 | ./gradlew -q ${mm}dependencies --configuration ${config} 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wave CLI 2 | 3 | Command line tool for [Wave containers provisioning service](https://github.com/seqeralabs/wave). 4 | 5 | ### Summary 6 | 7 | Wave allows augmenting existing containers and building containers on demand so 8 | that it can be used in your Docker (replace-with-your-own-fav-container-engine) workflow. 9 | 10 | ### Features 11 | 12 | * Build container images on-demand for a given container file (aka Dockerfile); 13 | * Build container images on-demand based on one or more [Conda](https://conda.io/) packages; 14 | * Build container images for a specified target platform (currently linux/amd64 and linux/arm64); 15 | * Push and cache built containers to a user-provided container repository; 16 | * Push Singularity native container images to OCI-compliant registries; 17 | * Mirror (ie. copy) container images on-demand to a given registry; 18 | * Scan container images on-demand for security vulnerabilities; 19 | 20 | ### Installation 21 | 22 | 23 | #### Binary download 24 | 25 | Download the Wave pre-compiled binary for your operating system from the 26 | [GitHub releases page](https://github.com/seqeralabs/wave-cli/releases/latest) and give execute permission to it. 27 | 28 | #### Homebrew (Linux and macOS) 29 | 30 | If you use [Homebrew](https://brew.sh/), you can install like this: 31 | 32 | ```bash 33 | brew install seqeralabs/tap/wave-cli 34 | ``` 35 | 36 | ### Get started 37 | 38 | 1. Create a basic Dockerfile file (or use an existing one) 39 | 40 | ```bash 41 | cat << EOF > ./Dockerfile 42 | FROM alpine 43 | 44 | RUN apk update && apk add bash cowsay \ 45 | --update-cache \ 46 | --repository https://alpine.global.ssl.fastly.net/alpine/edge/community \ 47 | --repository https://alpine.global.ssl.fastly.net/alpine/edge/main \ 48 | --repository https://dl-3.alpinelinux.org/alpine/edge/testing 49 | EOF 50 | ``` 51 | 52 | 2. Run it provisioning the container on-the-fly 53 | 54 | 55 | ```bash 56 | docker run --rm $(wave -f ./Dockerfile) cowsay "Hello world" 57 | ``` 58 | 59 | 60 | ### Examples 61 | 62 | #### Augment a container image 63 | 64 | 1. Create a directory holding the files to be added to your container: 65 | 66 | ```bash 67 | mkdir -p new-layer/usr/local/bin 68 | printf 'echo Hello world!' > new-layer/usr/local/bin/hello.sh 69 | chmod +x new-layer/usr/local/bin/hello.sh 70 | ``` 71 | 72 | 2. Augment the container with the local layer and run with Docker: 73 | 74 | ```bash 75 | container=$(wave -i alpine --layer new-layer) 76 | docker run $container sh -c hello.sh 77 | ``` 78 | 79 | #### Build a container with Dockerfile 80 | 81 | 1. Create a Dockerfile for your container image: 82 | 83 | ```bash 84 | cat << EOF > ./Dockerfile 85 | FROM alpine 86 | ADD hello.sh /usr/local/bin/ 87 | EOF 88 | ``` 89 | 90 | 2. Create the build context directory: 91 | 92 | ```bash 93 | mkdir -p build-context/ 94 | printf 'echo Hello world!' > build-context/hello.sh 95 | chmod +x build-context/hello.sh 96 | ``` 97 | 98 | 3. Build and run the container on the fly: 99 | 100 | ```bash 101 | container=$(wave -f Dockerfile --context build-context) 102 | docker run $container sh -c hello.sh 103 | ``` 104 | 105 | #### Build a Conda multi-packages container 106 | 107 | ```bash 108 | container=$(wave --conda-package bamtools=2.5.2 --conda-package samtools=1.17) 109 | docker run $container sh -c 'bamtools --version && samtools --version' 110 | ``` 111 | 112 | #### Build a container by using a Conda environment file 113 | 114 | 1. Create the Conda environment file: 115 | 116 | ```bash 117 | cat << EOF > ./conda.yaml 118 | name: my-conda 119 | channels: 120 | - bioconda 121 | - conda-forge 122 | dependencies: 123 | - bamtools=2.5.2 124 | - samtools=1.17 125 | EOF 126 | ``` 127 | 128 | 2. Build and run the container using the Conda environment: 129 | 130 | ```bash 131 | container=$(wave --conda-file ./conda.yaml) 132 | docker run $container sh -c 'bamtools --version' 133 | ``` 134 | 135 | 136 | #### Build a container by using a Conda lock file 137 | 138 | ```bash 139 | container=$(wave --conda-package https://prefix.dev/envs/pditommaso/wave/6x60arx3od13/conda-lock.yml) 140 | docker run $container cowpy 'Hello, world!' 141 | ``` 142 | 143 | 144 | #### Build a Conda package container arm64 architecture 145 | 146 | ```bash 147 | container=$(wave --conda-package fastp --platform linux/arm64) 148 | docker run --platform linux/arm64 $container sh -c 'fastp --version' 149 | ``` 150 | 151 | #### Build a Singularity container using a Conda package and pushing to a OCI registry 152 | 153 | ```bash 154 | container=$(wave --singularity --conda-package bamtools=2.5.2 --build-repo docker.io/user/repo --freeze --await) 155 | singularity exec $container bamtools --version 156 | ``` 157 | 158 | #### Mirror (aka copy) a container to another registry 159 | 160 | ```bash 161 | container=$(wave -i ubuntu:latest --mirror --build-repo --tower-token --await) 162 | docker pull $container 163 | ``` 164 | 165 | #### Build a container and scan it for vulnerabilities 166 | 167 | ```bash 168 | wave --conda-package bamtools=2.5.2 --scan-mode required --await -o yaml 169 | ``` 170 | 171 | ### Development 172 | 173 | 1. Install GraalVM-Java 21.0.1 174 | 175 | ```bash 176 | sdk install java 21.0.1-graal 177 | ``` 178 | 179 | or if it's already installed 180 | 181 | ```bash 182 | sdk use java 21.0.1-graal 183 | ``` 184 | 185 | 2. Compile & run tests 186 | 187 | ```bash 188 | ./gradlew check 189 | ``` 190 | 191 | 3. Native compile 192 | 193 | ```bash 194 | ./gradlew app:nativeCompile 195 | ``` 196 | 197 | 4. Run the native binary 198 | 199 | ```bash 200 | ./app/build/native/nativeCompile/wave --version 201 | ``` 202 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.6.1 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | id 'io.seqera.wave.cli.java-application-conventions' 4 | id 'org.graalvm.buildtools.native' version '0.10.2' 5 | id 'com.github.johnrengelman.shadow' version '7.1.2' 6 | } 7 | 8 | java { 9 | toolchain { 10 | languageVersion = JavaLanguageVersion.of(21) 11 | } 12 | sourceCompatibility = 17 13 | targetCompatibility = 17 14 | } 15 | 16 | repositories { 17 | maven { url = "https://s3-eu-west-1.amazonaws.com/maven.seqera.io/releases" } 18 | maven { url = "https://s3-eu-west-1.amazonaws.com/maven.seqera.io/snapshots" } 19 | } 20 | 21 | dependencies { 22 | implementation 'io.seqera:wave-api:0.16.0' 23 | implementation 'io.seqera:wave-utils:0.15.1' 24 | implementation 'info.picocli:picocli:4.6.1' 25 | implementation 'com.squareup.moshi:moshi:1.15.2' 26 | implementation 'com.squareup.moshi:moshi-adapters:1.15.2' 27 | implementation 'dev.failsafe:failsafe:3.1.0' 28 | implementation 'org.apache.commons:commons-lang3:3.12.0' 29 | implementation 'org.yaml:snakeyaml:2.1' 30 | implementation 'dev.langchain4j:langchain4j-open-ai:0.29.0' 31 | implementation 'org.semver4j:semver4j:5.4.0' 32 | annotationProcessor 'info.picocli:picocli-codegen:4.6.1' 33 | // bump commons-io version to address security vulnerabilities 34 | runtimeOnly 'commons-io:commons-io:2.18.0' 35 | 36 | testImplementation "org.codehaus.groovy:groovy:3.0.21" 37 | testImplementation "org.codehaus.groovy:groovy-nio:3.0.21" 38 | testImplementation ("org.codehaus.groovy:groovy-test:3.0.21") 39 | testImplementation ("org.codehaus.groovy:groovy-json:3.0.21") 40 | testImplementation ("cglib:cglib-nodep:3.3.0") 41 | testImplementation ("org.objenesis:objenesis:3.2") 42 | testImplementation ("org.spockframework:spock-core:2.3-groovy-3.0") { exclude group: 'org.codehaus.groovy'; exclude group: 'net.bytebuddy' } 43 | testImplementation ('org.spockframework:spock-junit4:2.3-groovy-3.0') { exclude group: 'org.codehaus.groovy'; exclude group: 'net.bytebuddy' } 44 | } 45 | 46 | test { 47 | useJUnitPlatform() 48 | } 49 | 50 | /* 51 | * Copyright 2023-2025, Seqera Labs 52 | * 53 | * Licensed under the Apache License, Version 2.0 (the "License"); 54 | * you may not use this file except in compliance with the License. 55 | * You may obtain a copy of the License at 56 | * 57 | * http://www.apache.org/licenses/LICENSE-2.0 58 | * 59 | * Unless required by applicable law or agreed to in writing, software 60 | * distributed under the License is distributed on an "AS IS" BASIS, 61 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 62 | * See the License for the specific language governing permissions and 63 | * limitations under the License. 64 | * 65 | */ 66 | 67 | // read the version from the `VERSION` file 68 | version = new File(rootDir,'VERSION').text.trim() 69 | group = 'io.seqera' 70 | 71 | application { 72 | // Define the main class for the application. 73 | mainClass = 'io.seqera.wave.cli.App' 74 | // Run the Graalvm agent to resolve dynamic proxies configuration 75 | applicationDefaultJvmArgs = ["-agentlib:native-image-agent=config-merge-dir=conf/"] 76 | } 77 | 78 | shadowJar { 79 | archiveBaseName.set('wave') 80 | archiveClassifier.set('') 81 | archiveVersion.set('') 82 | } 83 | 84 | run { 85 | if( environment['JVM_OPTS'] ) { 86 | jvmArgs(environment['JVM_OPTS']) 87 | } 88 | } 89 | 90 | graalvmNative { 91 | binaries { 92 | main { 93 | imageName = 'wave' 94 | mainClass = 'io.seqera.wave.cli.App' 95 | configurationFileDirectories.from(file('conf')) 96 | 97 | if (System.env.getOrDefault("PLATFORM", "") == "linux-x86_64") { 98 | buildArgs(['--static', '--libc=musl', '--gc=G1', '-march=compatibility']) 99 | } 100 | 101 | javaLauncher = javaToolchains.launcherFor { 102 | languageVersion = JavaLanguageVersion.of(21) 103 | vendor = JvmVendorSpec.matching("Oracle Corporation") 104 | } 105 | buildArgs.add('--enable-url-protocols=https') 106 | buildArgs.add('-R:MaxHeapSize=100M') 107 | buildArgs.add('-R:MinHeapSize=10M') 108 | buildArgs.add('-R:MaxNewSize=25M') 109 | } 110 | } 111 | toolchainDetection = true 112 | testSupport = false 113 | } 114 | 115 | task buildInfo { 116 | doLast { 117 | def version = rootProject.file('VERSION').text.trim() 118 | def commitId = System.env.getOrDefault("GITHUB_SHA", "unknown").substring(0,7) 119 | def info = """\ 120 | name=${rootProject.name} 121 | version=${version} 122 | commitId=${commitId} 123 | """.stripIndent().toString() 124 | def f = file("src/main/resources/META-INF/build-info.properties") 125 | f.parentFile.mkdirs() 126 | f.text = info 127 | } 128 | } 129 | 130 | compileJava { 131 | dependsOn buildInfo 132 | } 133 | -------------------------------------------------------------------------------- /app/conf/jni-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"io.seqera.wave.cli.App", 4 | "methods":[{"name":"main","parameterTypes":["java.lang.String[]"] }] 5 | }, 6 | { 7 | "name":"java.lang.Boolean", 8 | "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }] 9 | }, 10 | { 11 | "name":"java.lang.String", 12 | "methods":[{"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }] 13 | }, 14 | { 15 | "name":"java.lang.System", 16 | "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /app/conf/predefined-classes-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type":"agent-extracted", 4 | "classes":[ 5 | ] 6 | } 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /app/conf/proxy-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "interfaces":["dev.ai4j.openai4j.OpenAiApi"] 4 | } 5 | ] 6 | -------------------------------------------------------------------------------- /app/conf/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"[B" 4 | }, 5 | { 6 | "name":"[Ljava.lang.String;" 7 | }, 8 | { 9 | "name":"[Lsun.security.pkcs.SignerInfo;" 10 | }, 11 | { 12 | "name":"apple.security.AppleProvider", 13 | "methods":[{"name":"","parameterTypes":[] }] 14 | }, 15 | { 16 | "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", 17 | "queryAllPublicMethods":true, 18 | "methods":[{"name":"","parameterTypes":[] }] 19 | }, 20 | { 21 | "name":"ch.qos.logback.classic.joran.SerializedModelConfigurator", 22 | "methods":[{"name":"","parameterTypes":[] }] 23 | }, 24 | { 25 | "name":"ch.qos.logback.classic.pattern.DateConverter", 26 | "methods":[{"name":"","parameterTypes":[] }] 27 | }, 28 | { 29 | "name":"ch.qos.logback.classic.pattern.LevelConverter", 30 | "methods":[{"name":"","parameterTypes":[] }] 31 | }, 32 | { 33 | "name":"ch.qos.logback.classic.pattern.LineSeparatorConverter", 34 | "methods":[{"name":"","parameterTypes":[] }] 35 | }, 36 | { 37 | "name":"ch.qos.logback.classic.pattern.LoggerConverter", 38 | "methods":[{"name":"","parameterTypes":[] }] 39 | }, 40 | { 41 | "name":"ch.qos.logback.classic.pattern.MessageConverter", 42 | "methods":[{"name":"","parameterTypes":[] }] 43 | }, 44 | { 45 | "name":"ch.qos.logback.classic.pattern.ThreadConverter", 46 | "methods":[{"name":"","parameterTypes":[] }] 47 | }, 48 | { 49 | "name":"ch.qos.logback.classic.util.DefaultJoranConfigurator", 50 | "methods":[{"name":"","parameterTypes":[] }] 51 | }, 52 | { 53 | "name":"ch.qos.logback.core.ConsoleAppender", 54 | "queryAllPublicMethods":true, 55 | "methods":[{"name":"","parameterTypes":[] }, {"name":"setWithJansi","parameterTypes":["boolean"] }] 56 | }, 57 | { 58 | "name":"ch.qos.logback.core.OutputStreamAppender", 59 | "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] 60 | }, 61 | { 62 | "name":"ch.qos.logback.core.encoder.Encoder", 63 | "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] 64 | }, 65 | { 66 | "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", 67 | "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] 68 | }, 69 | { 70 | "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", 71 | "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] 72 | }, 73 | { 74 | "name":"ch.qos.logback.core.spi.ContextAware", 75 | "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] 76 | }, 77 | { 78 | "name":"com.sun.crypto.provider.AESCipher$General", 79 | "methods":[{"name":"","parameterTypes":[] }] 80 | }, 81 | { 82 | "name":"com.sun.crypto.provider.ARCFOURCipher", 83 | "methods":[{"name":"","parameterTypes":[] }] 84 | }, 85 | { 86 | "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", 87 | "methods":[{"name":"","parameterTypes":[] }] 88 | }, 89 | { 90 | "name":"com.sun.crypto.provider.DESCipher", 91 | "methods":[{"name":"","parameterTypes":[] }] 92 | }, 93 | { 94 | "name":"com.sun.crypto.provider.DESedeCipher", 95 | "methods":[{"name":"","parameterTypes":[] }] 96 | }, 97 | { 98 | "name":"com.sun.crypto.provider.DHParameters", 99 | "methods":[{"name":"","parameterTypes":[] }] 100 | }, 101 | { 102 | "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", 103 | "methods":[{"name":"","parameterTypes":[] }] 104 | }, 105 | { 106 | "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", 107 | "methods":[{"name":"","parameterTypes":[] }] 108 | }, 109 | { 110 | "name":"com.sun.crypto.provider.HmacCore$HmacSHA384", 111 | "methods":[{"name":"","parameterTypes":[] }] 112 | }, 113 | { 114 | "name":"com.sun.crypto.provider.TlsKeyMaterialGenerator", 115 | "methods":[{"name":"","parameterTypes":[] }] 116 | }, 117 | { 118 | "name":"com.sun.crypto.provider.TlsMasterSecretGenerator", 119 | "methods":[{"name":"","parameterTypes":[] }] 120 | }, 121 | { 122 | "name":"com.sun.crypto.provider.TlsPrfGenerator$V12", 123 | "methods":[{"name":"","parameterTypes":[] }] 124 | }, 125 | { 126 | "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", 127 | "methods":[{"name":"","parameterTypes":[] }] 128 | }, 129 | { 130 | "name":"dev.ai4j.openai4j.chat.AssistantMessage", 131 | "allDeclaredFields":true, 132 | "methods":[{"name":"","parameterTypes":[] }] 133 | }, 134 | { 135 | "name":"dev.ai4j.openai4j.chat.ChatCompletionChoice", 136 | "allDeclaredFields":true, 137 | "methods":[{"name":"","parameterTypes":[] }] 138 | }, 139 | { 140 | "name":"dev.ai4j.openai4j.chat.ChatCompletionRequest", 141 | "allDeclaredFields":true, 142 | "methods":[{"name":"","parameterTypes":[] }] 143 | }, 144 | { 145 | "name":"dev.ai4j.openai4j.chat.ChatCompletionResponse", 146 | "allDeclaredFields":true, 147 | "methods":[{"name":"","parameterTypes":[] }] 148 | }, 149 | { 150 | "name":"dev.ai4j.openai4j.chat.Delta", 151 | "allDeclaredFields":true, 152 | "methods":[{"name":"","parameterTypes":[] }] 153 | }, 154 | { 155 | "name":"dev.ai4j.openai4j.chat.Function", 156 | "allDeclaredFields":true, 157 | "methods":[{"name":"","parameterTypes":[] }] 158 | }, 159 | { 160 | "name":"dev.ai4j.openai4j.chat.FunctionCall", 161 | "allDeclaredFields":true, 162 | "methods":[{"name":"","parameterTypes":[] }] 163 | }, 164 | { 165 | "name":"dev.ai4j.openai4j.chat.Parameters", 166 | "allDeclaredFields":true, 167 | "methods":[{"name":"","parameterTypes":[] }] 168 | }, 169 | { 170 | "name":"dev.ai4j.openai4j.chat.ResponseFormat", 171 | "allDeclaredFields":true, 172 | "methods":[{"name":"","parameterTypes":[] }] 173 | }, 174 | { 175 | "name":"dev.ai4j.openai4j.chat.Tool", 176 | "allDeclaredFields":true, 177 | "methods":[{"name":"","parameterTypes":[] }] 178 | }, 179 | { 180 | "name":"dev.ai4j.openai4j.chat.ToolCall", 181 | "allDeclaredFields":true, 182 | "methods":[{"name":"","parameterTypes":[] }] 183 | }, 184 | { 185 | "name":"dev.ai4j.openai4j.chat.ToolChoice", 186 | "allDeclaredFields":true, 187 | "methods":[{"name":"","parameterTypes":[] }] 188 | }, 189 | { 190 | "name":"dev.ai4j.openai4j.shared.Usage", 191 | "allDeclaredFields":true, 192 | "methods":[{"name":"","parameterTypes":[] }] 193 | }, 194 | { 195 | "name":"groovy.lang.Closure" 196 | }, 197 | { 198 | "name":"io.seqera.wave.api.BuildCompression", 199 | "allDeclaredFields":true, 200 | "methods":[{"name":"","parameterTypes":[] }] 201 | }, 202 | { 203 | "name":"io.seqera.wave.api.BuildCompression$Mode", 204 | "fields":[{"name":"estargz"}, {"name":"gzip"}, {"name":"zstd"}] 205 | }, 206 | { 207 | "name":"io.seqera.wave.api.BuildContext", 208 | "allDeclaredFields":true, 209 | "methods":[{"name":"","parameterTypes":[] }] 210 | }, 211 | { 212 | "name":"io.seqera.wave.api.ContainerConfig", 213 | "allDeclaredFields":true, 214 | "methods":[{"name":"","parameterTypes":[] }] 215 | }, 216 | { 217 | "name":"io.seqera.wave.api.ContainerInspectRequest", 218 | "allDeclaredFields":true, 219 | "methods":[{"name":"","parameterTypes":[] }] 220 | }, 221 | { 222 | "name":"io.seqera.wave.api.ContainerInspectResponse", 223 | "allDeclaredFields":true, 224 | "methods":[{"name":"","parameterTypes":[] }] 225 | }, 226 | { 227 | "name":"io.seqera.wave.api.ContainerLayer", 228 | "allDeclaredFields":true, 229 | "methods":[{"name":"","parameterTypes":[] }] 230 | }, 231 | { 232 | "name":"io.seqera.wave.api.ContainerStatus", 233 | "fields":[{"name":"BUILDING"}, {"name":"DONE"}, {"name":"PENDING"}, {"name":"SCANNING"}] 234 | }, 235 | { 236 | "name":"io.seqera.wave.api.ContainerStatusResponse", 237 | "allDeclaredFields":true, 238 | "methods":[{"name":"","parameterTypes":[] }] 239 | }, 240 | { 241 | "name":"io.seqera.wave.api.PackagesSpec", 242 | "allDeclaredFields":true, 243 | "methods":[{"name":"","parameterTypes":[] }] 244 | }, 245 | { 246 | "name":"io.seqera.wave.api.PackagesSpec$Type", 247 | "fields":[{"name":"CONDA"}, {"name":"SPACK"}] 248 | }, 249 | { 250 | "name":"io.seqera.wave.api.ScanLevel", 251 | "fields":[{"name":"CRITICAL"}, {"name":"HIGH"}, {"name":"LOW"}, {"name":"MEDIUM"}] 252 | }, 253 | { 254 | "name":"io.seqera.wave.api.ScanMode", 255 | "fields":[{"name":"async"}, {"name":"none"}, {"name":"required"}] 256 | }, 257 | { 258 | "name":"io.seqera.wave.api.ServiceInfo", 259 | "allDeclaredFields":true, 260 | "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] 261 | }, 262 | { 263 | "name":"io.seqera.wave.api.ServiceInfoResponse", 264 | "allDeclaredFields":true, 265 | "methods":[{"name":"","parameterTypes":[] }] 266 | }, 267 | { 268 | "name":"io.seqera.wave.api.SubmitContainerTokenRequest", 269 | "allDeclaredFields":true, 270 | "methods":[{"name":"","parameterTypes":[] }] 271 | }, 272 | { 273 | "name":"io.seqera.wave.api.SubmitContainerTokenResponse", 274 | "allDeclaredFields":true, 275 | "queryAllPublicMethods":true, 276 | "methods":[{"name":"","parameterTypes":[] }] 277 | }, 278 | { 279 | "name":"io.seqera.wave.api.SubmitContainerTokenResponseBeanInfo" 280 | }, 281 | { 282 | "name":"io.seqera.wave.api.SubmitContainerTokenResponseCustomizer" 283 | }, 284 | { 285 | "name":"io.seqera.wave.cli.App", 286 | "allDeclaredFields":true, 287 | "queryAllDeclaredMethods":true 288 | }, 289 | { 290 | "name":"io.seqera.wave.cli.json.ByteArrayAdapter", 291 | "queryAllDeclaredMethods":true 292 | }, 293 | { 294 | "name":"io.seqera.wave.cli.json.DateTimeAdapter", 295 | "queryAllDeclaredMethods":true, 296 | "methods":[{"name":"deserializeDuration","parameterTypes":["java.lang.String"] }, {"name":"deserializeInstant","parameterTypes":["java.lang.String"] }, {"name":"serializeInstant","parameterTypes":["java.time.Instant"] }] 297 | }, 298 | { 299 | "name":"io.seqera.wave.cli.json.ImageNameStrategyAdapter", 300 | "queryAllDeclaredMethods":true, 301 | "methods":[{"name":"toJson","parameterTypes":["io.seqera.wave.api.ImageNameStrategy"] }] 302 | }, 303 | { 304 | "name":"io.seqera.wave.cli.json.LayerRefAdapter", 305 | "queryAllDeclaredMethods":true, 306 | "methods":[{"name":"toJson","parameterTypes":["io.seqera.wave.core.spec.ObjectRef"] }] 307 | }, 308 | { 309 | "name":"io.seqera.wave.cli.json.PathAdapter", 310 | "queryAllDeclaredMethods":true 311 | }, 312 | { 313 | "name":"io.seqera.wave.cli.model.ContainerInspectResponseEx", 314 | "allDeclaredFields":true, 315 | "methods":[{"name":"","parameterTypes":[] }] 316 | }, 317 | { 318 | "name":"io.seqera.wave.cli.model.ContainerSpecEx", 319 | "allDeclaredFields":true 320 | }, 321 | { 322 | "name":"io.seqera.wave.cli.model.LayerRef", 323 | "allDeclaredFields":true, 324 | "methods":[{"name":"","parameterTypes":[] }] 325 | }, 326 | { 327 | "name":"io.seqera.wave.cli.model.SubmitContainerTokenResponseEx", 328 | "allDeclaredFields":true, 329 | "queryAllPublicMethods":true 330 | }, 331 | { 332 | "name":"io.seqera.wave.cli.model.SubmitContainerTokenResponseExBeanInfo" 333 | }, 334 | { 335 | "name":"io.seqera.wave.cli.model.SubmitContainerTokenResponseExCustomizer" 336 | }, 337 | { 338 | "name":"io.seqera.wave.cli.util.CliVersionProvider", 339 | "allDeclaredFields":true, 340 | "queryAllDeclaredMethods":true, 341 | "methods":[{"name":"","parameterTypes":[] }] 342 | }, 343 | { 344 | "name":"io.seqera.wave.config.CondaOpts", 345 | "allDeclaredFields":true, 346 | "methods":[{"name":"","parameterTypes":[] }] 347 | }, 348 | { 349 | "name":"io.seqera.wave.config.SpackOpts", 350 | "allDeclaredFields":true, 351 | "methods":[{"name":"","parameterTypes":[] }] 352 | }, 353 | { 354 | "name":"io.seqera.wave.core.spec.ConfigSpec", 355 | "allDeclaredFields":true, 356 | "methods":[{"name":"","parameterTypes":[] }] 357 | }, 358 | { 359 | "name":"io.seqera.wave.core.spec.ConfigSpec$Config", 360 | "allDeclaredFields":true, 361 | "methods":[{"name":"","parameterTypes":[] }] 362 | }, 363 | { 364 | "name":"io.seqera.wave.core.spec.ConfigSpec$Rootfs", 365 | "allDeclaredFields":true, 366 | "methods":[{"name":"","parameterTypes":[] }] 367 | }, 368 | { 369 | "name":"io.seqera.wave.core.spec.ContainerSpec", 370 | "allDeclaredFields":true, 371 | "methods":[{"name":"","parameterTypes":[] }] 372 | }, 373 | { 374 | "name":"io.seqera.wave.core.spec.ManifestSpec", 375 | "allDeclaredFields":true, 376 | "methods":[{"name":"","parameterTypes":[] }] 377 | }, 378 | { 379 | "name":"io.seqera.wave.core.spec.ObjectRef", 380 | "allDeclaredFields":true, 381 | "methods":[{"name":"","parameterTypes":[] }] 382 | }, 383 | { 384 | "name":"java.beans.PropertyVetoException" 385 | }, 386 | { 387 | "name":"java.io.FilePermission" 388 | }, 389 | { 390 | "name":"java.lang.Class", 391 | "methods":[{"name":"getRecordComponents","parameterTypes":[] }, {"name":"isRecord","parameterTypes":[] }] 392 | }, 393 | { 394 | "name":"java.lang.Object", 395 | "allDeclaredFields":true, 396 | "queryAllDeclaredMethods":true, 397 | "queryAllPublicMethods":true 398 | }, 399 | { 400 | "name":"java.lang.ObjectBeanInfo" 401 | }, 402 | { 403 | "name":"java.lang.ObjectCustomizer" 404 | }, 405 | { 406 | "name":"java.lang.RuntimePermission" 407 | }, 408 | { 409 | "name":"java.lang.String" 410 | }, 411 | { 412 | "name":"java.lang.System", 413 | "methods":[{"name":"console","parameterTypes":[] }] 414 | }, 415 | { 416 | "name":"java.lang.Thread", 417 | "fields":[{"name":"threadLocalRandomProbe"}] 418 | }, 419 | { 420 | "name":"java.lang.invoke.MethodHandles$Lookup", 421 | "methods":[{"name":"","parameterTypes":["java.lang.Class","int"] }] 422 | }, 423 | { 424 | "name":"java.lang.reflect.RecordComponent", 425 | "methods":[{"name":"getName","parameterTypes":[] }, {"name":"getType","parameterTypes":[] }] 426 | }, 427 | { 428 | "name":"java.net.NetPermission" 429 | }, 430 | { 431 | "name":"java.net.SocketPermission" 432 | }, 433 | { 434 | "name":"java.net.URLPermission", 435 | "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] 436 | }, 437 | { 438 | "name":"java.nio.file.Path" 439 | }, 440 | { 441 | "name":"java.nio.file.Paths", 442 | "methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }] 443 | }, 444 | { 445 | "name":"java.security.AlgorithmParametersSpi" 446 | }, 447 | { 448 | "name":"java.security.AllPermission" 449 | }, 450 | { 451 | "name":"java.security.KeyStoreSpi" 452 | }, 453 | { 454 | "name":"java.security.SecureRandomParameters" 455 | }, 456 | { 457 | "name":"java.security.SecurityPermission" 458 | }, 459 | { 460 | "name":"java.security.cert.CertStoreParameters" 461 | }, 462 | { 463 | "name":"java.security.interfaces.ECPrivateKey" 464 | }, 465 | { 466 | "name":"java.security.interfaces.ECPublicKey" 467 | }, 468 | { 469 | "name":"java.security.interfaces.RSAPrivateKey" 470 | }, 471 | { 472 | "name":"java.security.interfaces.RSAPublicKey" 473 | }, 474 | { 475 | "name":"java.sql.Connection" 476 | }, 477 | { 478 | "name":"java.sql.Date" 479 | }, 480 | { 481 | "name":"java.sql.Driver" 482 | }, 483 | { 484 | "name":"java.sql.DriverManager", 485 | "methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }] 486 | }, 487 | { 488 | "name":"java.sql.Time", 489 | "methods":[{"name":"","parameterTypes":["long"] }] 490 | }, 491 | { 492 | "name":"java.sql.Timestamp", 493 | "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] 494 | }, 495 | { 496 | "name":"java.time.Duration", 497 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 498 | }, 499 | { 500 | "name":"java.time.Instant", 501 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 502 | }, 503 | { 504 | "name":"java.time.LocalDate", 505 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 506 | }, 507 | { 508 | "name":"java.time.LocalDateTime", 509 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 510 | }, 511 | { 512 | "name":"java.time.LocalTime", 513 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 514 | }, 515 | { 516 | "name":"java.time.MonthDay", 517 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 518 | }, 519 | { 520 | "name":"java.time.OffsetDateTime", 521 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 522 | }, 523 | { 524 | "name":"java.time.OffsetTime", 525 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 526 | }, 527 | { 528 | "name":"java.time.Period", 529 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 530 | }, 531 | { 532 | "name":"java.time.Year", 533 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 534 | }, 535 | { 536 | "name":"java.time.YearMonth", 537 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 538 | }, 539 | { 540 | "name":"java.time.ZoneId", 541 | "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] 542 | }, 543 | { 544 | "name":"java.time.ZoneOffset", 545 | "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] 546 | }, 547 | { 548 | "name":"java.time.ZonedDateTime", 549 | "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] 550 | }, 551 | { 552 | "name":"java.util.Date" 553 | }, 554 | { 555 | "name":"java.util.ImmutableCollections$MapN", 556 | "methods":[{"name":"","parameterTypes":[] }] 557 | }, 558 | { 559 | "name":"java.util.PropertyPermission" 560 | }, 561 | { 562 | "name":"java.util.concurrent.ForkJoinTask", 563 | "fields":[{"name":"aux"}, {"name":"status"}] 564 | }, 565 | { 566 | "name":"java.util.concurrent.atomic.AtomicBoolean", 567 | "fields":[{"name":"value"}] 568 | }, 569 | { 570 | "name":"java.util.concurrent.atomic.AtomicReference", 571 | "fields":[{"name":"value"}] 572 | }, 573 | { 574 | "name":"java.util.concurrent.atomic.Striped64", 575 | "fields":[{"name":"base"}, {"name":"cellsBusy"}] 576 | }, 577 | { 578 | "name":"javax.security.auth.x500.X500Principal", 579 | "fields":[{"name":"thisX500Name"}], 580 | "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] 581 | }, 582 | { 583 | "name":"javax.smartcardio.CardPermission" 584 | }, 585 | { 586 | "name":"jdk.internal.misc.Unsafe" 587 | }, 588 | { 589 | "name":"kotlin.Metadata" 590 | }, 591 | { 592 | "name":"kotlin.jvm.internal.DefaultConstructorMarker" 593 | }, 594 | { 595 | "name":"picocli.CommandLine$AutoHelpMixin", 596 | "allDeclaredFields":true, 597 | "queryAllDeclaredMethods":true 598 | }, 599 | { 600 | "name":"sun.misc.Unsafe", 601 | "fields":[{"name":"theUnsafe"}], 602 | "methods":[{"name":"allocateInstance","parameterTypes":["java.lang.Class"] }] 603 | }, 604 | { 605 | "name":"sun.security.pkcs12.PKCS12KeyStore", 606 | "methods":[{"name":"","parameterTypes":[] }] 607 | }, 608 | { 609 | "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", 610 | "methods":[{"name":"","parameterTypes":[] }] 611 | }, 612 | { 613 | "name":"sun.security.provider.DSA$SHA224withDSA", 614 | "methods":[{"name":"","parameterTypes":[] }] 615 | }, 616 | { 617 | "name":"sun.security.provider.DSA$SHA256withDSA", 618 | "methods":[{"name":"","parameterTypes":[] }] 619 | }, 620 | { 621 | "name":"sun.security.provider.JavaKeyStore$DualFormatJKS", 622 | "methods":[{"name":"","parameterTypes":[] }] 623 | }, 624 | { 625 | "name":"sun.security.provider.JavaKeyStore$JKS", 626 | "methods":[{"name":"","parameterTypes":[] }] 627 | }, 628 | { 629 | "name":"sun.security.provider.NativePRNG", 630 | "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] 631 | }, 632 | { 633 | "name":"sun.security.provider.SHA", 634 | "methods":[{"name":"","parameterTypes":[] }] 635 | }, 636 | { 637 | "name":"sun.security.provider.SHA2$SHA224", 638 | "methods":[{"name":"","parameterTypes":[] }] 639 | }, 640 | { 641 | "name":"sun.security.provider.SHA2$SHA256", 642 | "methods":[{"name":"","parameterTypes":[] }] 643 | }, 644 | { 645 | "name":"sun.security.provider.SHA5$SHA384", 646 | "methods":[{"name":"","parameterTypes":[] }] 647 | }, 648 | { 649 | "name":"sun.security.provider.SHA5$SHA512", 650 | "methods":[{"name":"","parameterTypes":[] }] 651 | }, 652 | { 653 | "name":"sun.security.provider.X509Factory", 654 | "methods":[{"name":"","parameterTypes":[] }] 655 | }, 656 | { 657 | "name":"sun.security.provider.certpath.CollectionCertStore", 658 | "methods":[{"name":"","parameterTypes":["java.security.cert.CertStoreParameters"] }] 659 | }, 660 | { 661 | "name":"sun.security.provider.certpath.PKIXCertPathValidator", 662 | "methods":[{"name":"","parameterTypes":[] }] 663 | }, 664 | { 665 | "name":"sun.security.provider.certpath.SunCertPathBuilder", 666 | "methods":[{"name":"","parameterTypes":[] }] 667 | }, 668 | { 669 | "name":"sun.security.rsa.PSSParameters", 670 | "methods":[{"name":"","parameterTypes":[] }] 671 | }, 672 | { 673 | "name":"sun.security.rsa.RSAKeyFactory$Legacy", 674 | "methods":[{"name":"","parameterTypes":[] }] 675 | }, 676 | { 677 | "name":"sun.security.rsa.RSAPSSSignature", 678 | "methods":[{"name":"","parameterTypes":[] }] 679 | }, 680 | { 681 | "name":"sun.security.rsa.RSASignature$SHA224withRSA", 682 | "methods":[{"name":"","parameterTypes":[] }] 683 | }, 684 | { 685 | "name":"sun.security.rsa.RSASignature$SHA256withRSA", 686 | "methods":[{"name":"","parameterTypes":[] }] 687 | }, 688 | { 689 | "name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509", 690 | "methods":[{"name":"","parameterTypes":[] }] 691 | }, 692 | { 693 | "name":"sun.security.ssl.SSLContextImpl$DefaultSSLContext", 694 | "methods":[{"name":"","parameterTypes":[] }] 695 | }, 696 | { 697 | "name":"sun.security.ssl.SSLContextImpl$TLSContext", 698 | "methods":[{"name":"","parameterTypes":[] }] 699 | }, 700 | { 701 | "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", 702 | "methods":[{"name":"","parameterTypes":[] }] 703 | }, 704 | { 705 | "name":"sun.security.util.ObjectIdentifier" 706 | }, 707 | { 708 | "name":"sun.security.x509.AuthorityInfoAccessExtension", 709 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 710 | }, 711 | { 712 | "name":"sun.security.x509.AuthorityKeyIdentifierExtension", 713 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 714 | }, 715 | { 716 | "name":"sun.security.x509.BasicConstraintsExtension", 717 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 718 | }, 719 | { 720 | "name":"sun.security.x509.CRLDistributionPointsExtension", 721 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 722 | }, 723 | { 724 | "name":"sun.security.x509.CertificateExtensions" 725 | }, 726 | { 727 | "name":"sun.security.x509.CertificatePoliciesExtension", 728 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 729 | }, 730 | { 731 | "name":"sun.security.x509.ExtendedKeyUsageExtension", 732 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 733 | }, 734 | { 735 | "name":"sun.security.x509.IssuerAlternativeNameExtension", 736 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 737 | }, 738 | { 739 | "name":"sun.security.x509.KeyUsageExtension", 740 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 741 | }, 742 | { 743 | "name":"sun.security.x509.NetscapeCertTypeExtension", 744 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 745 | }, 746 | { 747 | "name":"sun.security.x509.PrivateKeyUsageExtension", 748 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 749 | }, 750 | { 751 | "name":"sun.security.x509.SubjectAlternativeNameExtension", 752 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 753 | }, 754 | { 755 | "name":"sun.security.x509.SubjectKeyIdentifierExtension", 756 | "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] 757 | } 758 | ] 759 | -------------------------------------------------------------------------------- /app/conf/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources":{ 3 | "includes":[{ 4 | "pattern":"\\QMETA-INF/build-info.properties\\E" 5 | }, { 6 | "pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" 7 | }, { 8 | "pattern":"\\QMETA-INF/services/dev.ai4j.openai4j.spi.OpenAiClientBuilderFactory\\E" 9 | }, { 10 | "pattern":"\\QMETA-INF/services/dev.langchain4j.model.openai.spi.OpenAiChatModelBuilderFactory\\E" 11 | }, { 12 | "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" 13 | }, { 14 | "pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E" 15 | }, { 16 | "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" 17 | }, { 18 | "pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" 19 | }, { 20 | "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" 21 | }, { 22 | "pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" 23 | }, { 24 | "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" 25 | }, { 26 | "pattern":"\\Qcom/knuddels/jtokkit/cl100k_base.tiktoken\\E" 27 | }, { 28 | "pattern":"\\Qio/seqera/wave/cli/usage-examples.txt\\E" 29 | }, { 30 | "pattern":"\\Qlogback-test.scmo\\E" 31 | }, { 32 | "pattern":"\\Qlogback-test.xml\\E" 33 | }, { 34 | "pattern":"\\Qlogback.scmo\\E" 35 | }, { 36 | "pattern":"\\Qlogback.xml\\E" 37 | }, { 38 | "pattern":"\\Qtemplates/conda/dockerfile-conda-file.txt\\E" 39 | }, { 40 | "pattern":"\\Qtemplates/conda/dockerfile-conda-packages.txt\\E" 41 | }, { 42 | "pattern":"\\Qtemplates/conda/singularityfile-conda-file.txt\\E" 43 | }, { 44 | "pattern":"\\Qtemplates/conda/singularityfile-conda-packages.txt\\E" 45 | }, { 46 | "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E" 47 | }, { 48 | "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/nfkc.nrm\\E" 49 | }, { 50 | "pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/uprops.icu\\E" 51 | }, { 52 | "pattern":"java.base:\\Qsun/net/idn/uidna.spp\\E" 53 | }, { 54 | "pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E" 55 | }]}, 56 | "bundles":[] 57 | } 58 | -------------------------------------------------------------------------------- /app/conf/serialization-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "types":[ 3 | ], 4 | "lambdaCapturingTypes":[ 5 | ], 6 | "proxies":[ 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/Client.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli; 19 | 20 | import java.io.IOException; 21 | import java.net.MalformedURLException; 22 | import java.net.URI; 23 | import java.net.URL; 24 | import java.net.http.HttpClient; 25 | import java.net.http.HttpRequest; 26 | import java.net.http.HttpResponse; 27 | import java.time.Duration; 28 | import java.time.Instant; 29 | import java.time.temporal.ChronoUnit; 30 | import java.util.List; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.function.Predicate; 33 | 34 | import dev.failsafe.Failsafe; 35 | import dev.failsafe.FailsafeException; 36 | import dev.failsafe.RetryPolicy; 37 | import dev.failsafe.event.EventListener; 38 | import dev.failsafe.event.ExecutionAttemptedEvent; 39 | import dev.failsafe.function.CheckedSupplier; 40 | import io.seqera.wave.api.ContainerInspectRequest; 41 | import io.seqera.wave.api.ContainerInspectResponse; 42 | import io.seqera.wave.api.ContainerStatus; 43 | import io.seqera.wave.api.ContainerStatusResponse; 44 | import io.seqera.wave.api.ServiceInfo; 45 | import io.seqera.wave.api.ServiceInfoResponse; 46 | import io.seqera.wave.api.SubmitContainerTokenRequest; 47 | import io.seqera.wave.api.SubmitContainerTokenResponse; 48 | import io.seqera.wave.cli.config.RetryOpts; 49 | import io.seqera.wave.cli.exception.BadClientResponseException; 50 | import io.seqera.wave.cli.exception.ClientConnectionException; 51 | import io.seqera.wave.cli.exception.ReadyTimeoutException; 52 | import io.seqera.wave.cli.json.JsonHelper; 53 | import org.apache.commons.lang3.StringUtils; 54 | import org.slf4j.Logger; 55 | import org.slf4j.LoggerFactory; 56 | 57 | /** 58 | * Bare simple client for Wave service 59 | * 60 | * @author Paolo Di Tommaso 61 | */ 62 | public class Client { 63 | 64 | private static final Logger log = LoggerFactory.getLogger(Client.class); 65 | 66 | final static private String[] REQUEST_HEADERS = new String[]{ 67 | "Content-Type","application/json", 68 | "Accept","application/json", 69 | "Accept","application/vnd.oci.image.index.v1+json", 70 | "Accept","application/vnd.oci.image.manifest.v1+json", 71 | "Accept","application/vnd.docker.distribution.manifest.v1+prettyjws", 72 | "Accept","application/vnd.docker.distribution.manifest.v2+json", 73 | "Accept","application/vnd.docker.distribution.manifest.list.v2+json" }; 74 | 75 | final static private List SERVER_ERRORS = List.of(249,502,503,504); 76 | 77 | public static String DEFAULT_ENDPOINT = "https://wave.seqera.io"; 78 | 79 | private HttpClient httpClient; 80 | 81 | private String endpoint = DEFAULT_ENDPOINT; 82 | 83 | 84 | Client() { 85 | // create http client 86 | this.httpClient = HttpClient.newBuilder() 87 | .version(HttpClient.Version.HTTP_1_1) 88 | .followRedirects(HttpClient.Redirect.NEVER) 89 | .connectTimeout(Duration.ofSeconds(30)) 90 | .build(); 91 | } 92 | 93 | ContainerInspectResponse inspect(ContainerInspectRequest request) { 94 | final String body = JsonHelper.toJson(request); 95 | final URI uri = URI.create(endpoint + "/v1alpha1/inspect"); 96 | log.debug("Wave request: {} - payload: {}", uri, request); 97 | final HttpRequest req = HttpRequest.newBuilder() 98 | .uri(uri) 99 | .headers("Content-Type","application/json") 100 | .POST(HttpRequest.BodyPublishers.ofString(body)) 101 | .build(); 102 | 103 | try { 104 | final HttpResponse resp = httpSend(req); 105 | log.debug("Wave response: statusCode={}; body={}", resp.statusCode(), resp.body()); 106 | if( resp.statusCode()==200 ) 107 | return JsonHelper.fromJson(resp.body(), ContainerInspectResponse.class); 108 | else { 109 | String msg = String.format("Wave invalid response: [%s] %s", resp.statusCode(), resp.body()); 110 | throw new BadClientResponseException(msg); 111 | } 112 | } 113 | catch (IOException | FailsafeException e) { 114 | throw new ClientConnectionException("Unable to connect Wave service: " + endpoint, e); 115 | } 116 | } 117 | 118 | SubmitContainerTokenResponse submit(SubmitContainerTokenRequest request) { 119 | final String body = JsonHelper.toJson(request); 120 | final URI uri = URI.create(endpoint + "/v1alpha2/container"); 121 | log.debug("Wave request: {} - payload: {}", uri, request); 122 | final HttpRequest req = HttpRequest.newBuilder() 123 | .uri(uri) 124 | .headers("Content-Type","application/json") 125 | .POST(HttpRequest.BodyPublishers.ofString(body)) 126 | .build(); 127 | 128 | try { 129 | final HttpResponse resp = httpSend(req); 130 | log.debug("Wave response: statusCode={}; body={}", resp.statusCode(), resp.body()); 131 | if( resp.statusCode()==200 ) 132 | return JsonHelper.fromJson(resp.body(), SubmitContainerTokenResponse.class); 133 | else { 134 | String msg = String.format("Wave invalid response: [%s] %s", resp.statusCode(), resp.body()); 135 | throw new BadClientResponseException(msg); 136 | } 137 | } 138 | catch (IOException | FailsafeException e) { 139 | throw new ClientConnectionException("Unable to connect Wave service: " + endpoint, e); 140 | } 141 | } 142 | 143 | public Client withEndpoint(String endpoint) { 144 | if( !StringUtils.isEmpty(endpoint) ) { 145 | this.endpoint = StringUtils.stripEnd(endpoint, "/"); 146 | } 147 | return this; 148 | } 149 | 150 | protected RetryPolicy retryPolicy(Predicate cond) { 151 | final RetryOpts cfg = new RetryOpts(); 152 | final EventListener> listener = new EventListener>() { 153 | @Override 154 | public void accept(ExecutionAttemptedEvent event) throws Throwable { 155 | log.debug("Wave connection failure - attempt: " + event.getAttemptCount(), event.getLastFailure()); 156 | } 157 | }; 158 | 159 | return RetryPolicy.builder() 160 | .handleIf(cond) 161 | .withBackoff(cfg.delay.toMillis(), cfg.maxDelay.toMillis(), ChronoUnit.MILLIS) 162 | .withMaxAttempts(cfg.maxAttempts) 163 | .withJitter(cfg.jitter) 164 | .onRetry(listener) 165 | .build(); 166 | } 167 | 168 | protected T safeApply(CheckedSupplier action) { 169 | final Predicate cond = (e -> e instanceof IOException); 170 | final RetryPolicy policy = retryPolicy(cond); 171 | return Failsafe.with(policy).get(action); 172 | } 173 | 174 | protected HttpResponse httpSend(HttpRequest req) { 175 | return safeApply(() -> { 176 | HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); 177 | if( SERVER_ERRORS.contains(resp.statusCode())) { 178 | // throws an IOException so that the condition is handled by the retry policy 179 | throw new IOException("Unexpected server response code ${resp.statusCode()} - message: ${resp.body()}"); 180 | } 181 | return resp; 182 | }); 183 | } 184 | 185 | protected String protocol(String endpoint) { 186 | if( StringUtils.isEmpty(endpoint) ) 187 | return "https://"; 188 | try { 189 | return new URL(endpoint).getProtocol() + "://"; 190 | } catch (MalformedURLException e) { 191 | throw new RuntimeException("Invalid endpoint URL: " + endpoint, e); 192 | } 193 | } 194 | 195 | protected URI imageToManifestUri(String image) { 196 | final int p = image.indexOf('/'); 197 | if( p==-1 ) throw new IllegalArgumentException("Invalid container name: "+image); 198 | final String result = protocol(endpoint) + image.substring(0,p) + "/v2" + image.substring(p).replace(":","/manifests/"); 199 | return URI.create(result); 200 | } 201 | 202 | ContainerStatusResponse awaitCompletion(String requestId, Duration await) { 203 | if( StringUtils.isEmpty(requestId) ) 204 | throw new IllegalArgumentException("Argument 'requestId' cannot be empty"); 205 | log.debug("Waiting for build completion: {} - timeout: {} Seconds", requestId, await.toSeconds()); 206 | final long startTime = Instant.now().toEpochMilli(); 207 | while ( true ) { 208 | final ContainerStatusResponse response = checkStatus(requestId); 209 | if( response.status==ContainerStatus.DONE ) 210 | return response; 211 | 212 | if (System.currentTimeMillis() - startTime > await.toMillis()) { 213 | String msg = String.format("Container provisioning did not complete within the max await time (%s)", await.toString()); 214 | throw new ReadyTimeoutException(msg); 215 | } 216 | // await 217 | try { 218 | TimeUnit.SECONDS.sleep(10); 219 | } 220 | catch (InterruptedException e) { 221 | throw new RuntimeException("Execution interrupted", e); 222 | } 223 | } 224 | } 225 | 226 | protected ContainerStatusResponse checkStatus(String requestId) { 227 | final String statusEndpoint = endpoint + "/v1alpha2/container/"+requestId+"/status"; 228 | final HttpRequest req = HttpRequest.newBuilder() 229 | .uri(URI.create(statusEndpoint)) 230 | .headers("Content-Type","application/json") 231 | .GET() 232 | .build(); 233 | 234 | try { 235 | final HttpResponse resp = httpSend(req); 236 | log.debug("Wave response: statusCode={}; body={}", resp.statusCode(), resp.body()); 237 | if( resp.statusCode()==200 ) { 238 | return JsonHelper.fromJson(resp.body(), ContainerStatusResponse.class); 239 | } 240 | else { 241 | String msg = String.format("Wave invalid response: [%s] %s", resp.statusCode(), resp.body()); 242 | throw new BadClientResponseException(msg); 243 | } 244 | } 245 | catch (IOException | FailsafeException e) { 246 | throw new ClientConnectionException("Unable to connect Wave service: " + endpoint, e); 247 | } 248 | } 249 | 250 | ServiceInfo serviceInfo() { 251 | final URI uri = URI.create(endpoint + "/service-info"); 252 | final HttpRequest req = HttpRequest.newBuilder() 253 | .uri(uri) 254 | .headers("Content-Type","application/json") 255 | .GET() 256 | .build(); 257 | 258 | try { 259 | final HttpResponse resp = httpSend(req); 260 | log.debug("Wave response: statusCode={}; body={}", resp.statusCode(), resp.body()); 261 | if( resp.statusCode()==200 ) 262 | return JsonHelper.fromJson(resp.body(), ServiceInfoResponse.class).serviceInfo; 263 | else { 264 | String msg = String.format("Wave invalid response: [%s] %s", resp.statusCode(), resp.body()); 265 | throw new BadClientResponseException(msg); 266 | } 267 | } 268 | catch (IOException | FailsafeException e) { 269 | throw new ClientConnectionException("Unable to connect Wave service: " + endpoint, e); 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/config/RetryOpts.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.config; 19 | 20 | import java.time.Duration; 21 | 22 | /** 23 | * HTTP retry options 24 | * 25 | * @author Paolo Di Tommaso 26 | */ 27 | public class RetryOpts { 28 | 29 | public Duration delay = Duration.ofMillis(150); 30 | 31 | public Integer maxAttempts = 5; 32 | 33 | public Duration maxDelay = Duration.ofSeconds(90); 34 | 35 | public double jitter = 0.25; 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/exception/BadClientResponseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.exception; 19 | 20 | /** 21 | * Model a client response http error 22 | * 23 | * @author Paolo Di Tommaso 24 | */ 25 | public class BadClientResponseException extends RuntimeException { 26 | 27 | public BadClientResponseException(String message) { 28 | super(message); 29 | } 30 | 31 | public BadClientResponseException(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/exception/ClientConnectionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.exception; 19 | 20 | /** 21 | * Model a generic client connection exception 22 | * 23 | * @author Paolo Di Tommaso 24 | */ 25 | public class ClientConnectionException extends RuntimeException { 26 | 27 | public ClientConnectionException(String message) { 28 | super(message); 29 | } 30 | 31 | public ClientConnectionException(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/exception/IllegalCliArgumentException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.exception; 19 | 20 | /** 21 | * Exception thrown to report a CLI validation error 22 | * 23 | * @author Paolo Di Tommaso 24 | */ 25 | public class IllegalCliArgumentException extends RuntimeException { 26 | 27 | public IllegalCliArgumentException(String message) { 28 | super(message); 29 | } 30 | 31 | public IllegalCliArgumentException(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/exception/ReadyTimeoutException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.exception; 19 | 20 | /** 21 | * Exception thrown when a container do not reach a ready status with the max expected time 22 | * 23 | * @author Paolo Di Tommaso 24 | */ 25 | public class ReadyTimeoutException extends RuntimeException { 26 | 27 | public ReadyTimeoutException(String message) { 28 | super(message); 29 | } 30 | 31 | public ReadyTimeoutException(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/ByteArrayAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import java.util.Base64; 21 | 22 | import com.squareup.moshi.FromJson; 23 | import com.squareup.moshi.ToJson; 24 | 25 | /** 26 | * Moshi adapter for JSON serialization 27 | * 28 | * @author Paolo Di Tommaso 29 | */ 30 | class ByteArrayAdapter { 31 | @ToJson 32 | public String serialize(byte[] data) { 33 | return Base64.getEncoder().encodeToString(data); 34 | } 35 | 36 | @FromJson 37 | public byte[] deserialize(String data) { 38 | return Base64.getDecoder().decode(data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/DateTimeAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import java.time.Duration; 21 | import java.time.Instant; 22 | import java.time.format.DateTimeFormatter; 23 | 24 | import com.squareup.moshi.FromJson; 25 | import com.squareup.moshi.ToJson; 26 | /** 27 | * Date time adapter for Moshi JSON serialisation 28 | * 29 | * @author Paolo Di Tommaso 30 | */ 31 | class DateTimeAdapter { 32 | 33 | @ToJson 34 | public String serializeInstant(Instant value) { 35 | return value!=null ? DateTimeFormatter.ISO_INSTANT.format(value) : null; 36 | } 37 | 38 | @FromJson 39 | public Instant deserializeInstant(String value) { 40 | return value!=null ? Instant.from(DateTimeFormatter.ISO_INSTANT.parse(value)) : null; 41 | } 42 | 43 | @ToJson 44 | public String serializeDuration(Duration value) { 45 | return value != null ? String.valueOf(value.toNanos()) : null; 46 | } 47 | 48 | @FromJson 49 | public Duration deserializeDuration(String value) { 50 | if( value==null ) 51 | return null; 52 | // for backward compatibility duration may be encoded as float value 53 | // instead of long (number of nanoseconds) as expected 54 | final Long val0 = value.contains(".") ? Math.round(Double.valueOf(value) * 1_000_000_000) : Long.valueOf(value); 55 | return Duration.ofNanos(val0); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/ImageNameStrategyAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import com.squareup.moshi.FromJson; 21 | import com.squareup.moshi.ToJson; 22 | import io.seqera.wave.api.ImageNameStrategy; 23 | /** 24 | * Image Name Strategy adapter for Moshi JSON serialisation 25 | * 26 | * @author Munish Chouhan 27 | */ 28 | public class ImageNameStrategyAdapter { 29 | 30 | @ToJson 31 | public String toJson(ImageNameStrategy strategy) { 32 | return strategy.name(); 33 | } 34 | 35 | @FromJson 36 | public ImageNameStrategy fromJson(String strategy) { 37 | return ImageNameStrategy.valueOf(strategy); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/JsonHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import java.io.IOException; 21 | 22 | import com.squareup.moshi.JsonAdapter; 23 | import com.squareup.moshi.Moshi; 24 | import io.seqera.wave.api.ContainerInspectRequest; 25 | import io.seqera.wave.api.SubmitContainerTokenRequest; 26 | import io.seqera.wave.api.SubmitContainerTokenResponse; 27 | import io.seqera.wave.cli.model.ContainerInspectResponseEx; 28 | 29 | /** 30 | * Helper class to encode and decode JSON payloads 31 | * 32 | * @author Paolo Di Tommaso 33 | */ 34 | public class JsonHelper { 35 | 36 | private static final Moshi moshi = new Moshi.Builder() 37 | .add(new ByteArrayAdapter()) 38 | .add(new DateTimeAdapter()) 39 | .add(new PathAdapter()) 40 | .add(new LayerRefAdapter()) 41 | .add(new ImageNameStrategyAdapter()) 42 | .build(); 43 | 44 | public static String toJson(SubmitContainerTokenRequest request) { 45 | JsonAdapter adapter = moshi.adapter(SubmitContainerTokenRequest.class); 46 | return adapter.toJson(request); 47 | } 48 | 49 | public static String toJson(SubmitContainerTokenResponse response) { 50 | JsonAdapter adapter = moshi.adapter(SubmitContainerTokenResponse.class); 51 | return adapter.toJson(response); 52 | } 53 | 54 | public static String toJson(ContainerInspectRequest request) { 55 | JsonAdapter adapter = moshi.adapter(ContainerInspectRequest.class); 56 | return adapter.toJson(request); 57 | } 58 | 59 | public static String toJson(ContainerInspectResponseEx response) { 60 | JsonAdapter adapter = moshi.adapter(ContainerInspectResponseEx.class); 61 | return adapter.toJson(response); 62 | } 63 | 64 | public static T fromJson(String json, Class type) throws IOException { 65 | JsonAdapter adapter = moshi.adapter(type); 66 | return (T) adapter.fromJson(json); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/LayerRefAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import com.squareup.moshi.ToJson; 21 | import io.seqera.wave.cli.model.LayerRef; 22 | import io.seqera.wave.core.spec.ObjectRef; 23 | /** 24 | * Layer Ref adapter for Moshi JSON serialisation 25 | * 26 | * @author Munish Chouhan 27 | */ 28 | public class LayerRefAdapter{ 29 | 30 | @ToJson 31 | public LayerRef toJson(ObjectRef objectRef) { 32 | if(objectRef instanceof LayerRef) { 33 | return (LayerRef) objectRef; 34 | } else { 35 | return new LayerRef(objectRef, null); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/json/PathAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json; 19 | 20 | import java.nio.file.Path; 21 | 22 | import com.squareup.moshi.FromJson; 23 | import com.squareup.moshi.ToJson; 24 | 25 | /** 26 | * Mosh adapter for {@link Path}. Only support default file system provider 27 | * 28 | * @author Paolo Di Tommaso 29 | */ 30 | class PathAdapter { 31 | 32 | @ToJson 33 | public String serialize(Path path) { 34 | return path != null ? path.toString() : null; 35 | } 36 | 37 | @FromJson 38 | public Path deserialize(String data) { 39 | return data != null ? Path.of(data) : null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/model/ContainerInspectResponseEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.model; 19 | 20 | import io.seqera.wave.api.ContainerInspectResponse; 21 | import io.seqera.wave.core.spec.ContainerSpec; 22 | 23 | /** 24 | * @author Paolo Di Tommaso 25 | */ 26 | public class ContainerInspectResponseEx extends ContainerInspectResponse { 27 | 28 | public ContainerInspectResponseEx(ContainerInspectResponse response) { 29 | super(new ContainerSpecEx(response.getContainer())); 30 | } 31 | 32 | public ContainerInspectResponseEx(ContainerSpec spec) { 33 | super(new ContainerSpecEx(spec)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/model/ContainerSpecEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.model; 19 | 20 | import java.util.List; 21 | 22 | import io.seqera.wave.core.spec.ContainerSpec; 23 | import io.seqera.wave.core.spec.ObjectRef; 24 | 25 | /** 26 | * Wrapper for {@link ContainerSpec} that replaces 27 | * {@link ObjectRef} with {@link LayerRef} objects 28 | * 29 | * @author Paolo Di Tommaso 30 | */ 31 | public class ContainerSpecEx extends ContainerSpec { 32 | public ContainerSpecEx(ContainerSpec spec) { 33 | super(spec); 34 | // update the layers uri 35 | if( spec.getManifest()!=null && spec.getManifest().getLayers()!=null ) { 36 | List layers = spec.getManifest().getLayers(); 37 | for( int i=0; i 26 | */ 27 | public class LayerRef extends ObjectRef { 28 | 29 | final public String uri; 30 | 31 | public LayerRef(ObjectRef obj, String uri) { 32 | super(obj); 33 | this.uri = uri; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/model/SubmitContainerTokenResponseEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.model; 19 | 20 | import java.time.Duration; 21 | import java.util.Map; 22 | 23 | import io.seqera.wave.api.ContainerStatus; 24 | import io.seqera.wave.api.ContainerStatusResponse; 25 | import io.seqera.wave.api.SubmitContainerTokenResponse; 26 | 27 | /** 28 | * Extend the {@link SubmitContainerTokenResponse} object with extra fields 29 | * 30 | * @author Paolo Di Tommaso 31 | */ 32 | public class SubmitContainerTokenResponseEx extends SubmitContainerTokenResponse { 33 | 34 | /** 35 | * The status of this request 36 | */ 37 | public ContainerStatus status; 38 | 39 | /** 40 | * The request duration 41 | */ 42 | public Duration duration; 43 | 44 | /** 45 | * The found vulnerabilities 46 | */ 47 | public Map vulnerabilities; 48 | 49 | /** 50 | * Descriptive reason for returned status, used for failures 51 | */ 52 | public String reason; 53 | 54 | /** 55 | * Link to detail page 56 | */ 57 | public String detailsUri; 58 | 59 | public SubmitContainerTokenResponseEx(SubmitContainerTokenResponse resp1, ContainerStatusResponse resp2) { 60 | super(resp1); 61 | this.status = resp2.status; 62 | this.duration = resp2.duration; 63 | this.vulnerabilities = resp2.vulnerabilities; 64 | this.succeeded = resp2.succeeded; 65 | this.reason = resp2.reason; 66 | this.detailsUri = resp2.detailsUri; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/BuildInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import java.util.Properties; 21 | 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | /** 26 | * @author Paolo Di Tommaso 27 | */ 28 | public class BuildInfo { 29 | 30 | private static final Logger log = LoggerFactory.getLogger(BuildInfo.class); 31 | 32 | private static Properties properties; 33 | 34 | static { 35 | final String BUILD_INFO = "/META-INF/build-info.properties"; 36 | properties = new Properties(); 37 | try { 38 | properties.load( BuildInfo.class.getResourceAsStream(BUILD_INFO) ); 39 | } 40 | catch( Exception e ) { 41 | log.warn("Unable to parse $BUILD_INFO - Cause: " + e.getMessage()); 42 | } 43 | } 44 | 45 | static Properties getProperties() { return properties; } 46 | 47 | static public String getVersion() { return properties.getProperty("version"); } 48 | 49 | static public String getCommitId() { return properties.getProperty("commitId"); } 50 | 51 | static public String getName() { return properties.getProperty("name"); } 52 | 53 | static public String getFullVersion() { 54 | return getVersion() + "_" + getCommitId(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/Checkers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import java.util.List; 21 | import java.util.regex.Pattern; 22 | 23 | /** 24 | * @author Paolo Di Tommaso 25 | */ 26 | public class Checkers { 27 | 28 | private static final Pattern ENV_REGEX = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*=.*$"); 29 | 30 | static public boolean isEmpty(String value) { 31 | return value==null || "".equals(value.trim()); 32 | } 33 | 34 | static public boolean isEmpty(List list) { 35 | return list==null || list.size()==0; 36 | } 37 | 38 | static public boolean isEnvVar(String value) { 39 | return value!=null && ENV_REGEX.matcher(value).matches(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/CliVersionProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import picocli.CommandLine; 21 | 22 | /** 23 | * @author Paolo Di Tommaso 24 | */ 25 | public class CliVersionProvider implements CommandLine.IVersionProvider { 26 | @Override 27 | public String[] getVersion() throws Exception { 28 | return new String[] { BuildInfo.getVersion() }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/DurationConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | 19 | package io.seqera.wave.cli.util; 20 | 21 | import picocli.CommandLine; 22 | 23 | import java.time.Duration; 24 | /** 25 | * Converter to convert cli argument to duration 26 | * 27 | * @author Munish Chouhan 28 | */ 29 | public class DurationConverter implements CommandLine.ITypeConverter { 30 | @Override 31 | public Duration convert(String value) { 32 | if (value == null || value.trim().isEmpty()) { 33 | return Duration.ofMinutes(15); 34 | } 35 | return Duration.parse("PT" + value.toUpperCase()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/GptHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import java.io.IOException; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import dev.langchain4j.agent.tool.ToolExecutionRequest; 26 | import dev.langchain4j.agent.tool.ToolParameters; 27 | import dev.langchain4j.agent.tool.ToolSpecification; 28 | import dev.langchain4j.data.message.AiMessage; 29 | import dev.langchain4j.data.message.ChatMessage; 30 | import dev.langchain4j.model.openai.OpenAiChatModel; 31 | import dev.langchain4j.model.output.Response; 32 | import io.seqera.wave.api.PackagesSpec; 33 | import io.seqera.wave.cli.App; 34 | import io.seqera.wave.cli.exception.BadClientResponseException; 35 | import io.seqera.wave.cli.json.JsonHelper; 36 | import org.apache.commons.lang3.StringUtils; 37 | import org.jetbrains.annotations.NotNull; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | 41 | /** 42 | * @author Paolo Di Tommaso 43 | */ 44 | public class GptHelper { 45 | 46 | private static final Logger log = LoggerFactory.getLogger(GptHelper.class); 47 | 48 | static private OpenAiChatModel client() { 49 | String key = System.getenv("OPENAI_API_KEY"); 50 | if( StringUtils.isEmpty(key) ) 51 | throw new IllegalArgumentException("Missing OPENAI_API_KEY environment variable"); 52 | String model = System.getenv("OPENAI_MODEL"); 53 | if( model==null ) 54 | model = "gpt-3.5-turbo"; 55 | 56 | return OpenAiChatModel.builder() 57 | .apiKey(key) 58 | .modelName(model) 59 | .maxRetries(1) 60 | .build(); 61 | } 62 | 63 | static public PackagesSpec grabPackages(String prompt) { 64 | try { 65 | return grabPackages0(prompt); 66 | } 67 | catch (RuntimeException e) { 68 | String msg = "Unexpected OpenAI response - cause: " + e.getMessage(); 69 | throw new BadClientResponseException(msg, e); 70 | } 71 | } 72 | 73 | static PackagesSpec grabPackages0(String prompt) { 74 | final ToolSpecification toolSpec = ToolSpecification 75 | .builder() 76 | .name("wave_container") 77 | .description("This function get a container with one or more tools specified via Conda packages. If the container image does not yet exists it does create it to fulfill the requirement") 78 | .parameters(getToolParameters()) 79 | .build(); 80 | final AiMessage msg = AiMessage.from(prompt); 81 | 82 | final OpenAiChatModel client = client(); 83 | final Response resp = client.generate(List.of(msg), toolSpec); 84 | if( Checkers.isEmpty(resp.content().toolExecutionRequests()) ) 85 | throw new IllegalArgumentException("Unable to resolve container for prompt: " + prompt); 86 | ToolExecutionRequest tool = resp.content().toolExecutionRequests().get(0); 87 | String json = tool.arguments(); 88 | log.debug("GPT response: {}", json); 89 | 90 | return jsonToPackageSpec(json); 91 | } 92 | 93 | protected static ToolParameters getToolParameters() { 94 | return ToolParameters 95 | .builder() 96 | .properties(getToolProperties()) 97 | .required(List.of("packages")) 98 | .build(); 99 | } 100 | 101 | @NotNull 102 | protected static Map> getToolProperties() { 103 | final Map PACKAGES = Map.of( 104 | "type", "array", 105 | "description", "A list of one more Conda package", 106 | "items", Map.of("type","string", "description", "A Conda package specification provided as the pair name and version, separated by the equals character, for example: foo=1.2.3")); 107 | final Map CHANNELS = Map.of( 108 | "type", "array", 109 | "description", "A list of one more Conda channels", 110 | "items", Map.of("type", "string", "description", "A Conda channel name")); 111 | return Map.of("packages", PACKAGES, "channels", CHANNELS); 112 | } 113 | 114 | static protected PackagesSpec jsonToPackageSpec(String json) { 115 | try { 116 | Map object = JsonHelper.fromJson(json, Map.class); 117 | List packages = (List) object.get("packages"); 118 | if( Checkers.isEmpty(packages) ) 119 | throw new IllegalArgumentException("Unable to resolve packages from json response: " + json); 120 | List channels = (List) object.get("channels"); 121 | if( Checkers.isEmpty(channels) ) 122 | channels = Arrays.asList(App.DEFAULT_CONDA_CHANNELS.split(",")); 123 | return new PackagesSpec() 124 | .withType(PackagesSpec.Type.CONDA) 125 | .withEntries(packages) 126 | .withChannels(channels); 127 | } 128 | catch (IOException e) { 129 | throw new IllegalArgumentException("Unable to parse json object: " + json); 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/StreamHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | /** 28 | * @author Paolo Di Tommaso 29 | */ 30 | public class StreamHelper { 31 | 32 | private static final Logger log = LoggerFactory.getLogger(StreamHelper.class); 33 | 34 | static public String tryReadStdin() { 35 | return tryReadStream(System.in); 36 | } 37 | 38 | static public String tryReadStream(InputStream stream) { 39 | try { 40 | if( stream.available()==0 ) 41 | return null; 42 | ByteArrayOutputStream result = new ByteArrayOutputStream(); 43 | byte[] buffer = new byte[1024]; 44 | int len; 45 | while( (len=stream.read(buffer))!=-1 ) { 46 | result.write(buffer,0,len); 47 | } 48 | return new String(result.toByteArray()); 49 | } 50 | catch (IOException e){ 51 | log.debug("Unable to read system.in", e); 52 | } 53 | return null; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/io/seqera/wave/cli/util/YamlHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util; 19 | 20 | import java.time.Duration; 21 | import java.time.Instant; 22 | 23 | import io.seqera.wave.api.ContainerInspectResponse; 24 | import io.seqera.wave.api.SubmitContainerTokenResponse; 25 | import io.seqera.wave.cli.model.ContainerInspectResponseEx; 26 | import io.seqera.wave.cli.model.ContainerSpecEx; 27 | import io.seqera.wave.cli.model.LayerRef; 28 | import io.seqera.wave.cli.model.SubmitContainerTokenResponseEx; 29 | import io.seqera.wave.core.spec.ConfigSpec; 30 | import io.seqera.wave.core.spec.ContainerSpec; 31 | import io.seqera.wave.core.spec.ManifestSpec; 32 | import org.yaml.snakeyaml.DumperOptions; 33 | import org.yaml.snakeyaml.Yaml; 34 | import org.yaml.snakeyaml.introspector.BeanAccess; 35 | import org.yaml.snakeyaml.introspector.Property; 36 | import org.yaml.snakeyaml.nodes.NodeTuple; 37 | import org.yaml.snakeyaml.nodes.Tag; 38 | import org.yaml.snakeyaml.representer.Representer; 39 | 40 | /** 41 | * Helper methods to handle YAML content 42 | * 43 | * @author Paolo Di Tommaso 44 | */ 45 | public class YamlHelper { 46 | 47 | public static String toYaml(SubmitContainerTokenResponse resp) { 48 | final DumperOptions opts = new DumperOptions(); 49 | opts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 50 | final Representer representer = new Representer(opts) { 51 | { 52 | addClassTag(SubmitContainerTokenResponse.class, Tag.MAP); 53 | addClassTag(SubmitContainerTokenResponseEx.class, Tag.MAP); 54 | representers.put(Instant.class, data -> representScalar(Tag.STR, data.toString())); 55 | representers.put(Duration.class, data -> representScalar(Tag.STR, data.toString())); 56 | } 57 | 58 | // skip null values in the resulting yaml 59 | @Override 60 | protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { 61 | if (propertyValue == null) { 62 | return null; 63 | } 64 | return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); 65 | } 66 | }; 67 | 68 | Yaml yaml = new Yaml(representer, opts); 69 | return yaml.dump(resp); 70 | } 71 | 72 | public static String toYaml(ContainerInspectResponseEx resp) { 73 | final DumperOptions opts = new DumperOptions(); 74 | opts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 75 | opts.setAllowReadOnlyProperties(true); 76 | 77 | final Representer representer = new Representer(opts) { 78 | { 79 | addClassTag(ContainerSpec.class, Tag.MAP); 80 | addClassTag(ContainerSpecEx.class, Tag.MAP); 81 | addClassTag(ConfigSpec.class, Tag.MAP); 82 | addClassTag(ManifestSpec.class, Tag.MAP); 83 | addClassTag(ContainerInspectResponse.class, Tag.MAP); 84 | addClassTag(ContainerInspectResponseEx.class, Tag.MAP); 85 | addClassTag(LayerRef.class, Tag.MAP); 86 | representers.put(Instant.class, data -> representScalar(Tag.STR, data.toString())); 87 | } 88 | 89 | // skip null values in the resulting yaml 90 | @Override 91 | protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { 92 | // Skip null values 93 | if (propertyValue == null) { 94 | return null; 95 | } 96 | return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); 97 | } 98 | }; 99 | 100 | representer.getPropertyUtils().setSkipMissingProperties(true); 101 | 102 | Yaml yaml = new Yaml(representer, opts); 103 | yaml.setBeanAccess(BeanAccess.FIELD); 104 | return yaml.dump(resp); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/build-info.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023-2025, Seqera Labs 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 | 18 | name=wave-cli 19 | version=1.5.0-rc.3 20 | commitId=unknown 21 | -------------------------------------------------------------------------------- /app/src/main/resources/io/seqera/wave/cli/usage-examples.txt: -------------------------------------------------------------------------------- 1 | 2 | Examples: 3 | # Augment a container image with the content of a local directory 4 | wave -i alpine --layer layer-dir/ 5 | 6 | # Build a container with Dockerfile 7 | wave -f Dockerfile --context context-dir/ 8 | 9 | # Build a container based on Conda packages 10 | wave --conda-package bamtools=2.5.2 --conda-package samtools=1.17 11 | 12 | # Build a container based on Conda packages using arm64 architecture 13 | wave --conda-package fastp --platform linux/arm64 14 | 15 | # Build a container based on Conda lock file served via prefix.dev service 16 | wave --conda-package https://prefix.dev/envs/pditommaso/wave/conda-lock.yml 17 | 18 | # Build a container getting a persistent image name 19 | wave -i alpine --freeze --build-repo docker.io/user/repo --tower-token 20 | 21 | # Build a Singularity container and push it to an OCI registry 22 | wave -f Singularityfile --singularity --freeze --build-repo docker.io/user/repo 23 | 24 | # Build a Singularity container based on Conda packages 25 | wave --conda-package bamtools=2.5.2 --singularity --freeze --build-repo docker.io/user/repo 26 | 27 | # Copy a container image to another registry 28 | wave -i quay.io/biocontainers/bwa:0.7.15--0 --mirror --build-repository --tower-token 29 | 30 | # Scan a container for security vulnerability 31 | wave -i ubuntu --scan-mode required --await 32 | -------------------------------------------------------------------------------- /app/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | false 25 | 26 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/AppCondaOptsTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli 19 | 20 | import java.nio.file.Files 21 | 22 | import io.seqera.wave.api.PackagesSpec 23 | import io.seqera.wave.cli.exception.IllegalCliArgumentException 24 | import io.seqera.wave.config.CondaOpts 25 | import picocli.CommandLine 26 | import spock.lang.Specification 27 | /** 28 | * Test App Conda prefixed options 29 | * 30 | * @author Paolo Di Tommaso 31 | */ 32 | class AppCondaOptsTest extends Specification { 33 | 34 | def 'should fail when passing both conda file and packages' () { 35 | given: 36 | def app = new App() 37 | String[] args = ["--conda-file", "foo", "--conda-package", "bar"] 38 | 39 | when: 40 | new CommandLine(app).parseArgs(args) 41 | and: 42 | app.validateArgs() 43 | then: 44 | thrown(IllegalCliArgumentException) 45 | } 46 | 47 | def 'should fail when passing both conda file and image' () { 48 | given: 49 | def app = new App() 50 | String[] args = ["--conda-file", "foo", "--image", "bar"] 51 | 52 | when: 53 | new CommandLine(app).parseArgs(args) 54 | and: 55 | app.validateArgs() 56 | then: 57 | thrown(IllegalCliArgumentException) 58 | } 59 | 60 | def 'should fail when passing both conda file and container file' () { 61 | given: 62 | def app = new App() 63 | String[] args = ["--conda-file", "foo", "--containerfile", "bar"] 64 | 65 | when: 66 | new CommandLine(app).parseArgs(args) 67 | and: 68 | app.validateArgs() 69 | then: 70 | thrown(IllegalCliArgumentException) 71 | } 72 | 73 | def 'should fail when the conda file does not exist' () { 74 | given: 75 | def app = new App() 76 | String[] args = ["--conda-file", "foo"] 77 | 78 | when: 79 | new CommandLine(app).parseArgs(args) 80 | and: 81 | app.validateArgs() 82 | then: 83 | def e = thrown(IllegalCliArgumentException) 84 | e.message == "The specified Conda file path cannot be accessed - offending file path: foo" 85 | } 86 | 87 | def 'should fail when passing both conda package and image' () { 88 | given: 89 | def app = new App() 90 | String[] args = ["--conda-package", "foo", "--image", "bar"] 91 | 92 | when: 93 | new CommandLine(app).parseArgs(args) 94 | and: 95 | app.validateArgs() 96 | then: 97 | thrown(IllegalCliArgumentException) 98 | } 99 | 100 | def 'should fail when passing both conda package and container file' () { 101 | given: 102 | def app = new App() 103 | String[] args = ["--conda-package", "foo", "--containerfile", "bar"] 104 | 105 | when: 106 | new CommandLine(app).parseArgs(args) 107 | and: 108 | app.validateArgs() 109 | then: 110 | thrown(IllegalCliArgumentException) 111 | } 112 | 113 | def 'should create docker file from conda file' () { 114 | given: 115 | def CONDA_RECIPE = ''' 116 | name: my-recipe 117 | dependencies: 118 | - one=1.0 119 | - two:2.0 120 | '''.stripIndent(true) 121 | and: 122 | def folder = Files.createTempDirectory('test') 123 | def condaFile = folder.resolve('conda.yml'); 124 | condaFile.text = CONDA_RECIPE 125 | and: 126 | def app = new App() 127 | String[] args = ["--conda-file", condaFile.toString()] 128 | 129 | when: 130 | new CommandLine(app).parseArgs(args) 131 | and: 132 | def req = app.createRequest() 133 | then: 134 | req.packages.type == PackagesSpec.Type.CONDA 135 | and: 136 | new String(req.packages.environment.decodeBase64()) == ''' 137 | name: my-recipe 138 | dependencies: 139 | - one=1.0 140 | - two:2.0 141 | '''.stripIndent(true) 142 | and: 143 | req.packages.condaOpts == new CondaOpts(mambaImage: CondaOpts.DEFAULT_MAMBA_IMAGE, basePackages: CondaOpts.DEFAULT_PACKAGES) 144 | req.packages.channels == ['conda-forge', 'bioconda'] 145 | and: 146 | !req.packages.entries 147 | and: 148 | !req.condaFile 149 | 150 | cleanup: 151 | folder?.deleteDir() 152 | } 153 | 154 | 155 | def 'should create docker file from conda package' () { 156 | given: 157 | def app = new App() 158 | String[] args = ["--conda-package", "foo"] 159 | 160 | when: 161 | new CommandLine(app).parseArgs(args) 162 | and: 163 | def req = app.createRequest() 164 | then: 165 | req.packages.type == PackagesSpec.Type.CONDA 166 | req.packages.entries == ['foo'] 167 | and: 168 | req.packages.condaOpts == new CondaOpts(mambaImage: CondaOpts.DEFAULT_MAMBA_IMAGE, basePackages: CondaOpts.DEFAULT_PACKAGES) 169 | req.packages.channels == ['conda-forge', 'bioconda'] 170 | and: 171 | !req.packages.environment 172 | and: 173 | !req.condaFile 174 | } 175 | 176 | def 'should create docker env from conda lock file' () { 177 | given: 178 | def app = new App() 179 | String[] args = ["--conda-package", "https://host.com/file-lock.yml"] 180 | 181 | when: 182 | new CommandLine(app).parseArgs(args) 183 | and: 184 | def req = app.createRequest() 185 | then: 186 | req.packages.type == PackagesSpec.Type.CONDA 187 | req.packages.entries == ['https://host.com/file-lock.yml'] 188 | and: 189 | req.packages.condaOpts == new CondaOpts(mambaImage: CondaOpts.DEFAULT_MAMBA_IMAGE, basePackages: CondaOpts.DEFAULT_PACKAGES) 190 | req.packages.channels == ['conda-forge', 'bioconda'] 191 | and: 192 | !req.packages.environment 193 | and: 194 | !req.condaFile 195 | } 196 | 197 | def 'should create docker file from conda package and custom options' () { 198 | given: 199 | def app = new App() 200 | String[] args = [ 201 | "--conda-package", "foo", 202 | "--conda-package", "bar", 203 | "--conda-base-image", "my/mamba:latest", 204 | "--conda-channels", "alpha,beta", 205 | "--conda-run-command", "RUN one", 206 | "--conda-run-command", "RUN two", 207 | ] 208 | 209 | when: 210 | new CommandLine(app).parseArgs(args) 211 | and: 212 | def req = app.createRequest() 213 | then: 214 | req.packages.type == PackagesSpec.Type.CONDA 215 | req.packages.entries == ['foo','bar'] 216 | req.packages.channels == ['alpha','beta'] 217 | and: 218 | req.packages.condaOpts == new CondaOpts(mambaImage: 'my/mamba:latest', basePackages: CondaOpts.DEFAULT_PACKAGES, commands: ['RUN one','RUN two']) 219 | and: 220 | !req.packages.environment 221 | and: 222 | !req.condaFile 223 | } 224 | 225 | 226 | def 'should get conda channels' () { 227 | expect: 228 | new App(condaChannels: null) 229 | .condaChannels() == null 230 | 231 | new App(condaChannels: 'foo , bar') 232 | .condaChannels() == ['foo','bar'] 233 | 234 | new App(condaChannels: 'foo bar') 235 | .condaChannels() == ['foo','bar'] 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/AppConfigOptsTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli 19 | 20 | import java.nio.file.Files 21 | 22 | import com.sun.net.httpserver.HttpExchange 23 | import com.sun.net.httpserver.HttpHandler 24 | import com.sun.net.httpserver.HttpServer 25 | import io.seqera.wave.api.ContainerConfig 26 | import io.seqera.wave.cli.exception.IllegalCliArgumentException 27 | import picocli.CommandLine 28 | import spock.lang.Specification 29 | /** 30 | * Test App config prefixed options 31 | * 32 | * @author Paolo Di Tommaso 33 | */ 34 | class AppConfigOptsTest extends Specification { 35 | 36 | def CONFIG_JSON = '''\ 37 | { 38 | "entrypoint": [ "/some", "--entrypoint" ], 39 | "layers": [ 40 | { 41 | "location": "https://location", 42 | "gzipDigest": "sha256:gzipDigest", 43 | "gzipSize": 100, 44 | "tarDigest": "sha256:tarDigest", 45 | "skipHashing": true 46 | } 47 | ] 48 | } 49 | ''' 50 | 51 | 52 | def "test valid entrypoint"() { 53 | given: 54 | def app = new App() 55 | String[] args = ["--config-entrypoint", "entryPoint"] 56 | def cli = new CommandLine(app) 57 | 58 | when: 59 | cli.parseArgs(args) 60 | then: 61 | app.@entrypoint == "entryPoint" 62 | 63 | when: 64 | def config = app.prepareConfig() 65 | then: 66 | config == new ContainerConfig(entrypoint: ['entryPoint']) 67 | } 68 | 69 | def "test invalid entrypoint"() { 70 | given: 71 | def app = new App() 72 | String[] args = ["--config-entrypoint"] 73 | 74 | when: 75 | new CommandLine(app).parseArgs(args) 76 | 77 | then: 78 | thrown(CommandLine.MissingParameterException) 79 | } 80 | 81 | def "test valid command"() { 82 | given: 83 | def app = new App() 84 | String[] args = ["--config-cmd", "/some/command"] 85 | 86 | when: 87 | new CommandLine(app).parseArgs(args) 88 | then: 89 | app.@command == "/some/command" 90 | 91 | when: 92 | def config = app.prepareConfig() 93 | then: 94 | config == new ContainerConfig(cmd: ['/some/command']) 95 | } 96 | 97 | def "test invalid command"() { 98 | given: 99 | def app = new App() 100 | String[] args = ["--config-cmd", ""] 101 | 102 | when: 103 | new CommandLine(app).parseArgs(args) 104 | app.prepareConfig() 105 | then: 106 | thrown(IllegalCliArgumentException) 107 | 108 | } 109 | 110 | def "test valid environment"() { 111 | given: 112 | def app = new App() 113 | String[] args = ["--config-env", "var1=value1","--config-env", "var2=value2"] 114 | 115 | when: 116 | new CommandLine(app).parseArgs(args) 117 | then: 118 | app.@environment[0] == "var1=value1" 119 | app.@environment[1] == "var2=value2" 120 | 121 | when: 122 | def config = app.prepareConfig() 123 | then: 124 | config == new ContainerConfig(env: ['var1=value1', 'var2=value2']) 125 | } 126 | 127 | def "test invalid environment"() { 128 | given: 129 | def app = new App() 130 | String[] args = ["--config-env", "VAR"] 131 | 132 | when: 133 | new CommandLine(app).parseArgs(args) 134 | app.prepareConfig() 135 | then: 136 | def e = thrown(IllegalCliArgumentException) 137 | e.message == 'Invalid environment variable syntax - offending value: VAR' 138 | 139 | } 140 | 141 | def "test valid working directory"() { 142 | given: 143 | def app = new App() 144 | String[] args = ["--config-working-dir", "/work/dir"] 145 | 146 | when: 147 | new CommandLine(app).parseArgs(args) 148 | then: 149 | app.@workingDir == "/work/dir" 150 | 151 | when: 152 | def config = app.prepareConfig() 153 | then: 154 | config == new ContainerConfig(workingDir: '/work/dir') 155 | } 156 | 157 | def "test invalid working directory"() { 158 | given: 159 | def app = new App() 160 | String[] args = ["--config-working-dir", " "] 161 | 162 | when: 163 | new CommandLine(app).parseArgs(args) 164 | app.prepareConfig() 165 | then: 166 | thrown(IllegalCliArgumentException) 167 | 168 | } 169 | 170 | def "test valid config file from a path"() { 171 | given: 172 | def folder = Files.createTempDirectory('test') 173 | def configFile = folder.resolve('config.json') 174 | configFile.text = CONFIG_JSON 175 | and: 176 | def app = new App() 177 | String[] args = ["--config-file", configFile.toString()] 178 | 179 | when: 180 | new CommandLine(app).parseArgs(args) 181 | then: 182 | app.@configFile == configFile.toString() 183 | 184 | when: 185 | def config = app.prepareConfig() 186 | then: 187 | config.entrypoint == [ "/some", "--entrypoint" ] 188 | def layer = config.layers[0] 189 | layer.location == "https://location" 190 | layer.gzipDigest == "sha256:gzipDigest" 191 | layer.tarDigest == "sha256:tarDigest" 192 | layer.gzipSize == 100 193 | 194 | cleanup: 195 | folder?.deleteDir() 196 | } 197 | 198 | def "test valid config file from a URL"() { 199 | given: 200 | HttpHandler handler = { HttpExchange exchange -> 201 | String body = CONFIG_JSON 202 | exchange.getResponseHeaders().add("Content-Type", "text/json") 203 | exchange.sendResponseHeaders(200, body.size()) 204 | exchange.getResponseBody() << body 205 | exchange.getResponseBody().close() 206 | 207 | } 208 | 209 | HttpServer server = HttpServer.create(new InetSocketAddress(9901), 0); 210 | server.createContext("/", handler); 211 | server.start() 212 | 213 | 214 | def app = new App() 215 | String[] args = ["--config-file", "http://localhost:9901/api/data"] 216 | 217 | when: 218 | new CommandLine(app).parseArgs(args) 219 | then: 220 | app.@configFile == "http://localhost:9901/api/data" 221 | 222 | when: 223 | def config = app.prepareConfig() 224 | then: 225 | config.entrypoint == [ "/some", "--entrypoint" ] 226 | def layer = config.layers[0] 227 | layer.location == "https://location" 228 | layer.gzipDigest == "sha256:gzipDigest" 229 | layer.tarDigest == "sha256:tarDigest" 230 | layer.gzipSize == 100 231 | 232 | cleanup: 233 | server?.stop(0) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/AppTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli 19 | 20 | import java.nio.file.Files 21 | import java.time.Duration 22 | import java.time.Instant 23 | 24 | import io.seqera.wave.api.BuildCompression 25 | import io.seqera.wave.api.ContainerStatus 26 | import io.seqera.wave.api.ContainerStatusResponse 27 | import io.seqera.wave.api.ImageNameStrategy 28 | import io.seqera.wave.api.ScanLevel 29 | import io.seqera.wave.api.ScanMode 30 | import io.seqera.wave.api.SubmitContainerTokenResponse 31 | import io.seqera.wave.cli.exception.BadClientResponseException 32 | import io.seqera.wave.cli.exception.IllegalCliArgumentException 33 | import io.seqera.wave.cli.model.ContainerInspectResponseEx 34 | import io.seqera.wave.cli.model.SubmitContainerTokenResponseEx 35 | import io.seqera.wave.cli.util.DurationConverter 36 | import io.seqera.wave.core.spec.ContainerSpec 37 | import io.seqera.wave.util.TarUtils 38 | import picocli.CommandLine 39 | import spock.lang.Specification 40 | import spock.lang.Unroll 41 | 42 | class AppTest extends Specification { 43 | 44 | 45 | def "test valid no entrypoint"() { 46 | given: 47 | def app = new App() 48 | String[] args = [] 49 | def cli = new CommandLine(app) 50 | 51 | when: 52 | cli.parseArgs(args) 53 | then: 54 | app.@entrypoint == null 55 | 56 | when: 57 | def config = app.prepareConfig() 58 | then: 59 | config == null 60 | } 61 | 62 | def 'should dump response to yaml' () { 63 | given: 64 | def app = new App() 65 | String[] args = ["--output", "yaml"] 66 | and: 67 | def resp = new SubmitContainerTokenResponse( 68 | containerToken: "12345", 69 | targetImage: 'docker.io/some/repo', 70 | containerImage: 'docker.io/some/container', 71 | expiration: Instant.ofEpochMilli(1691839913), 72 | buildId: '98765', 73 | cached: true 74 | ) 75 | 76 | when: 77 | new CommandLine(app).parseArgs(args) 78 | def result = app.dumpOutput(resp) 79 | then: 80 | result == '''\ 81 | buildId: '98765' 82 | cached: true 83 | containerImage: docker.io/some/container 84 | containerToken: '12345' 85 | expiration: '1970-01-20T13:57:19.913Z' 86 | targetImage: docker.io/some/repo 87 | '''.stripIndent(true) 88 | } 89 | 90 | def 'should dump response with status to yaml' () { 91 | given: 92 | def app = new App() 93 | String[] args = ["--output", "yaml"] 94 | and: 95 | def resp = new SubmitContainerTokenResponse( 96 | containerToken: "12345", 97 | targetImage: 'docker.io/some/repo', 98 | containerImage: 'docker.io/some/container', 99 | expiration: Instant.ofEpochMilli(1691839913), 100 | buildId: '98765', 101 | cached: true 102 | ) 103 | def status = new ContainerStatusResponse( 104 | "12345", 105 | ContainerStatus.DONE, 106 | "98765", 107 | null, 108 | "scan-1234", 109 | [MEDIUM:1, HIGH:2], 110 | true, 111 | "All ok", 112 | "http://foo.com", 113 | Instant.now(), 114 | Duration.ofMinutes(1) 115 | ) 116 | 117 | when: 118 | new CommandLine(app).parseArgs(args) 119 | def result = app.dumpOutput(new SubmitContainerTokenResponseEx(resp, status)) 120 | then: 121 | result == '''\ 122 | buildId: '98765' 123 | cached: true 124 | containerImage: docker.io/some/container 125 | containerToken: '12345' 126 | detailsUri: http://foo.com 127 | duration: PT1M 128 | expiration: '1970-01-20T13:57:19.913Z' 129 | reason: All ok 130 | status: DONE 131 | succeeded: true 132 | targetImage: docker.io/some/repo 133 | vulnerabilities: 134 | MEDIUM: 1 135 | HIGH: 2 136 | '''.stripIndent(true) 137 | } 138 | 139 | def 'should throw exception on failure' (){ 140 | given: 141 | def app = new App() 142 | String[] args = [] 143 | and: 144 | def resp = new SubmitContainerTokenResponse( 145 | containerToken: "12345", 146 | targetImage: 'docker.io/some/repo', 147 | containerImage: 'docker.io/some/container', 148 | expiration: Instant.ofEpochMilli(1691839913), 149 | buildId: '98765', 150 | cached: false 151 | ) 152 | def status = new ContainerStatusResponse( 153 | "12345", 154 | ContainerStatus.DONE, 155 | "98765", 156 | null, 157 | "scan-1234", 158 | [MEDIUM:1, HIGH:2], 159 | false, 160 | "Something went wrong", 161 | "http://foo.com/bar/1234", 162 | Instant.now(), 163 | Duration.ofMinutes(1) 164 | ) 165 | 166 | when: 167 | new CommandLine(app).parseArgs(args) 168 | app.dumpOutput(new SubmitContainerTokenResponseEx(resp, status)) 169 | then: 170 | def e = thrown(BadClientResponseException) 171 | e.message == '''\ 172 | Container provisioning did not complete successfully 173 | - Reason: Something went wrong 174 | - Find out more here: http://foo.com/bar/1234\ 175 | '''.stripIndent() 176 | } 177 | 178 | def 'should dump response to json' () { 179 | given: 180 | def app = new App() 181 | String[] args = ["--output", "json"] 182 | and: 183 | def resp = new SubmitContainerTokenResponse( 184 | containerToken: "12345", 185 | targetImage: 'docker.io/some/repo', 186 | containerImage: 'docker.io/some/container', 187 | expiration: Instant.ofEpochMilli(1691839913), 188 | buildId: '98765' 189 | ) 190 | 191 | when: 192 | new CommandLine(app).parseArgs(args) 193 | def result = app.dumpOutput(resp) 194 | then: 195 | result == '{"buildId":"98765","containerImage":"docker.io/some/container","containerToken":"12345","expiration":"1970-01-20T13:57:19.913Z","targetImage":"docker.io/some/repo"}' 196 | } 197 | 198 | def 'should dump inspect to json' () { 199 | given: 200 | def app = new App() 201 | String[] args = ["--output", "json"] 202 | and: 203 | def resp = new ContainerInspectResponseEx( new ContainerSpec('docker.io', 'https://docker.io', 'busybox', 'latest', 'sha:12345', null, null) ) 204 | 205 | when: 206 | new CommandLine(app).parseArgs(args) 207 | def result = app.dumpOutput(resp) 208 | then: 209 | result == '{"container":{"digest":"sha:12345","hostName":"https://docker.io","imageName":"busybox","reference":"latest","registry":"docker.io"}}' 210 | } 211 | 212 | def 'should dump inspect to yaml' () { 213 | given: 214 | def app = new App() 215 | String[] args = ["--output", "yaml"] 216 | and: 217 | def resp = new ContainerInspectResponseEx( new ContainerSpec('docker.io', 'https://docker.io', 'busybox', 'latest', 'sha:12345', null, null) ) 218 | 219 | when: 220 | new CommandLine(app).parseArgs(args) 221 | def result = app.dumpOutput(resp) 222 | then: 223 | result == '''\ 224 | container: 225 | digest: sha:12345 226 | hostName: https://docker.io 227 | imageName: busybox 228 | reference: latest 229 | registry: docker.io 230 | '''.stripIndent() 231 | } 232 | 233 | def 'should prepare context' () { 234 | given: 235 | def folder = Files.createTempDirectory('test') 236 | def source = Files.createDirectory(folder.resolve('source')) 237 | def target = Files.createDirectory(folder.resolve('target')) 238 | folder.resolve('source/.dockerignore').text = '''\ 239 | **.txt 240 | !README.txt 241 | ''' 242 | and: 243 | source.resolve('hola.txt').text = 'Hola' 244 | source.resolve('ciao.txt').text = 'Ciao' 245 | source.resolve('script.sh').text = 'echo Hello' 246 | source.resolve('README.txt').text = 'Do this and that' 247 | and: 248 | def app = new App() 249 | String[] args = ["--context", source.toString()] 250 | 251 | when: 252 | new CommandLine(app).parseArgs(args) 253 | def layer = app.prepareContext() 254 | then: 255 | noExceptionThrown() 256 | 257 | when: 258 | def gzip = layer.location.replace('data:','').decodeBase64() 259 | TarUtils.untarGzip( new ByteArrayInputStream(gzip), target) 260 | then: 261 | target.resolve('script.sh').text == 'echo Hello' 262 | target.resolve('README.txt').text == 'Do this and that' 263 | and: 264 | !Files.exists(target.resolve('hola.txt')) 265 | !Files.exists(target.resolve('ciao.txt')) 266 | 267 | cleanup: 268 | folder?.deleteDir() 269 | } 270 | 271 | def 'should enable dry run mode' () { 272 | given: 273 | def app = new App() 274 | String[] args = ["--dry-run"] 275 | 276 | when: 277 | new CommandLine(app).parseArgs(args) 278 | and: 279 | def req = app.createRequest() 280 | then: 281 | req.dryRun 282 | } 283 | 284 | def 'should set scan mode' () { 285 | given: 286 | def app = new App() 287 | String[] args = ["--scan-mode", 'async'] 288 | 289 | when: 290 | new CommandLine(app).parseArgs(args) 291 | and: 292 | def req = app.createRequest() 293 | then: 294 | req.scanMode == ScanMode.async 295 | req.scanLevels == null 296 | } 297 | 298 | def 'should set scan levels' () { 299 | given: 300 | def app = new App() 301 | String[] args = ["--scan-level", 'LOW', "--scan-level", 'MEDIUM'] 302 | 303 | when: 304 | new CommandLine(app).parseArgs(args) 305 | and: 306 | def req = app.createRequest() 307 | then: 308 | req.scanMode == null 309 | req.scanLevels == List.of(ScanLevel.LOW, ScanLevel.MEDIUM) 310 | } 311 | 312 | def 'should set build compression gzip' () { 313 | given: 314 | def app = new App() 315 | String[] args = ["--build-compression", 'gzip'] 316 | 317 | when: 318 | new CommandLine(app).parseArgs(args) 319 | and: 320 | def req = app.createRequest() 321 | then: 322 | req.buildCompression == new BuildCompression().withMode(BuildCompression.Mode.gzip) 323 | } 324 | 325 | def 'should set build compression estargz' () { 326 | given: 327 | def app = new App() 328 | String[] args = ["--build-compression", 'estargz'] 329 | 330 | when: 331 | new CommandLine(app).parseArgs(args) 332 | and: 333 | def req = app.createRequest() 334 | then: 335 | req.buildCompression == new BuildCompression().withMode(BuildCompression.Mode.estargz) 336 | } 337 | 338 | def 'should not allow dry-run and await' () { 339 | given: 340 | def app = new App() 341 | String[] args = ["-i", "ubuntu:latest","--dry-run", '--await'] 342 | 343 | when: 344 | def cli = new CommandLine(app) 345 | cli.registerConverter(Duration.class, new DurationConverter()) 346 | cli.parseArgs(args) 347 | and: 348 | app.validateArgs() 349 | then: 350 | def e = thrown(IllegalCliArgumentException) 351 | e.message == 'Options --dry-run and --await conflicts each other' 352 | } 353 | 354 | @Unroll 355 | def 'should allow platform option' () { 356 | given: 357 | def app = new App() 358 | String[] args = ["-i", "ubuntu:latest","--platform", PLATFORM] 359 | 360 | when: 361 | new CommandLine(app).parseArgs(args) 362 | and: 363 | app.validateArgs() 364 | then: 365 | app.@platform == PLATFORM 366 | 367 | where: 368 | PLATFORM || _ 369 | 'amd64' || _ 370 | 'x86_64' || _ 371 | 'arm64' || _ 372 | 'linux/amd64' || _ 373 | 'linux/x86_64' || _ 374 | 'linux/arm64' || _ 375 | } 376 | 377 | @Unroll 378 | def 'should fail with unsupported platform' () { 379 | given: 380 | def app = new App() 381 | String[] args = ["-i", "ubuntu:latest","--platform", 'foo'] 382 | 383 | when: 384 | new CommandLine(app).parseArgs(args) 385 | and: 386 | app.validateArgs() 387 | then: 388 | def e = thrown(IllegalCliArgumentException) 389 | e.message == "Unsupported container platform: 'foo'" 390 | } 391 | 392 | def 'should allow platform amd64 with singularity' () { 393 | given: 394 | def app = new App() 395 | String[] args = [ '--singularity', "--platform", 'linux/amd64', '-i', 'ubuntu', '--freeze', '--build-repo', 'docker.io/foo', '--tower-token', 'xyz'] 396 | 397 | when: 398 | new CommandLine(app).parseArgs(args) 399 | and: 400 | app.validateArgs() 401 | 402 | then: 403 | noExceptionThrown() 404 | } 405 | 406 | def 'should allow platform arm64 with singularity' () { 407 | given: 408 | def app = new App() 409 | String[] args = [ '--singularity', "--platform", 'linux/arm64', '-i', 'ubuntu', '--freeze', '--build-repo', 'docker.io/foo', '--tower-token', 'xyz'] 410 | 411 | when: 412 | new CommandLine(app).parseArgs(args) 413 | and: 414 | app.validateArgs() 415 | 416 | then: 417 | noExceptionThrown() 418 | and: 419 | app.@platform == 'linux/arm64' 420 | app.@image == 'ubuntu' 421 | app.@singularity 422 | app.@freeze 423 | app.@buildRepository == 'docker.io/foo' 424 | app.@towerToken == 'xyz' 425 | } 426 | 427 | def 'should get the correct await duration in minutes'(){ 428 | given: 429 | def app = new App() 430 | String[] args = ["-i", "ubuntu:latest", '--await', '10m'] 431 | 432 | when: 433 | def cli = new CommandLine(app) 434 | cli.registerConverter(Duration.class, new DurationConverter()) 435 | cli.parseArgs(args) 436 | and: 437 | app.validateArgs() 438 | then: 439 | noExceptionThrown() 440 | and: 441 | app.@await == Duration.ofMinutes(10) 442 | } 443 | 444 | def 'should get the correct await duration in seconds'(){ 445 | given: 446 | def app = new App() 447 | String[] args = ["-i", "ubuntu:latest", '--await', '10s'] 448 | 449 | when: 450 | def cli = new CommandLine(app) 451 | cli.registerConverter(Duration.class, new DurationConverter()) 452 | cli.parseArgs(args) 453 | and: 454 | app.validateArgs() 455 | then: 456 | noExceptionThrown() 457 | and: 458 | app.@await == Duration.ofSeconds(10) 459 | } 460 | 461 | def 'should get the default await duration'(){ 462 | given: 463 | def app = new App() 464 | String[] args = ["-i", "ubuntu:latest", '--await'] 465 | 466 | when: 467 | def cli = new CommandLine(app) 468 | cli.registerConverter(Duration.class, new DurationConverter()) 469 | cli.parseArgs(args) 470 | and: 471 | app.validateArgs() 472 | then: 473 | noExceptionThrown() 474 | and: 475 | app.@await == Duration.ofMinutes(15) 476 | } 477 | 478 | def 'should generate a container' () { 479 | given: 480 | def app = new App() 481 | String[] args = [ 'Get a docker container'] 482 | 483 | when: 484 | new CommandLine(app).parseArgs(args) 485 | then: 486 | app.prompt == ['Get a docker container'] 487 | } 488 | 489 | def 'should get the correct name strategy'(){ 490 | given: 491 | def app = new App() 492 | String[] args = ["-i", "ubuntu:latest", "--name-strategy", "tagPrefix"] 493 | 494 | when: 495 | def cli = new CommandLine(app) 496 | cli.parseArgs(args) 497 | and: 498 | app.validateArgs() 499 | then: 500 | noExceptionThrown() 501 | and: 502 | app.@nameStrategy == ImageNameStrategy.tagPrefix 503 | } 504 | 505 | def 'should get the correct name strategy'(){ 506 | given: 507 | def app = new App() 508 | String[] args = ["-i", "ubuntu:latest", "--name-strategy", "imageSuffix"] 509 | 510 | when: 511 | def cli = new CommandLine(app) 512 | cli.parseArgs(args) 513 | and: 514 | app.validateArgs() 515 | then: 516 | noExceptionThrown() 517 | and: 518 | app.@nameStrategy == ImageNameStrategy.imageSuffix 519 | } 520 | 521 | def 'should fail when passing incorrect name strategy'(){ 522 | given: 523 | def app = new App() 524 | String[] args = ["-i", "ubuntu:latest", "--name-strategy", "wrong"] 525 | 526 | when: 527 | def cli = new CommandLine(app) 528 | cli.parseArgs(args) 529 | and: 530 | app.validateArgs() 531 | then: 532 | def e = thrown(CommandLine.ParameterException) 533 | and: 534 | e.getMessage() == "Invalid value for option '--name-strategy': expected one of [none, tagPrefix, imageSuffix] (case-sensitive) but was 'wrong'" 535 | } 536 | 537 | def 'should fail when specifying mirror registry and container file' () { 538 | given: 539 | def app = new App() 540 | String[] args = ["--mirror", "true", "-f", "foo"] 541 | 542 | when: 543 | def cli = new CommandLine(app) 544 | cli.parseArgs(args) 545 | and: 546 | app.validateArgs() 547 | then: 548 | def e = thrown(IllegalCliArgumentException) 549 | and: 550 | e.getMessage() == "Argument --mirror and --containerfile conflict each other" 551 | } 552 | 553 | def 'should fail when specifying mirror registry and conda package' () { 554 | given: 555 | def app = new App() 556 | String[] args = ["--mirror", "true", "--conda-package", "foo"] 557 | 558 | when: 559 | def cli = new CommandLine(app) 560 | cli.parseArgs(args) 561 | and: 562 | app.validateArgs() 563 | then: 564 | def e = thrown(IllegalCliArgumentException) 565 | and: 566 | e.getMessage() == "Argument --mirror and --conda-package conflict each other" 567 | } 568 | 569 | def 'should fail when specifying mirror registry and freeze' () { 570 | given: 571 | def app = new App() 572 | String[] args = ["--mirror", "true", "--image", "foo", "--freeze"] 573 | 574 | when: 575 | def cli = new CommandLine(app) 576 | cli.parseArgs(args) 577 | and: 578 | app.validateArgs() 579 | then: 580 | def e = thrown(IllegalCliArgumentException) 581 | and: 582 | e.getMessage() == "Argument --mirror and --freeze conflict each other" 583 | } 584 | 585 | def 'should fail when specifying mirror and missing build repo' () { 586 | given: 587 | def app = new App() 588 | String[] args = ["--mirror", "true"] 589 | 590 | when: 591 | def cli = new CommandLine(app) 592 | cli.parseArgs(args) 593 | and: 594 | app.validateArgs() 595 | then: 596 | def e = thrown(IllegalCliArgumentException) 597 | and: 598 | e.getMessage() == "Option --mirror and requires the use of a build repository" 599 | } 600 | 601 | @Unroll 602 | def 'should check service version'() { 603 | given: 604 | def app = new App() 605 | expect: 606 | app.serviceVersion0(CURRENT, REQUIRED) == EXPECTED 607 | 608 | where: 609 | CURRENT | REQUIRED | EXPECTED 610 | '2.0.0' | '1.1.1' | '2.0.0' 611 | '2.0.0' | '2.0.0' | '2.0.0' 612 | '2.0.0' | '2.1.0' | '2.0.0 (required: 2.1.0)' 613 | } 614 | 615 | } 616 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/ClientTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli 19 | 20 | import spock.lang.Specification 21 | import spock.lang.Unroll 22 | 23 | /** 24 | * 25 | * @author Paolo Di Tommaso 26 | */ 27 | class ClientTest extends Specification { 28 | 29 | @Unroll 30 | def 'should get endpoint protocol' () { 31 | given: 32 | def client = new Client() 33 | expect: 34 | client.protocol(ENDPOINT) == EXPECTED 35 | 36 | where: 37 | ENDPOINT | EXPECTED 38 | null | 'https://' 39 | 'http://foo' | 'http://' 40 | 'https://bar.com' | 'https://' 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/json/JsonHelperTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.json 19 | 20 | import io.seqera.wave.api.SubmitContainerTokenRequest 21 | import io.seqera.wave.cli.model.ContainerInspectResponseEx 22 | import io.seqera.wave.core.spec.ContainerSpec 23 | import io.seqera.wave.core.spec.ManifestSpec 24 | import io.seqera.wave.core.spec.ObjectRef 25 | import spock.lang.Specification 26 | /** 27 | * @author Paolo Di Tommaso 28 | */ 29 | class JsonHelperTest extends Specification { 30 | 31 | def 'should encode request' () { 32 | given: 33 | def req = new SubmitContainerTokenRequest(containerImage: 'quay.io/nextflow/bash:latest') 34 | when: 35 | def json = JsonHelper.toJson(req) 36 | then: 37 | json == '{"containerImage":"quay.io/nextflow/bash:latest","freeze":false,"mirror":false}' 38 | } 39 | 40 | def 'should decode request' () { 41 | given: 42 | def REQ = '{"containerImage":"quay.io/nextflow/bash:latest","freeze":false}' 43 | when: 44 | def result = JsonHelper.fromJson(REQ, SubmitContainerTokenRequest) 45 | then: 46 | result.containerImage == 'quay.io/nextflow/bash:latest' 47 | } 48 | 49 | def 'should convert response to json' () { 50 | given: 51 | def layers = [new ObjectRef('text', 'sha256:12345', 100, null), new ObjectRef('text', 'sha256:67890', 200, null) ] 52 | def manifest = new ManifestSpec(2, 'some/media', null, layers, [one: '1', two:'2']) 53 | def spec = new ContainerSpec('docker.io', 'https://docker.io', 'ubuntu', '22.04', 'sha:12345', null, manifest) 54 | def resp = new ContainerInspectResponseEx(spec) 55 | 56 | when: 57 | def result = JsonHelper.toJson(resp) 58 | then: 59 | result == '''\ 60 | { 61 | "container":{ 62 | "digest": 63 | "sha:12345","hostName":"https://docker.io", 64 | "imageName":"ubuntu", 65 | "manifest":{ 66 | "annotations":{"one":"1","two":"2"}, 67 | "layers":[ 68 | {"digest":"sha256:12345", 69 | "mediaType":"text", 70 | "size":100, 71 | "uri":"https://docker.io/v2/ubuntu/blobs/sha256:12345"}, 72 | {"digest":"sha256:67890", 73 | "mediaType":"text", 74 | "size":200, 75 | "uri":"https://docker.io/v2/ubuntu/blobs/sha256:67890"} 76 | ], 77 | "mediaType":"some/media", 78 | "schemaVersion":2 79 | }, 80 | "reference":"22.04", 81 | "registry":"docker.io" 82 | } 83 | } 84 | '''.replaceAll("\\s+", "").trim() 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/util/BuildInfoTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util 19 | 20 | 21 | import spock.lang.Specification 22 | /** 23 | * 24 | * @author Paolo Di Tommaso 25 | */ 26 | class BuildInfoTest extends Specification { 27 | 28 | def 'should load version and commit id' () { 29 | expect: 30 | BuildInfo.getName() == 'wave-cli' 31 | BuildInfo.getVersion() 32 | BuildInfo.getCommitId() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/util/CheckersTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util 19 | 20 | 21 | import spock.lang.Specification 22 | import spock.lang.Unroll 23 | 24 | /** 25 | * 26 | * @author Paolo Di Tommaso 27 | */ 28 | class CheckersTest extends Specification { 29 | 30 | @Unroll 31 | def 'should check if the string is empty' () { 32 | expect: 33 | Checkers.isEmpty(STR) == EXPECTED 34 | where: 35 | STR | EXPECTED 36 | null | true 37 | '' | true 38 | ' ' | true 39 | 'foo' | false 40 | } 41 | 42 | @Unroll 43 | def 'should check if the list is empty' () { 44 | expect: 45 | Checkers.isEmpty(STR) == EXPECTED 46 | where: 47 | STR | EXPECTED 48 | null | true 49 | [] | true 50 | ['foo'] | false 51 | } 52 | 53 | @Unroll 54 | def 'should check env variable' () { 55 | expect: 56 | Checkers.isEnvVar(STR) == EXPECTED 57 | where: 58 | STR | EXPECTED 59 | null | false 60 | '' | false 61 | 'foo' | false 62 | '=' | false 63 | '100=1' | false 64 | and: 65 | 'a=b' | true 66 | 'FOO=1' | true 67 | 'FOO=' | true 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/util/GptHelperTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util 19 | 20 | import io.seqera.wave.api.PackagesSpec 21 | import spock.lang.Requires 22 | import spock.lang.Specification 23 | 24 | /** 25 | * 26 | * @author Paolo Di Tommaso 27 | */ 28 | class GptHelperTest extends Specification { 29 | 30 | 31 | def 'should map json to spec'() { 32 | given: 33 | def JSON = ''' 34 | {"packages":["multiqc=1.17","samtools"],"channels":["conda-forge"]} 35 | ''' 36 | when: 37 | def spec = GptHelper.jsonToPackageSpec(JSON) 38 | then: 39 | spec.entries == ['multiqc=1.17', 'samtools'] 40 | spec.channels == ['conda-forge'] 41 | } 42 | 43 | @Requires({ System.getenv('OPENAI_API_KEY') }) 44 | def 'should get a package spec from a prompt' () { 45 | when: 46 | def spec = GptHelper.grabPackages("Give me a container image for multiqc 1.15") 47 | then: 48 | spec == new PackagesSpec(type: PackagesSpec.Type.CONDA, entries: ['multiqc=1.15'], channels: ['bioconda','conda-forge']) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/util/StreamHelperTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util 19 | 20 | 21 | import spock.lang.Specification 22 | /** 23 | * 24 | * @author Paolo Di Tommaso 25 | */ 26 | class StreamHelperTest extends Specification { 27 | 28 | def 'should read from stream' () { 29 | expect: 30 | StreamHelper.tryReadStream(new ByteArrayInputStream('Hello\nworld!'.bytes)) == 'Hello\nworld!' 31 | StreamHelper.tryReadStream(new ByteArrayInputStream('Hello\nworld!\n\n'.bytes)) == 'Hello\nworld!\n\n' 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/test/groovy/io/seqera/wave/cli/util/YamlHelperTest.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | package io.seqera.wave.cli.util 19 | 20 | import java.time.Instant 21 | 22 | import io.seqera.wave.api.SubmitContainerTokenResponse 23 | import io.seqera.wave.cli.model.ContainerInspectResponseEx 24 | import io.seqera.wave.core.spec.ContainerSpec 25 | import io.seqera.wave.core.spec.ManifestSpec 26 | import io.seqera.wave.core.spec.ObjectRef 27 | import spock.lang.Specification 28 | /** 29 | * 30 | * @author Paolo Di Tommaso 31 | */ 32 | class YamlHelperTest extends Specification { 33 | 34 | def 'should convert container response' () { 35 | given: 36 | def resp = new SubmitContainerTokenResponse( 37 | containerToken: "12345", 38 | targetImage: 'docker.io/some/repo', 39 | containerImage: 'docker.io/some/container', 40 | expiration: Instant.ofEpochMilli(1691839913), 41 | buildId: '98765', 42 | cached: false, 43 | freeze: false, 44 | mirror: false, 45 | scanId: 'scan-123', 46 | succeeded: true 47 | ) 48 | 49 | when: 50 | def result = YamlHelper.toYaml(resp) 51 | then: 52 | result == '''\ 53 | buildId: '98765' 54 | cached: false 55 | containerImage: docker.io/some/container 56 | containerToken: '12345' 57 | expiration: '1970-01-20T13:57:19.913Z' 58 | freeze: false 59 | mirror: false 60 | scanId: scan-123 61 | succeeded: true 62 | targetImage: docker.io/some/repo 63 | '''.stripIndent(true) 64 | } 65 | 66 | def 'should convert response to yaml' () { 67 | given: 68 | def layers = [ new ObjectRef('text', 'sha256:12345', 100, null), new ObjectRef('text', 'sha256:67890', 200, null) ] 69 | def manifest = new ManifestSpec(2, 'some/media', null, layers, [one: '1', two:'2']) 70 | def spec = new ContainerSpec('docker.io', 'https://docker.io', 'ubuntu','22.04','sha:12345', null, manifest) 71 | def resp = new ContainerInspectResponseEx(spec) 72 | 73 | when: 74 | def result = YamlHelper.toYaml(resp) 75 | then: 76 | result == '''\ 77 | container: 78 | digest: sha:12345 79 | hostName: https://docker.io 80 | imageName: ubuntu 81 | manifest: 82 | annotations: 83 | one: '1' 84 | two: '2' 85 | layers: 86 | - digest: sha256:12345 87 | mediaType: text 88 | size: 100 89 | uri: https://docker.io/v2/ubuntu/blobs/sha256:12345 90 | - digest: sha256:67890 91 | mediaType: text 92 | size: 200 93 | uri: https://docker.io/v2/ubuntu/blobs/sha256:67890 94 | mediaType: some/media 95 | schemaVersion: 2 96 | reference: '22.04' 97 | registry: docker.io 98 | '''.stripIndent(true) 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | /* 19 | * This file was generated by the Gradle 'init' task. 20 | */ 21 | 22 | plugins { 23 | // Support convention plugins written in Groovy. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. 24 | id 'groovy-gradle-plugin' 25 | } 26 | 27 | repositories { 28 | // Use the plugin portal to apply community plugins in convention plugins. 29 | gradlePluginPortal() 30 | } 31 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | /* 19 | * This file was generated by the Gradle 'init' task. 20 | * 21 | * This settings file is used to specify which projects to include in your build-logic build. 22 | */ 23 | 24 | rootProject.name = 'buildSrc' 25 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io.seqera.wave.cli.java-application-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | /* 19 | * This file was generated by the Gradle 'init' task. 20 | */ 21 | 22 | plugins { 23 | // Apply the common convention plugin for shared build configuration between library and application projects. 24 | id 'io.seqera.wave.cli.java-common-conventions' 25 | 26 | // Apply the application plugin to add support for building a CLI application in Java. 27 | id 'application' 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io.seqera.wave.cli.java-common-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | /* 19 | * This file was generated by the Gradle 'init' task. 20 | */ 21 | 22 | plugins { 23 | // Apply the java Plugin to add support for Java. 24 | id 'java' 25 | } 26 | 27 | repositories { 28 | // Use Maven Central for resolving dependencies. 29 | mavenCentral() 30 | } 31 | 32 | dependencies { 33 | constraints { 34 | // Define dependency versions as constraints 35 | implementation 'org.apache.commons:commons-text:1.9' 36 | } 37 | 38 | implementation "org.slf4j:slf4j-api:2.0.16" 39 | implementation "org.slf4j:jcl-over-slf4j:2.0.16" 40 | implementation "org.slf4j:jul-to-slf4j:2.0.16" 41 | implementation "org.slf4j:log4j-over-slf4j:2.0.16" 42 | implementation "ch.qos.logback:logback-classic:1.5.15" 43 | implementation "ch.qos.logback:logback-core:1.5.15" 44 | 45 | // Use JUnit Jupiter for testing. 46 | testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' 47 | } 48 | 49 | tasks.named('test') { 50 | // Use JUnit Platform for unit tests. 51 | useJUnitPlatform() 52 | } 53 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/io.seqera.wave.cli.java-library-conventions.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023-2025, Seqera Labs 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 | 18 | /* 19 | * This file was generated by the Gradle 'init' task. 20 | */ 21 | 22 | plugins { 23 | // Apply the common convention plugin for shared build configuration between library and application projects. 24 | id 'io.seqera.wave.cli.java-common-conventions' 25 | // Apply the java-library plugin for API and implementation separation. 26 | id 'java-library' 27 | } 28 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | WAVE CLI CHANGE-LOG 2 | =================== 3 | 1.6.1 - 24 May 2025 4 | - Add --build-compression option 5 | 6 | 1.6.0 (skipped) 7 | 8 | 1.5.1 - 23 Dec 2024 9 | - Fix logback serialization vulnerability (#88) [6ae7be32] 10 | - Bump commons-io 2.18.0 [00b0ba28] 11 | - Bump logback 1.5.15 [74c8b85c] 12 | - Bump moshi 1.15.2 [23a2522f] 13 | - Bump wave deps (#89) [310f0ab9] 14 | - Updating mac runner for intel (#84) [d38295d0] 15 | 16 | 1.5.0 - 15 Oct 2024 17 | - [BREAKING] this version requires Wave backend 1.13.0 or later 18 | - Add required version info [fd01d1a5] 19 | - Add support for registry mirror and container scan feature (#78) [89ed9355] 20 | - Add support for succeced field in container response [6a5989d4] 21 | - Prevent false gpt prompts [2a82000e] 22 | - Remove hidden to --name-strategy option (#77) [fe4d00f2] 23 | - Removed spack support (#79) [72e23e1c] 24 | - Update reflect-config.json [fe91f4b1] 25 | - Upgrade gradle build [a1653d91] 26 | - Use build repository for mirror registry [b2b6ccf8] 27 | - Bump gradle 8.10 [592e5ad9] 28 | 29 | 1.4.1 - 28 May 2024 30 | - Add uri in wave inpect json output (#70) [08b17e4f] 31 | - Add --name-strategy option to wave cli (#71) [269df0e5] 32 | - fixed image strategy (#72) [eedd906e] 33 | - Fix conda channels ordering for gpt client [ed3c0be3] 34 | - Fix compatibility issues with old x86 machines (#74) [014154b7] 35 | - changed macos-latest to macos-12 (#76) [bf7e255f] 36 | 37 | 1.4.0 - 17 May 2024 38 | - Add cli opts shortcuts (#68) [ae79c6f9] 39 | - Remove seqera from default Conda channels (#67) [d7b12893] 40 | 41 | 1.3.1 - 22 Apr 2024 42 | - Remove build repository check [76c92766] 43 | - Improve error handling [b2cfdd3f] 44 | - Improve Gpt helper class [85a01baa] 45 | - Bump groovy 3.0.21 for tests [110ad94d] 46 | 47 | 1.3.0 - 11 Apr 2024 48 | - Add generative containers [26bb08de] 49 | - Use container-alpha1v2 endpoint (#62) [a923aacf] 50 | - change gc to g1gc for linux distribution of cli (#63) [9decb2a4] 51 | - Update default endpoint to api.cloud.seqera.io (#61) [416005cf] 52 | - bump github actions to v4 (#58) [2d0e2672] 53 | - Handle client connection errors [f5d6d9a5] 54 | 55 | 1.2.0 - 12 Feb 2024 56 | - Add support for container includes option [01d5904d] 57 | - Add support for inspect option [a29f905f] 58 | 59 | 1.1.3 - 4 Feb 2024 60 | - Add preserve timestamp option [4b710ea9] 61 | - Bump reflect and resource configs for native-image build [8875dd9d] 62 | - Bump Moshi 1.15.0 [e4c8c7c7] 63 | 64 | 1.1.2 - 20 Dec 2023 65 | - Add static compilation based on musl (#30) [f880d302] 66 | - Add multiplatform brew support (#49) [aa618329] 67 | - Add CI action to publish wave-cli to homebrew (#37) [a25adbd9] 68 | - Add fat JAR config (#36) [bb3ec2b2] 69 | - Max heap set to 256 MB (#48) [d70972eb] 70 | 71 | 1.1.1 - 30 Nov 2023 72 | - Fix invalid protol prefix with await option [d37faca1] 73 | - Fix execute CI tests on pull requests (#44) [5e236b0a] 74 | - Fix error when a context file path is longer than 100 chars (#26) [d8eed21b] 75 | - Bump org.graalvm.buildtools.native to 0.9.28 (#40) [244be075] 76 | - Bump gradle 8.4 (#39) [399f0b22] 77 | 78 | 1.1.0 - 10 Nov 2023 79 | - Add ability to specify platform for Singularity build [c19727a5] 80 | - Improve err handling for missing files [98df6ea3] 81 | 82 | 1.0.0 - 16 Oct 2023 83 | - Initial release -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seqeralabs/wave-cli/23a55f8b1b0018e8a35b6dc6a204584078bd4dc4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023-2025, Seqera Labs 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 | 18 | distributionBase=GRADLE_USER_HOME 19 | distributionPath=wrapper/dists 20 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 21 | networkTimeout=10000 22 | zipStoreBase=GRADLE_USER_HOME 23 | zipStorePath=wrapper/dists 24 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /jreleaser.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: wave-cli 3 | description: Wave CLI 4 | longDescription: Command line tool for Wave container provisioning 5 | website: https://github.com/seqeralabs/wave-cli 6 | authors: 7 | - Seqera 8 | license: Apache-2.0 9 | extraProperties: 10 | inceptionYear: 2023 11 | java: 12 | groupId: io.seqera.wave.cli 13 | version: 21 14 | release: 15 | github: 16 | overwrite: true 17 | draft: false 18 | prerelease: 19 | pattern: .*-beta 20 | changelog: 21 | formatted: ALWAYS 22 | format: '- {{commitShortHash}} {{commitTitle}}' 23 | contributors: 24 | format: '- {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}}' 25 | labelers: 26 | - label: 'feature' 27 | title: 'Resolves #' 28 | body: 'Resolves #' 29 | - label: 'feature' 30 | title: '[feature]' 31 | - label: 'issue' 32 | title: 'Fixes #' 33 | body: 'Fixes #' 34 | - label: 'issue' 35 | title: 'Relates to #' 36 | body: 'Relates to #' 37 | - label: 'issue' 38 | title: '[bug]' 39 | - label: 'task' 40 | title: '[task]' 41 | - label: 'merge_pull' 42 | title: 'Merge pull' 43 | - label: 'merge_branch' 44 | title: 'Merge branch' 45 | - label: 'release' 46 | title: '[release]' 47 | categories: 48 | - title: '🚀 Features' 49 | labels: 50 | - 'feature' 51 | - title: '✅ Issues' 52 | labels: 53 | - 'issue' 54 | - title: '🧰 Tasks' 55 | labels: 56 | - 'task' 57 | - title: 'Merge' 58 | labels: 59 | - 'merge_pull' 60 | - 'merge_branch' 61 | - title: 'Release' 62 | labels: 63 | - 'release' 64 | hide: 65 | categories: 66 | - 'Merge' 67 | - 'Release' 68 | contributors: 69 | - 'GitHub' 70 | replacers: 71 | - search: ' \[feature\]' 72 | - search: ' \[bug\]' 73 | - search: ' \[task\]' 74 | - search: ' \[skip ci\]' 75 | - search: ' \[release\]' 76 | 77 | distributions: 78 | wave-cli: 79 | type: FLAT_BINARY 80 | executable: 81 | name: wave 82 | artifacts: 83 | - path: "nativeCompile-ubuntu-latest/wave" 84 | transform: "wave-{{projectEffectiveVersion}}-linux-x86_64" 85 | platform: linux-x86_64 86 | extraProperties: 87 | graalVMNativeImage: true 88 | - path: "nativeCompile-ubuntu-24.04-arm/wave" 89 | transform: "wave-{{projectEffectiveVersion}}-linux-arm64" 90 | platform: linux-aarch_64 91 | extraProperties: 92 | graalVMNativeImage: true 93 | - path: "nativeCompile-windows-latest/wave.exe" 94 | transform: "wave-{{projectEffectiveVersion}}-windows-x86_64.exe" 95 | platform: windows-x86_64 96 | extraProperties: 97 | graalVMNativeImage: true 98 | - path: "nativeCompile-macos-latest-large/wave" 99 | transform: "wave-{{projectEffectiveVersion}}-macos-x86_64" 100 | platform: osx-x86_64 101 | extraProperties: 102 | graalVMNativeImage: true 103 | - path: "nativeCompile-macos-latest-xlarge/wave" 104 | transform: "wave-{{projectEffectiveVersion}}-macos-arm64" 105 | platform: osx-aarch_64 106 | extraProperties: 107 | graalVMNativeImage: true 108 | 109 | wave-cli-jar: 110 | type: SINGLE_JAR 111 | artifacts: 112 | - path: "wave-jar/wave.jar" 113 | transform: "wave-{{projectEffectiveVersion}}.jar" 114 | 115 | packagers: 116 | brew: 117 | continueOnError: false 118 | multiPlatform: true 119 | repository: 120 | active: RELEASE 121 | tagName: '{{distributionName}}-{{tagName}}' 122 | branch: HEAD 123 | commitMessage: '{{distributionName}} {{tagName}}' 124 | 125 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023-2025, Seqera Labs 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 | 18 | args="${@:--h}" 19 | ./gradlew run --rerun-tasks --args="$args" 20 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/7.6.1/userguide/multi_project_builds.html 8 | */ 9 | pluginManagement { 10 | repositories { 11 | mavenCentral() 12 | gradlePluginPortal() 13 | } 14 | } 15 | 16 | plugins { 17 | // required to download the toolchain (jdk) from a remote repository 18 | // https://github.com/gradle/foojay-toolchains 19 | // https://docs.gradle.org/current/userguide/toolchains.html#sub:download_repositories 20 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" 21 | } 22 | 23 | rootProject.name = 'wave-cli' 24 | include('app') 25 | 26 | // enable for local development 27 | // includeBuild("../libseqera") 28 | --------------------------------------------------------------------------------