From cef592cbf754b6417bf1c71d37330e7b91324a43 Mon Sep 17 00:00:00 2001 From: Timothy Rediehs Date: Thu, 13 Jun 2024 14:09:14 -0700 Subject: [PATCH] Add Artifacts Manifest Extension (#1649) Explicitly list main container image in artifacts extension --- changelog/@unreleased/pr-1649.v2.yml | 5 ++ .../dist/BaseDistributionExtension.java | 23 ++++++++++ .../dist/artifacts/ArtifactLocator.java | 46 +++++++++++++++++++ .../dist/artifacts/JsonArtifactLocator.java | 42 +++++++++++++++++ .../gradle/dist/tasks/CreateManifestTask.java | 29 ++++++++++-- .../CreateManifestTaskIntegrationSpec.groovy | 24 ++++++++++ readme.md | 42 +++++++++++++++++ 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 changelog/@unreleased/pr-1649.v2.yml create mode 100644 gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/ArtifactLocator.java create mode 100644 gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/JsonArtifactLocator.java diff --git a/changelog/@unreleased/pr-1649.v2.yml b/changelog/@unreleased/pr-1649.v2.yml new file mode 100644 index 000000000..d0986b030 --- /dev/null +++ b/changelog/@unreleased/pr-1649.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: add artifacts manifest extension + links: + - https://github.com/palantir/sls-packaging/pull/1649 diff --git a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/BaseDistributionExtension.java b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/BaseDistributionExtension.java index 648b37694..78af1c056 100644 --- a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/BaseDistributionExtension.java +++ b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/BaseDistributionExtension.java @@ -20,6 +20,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.palantir.gradle.dist.artifacts.ArtifactLocator; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.exceptions.SafeRuntimeException; import groovy.lang.Closure; @@ -30,6 +31,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; +import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.RegularFileProperty; @@ -59,6 +62,7 @@ public class BaseDistributionExtension { private final ListProperty productDependencies; private final SetProperty optionalProductDependencies; private final SetProperty ignoredProductDependencies; + private final DomainObjectSet artifacts; private final ProviderFactory providerFactory; private final MapProperty manifestExtensions; private final RegularFileProperty configurationYml; @@ -75,6 +79,8 @@ public BaseDistributionExtension(Project project) { productDependencies = project.getObjects().listProperty(ProductDependency.class); optionalProductDependencies = project.getObjects().setProperty(ProductId.class); ignoredProductDependencies = project.getObjects().setProperty(ProductId.class); + artifacts = project.getObjects().domainObjectSet(ArtifactLocator.class); + artifacts.whenObjectAdded(ArtifactLocator::isValid); serviceGroup.set(project.provider(() -> project.getGroup().toString())); serviceName.set(project.provider(project::getName)); @@ -131,6 +137,23 @@ public final void setProductType(ProductType productType) { this.productType.set(productType); } + public final DomainObjectSet getArtifacts() { + return artifacts; + } + + /** Lazily configures and adds a {@link ArtifactLocator}. */ + public final void artifact(@DelegatesTo(ArtifactLocator.class) Closure closure) { + ArtifactLocator artifactLocator = project.getObjects().newInstance(ArtifactLocator.class); + project.configure(artifactLocator, closure); + artifacts.add(artifactLocator); + } + + public final void artifact(Action action) { + ArtifactLocator artifactLocator = project.getObjects().newInstance(ArtifactLocator.class); + action.execute(artifactLocator); + artifacts.add(artifactLocator); + } + /** * The product dependencies of this distribution. * diff --git a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/ArtifactLocator.java b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/ArtifactLocator.java new file mode 100644 index 000000000..f2fae7267 --- /dev/null +++ b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/ArtifactLocator.java @@ -0,0 +1,46 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 com.palantir.gradle.dist.artifacts; + +import com.google.common.base.Preconditions; +import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; +import java.net.URI; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; + +public interface ArtifactLocator { + @Input + Property getType(); + + @Input + Property getUri(); + + default void isValid() { + Preconditions.checkNotNull(getType().get(), "type must be specified"); + Preconditions.checkNotNull(getUri().get(), "uri must be specified"); + uriIsValid(getUri().get()); + } + + private static void uriIsValid(String uri) { + try { + // Throws IllegalArgumentException if URI does not conform to RFC 2396 + URI.create(uri); + } catch (IllegalArgumentException e) { + throw new SafeIllegalArgumentException("uri is not valid", e); + } + } +} diff --git a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/JsonArtifactLocator.java b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/JsonArtifactLocator.java new file mode 100644 index 000000000..3abfe2c31 --- /dev/null +++ b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/artifacts/JsonArtifactLocator.java @@ -0,0 +1,42 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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 com.palantir.gradle.dist.artifacts; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public final class JsonArtifactLocator { + + @JsonProperty("type") + @SuppressWarnings("unused") + private String type; + + @JsonProperty("uri") + @SuppressWarnings("unused") + private String uri; + + public JsonArtifactLocator(String type, String uri) { + this.type = type; + this.uri = uri; + } + + public static JsonArtifactLocator from(ArtifactLocator artifactLocator) { + return new JsonArtifactLocator( + artifactLocator.getType().get(), artifactLocator.getUri().get()); + } +} diff --git a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/tasks/CreateManifestTask.java b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/tasks/CreateManifestTask.java index 7ad973e42..8a1412a12 100644 --- a/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/tasks/CreateManifestTask.java +++ b/gradle-sls-packaging/src/main/java/com/palantir/gradle/dist/tasks/CreateManifestTask.java @@ -31,6 +31,8 @@ import com.palantir.gradle.dist.SchemaMigration; import com.palantir.gradle.dist.SchemaVersionLockFile; import com.palantir.gradle.dist.SlsManifest; +import com.palantir.gradle.dist.artifacts.ArtifactLocator; +import com.palantir.gradle.dist.artifacts.JsonArtifactLocator; import com.palantir.gradle.dist.pdeps.ProductDependencies; import com.palantir.gradle.dist.pdeps.ProductDependencyManifest; import com.palantir.gradle.dist.pdeps.ResolveProductDependenciesTask; @@ -65,6 +67,8 @@ public abstract class CreateManifestTask extends DefaultTask { + public static final String CREATE_MANIFEST_TASK_NAME = "createManifest"; + @Input public abstract SetProperty getInRepoProductIds(); @@ -80,6 +84,9 @@ public abstract class CreateManifestTask extends DefaultTask { @Input public abstract MapProperty getManifestExtensions(); + @Input + public abstract SetProperty getArtifacts(); + @InputFile public abstract RegularFileProperty getProductDependenciesFile(); @@ -144,6 +151,8 @@ final void createManifest() throws IOException { ensureSchemaLockfileIsUpToDate(schemaMigrations); } + validateEmptyArtifactsExtension(); + ObjectMappers.jsonMapper.writeValue( getManifestFile().getAsFile().get(), SlsManifest.builder() @@ -154,9 +163,22 @@ final void createManifest() throws IOException { .productVersion(getProjectVersion()) .putAllExtensions(getManifestExtensions().get()) .putExtensions("product-dependencies", productDependencies) + .putExtensions( + "artifacts", + getArtifacts().get().stream() + .map(JsonArtifactLocator::from) + .collect(Collectors.toList())) .build()); } + private void validateEmptyArtifactsExtension() { + Preconditions.checkArgument( + !getManifestExtensions().get().containsKey("artifacts"), + "Specifying artifacts directly the using the manifest-extensions block in the 'distributions' " + + "extension is not allowed. Please use the 'artifact' closure in the 'distributions' " + + "extension to add artifacts instead."); + } + private List getSchemaMigrations() { Object raw = getManifestExtensions().get().get("schema-migrations"); if (raw == null) { @@ -294,7 +316,7 @@ public static TaskProvider createManifestTask(Project projec ProductDependencies.registerProductDependencyTasks(project, ext); TaskProvider createManifest = project.getTasks() - .register("createManifest", CreateManifestTask.class, task -> { + .register(CREATE_MANIFEST_TASK_NAME, CreateManifestTask.class, task -> { task.getServiceName().set(ext.getDistributionServiceName()); task.getServiceGroup().set(ext.getDistributionServiceGroup()); task.getProductType().set(ext.getProductType()); @@ -303,6 +325,7 @@ public static TaskProvider createManifestTask(Project projec .set(resolveProductDependenciesTask.flatMap( ResolveProductDependenciesTask::getManifestFile)); task.getManifestExtensions().set(ext.getManifestExtensions()); + task.getArtifacts().addAll(ext.getArtifacts()); task.getInRepoProductIds() .set(project.provider(() -> ProductDependencyIntrospectionPlugin.getInRepoProductIds( project.getRootProject()) @@ -341,10 +364,10 @@ public boolean isSatisfiedBy(Task _task) { // We want `./gradlew --write-locks` to magically fix up the product-dependencies.lock file // We can't do this at configuration time because it would mess up gradle-consistent-versions. StartParameter startParam = project.getGradle().getStartParameter(); - if (startParam.isWriteDependencyLocks() && !startParam.getTaskNames().contains("createManifest")) { + if (startParam.isWriteDependencyLocks() && !startParam.getTaskNames().contains(CREATE_MANIFEST_TASK_NAME)) { List taskNames = ImmutableList.builder() .addAll(startParam.getTaskNames()) - .add("createManifest") + .add(CREATE_MANIFEST_TASK_NAME) .build(); startParam.setTaskNames(taskNames); } diff --git a/gradle-sls-packaging/src/test/groovy/com/palantir/gradle/dist/tasks/CreateManifestTaskIntegrationSpec.groovy b/gradle-sls-packaging/src/test/groovy/com/palantir/gradle/dist/tasks/CreateManifestTaskIntegrationSpec.groovy index ec432c510..6d08e10f1 100644 --- a/gradle-sls-packaging/src/test/groovy/com/palantir/gradle/dist/tasks/CreateManifestTaskIntegrationSpec.groovy +++ b/gradle-sls-packaging/src/test/groovy/com/palantir/gradle/dist/tasks/CreateManifestTaskIntegrationSpec.groovy @@ -202,6 +202,30 @@ class CreateManifestTaskIntegrationSpec extends IntegrationSpec { writeLocksTask << ['--write-locks', 'writeProductDependenciesLocks', 'wPDL'] } + def 'write artifacts to manifest'() { + buildFile << """ + distribution { + artifact { + type = "oci" + uri = "registry.example.io/foo/bar:v1.3.0" + } + } + """.stripIndent() + + when: + def buildResult = runTasksSuccessfully('createManifest') + + then: + buildResult.wasExecuted('createManifest') + def manifest = ObjectMappers.jsonMapper.readValue(file('build/deployment/manifest.yml').text, Map) + manifest.get("extensions").get("artifacts") == [ + [ + "type": "oci", + "uri" : "registry.example.io/foo/bar:v1.3.0" + ] + ] + } + def "check depends on createManifest"() { when: def result = runTasks(':check') diff --git a/readme.md b/readme.md index 35b317f6d..8845004df 100644 --- a/readme.md +++ b/readme.md @@ -133,6 +133,48 @@ dependencies { } ``` +### Container Images + +You can specify container images that your product requires using the `artifact` declaration on the `distribution` +extension. For example, if a container could be pulled from `registry.example.io/foo/bar:v1.3.0`, you could add the +following to the `distribution` extension. The entry should contain the URI for the artifact in the `uri` field and +should always contain `'oci'` (the only currently supported type) in the `type` field: + +```gradle +distribution { + artifact { + type = 'oci' + uri = 'registry.example.io/foo/bar:v1.3.0' + } +} +``` + +The result will be embedded in the `deployment/manifest.yml` file. The file will look something like this: + +```json +{ + "manifest-version" : "1.0", + "product-type" : "service.v1", + "product-group" : "com.example.foo", + "product-name" : "bar", + "product-version" : "1.1.0", + "extensions" : { + "product-dependencies" : [ { + "product-group" : "com.example", + "product-name" : "dependency", + "minimum-version" : "1.0.0", + "recommended-version" : "1.0.0", + "maximum-version" : "1.x.x", + "optional" : false + } ], + "artifacts" : [ { + "type": "oci", + "uri": "registry.example.io/foo/bar:v1.1.0" + } ] + } +} +``` + ### Schema versions sls-packaging also maintains a lockfile, `schema-versions.lock`, which should be checked in to Git.