From 5855fba8924a6e65495f2e0b44c3ce4d46984688 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 28 Mar 2024 08:45:32 +0100 Subject: [PATCH 01/20] Add support for PackagesSpec (#19) Signed-off-by: Paolo Di Tommaso Signed-off-by: munishchouhan Co-authored-by: munishchouhan --- .../java/io/seqera/wave/api/PackagesSpec.java | 120 +++++++++++++ .../wave/api/SubmitContainerTokenRequest.java | 17 ++ .../api/SubmitContainerTokenResponse.java | 8 +- .../java/io/seqera/wave/config/CondaOpts.java | 14 ++ .../java/io/seqera/wave/config/SpackOpts.java | 16 ++ .../seqera/wave/api/PackagesSpecTest.groovy | 71 ++++++++ .../SubmitContainerTokenRequestTest.groovy | 8 +- .../seqera/wave/config/CondaOptsTest.groovy | 14 ++ .../seqera/wave/config/SpackOptsTest.groovy | 14 ++ .../io/seqera/wave/util/DockerHelper.java | 93 +++++++--- .../seqera/wave/util/DockerHelperTest.groovy | 167 ++++++++++++++++++ 11 files changed, 514 insertions(+), 28 deletions(-) create mode 100644 wave-api/src/main/java/io/seqera/wave/api/PackagesSpec.java rename {wave-utils => wave-api}/src/main/java/io/seqera/wave/config/CondaOpts.java (79%) rename {wave-utils => wave-api}/src/main/java/io/seqera/wave/config/SpackOpts.java (77%) create mode 100644 wave-api/src/test/groovy/io/seqera/wave/api/PackagesSpecTest.groovy rename {wave-utils => wave-api}/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy (79%) rename {wave-utils => wave-api}/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy (78%) diff --git a/wave-api/src/main/java/io/seqera/wave/api/PackagesSpec.java b/wave-api/src/main/java/io/seqera/wave/api/PackagesSpec.java new file mode 100644 index 0000000..1ca5e41 --- /dev/null +++ b/wave-api/src/main/java/io/seqera/wave/api/PackagesSpec.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.wave.api; + +import java.util.List; +import java.util.Objects; + +import io.seqera.wave.config.CondaOpts; +import io.seqera.wave.config.SpackOpts; +/** + * Model a Package environment requirements + * + * @author Paolo Di Tommaso + */ +public class PackagesSpec { + + public enum Type { CONDA, SPACK } + + public Type type; + + /** + * The package environment file encoded as a base64 string. When this is provided the field {@link #entries} is not allowed + */ + public String environment; + + /** + * A list of one or more packages. When this is provided the field {@link #environment} is not allowed + */ + public List entries; + + /** + * Conda build options + */ + public CondaOpts condaOpts; + + /** + * Spack build options + */ + public SpackOpts spackOpts; + + /** + * channels used for downloading packages + */ + public List channels; + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + PackagesSpec that = (PackagesSpec) object; + return type == that.type + && Objects.equals(environment, that.environment) + && Objects.equals(entries, that.entries) + && Objects.equals(condaOpts, that.condaOpts) + && Objects.equals(spackOpts, that.spackOpts) + && Objects.equals(channels, that.channels); + } + + @Override + public int hashCode() { + return Objects.hash(type, environment, entries, condaOpts, spackOpts, channels); + } + + @Override + public String toString() { + return "PackagesSpec{" + + "type=" + type + + ", envFile='" + environment + '\'' + + ", packages=" + entries + + ", condaOpts=" + condaOpts + + ", spackOpts=" + spackOpts + + ", channels=" + ObjectUtils.toString(channels) + + '}'; + } + + public PackagesSpec withType(Type type) { + this.type = type; + return this; + } + + public PackagesSpec withEnvironment(String encoded) { + this.environment = encoded; + return this; + } + + public PackagesSpec withEntries(List entries) { + this.entries = entries; + return this; + } + + public PackagesSpec withChannels(List channels) { + this.channels = channels; + return this; + } + + public PackagesSpec withCondaOpts(CondaOpts opts) { + this.condaOpts = opts; + return this; + } + + public PackagesSpec withSpackOpts(SpackOpts opts) { + this.spackOpts = opts; + return this; + } +} diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java index 25a6ea7..b4e6871 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java @@ -68,11 +68,13 @@ public class SubmitContainerTokenRequest implements Cloneable { /** * Conda recipe file used to build the container */ + @Deprecated public String condaFile; /** * Spack recipe file used to build the container */ + @Deprecated public String spackFile; /** @@ -132,6 +134,11 @@ public class SubmitContainerTokenRequest implements Cloneable { */ public List containerIncludes; + /** + * Defines the packages to be included in this container request + */ + public PackagesSpec packages; + public SubmitContainerTokenRequest copyWith(Map opts) { try { final SubmitContainerTokenRequest copy = (SubmitContainerTokenRequest) this.clone(); @@ -175,6 +182,8 @@ public SubmitContainerTokenRequest copyWith(Map opts) { copy.workflowId = (String) opts.get("workflowId"); if( opts.containsKey("containerIncludes")) copy.containerIncludes = (List) opts.get("containerIncludes"); + if( opts.containsKey("packages")) + copy.packages = (PackagesSpec) opts.get("packages"); // done return copy; } @@ -218,11 +227,13 @@ public SubmitContainerTokenRequest withContainerConfig(ContainerConfig config) { return this; } + @Deprecated public SubmitContainerTokenRequest withCondaFile(String condaFile) { this.condaFile = condaFile; return this; } + @Deprecated public SubmitContainerTokenRequest withSpackFile(String spackFile) { this.spackFile = spackFile; return this; @@ -283,6 +294,11 @@ public SubmitContainerTokenRequest withContainerIncludes(List containerI return this; } + public SubmitContainerTokenRequest withPackages(PackagesSpec packages) { + this.packages = packages; + return this; + } + public boolean formatSingularity() { return "sif".equals(format); } @@ -310,6 +326,7 @@ public String toString() { ", dryRun=" + dryRun + ", workflowId=" + workflowId + ", containerIncludes=" + ObjectUtils.toString(containerIncludes) + + ", packages=" + packages + '}'; } } diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java index 0693bee..3486ab4 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java @@ -52,13 +52,19 @@ public class SubmitContainerTokenResponse { */ public String buildId; + /** + * Whenever it's a cached build image. Only supported by API version v1alpha2 + */ + public Boolean cached; + public SubmitContainerTokenResponse() { } - public SubmitContainerTokenResponse(String token, String target, Instant expiration, String containerImage, String buildId) { + public SubmitContainerTokenResponse(String token, String target, Instant expiration, String containerImage, String buildId, Boolean cached) { this.containerToken = token; this.targetImage = target; this.expiration = expiration; this.containerImage = containerImage; this.buildId = buildId; + this.cached = cached; } } diff --git a/wave-utils/src/main/java/io/seqera/wave/config/CondaOpts.java b/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java similarity index 79% rename from wave-utils/src/main/java/io/seqera/wave/config/CondaOpts.java rename to wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java index a4174ef..45fe4ba 100644 --- a/wave-utils/src/main/java/io/seqera/wave/config/CondaOpts.java +++ b/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; /** * Conda build options @@ -65,4 +66,17 @@ public String toString() { commands != null ? String.join(",", commands) : "null" ); } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + CondaOpts condaOpts = (CondaOpts) object; + return Objects.equals(mambaImage, condaOpts.mambaImage) && Objects.equals(commands, condaOpts.commands) && Objects.equals(basePackages, condaOpts.basePackages); + } + + @Override + public int hashCode() { + return Objects.hash(mambaImage, commands, basePackages); + } } diff --git a/wave-utils/src/main/java/io/seqera/wave/config/SpackOpts.java b/wave-api/src/main/java/io/seqera/wave/config/SpackOpts.java similarity index 77% rename from wave-utils/src/main/java/io/seqera/wave/config/SpackOpts.java rename to wave-api/src/main/java/io/seqera/wave/config/SpackOpts.java index 8356071..f30f58e 100644 --- a/wave-utils/src/main/java/io/seqera/wave/config/SpackOpts.java +++ b/wave-api/src/main/java/io/seqera/wave/config/SpackOpts.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; /** * Spack build options @@ -27,6 +28,8 @@ */ public class SpackOpts { + public static final SpackOpts EMPTY = new SpackOpts(); + /** * Custom Dockerfile `RUN` commands that can be used to customise the target container */ @@ -62,4 +65,17 @@ public String toString() { commands != null ? String.join(",", commands) : "null" ); } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + SpackOpts spackOpts = (SpackOpts) object; + return Objects.equals(commands, spackOpts.commands) && Objects.equals(basePackages, spackOpts.basePackages); + } + + @Override + public int hashCode() { + return Objects.hash(commands, basePackages); + } } diff --git a/wave-api/src/test/groovy/io/seqera/wave/api/PackagesSpecTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/api/PackagesSpecTest.groovy new file mode 100644 index 0000000..63b9c22 --- /dev/null +++ b/wave-api/src/test/groovy/io/seqera/wave/api/PackagesSpecTest.groovy @@ -0,0 +1,71 @@ +/* + * Copyright 2024, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.wave.api + +import io.seqera.wave.config.CondaOpts +import io.seqera.wave.config.SpackOpts +import spock.lang.Specification +/** + * + * @author Munish Chouhan + */ +class PackagesSpecTest extends Specification { + + def 'should check equals and hashcode' () { + given: + def packages1 = new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar'], channels: ['1', '2']) + def packages2 = new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar'], channels: ['1', '2']) + def packages3 = new PackagesSpec(type: PackagesSpec.Type.SPACK, environment: 'foo', entries: ['bar']) + + expect: + packages1 == packages2 + packages1 != packages3 + + and: + packages1.hashCode() == packages2.hashCode() + packages1.hashCode() != packages3.hashCode() + } + + def 'should infer the correct type' () { + given: + def packages1 = new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar'], channels: ['1', '2']) + def packages2 = new PackagesSpec(type: PackagesSpec.Type.SPACK, environment: 'foo', entries: ['bar']) + + expect: + packages1.type == PackagesSpec.Type.CONDA + packages2.type == PackagesSpec.Type.SPACK + } + + def 'should set values' () { + when: + def spec = new PackagesSpec() + .withType(PackagesSpec.Type.CONDA) + .withCondaOpts(new CondaOpts(basePackages: 'base:one')) + .withSpackOpts(new SpackOpts(basePackages: 'base:two')) + .withChannels(['c1','c2']) + .withEntries(['p1', 'p2']) + .withEnvironment('foo-env') + then: + spec.type == PackagesSpec.Type.CONDA + spec.condaOpts == new CondaOpts(basePackages: 'base:one') + spec.spackOpts == new SpackOpts(basePackages: 'base:two') + spec.channels == ['c1','c2'] + spec.entries == ['p1', 'p2'] + spec.environment == 'foo-env' + } +} diff --git a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy index 585960e..8878b33 100644 --- a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy @@ -46,7 +46,8 @@ class SubmitContainerTokenRequestTest extends Specification { format: 'sif', dryRun: true, workflowId: 'id123', - containerIncludes: ['busybox:latest'] + containerIncludes: ['busybox:latest'], + packages: new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar']) ) when: @@ -71,6 +72,7 @@ class SubmitContainerTokenRequestTest extends Specification { copy.dryRun == req.dryRun copy.workflowId == req.workflowId copy.containerIncludes == req.containerIncludes + copy.packages == req.packages and: copy.formatSingularity() @@ -94,7 +96,8 @@ class SubmitContainerTokenRequestTest extends Specification { format: 'foo', dryRun: false, workflowId: 'id123', - containerIncludes: ['other:image'] + containerIncludes: ['other:image'], + packages: new PackagesSpec(type: PackagesSpec.Type.SPACK) ) then: other.towerAccessToken == 'b1' @@ -116,6 +119,7 @@ class SubmitContainerTokenRequestTest extends Specification { other.dryRun == false other.workflowId == 'id123' other.containerIncludes == ['other:image'] + other.packages == new PackagesSpec(type: PackagesSpec.Type.SPACK) and: !other.formatSingularity() } diff --git a/wave-utils/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy similarity index 79% rename from wave-utils/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy rename to wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy index 8f66c0e..b2b2be3 100644 --- a/wave-utils/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy @@ -26,6 +26,20 @@ import spock.lang.Unroll */ class CondaOptsTest extends Specification { + def 'should validate equals and hashcode' () { + given: + def c1 = new CondaOpts(mambaImage: 'foo:one', basePackages: 'x y', commands: ['this','that']) + def c2 = new CondaOpts(mambaImage: 'foo:one', basePackages: 'x y', commands: ['this','that']) + def c3 = new CondaOpts(mambaImage: 'foo:two', basePackages: 'x y', commands: ['this','that']) + + expect: + c1 == c2 + c1 != c3 + and: + c1.hashCode() == c2.hashCode() + c1.hashCode() != c3.hashCode() + } + def 'check conda options' () { when: def opts = new CondaOpts([:]) diff --git a/wave-utils/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy similarity index 78% rename from wave-utils/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy rename to wave-api/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy index bc75a63..601152d 100644 --- a/wave-utils/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/config/SpackOptsTest.groovy @@ -26,6 +26,20 @@ import spock.lang.Unroll */ class SpackOptsTest extends Specification { + def 'should validate equals and hashcode' () { + given: + def c1 = new SpackOpts(basePackages: 'x y', commands: ['this','that']) + def c2 = new SpackOpts(basePackages: 'x y', commands: ['this','that']) + def c3 = new SpackOpts(basePackages: 'x z', commands: ['this','that']) + + expect: + c1 == c2 + c1 != c3 + and: + c1.hashCode() == c2.hashCode() + c1.hashCode() != c3.hashCode() + } + def 'check spack default options' () { given: def opts = new SpackOpts() diff --git a/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java b/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java index 711e252..2c7c1c4 100644 --- a/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java +++ b/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java @@ -19,7 +19,6 @@ import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -90,7 +89,7 @@ protected static String trim0(String value) { return value; } - static String condaPackagesToCondaYaml(String packages, List channels) { + public static String condaPackagesToCondaYaml(String packages, List channels) { if( packages==null || packages.isBlank() ) return null; @@ -163,28 +162,36 @@ public static Path condaFileFromPath(String condaFile, List channels) { } // => parse the conda file yaml, add the base packages to it - final Yaml yaml = new Yaml(); try { - // 1. parse the file - Map root = yaml.load(new FileReader(condaFile)); - // 2. append channels - if( channels!=null ) { - List channels0 = (List)root.get("channels"); - if( channels0==null ) { - channels0 = new ArrayList<>(); - root.put("channels", channels0); - } - for( String it : channels ) { - if( !channels0.contains(it) ) - channels0.add(it); - } - } - // 3. return it as a new temp file - return toYamlTempFile( dumpCondaYaml(root) ); + final String result = condaEnvironmentToCondaYaml(Files.readString(condaEnvPath), channels); + return toYamlTempFile(result); } catch (FileNotFoundException e) { throw new IllegalArgumentException("The specified Conda environment file cannot be found: " + condaFile, e); } + catch (IOException e) { + throw new IllegalArgumentException("Unable to parse conda file: " + condaFile, e); + } + } + + public static String condaEnvironmentToCondaYaml(String env, List channels) { + final Yaml yaml = new Yaml(); + // 1. parse the file + Map root = yaml.load(env); + // 2. append channels + if( channels!=null ) { + List channels0 = (List)root.get("channels"); + if( channels0==null ) { + channels0 = new ArrayList<>(); + root.put("channels", channels0); + } + for( String it : channels ) { + if( !channels0.contains(it) ) + channels0.add(it); + } + } + // 3. return it + return dumpCondaYaml(root); } static public List spackPackagesToList(String packages) { @@ -214,6 +221,9 @@ static public List spackPackagesToList(String packages) { } static public String spackPackagesToSpackYaml(String packages, SpackOpts opts) { + if( opts==null ) + opts = SpackOpts.EMPTY; + final List base = spackPackagesToList(opts.basePackages); final List custom = spackPackagesToList(packages); if( base==null && custom==null ) @@ -412,18 +422,51 @@ public static Path addPackagesToSpackFile(String spackFile, SpackOpts opts) { return spackEnvPath; } + // Case D - last case, both spack file and base packages are specified + // => parse the spack file yaml, add the base packages to it + try { + final String result = addPackagesToSpackYaml(Files.readString(spackEnvPath), opts); + return toYamlTempFile( result ); + } + catch (FileNotFoundException e) { + throw new IllegalArgumentException("The specified Spack environment file cannot be found: " + spackFile, e); + } + catch (IOException e) { + throw new IllegalArgumentException("Unable to parse Spack environment file: " + spackFile, e); + } + } + + public static String addPackagesToSpackYaml(String spackYaml, SpackOpts opts) { + if( opts==null ) + opts = SpackOpts.EMPTY; + + // Case A - both empty, nothing to do + if( StringUtils.isEmpty(spackYaml) && StringUtils.isEmpty(opts.basePackages) ) + return null; + + // Case B - the spack file is empty, but some base package are given + // create a spack file with those packages + if( StringUtils.isEmpty(spackYaml) ) { + return spackPackagesToSpackYaml(null, opts); + } + + // Case C - if not base packages are given just return the spack file as a path + if( StringUtils.isEmpty(opts.basePackages) ) { + return spackYaml; + } + // Case D - last case, both spack file and base packages are specified // => parse the spack file yaml, add the base packages to it final Yaml yaml = new Yaml(); try { // 1. parse the file - Map data = yaml.load(new FileReader(spackFile)); + Map data = yaml.load(spackYaml); // 2. parse the base packages final List base = spackPackagesToList(opts.basePackages); // 3. append to the specs Map spack = (Map) data.get("spack"); if( spack==null ) { - throw new IllegalArgumentException("The specified Spack environment file does not contain a root entry 'spack:' - offending file path: " + spackFile); + throw new IllegalArgumentException("The specified Spack environment file does not contain a root entry 'spack:'"); } List specs = (List)spack.get("specs"); if( specs==null ) { @@ -431,11 +474,11 @@ public static Path addPackagesToSpackFile(String spackFile, SpackOpts opts) { spack.put("specs", specs); } specs.addAll(base); - // 5. return it as a new temp file - return toYamlTempFile( yaml.dump(data) ); + // 5. return it + return yaml.dump(data) ; } - catch (FileNotFoundException e) { - throw new IllegalArgumentException("The specified Spack environment file cannot be found: " + spackFile, e); + catch (Exception e) { + throw new IllegalArgumentException("Unable to parse Spack yaml:\n" + spackYaml); } } diff --git a/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy b/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy index 1b622e4..09535e7 100644 --- a/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy +++ b/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy @@ -129,6 +129,114 @@ class DockerHelperTest extends Specification { DockerHelper.condaPackagesToCondaYaml(' ', ['foo']) == null } + def 'should add conda packages to conda yaml /1' () { + given: + def text = '''\ + dependencies: + - foo=1.0 + - bar=2.0 + '''.stripIndent(true) + + when: + def result = DockerHelper.condaEnvironmentToCondaYaml(text, null) + then: + result == '''\ + dependencies: + - foo=1.0 + - bar=2.0 + '''.stripIndent(true) + + when: + result = DockerHelper.condaEnvironmentToCondaYaml(text, ['ch1', 'ch2']) + then: + result == '''\ + dependencies: + - foo=1.0 + - bar=2.0 + channels: + - ch1 + - ch2 + '''.stripIndent(true) + } + + def 'should add conda packages to conda yaml /2' () { + given: + def text = '''\ + dependencies: + - foo=1.0 + - bar=2.0 + channels: + - hola + - ciao + '''.stripIndent(true) + + when: + def result = DockerHelper.condaEnvironmentToCondaYaml(text, null) + then: + result == '''\ + dependencies: + - foo=1.0 + - bar=2.0 + channels: + - hola + - ciao + '''.stripIndent(true) + + when: + result = DockerHelper.condaEnvironmentToCondaYaml(text, ['ch1', 'ch2']) + then: + result == '''\ + dependencies: + - foo=1.0 + - bar=2.0 + channels: + - hola + - ciao + - ch1 + - ch2 + '''.stripIndent(true) + } + + def 'should add conda packages to conda yaml /3' () { + given: + def text = '''\ + channels: + - hola + - ciao + '''.stripIndent(true) + + when: + def result = DockerHelper.condaEnvironmentToCondaYaml(text, null) + then: + result == '''\ + channels: + - hola + - ciao + '''.stripIndent(true) + + when: + result = DockerHelper.condaEnvironmentToCondaYaml(text, ['ch1', 'ch2']) + then: + result == '''\ + channels: + - hola + - ciao + - ch1 + - ch2 + '''.stripIndent(true) + + when: + result = DockerHelper.condaEnvironmentToCondaYaml(text, ['bioconda']) + then: + result == '''\ + channels: + - hola + - ciao + - bioconda + '''.stripIndent(true) + } + + def 'should add conda packages to conda file /1' () { given: def condaFile = Files.createTempFile('conda','yaml') @@ -606,6 +714,65 @@ class DockerHelperTest extends Specification { folder?.deleteDir() } + + def 'should merge spack yaml and base package' () { + given: + def SPACK_FILE1 = '''\ + spack: + specs: [foo@1.2.3 x=one, bar @2] + concretizer: {unify: true, reuse: false} + '''.stripIndent(true) + and: + def SPACK_FILE2 = '''\ + spack: + concretizer: {unify: true, reuse: false} + '''.stripIndent(true) + and: + def SPACK_FILE3 = '''\ + foo: + concretizer: {unify: true, reuse: false} + '''.stripIndent(true) + + when: + def result = DockerHelper.addPackagesToSpackYaml(null, new SpackOpts()) + then: + result == null + + when: + result = DockerHelper.addPackagesToSpackYaml(SPACK_FILE1, new SpackOpts()) + then: + result == SPACK_FILE1 + + when: + result = DockerHelper.addPackagesToSpackYaml(SPACK_FILE1, new SpackOpts(basePackages: 'alpha delta')) + then: + result == '''\ + spack: + specs: [foo@1.2.3 x=one, bar @2, alpha, delta] + concretizer: {unify: true, reuse: false} + '''.stripIndent(true) + + + when: + result = DockerHelper.addPackagesToSpackYaml(SPACK_FILE2, new SpackOpts(basePackages: 'alpha delta')) + then: + result == '''\ + spack: + concretizer: {unify: true, reuse: false} + specs: [alpha, delta] + '''.stripIndent(true) + + when: + DockerHelper.addPackagesToSpackYaml(SPACK_FILE3, new SpackOpts(basePackages: 'foo')) + then: + thrown(IllegalArgumentException) + + when: + DockerHelper.addPackagesToSpackYaml('missing file', new SpackOpts(basePackages: 'foo')) + then: + thrown(IllegalArgumentException) + } + /* ********************************************************************************* * conda packages to singularity tests * *********************************************************************************/ From b25580a6550a6cc513d225a3cd82b77c4c7031e4 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 28 Mar 2024 08:46:17 +0100 Subject: [PATCH 02/20] [release] bump wave-api@0.8.0-beta1 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index 7486fdb..6cf5099 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.7.2 +0.8.0-beta1 From a79f4b153d866e182fbcd848f196e2b16c86f1d7 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 28 Mar 2024 10:50:01 +0100 Subject: [PATCH 03/20] [release] bump wave-utils@0.12.0-beta1 Signed-off-by: Paolo Di Tommaso --- wave-utils/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-utils/VERSION b/wave-utils/VERSION index d9df1bb..146d8e3 100644 --- a/wave-utils/VERSION +++ b/wave-utils/VERSION @@ -1 +1 @@ -0.11.0 +0.12.0-beta1 From c84d2341a9a517f08b97d4a80ae4b236ad44cc6c Mon Sep 17 00:00:00 2001 From: Munish Chouhan Date: Wed, 3 Apr 2024 12:30:32 +0200 Subject: [PATCH 04/20] Added BuildStatusResponse (#20) Signed-off-by: munishchouhan Signed-off-by: Paolo Di Tommaso Co-authored-by: Paolo Di Tommaso --- .../seqera/wave/api/BuildStatusResponse.java | 90 +++++++++++++++++++ .../wave/api/BuildStatusResponseTest.groovy | 61 +++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 wave-api/src/main/java/io/seqera/wave/api/BuildStatusResponse.java create mode 100644 wave-api/src/test/groovy/io/seqera/wave/api/BuildStatusResponseTest.groovy diff --git a/wave-api/src/main/java/io/seqera/wave/api/BuildStatusResponse.java b/wave-api/src/main/java/io/seqera/wave/api/BuildStatusResponse.java new file mode 100644 index 0000000..b6c03b5 --- /dev/null +++ b/wave-api/src/main/java/io/seqera/wave/api/BuildStatusResponse.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.seqera.wave.api; + +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; + + +/** + * Build status response + * + * @author Munish Chouhan + */ +public class BuildStatusResponse { + public enum Status { PENDING, COMPLETED } + + /** Build Id */ + final public String id; + + /** Status of image build */ + final public Status status; + + /** Build start time */ + final public Instant startTime; + + /** Duration to complete build */ + final public Duration duration; + + /** Build success status */ + final public Boolean succeeded; + + /** + * This is required to allow jackson serialization - do not remove + */ + private BuildStatusResponse() { + id = null; + status = null; + startTime = null; + duration = null; + succeeded = null; + } + + public BuildStatusResponse(String id, Status status, Instant startTime, Duration duration, Boolean succeeded) { + this.id = id; + this.status = status; + this.startTime = startTime; + this.duration = duration; + this.succeeded = succeeded; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + BuildStatusResponse that = (BuildStatusResponse) object; + return Objects.equals(id, that.id) && status == that.status && Objects.equals(startTime, that.startTime) && Objects.equals(duration, that.duration) && Objects.equals(succeeded, that.succeeded); + } + + @Override + public int hashCode() { + return Objects.hash(id, status, startTime, duration, succeeded); + } + + @Override + public String toString() { + return "BuildStatusResponse{" + + "id='" + id + '\'' + + ", status=" + status + + ", startTime=" + startTime + + ", duration=" + duration + + ", succeeded=" + succeeded + + '}'; + } +} diff --git a/wave-api/src/test/groovy/io/seqera/wave/api/BuildStatusResponseTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/api/BuildStatusResponseTest.groovy new file mode 100644 index 0000000..488b94f --- /dev/null +++ b/wave-api/src/test/groovy/io/seqera/wave/api/BuildStatusResponseTest.groovy @@ -0,0 +1,61 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2023-2024, Seqera Labs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.seqera.wave.api + +import java.time.Duration +import java.time.Instant + +import spock.lang.Specification +/** + * + * @author Paolo Di Tommaso + */ +class BuildStatusResponseTest extends Specification { + + def 'should validate equals and hashcode' () { + given: + def n = Instant.now() + def d = Duration.ofMinutes(1) + def c1 = new BuildStatusResponse('123', BuildStatusResponse.Status.PENDING, n, d, true) + def c2 = new BuildStatusResponse('123', BuildStatusResponse.Status.PENDING, n, d, true) + def c3 = new BuildStatusResponse('321', BuildStatusResponse.Status.PENDING, n, d, true) + + expect: + c1 == c2 + c1 != c3 + and: + c1.hashCode() == c2.hashCode() + c1.hashCode() != c3.hashCode() + } + + def 'should validate creation' () { + given: + def n = Instant.now() + def d = Duration.ofMinutes(1) + def c1 = new BuildStatusResponse('123', BuildStatusResponse.Status.PENDING, n, d, true) + + expect: + c1.id == '123' + c1.status == BuildStatusResponse.Status.PENDING + c1.startTime == n + c1.duration == d + c1.succeeded == true + } + +} From 874df3c3230d2a32d38a47026b4363bca12440ad Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Wed, 3 Apr 2024 12:39:12 +0200 Subject: [PATCH 05/20] [release] bump 0.8.0-beta2 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index 6cf5099..fd08366 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.8.0-beta1 +0.8.0-beta2 From 07ce51c9aaf1dff2e62056cd9bf39152547353fe Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 4 Apr 2024 09:31:12 +0200 Subject: [PATCH 06/20] Add freeze attribute to container response Signed-off-by: Paolo Di Tommaso --- .../api/SubmitContainerTokenResponse.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java index 3486ab4..545600c 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java @@ -18,6 +18,7 @@ package io.seqera.wave.api; import java.time.Instant; +import java.util.Objects; /** @@ -38,17 +39,20 @@ public class SubmitContainerTokenResponse { public String targetImage; /** - * The time instant when the container token is going to expire + * The time instant when the container token is going to expire. + * This attribute is only available when {@link #freeze} is {@code false} */ public Instant expiration; /** * The source container image that originated this request */ + @Deprecated public String containerImage; /** - * The ID of the build associated with this request or null of the image already exists + * The ID of the build associated with this request or null of the image already exists. + * Version v1alpha2 as later. */ public String buildId; @@ -57,14 +61,52 @@ public class SubmitContainerTokenResponse { */ public Boolean cached; + /** + * When the result is a freeze container. Version v1alpha2 as later. + */ + public Boolean freeze; + public SubmitContainerTokenResponse() { } - public SubmitContainerTokenResponse(String token, String target, Instant expiration, String containerImage, String buildId, Boolean cached) { + public SubmitContainerTokenResponse(String token, String target, Instant expiration, String containerImage, String buildId, Boolean cached, Boolean freeze) { this.containerToken = token; this.targetImage = target; this.expiration = expiration; this.containerImage = containerImage; this.buildId = buildId; this.cached = cached; + this.freeze = freeze; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + SubmitContainerTokenResponse that = (SubmitContainerTokenResponse) object; + return Objects.equals(containerToken, that.containerToken) + && Objects.equals(targetImage, that.targetImage) + && Objects.equals(expiration, that.expiration) + && Objects.equals(containerImage, that.containerImage) + && Objects.equals(buildId, that.buildId) + && Objects.equals(cached, that.cached) + && Objects.equals(freeze, that.freeze); + } + + @Override + public int hashCode() { + return Objects.hash(containerToken, targetImage, expiration, containerImage, buildId, cached, freeze); + } + + @Override + public String toString() { + return "SubmitContainerTokenResponse{" + + "containerToken='" + containerToken + '\'' + + ", targetImage='" + targetImage + '\'' + + ", expiration=" + expiration + + ", containerImage='" + containerImage + '\'' + + ", buildId='" + buildId + '\'' + + ", cached=" + cached + + ", freeze=" + freeze + + '}'; } } From 9d53492778cce8594642396f39669685e4023eba Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 4 Apr 2024 09:31:41 +0200 Subject: [PATCH 07/20] [release] bump version 0.8.0-beta3 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index fd08366..cfabf68 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.8.0-beta2 +0.8.0-beta3 From bf387ff46d0b5a8a2682fac371bbacdfda77a82c Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 09:31:08 +0200 Subject: [PATCH 08/20] Bump micromamba:1.5.8-lunar as default base image Signed-off-by: Paolo Di Tommaso --- .../java/io/seqera/wave/config/CondaOpts.java | 2 +- .../seqera/wave/config/CondaOptsTest.groovy | 2 +- .../seqera/wave/util/DockerHelperTest.groovy | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java b/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java index 45fe4ba..f905172 100644 --- a/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java +++ b/wave-api/src/main/java/io/seqera/wave/config/CondaOpts.java @@ -27,7 +27,7 @@ * @author Paolo Di Tommaso */ public class CondaOpts { - final public static String DEFAULT_MAMBA_IMAGE = "mambaorg/micromamba:1.5.5"; + final public static String DEFAULT_MAMBA_IMAGE = "mambaorg/micromamba:1.5.8-lunar"; final public static String DEFAULT_PACKAGES = "conda-forge::procps-ng"; public String mambaImage; diff --git a/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy index b2b2be3..6efda5b 100644 --- a/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/config/CondaOptsTest.groovy @@ -75,7 +75,7 @@ class CondaOptsTest extends Specification { new CondaOpts(OPTS).toString() == EXPECTED where: OPTS | EXPECTED - [:] | "CondaOpts(mambaImage=mambaorg/micromamba:1.5.5; basePackages=conda-forge::procps-ng, commands=null)" + [:] | "CondaOpts(mambaImage=mambaorg/micromamba:1.5.8-lunar; basePackages=conda-forge::procps-ng, commands=null)" [mambaImage: 'foo:1.0', basePackages: 'this that', commands: ['X','Y']] \ | "CondaOpts(mambaImage=foo:1.0; basePackages=this that, commands=X,Y)" } diff --git a/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy b/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy index 09535e7..d539bdc 100644 --- a/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy +++ b/wave-utils/src/test/groovy/io/seqera/wave/util/DockerHelperTest.groovy @@ -362,7 +362,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaFileToDockerFile(CONDA_OPTS)== '''\ - FROM mambaorg/micromamba:1.5.5 + FROM mambaorg/micromamba:1.5.8-lunar COPY --chown=$MAMBA_USER:$MAMBA_USER conda.yml /tmp/conda.yml RUN micromamba install -y -n base -f /tmp/conda.yml \\ && micromamba install -y -n base foo::bar \\ @@ -376,7 +376,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaFileToDockerFile(new CondaOpts([:]))== '''\ - FROM mambaorg/micromamba:1.5.5 + FROM mambaorg/micromamba:1.5.8-lunar COPY --chown=$MAMBA_USER:$MAMBA_USER conda.yml /tmp/conda.yml RUN micromamba install -y -n base -f /tmp/conda.yml \\ && micromamba install -y -n base conda-forge::procps-ng \\ @@ -393,7 +393,7 @@ class DockerHelperTest extends Specification { def CHANNELS = ['conda-forge', 'defaults'] expect: DockerHelper.condaPackagesToDockerFile(PACKAGES, CHANNELS, new CondaOpts([:])) == '''\ - FROM mambaorg/micromamba:1.5.5 + FROM mambaorg/micromamba:1.5.8-lunar RUN \\ micromamba install -y -n base -c conda-forge -c defaults bwa=0.7.15 salmon=1.1.1 \\ && micromamba install -y -n base conda-forge::procps-ng \\ @@ -411,7 +411,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaPackagesToDockerFile(PACKAGES, CHANNELS, CONDA_OPTS) == '''\ - FROM mambaorg/micromamba:1.5.5 + FROM mambaorg/micromamba:1.5.8-lunar RUN \\ micromamba install -y -n base -c conda-forge -c defaults bwa=0.7.15 salmon=1.1.1 \\ && micromamba install -y -n base foo::one bar::two \\ @@ -428,7 +428,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaPackagesToDockerFile(PACKAGES, CHANNELS, new CondaOpts([:])) == '''\ - FROM mambaorg/micromamba:1.5.5 + FROM mambaorg/micromamba:1.5.8-lunar RUN \\ micromamba install -y -n base -c foo -c bar bwa=0.7.15 salmon=1.1.1 \\ && micromamba install -y -n base conda-forge::procps-ng \\ @@ -784,7 +784,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaFileToSingularityFile(CONDA_OPTS)== '''\ BootStrap: docker - From: mambaorg/micromamba:1.5.5 + From: mambaorg/micromamba:1.5.8-lunar %files {{wave_context_dir}}/conda.yml /scratch/conda.yml %post @@ -801,7 +801,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaFileToSingularityFile(new CondaOpts([:]))== '''\ BootStrap: docker - From: mambaorg/micromamba:1.5.5 + From: mambaorg/micromamba:1.5.8-lunar %files {{wave_context_dir}}/conda.yml /scratch/conda.yml %post @@ -821,7 +821,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaPackagesToSingularityFile(PACKAGES, CHANNELS, new CondaOpts([:])) == '''\ BootStrap: docker - From: mambaorg/micromamba:1.5.5 + From: mambaorg/micromamba:1.5.8-lunar %post micromamba install -y -n base -c conda-forge -c defaults bwa=0.7.15 salmon=1.1.1 micromamba install -y -n base conda-forge::procps-ng @@ -840,7 +840,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaPackagesToSingularityFile(PACKAGES, CHANNELS, CONDA_OPTS) == '''\ BootStrap: docker - From: mambaorg/micromamba:1.5.5 + From: mambaorg/micromamba:1.5.8-lunar %post micromamba install -y -n base -c conda-forge -c defaults bwa=0.7.15 salmon=1.1.1 micromamba install -y -n base foo::one bar::two @@ -858,7 +858,7 @@ class DockerHelperTest extends Specification { expect: DockerHelper.condaPackagesToSingularityFile(PACKAGES, CHANNELS, new CondaOpts([:])) == '''\ BootStrap: docker - From: mambaorg/micromamba:1.5.5 + From: mambaorg/micromamba:1.5.8-lunar %post micromamba install -y -n base -c foo -c bar bwa=0.7.15 salmon=1.1.1 micromamba install -y -n base conda-forge::procps-ng From ed4f253b903585b0fa0d046e906fd50d2d302e71 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 10:22:33 +0200 Subject: [PATCH 09/20] Bump wave-api@0.8.0 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index cfabf68..a3df0a6 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.8.0-beta3 +0.8.0 From e276c0da3d9c2e388c3d87d2c414d310c0acaff9 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 10:22:55 +0200 Subject: [PATCH 10/20] Bump wave-utils@0.12.0 Signed-off-by: Paolo Di Tommaso --- wave-utils/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-utils/VERSION b/wave-utils/VERSION index 146d8e3..ac454c6 100644 --- a/wave-utils/VERSION +++ b/wave-utils/VERSION @@ -1 +1 @@ -0.12.0-beta1 +0.12.0 From 6edd2b5bae39cb22f4ef5e032508c171144fc99b Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 10:23:34 +0200 Subject: [PATCH 11/20] [release] Signed-off-by: Paolo Di Tommaso From ec77d6ae0bd8479abbbb43a40f7ce702c694981d Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 15:20:56 +0200 Subject: [PATCH 12/20] Add hostname + constructors Signed-off-by: Paolo Di Tommaso --- .../seqera/wave/core/spec/ContainerSpec.java | 32 ++++++++++++++++--- .../io/seqera/wave/core/spec/ObjectRef.java | 7 ++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/wave-api/src/main/java/io/seqera/wave/core/spec/ContainerSpec.java b/wave-api/src/main/java/io/seqera/wave/core/spec/ContainerSpec.java index ac0e1c0..c74b95c 100644 --- a/wave-api/src/main/java/io/seqera/wave/core/spec/ContainerSpec.java +++ b/wave-api/src/main/java/io/seqera/wave/core/spec/ContainerSpec.java @@ -17,7 +17,6 @@ package io.seqera.wave.core.spec; -import java.util.List; import java.util.Objects; import io.seqera.wave.model.ContentType; @@ -28,8 +27,8 @@ * @author Paolo Di Tommaso */ public class ContainerSpec { - String registry; + String hostName; String imageName; String reference; String digest; @@ -39,8 +38,9 @@ public class ContainerSpec { /* REQUIRED BY SERIALIZATION */ private ContainerSpec() {} - public ContainerSpec(String registry, String imageName, String reference, String digest, ConfigSpec config, ManifestSpec manifest, List layerUrls) { + public ContainerSpec(String registry, String hostName, String imageName, String reference, String digest, ConfigSpec config, ManifestSpec manifest) { this.registry = registry; + this.hostName = hostName; this.imageName = imageName; this.reference = reference; this.digest = digest; @@ -48,10 +48,24 @@ public ContainerSpec(String registry, String imageName, String reference, String this.manifest = manifest; } + public ContainerSpec(ContainerSpec that) { + this.registry = that.registry; + this.hostName = that.hostName; + this.imageName = that.imageName; + this.reference = that.reference; + this.digest = that.digest; + this.config = that.config; + this.manifest = that.manifest; + } + public String getRegistry() { return registry; } + public String getHostName() { + return hostName; + } + public String getImageName() { return imageName; } @@ -83,18 +97,25 @@ public boolean equals(Object object) { if (this == object) return true; if (object == null || getClass() != object.getClass()) return false; ContainerSpec that = (ContainerSpec) object; - return Objects.equals(registry, that.registry) && Objects.equals(imageName, that.imageName) && Objects.equals(reference, that.reference) && Objects.equals(digest, that.digest) && Objects.equals(config, that.config) && Objects.equals(manifest, that.manifest); + return Objects.equals(registry, that.registry) + && Objects.equals(hostName, that.hostName) + && Objects.equals(imageName, that.imageName) + && Objects.equals(reference, that.reference) + && Objects.equals(digest, that.digest) + && Objects.equals(config, that.config) + && Objects.equals(manifest, that.manifest); } @Override public int hashCode() { - return Objects.hash(registry, imageName, reference, digest, config, manifest); + return Objects.hash(registry, hostName, imageName, reference, digest, config, manifest); } @Override public String toString() { return "ContainerSpec{" + "registry='" + registry + '\'' + + ", hostName='" + hostName + '\'' + ", imageName='" + imageName + '\'' + ", reference='" + reference + '\'' + ", digest='" + digest + '\'' + @@ -102,4 +123,5 @@ public String toString() { ", manifest=" + manifest + '}'; } + } diff --git a/wave-api/src/main/java/io/seqera/wave/core/spec/ObjectRef.java b/wave-api/src/main/java/io/seqera/wave/core/spec/ObjectRef.java index 72f10c7..76a817d 100644 --- a/wave-api/src/main/java/io/seqera/wave/core/spec/ObjectRef.java +++ b/wave-api/src/main/java/io/seqera/wave/core/spec/ObjectRef.java @@ -48,6 +48,13 @@ public ObjectRef(String mediaType, String digest, Long size, Map this.annotations = annotations; } + public ObjectRef(ObjectRef that) { + this.mediaType = that.mediaType; + this.digest = that.digest; + this.size = that.size; + this.annotations = that.annotations; + } + static public ObjectRef of(String json) { Moshi moshi = new Moshi.Builder().build(); try { From 27539557df5a9de764bc0487c194ac56b30cf18d Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sat, 6 Apr 2024 15:21:25 +0200 Subject: [PATCH 13/20] [release] bump wave-api@0.9.0 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index a3df0a6..ac39a10 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.8.0 +0.9.0 From d5ed71bb7a0f8116443c7024c91aa0ed890bcb56 Mon Sep 17 00:00:00 2001 From: Munish Chouhan Date: Thu, 11 Apr 2024 10:13:26 +0200 Subject: [PATCH 14/20] Add imageName to SubmitContainerTokenRequest (#13) Signed-off-by: munishchouhan Signed-off-by: Dr Marco Claudio De La Pierre Co-authored-by: Dr Marco Claudio De La Pierre --- .../wave/api/SubmitContainerTokenRequest.java | 18 ++++++++++++++++++ .../api/SubmitContainerTokenRequestTest.groovy | 2 ++ 2 files changed, 20 insertions(+) diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java index b4e6871..951ae69 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java @@ -92,6 +92,11 @@ public class SubmitContainerTokenRequest implements Cloneable { */ public String cacheRepository; + /** + * Image name of the container + */ + public String imageName; + /** * Request timestamp */ @@ -166,6 +171,8 @@ public SubmitContainerTokenRequest copyWith(Map opts) { copy.buildRepository = (String)opts.get("buildRepository"); if( opts.containsKey("cacheRepository") ) copy.cacheRepository = (String)opts.get("cacheRepository"); + if( opts.containsKey("imageName") ) + copy.imageName = (String)opts.get("imageName"); if( opts.containsKey("timestamp") ) copy.timestamp = (String)opts.get("timestamp"); if( opts.containsKey("fingerprint") ) @@ -254,6 +261,11 @@ public SubmitContainerTokenRequest withCacheRepository(String cacheRepository) { return this; } + public SubmitContainerTokenRequest withImageName(String imageName) { + this.imageName = imageName; + return this; + } + public SubmitContainerTokenRequest withTimestamp(String timestamp) { this.timestamp = timestamp; return this; @@ -289,6 +301,11 @@ public SubmitContainerTokenRequest withDryRun(Boolean dryRun) { return this; } + public SubmitContainerTokenRequest withWorkflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } + public SubmitContainerTokenRequest withContainerIncludes(List containerIncludes) { this.containerIncludes = containerIncludes; return this; @@ -318,6 +335,7 @@ public String toString() { ", containerPlatform='" + containerPlatform + '\'' + ", buildRepository='" + buildRepository + '\'' + ", cacheRepository='" + cacheRepository + '\'' + + ", imageName='" + imageName + '\'' + ", timestamp='" + timestamp + '\'' + ", fingerprint='" + fingerprint + '\'' + ", freeze=" + freeze + diff --git a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy index 8878b33..d6c9032 100644 --- a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy @@ -90,6 +90,7 @@ class SubmitContainerTokenRequestTest extends Specification { containerPlatform: 'b10', buildRepository: 'b11', cacheRepository: 'b12', + imageName: 'testImageName', timestamp: 'b13', fingerprint: 'b14', freeze: false, @@ -112,6 +113,7 @@ class SubmitContainerTokenRequestTest extends Specification { other.containerPlatform == 'b10' other.buildRepository == 'b11' other.cacheRepository == 'b12' + other.imageName== 'testImageName' other.timestamp == 'b13' other.fingerprint == 'b14' other.freeze == false From b5e0242c53dbaedfbbde576d0f6a2952c40aa876 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 11 Apr 2024 10:25:41 +0200 Subject: [PATCH 15/20] Updeprecated containerImage attribute Signed-off-by: Paolo Di Tommaso --- .../java/io/seqera/wave/api/SubmitContainerTokenResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java index 545600c..0d91d44 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenResponse.java @@ -47,7 +47,6 @@ public class SubmitContainerTokenResponse { /** * The source container image that originated this request */ - @Deprecated public String containerImage; /** From ac7e9674879fc9bf51b38195c17c49bc18da8690 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 11 Apr 2024 11:06:13 +0200 Subject: [PATCH 16/20] [release] bump wave-api version 0.9.1 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index ac39a10..f374f66 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.9.0 +0.9.1 From e7c2611399ae84f38433f655148515be44e510b7 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 11 Apr 2024 20:37:12 +0200 Subject: [PATCH 17/20] Make condaPackagesToList public Signed-off-by: Paolo Di Tommaso --- wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java b/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java index 2c7c1c4..07bf405 100644 --- a/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java +++ b/wave-utils/src/main/java/io/seqera/wave/util/DockerHelper.java @@ -64,7 +64,7 @@ static public Path condaFileFromPackages(String packages, List condaChan return toYamlTempFile(yaml); } - static List condaPackagesToList(String packages) { + static public List condaPackagesToList(String packages) { if (packages == null || packages.isEmpty()) return null; return Arrays From 1a0ef7a4574c0cfe952d7c8c8fb1502a44f8673c Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 11 Apr 2024 20:37:43 +0200 Subject: [PATCH 18/20] [release] bump wave-utils@0.12.1 Signed-off-by: Paolo Di Tommaso --- wave-utils/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-utils/VERSION b/wave-utils/VERSION index ac454c6..34a8361 100644 --- a/wave-utils/VERSION +++ b/wave-utils/VERSION @@ -1 +1 @@ -0.12.0 +0.12.1 From f639b6ae66866d3d407c5dbdabe3a2388f2dfbc7 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 18 Apr 2024 17:40:33 +0200 Subject: [PATCH 19/20] Add ImageNameStrategy Signed-off-by: Paolo Di Tommaso --- .../io/seqera/wave/api/ImageNameStrategy.java | 30 +++++++++++++++++++ .../wave/api/SubmitContainerTokenRequest.java | 22 +++++++------- .../SubmitContainerTokenRequestTest.groovy | 9 ++++-- 3 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 wave-api/src/main/java/io/seqera/wave/api/ImageNameStrategy.java diff --git a/wave-api/src/main/java/io/seqera/wave/api/ImageNameStrategy.java b/wave-api/src/main/java/io/seqera/wave/api/ImageNameStrategy.java new file mode 100644 index 0000000..82aef6a --- /dev/null +++ b/wave-api/src/main/java/io/seqera/wave/api/ImageNameStrategy.java @@ -0,0 +1,30 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2023-2024, Seqera Labs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.seqera.wave.api; + +/** + * Model automatic naming strategy for containers built by wave + * + * @author Paolo Di Tommaso + */ +public enum ImageNameStrategy { + none, + tagPrefix, + imageSuffix +} diff --git a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java index 951ae69..8023c18 100644 --- a/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java +++ b/wave-api/src/main/java/io/seqera/wave/api/SubmitContainerTokenRequest.java @@ -92,11 +92,6 @@ public class SubmitContainerTokenRequest implements Cloneable { */ public String cacheRepository; - /** - * Image name of the container - */ - public String imageName; - /** * Request timestamp */ @@ -144,6 +139,8 @@ public class SubmitContainerTokenRequest implements Cloneable { */ public PackagesSpec packages; + public ImageNameStrategy nameStrategy; + public SubmitContainerTokenRequest copyWith(Map opts) { try { final SubmitContainerTokenRequest copy = (SubmitContainerTokenRequest) this.clone(); @@ -171,8 +168,6 @@ public SubmitContainerTokenRequest copyWith(Map opts) { copy.buildRepository = (String)opts.get("buildRepository"); if( opts.containsKey("cacheRepository") ) copy.cacheRepository = (String)opts.get("cacheRepository"); - if( opts.containsKey("imageName") ) - copy.imageName = (String)opts.get("imageName"); if( opts.containsKey("timestamp") ) copy.timestamp = (String)opts.get("timestamp"); if( opts.containsKey("fingerprint") ) @@ -191,6 +186,8 @@ public SubmitContainerTokenRequest copyWith(Map opts) { copy.containerIncludes = (List) opts.get("containerIncludes"); if( opts.containsKey("packages")) copy.packages = (PackagesSpec) opts.get("packages"); + if( opts.containsKey("nameStrategy")) + copy.nameStrategy = (ImageNameStrategy) opts.get("nameStrategy"); // done return copy; } @@ -261,10 +258,6 @@ public SubmitContainerTokenRequest withCacheRepository(String cacheRepository) { return this; } - public SubmitContainerTokenRequest withImageName(String imageName) { - this.imageName = imageName; - return this; - } public SubmitContainerTokenRequest withTimestamp(String timestamp) { this.timestamp = timestamp; @@ -316,6 +309,11 @@ public SubmitContainerTokenRequest withPackages(PackagesSpec packages) { return this; } + public SubmitContainerTokenRequest withNameStrategy(ImageNameStrategy value) { + this.nameStrategy = value; + return this; + } + public boolean formatSingularity() { return "sif".equals(format); } @@ -335,7 +333,6 @@ public String toString() { ", containerPlatform='" + containerPlatform + '\'' + ", buildRepository='" + buildRepository + '\'' + ", cacheRepository='" + cacheRepository + '\'' + - ", imageName='" + imageName + '\'' + ", timestamp='" + timestamp + '\'' + ", fingerprint='" + fingerprint + '\'' + ", freeze=" + freeze + @@ -345,6 +342,7 @@ public String toString() { ", workflowId=" + workflowId + ", containerIncludes=" + ObjectUtils.toString(containerIncludes) + ", packages=" + packages + + ", nameStrategy=" + nameStrategy + '}'; } } diff --git a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy index d6c9032..b95c5e8 100644 --- a/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy +++ b/wave-api/src/test/groovy/io/seqera/wave/api/SubmitContainerTokenRequestTest.groovy @@ -47,7 +47,8 @@ class SubmitContainerTokenRequestTest extends Specification { dryRun: true, workflowId: 'id123', containerIncludes: ['busybox:latest'], - packages: new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar']) + packages: new PackagesSpec(type: PackagesSpec.Type.CONDA, environment: 'foo', entries: ['bar']), + nameStrategy: ImageNameStrategy.imageSuffix ) when: @@ -73,6 +74,7 @@ class SubmitContainerTokenRequestTest extends Specification { copy.workflowId == req.workflowId copy.containerIncludes == req.containerIncludes copy.packages == req.packages + copy.nameStrategy == req.nameStrategy and: copy.formatSingularity() @@ -98,7 +100,8 @@ class SubmitContainerTokenRequestTest extends Specification { dryRun: false, workflowId: 'id123', containerIncludes: ['other:image'], - packages: new PackagesSpec(type: PackagesSpec.Type.SPACK) + packages: new PackagesSpec(type: PackagesSpec.Type.SPACK), + nameStrategy: ImageNameStrategy.tagPrefix ) then: other.towerAccessToken == 'b1' @@ -113,7 +116,6 @@ class SubmitContainerTokenRequestTest extends Specification { other.containerPlatform == 'b10' other.buildRepository == 'b11' other.cacheRepository == 'b12' - other.imageName== 'testImageName' other.timestamp == 'b13' other.fingerprint == 'b14' other.freeze == false @@ -122,6 +124,7 @@ class SubmitContainerTokenRequestTest extends Specification { other.workflowId == 'id123' other.containerIncludes == ['other:image'] other.packages == new PackagesSpec(type: PackagesSpec.Type.SPACK) + other.nameStrategy == ImageNameStrategy.tagPrefix and: !other.formatSingularity() } From 3fd32aef68a51912f31d0cdc4f3b2eb65909cb93 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Thu, 18 Apr 2024 17:41:13 +0200 Subject: [PATCH 20/20] [release] bump wave-api@0.10.0 Signed-off-by: Paolo Di Tommaso --- wave-api/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wave-api/VERSION b/wave-api/VERSION index f374f66..78bc1ab 100644 --- a/wave-api/VERSION +++ b/wave-api/VERSION @@ -1 +1 @@ -0.9.1 +0.10.0