From 6a1d1155d60e5e8945cb5df374a87f82e318be7a Mon Sep 17 00:00:00 2001 From: Jonathan Giles Date: Thu, 4 Jul 2024 11:37:56 +1200 Subject: [PATCH] Introduction of resource lifecycle --- .../azure/storage/AzureStorageExtension.java | 5 -- .../templates/bicep/storage.module.bicep | 4 +- .../extensions/spring/SpringExtension.java | 5 -- .../spring/resources/SpringProject.java | 6 +++ aspire4j/aspire4j-maven-tools/pom.xml | 8 +--- .../com/microsoft/aspire/AspireManifest.java | 15 ++++-- .../aspire/DistributedApplication.java | 3 +- .../java/com/microsoft/aspire/Extension.java | 8 ---- .../microsoft/aspire/ManifestGenerator.java | 4 ++ .../microsoft/aspire/resources/Resource.java | 9 ++-- .../traits/IntrospectiveResource.java | 12 +++++ .../traits/ResourceWithLifecycle.java | 46 +++++++++++++++++++ 12 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithLifecycle.java diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageExtension.java b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageExtension.java index 81f6bdf..0b5df5e 100644 --- a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageExtension.java +++ b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageExtension.java @@ -20,11 +20,6 @@ public String getDescription() { return "Provides resources for Azure Storage"; } - @Override - public List>> getAvailableResources() { - return List.of(AzureStorageResource.class, AzureStorageBlobsResource.class); - } - public AzureStorageResource addAzureStorage(String name) { return DistributedApplication.getInstance().addResource(new AzureStorageResource(name)); } diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep b/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep index 04e48cb..8ffd4b7 100644 --- a/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep +++ b/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep @@ -65,4 +65,6 @@ resource roleAssignment_r0wA6OpKE 'Microsoft.Authorization/roleAssignments@2022- } } -output blobEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.blob \ No newline at end of file +output blobEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.blob +output queueEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.queue +output tableEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.table \ No newline at end of file diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringExtension.java b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringExtension.java index 7484c15..eacfbe2 100644 --- a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringExtension.java +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringExtension.java @@ -20,11 +20,6 @@ public String getDescription() { return "Provides support for working with Spring applications"; } - @Override - public List>> getAvailableResources() { - return List.of(SpringProject.class); - } - /** * Adds a new Spring project to the app host. * @param name The name of the spring project. diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java index ab3d8a8..af006a0 100644 --- a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java @@ -31,6 +31,12 @@ public SpringProject(String name) { withBinding(new Binding(Binding.Scheme.HTTPS, Binding.Protocol.TCP, Binding.Transport.HTTP).withTargetPort(8080)); } + @Override + public void onResourcePrecommit() { + super.onResourcePrecommit(); + introspect(); + } + @Override public void introspect() { // we add the available strategies to the aspire manifest and leave it to azd to try its best... diff --git a/aspire4j/aspire4j-maven-tools/pom.xml b/aspire4j/aspire4j-maven-tools/pom.xml index e084351..ae47aff 100644 --- a/aspire4j/aspire4j-maven-tools/pom.xml +++ b/aspire4j/aspire4j-maven-tools/pom.xml @@ -13,16 +13,12 @@ aspire4j-maven-tools 1.0-SNAPSHOT - - 3.13.1 - - org.apache.velocity - velocity - 1.7 + velocity-engine-core + 2.3 \ No newline at end of file diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/AspireManifest.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/AspireManifest.java index a5c9ec1..bdb83d7 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/AspireManifest.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/AspireManifest.java @@ -24,13 +24,19 @@ class AspireManifest { > T addResource(T resource) { Objects.requireNonNull(resource); - // We eagerly do the introspection, so that the resource is ready to be used - // before the user does any further calls - if (resource instanceof IntrospectiveResource introspectiveResource) { - introspectiveResource.introspect(); + if (resources.containsKey(resource.getName())) { + throw new IllegalArgumentException("Resource with name " + resource.getName() + " already exists in manifest"); } resources.put(resource.getName(), resource); + resource.onResourceAdded(); + return resource; + } + + > T removeResource(T resource) { + Objects.requireNonNull(resource); + resources.remove(resource.getName()); + resource.onResourceRemoved(); return resource; } @@ -69,5 +75,6 @@ public void substituteResource(Resource oldResource, Resource... newResour } } resources = newResourcesMap; + oldResource.onResourceRemoved(); } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java index 2b10d82..9a5cc0b 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java @@ -230,8 +230,7 @@ public void printExtensions() { public void printExtensions(PrintStream out) { out.println("Available Aspire4J Extensions:"); extensions.forEach(e -> { - out.println(" " + e.getName() + " (" + e.getClass().getSimpleName() + ".class): " + e.getDescription()); - e.getAvailableResources().forEach(r -> out.println(" - " + r.getSimpleName())); + out.println(" - " + e.getName() + " (" + e.getClass().getSimpleName() + ".class): " + e.getDescription()); }); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/Extension.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/Extension.java index 122ac76..115534f 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/Extension.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/Extension.java @@ -21,12 +21,4 @@ public interface Extension { * @return The description of the extension. */ String getDescription(); - - /** - * Returns a list of resources that are available in this extension. These resources can then be used by calling the - * {@link DistributedApplication#withExtension(Class)} method. - * - * @return A list of resources that are available in this extension. - */ - List>> getAvailableResources(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java index f6edfe1..d169cee 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java @@ -10,6 +10,7 @@ import java.util.logging.Logger; import com.microsoft.aspire.implementation.json.RelativePathSerializer; +import com.microsoft.aspire.resources.traits.ResourceWithLifecycle; import com.microsoft.aspire.resources.traits.ResourceWithTemplate; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; @@ -52,6 +53,9 @@ private void writeManifest(DistributedApplication app) { System.exit(-1); } + // run the precommit lifecycle hook on all resources + app.manifest.getResources().values().forEach(ResourceWithLifecycle::onResourcePrecommit); + LOGGER.info("Validating models..."); // Firstly, disable the info logging messages that are printed by Hibernate Validator Logger.getLogger("org.hibernate.validator.internal.util.Version").setLevel(Level.OFF); diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Resource.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Resource.java index 0ef304a..df2dec4 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Resource.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Resource.java @@ -8,8 +8,12 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +/** + * + * @param + */ @JsonPropertyOrder({"type", "params"}) -public abstract class Resource> implements SelfAware { +public abstract class Resource> implements ResourceWithLifecycle, SelfAware { @Valid @NotNull(message = "Resource Type cannot be null") @@ -35,7 +39,7 @@ public final String getName() { return name; } - public > T copyInto(T newResource) { + public void copyInto(Resource newResource) { // TODO this is incomplete // look at the traits of this resource, and copy them into the new resource if (this instanceof ResourceWithArguments oldResource && newResource instanceof ResourceWithArguments _newResource) { @@ -56,6 +60,5 @@ public > T copyInto(T newResource) { if (this instanceof ResourceWithEnvironment oldResource && newResource instanceof ResourceWithEnvironment _newResource) { oldResource.getEnvironment().forEach(_newResource::withEnvironment); } - return newResource; } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/IntrospectiveResource.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/IntrospectiveResource.java index 0552f7b..f810f74 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/IntrospectiveResource.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/IntrospectiveResource.java @@ -1,5 +1,17 @@ package com.microsoft.aspire.resources.traits; +/** + * Interface for resources that can introspect themselves. This is useful for resources that need to look at the + * environment they are running in to determine their configuration, for example to look at referenced projects, or + * to determine properties from remote resources. + *

+ * Note that the Aspire App Host never directly calls the introspect() method. Instead, calling the introspect() method + * is the responsibility of the resource creator. This is because the resource creator is the one who knows when the + * resource is ready to be introspected. Introspective resources should implement the appropriate lifecycle methods + * in {@link ResourceWithLifecycle} to ensure that they are introspected at the right time. + * + * @see ResourceWithLifecycle + */ public interface IntrospectiveResource { void introspect(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithLifecycle.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithLifecycle.java new file mode 100644 index 0000000..502a4db --- /dev/null +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithLifecycle.java @@ -0,0 +1,46 @@ +package com.microsoft.aspire.resources.traits; + +/** + * All resources have a lifecycle, beginning with creation and ending with the resource being written out to the + * aspire manifest. Within this lifecycle, there are times when the resource should ideally be configured, introspected, + * and then written out. The stages of a resources lifecycle are: + * + *

    + *
  1. Resource is created - Resource constructor is called.
  2. + *
  3. Resource is added to the distributed application - onResourceAdded() is called.
  4. + *
  5. Prior to writing the resource to the aspire manifest, onResourcePrecommit() is called on all resources in the + * order they were added to the distributed application.
  6. + *
+ * + *

The most important point to understand about resource lifecycles is that resources work best when they are + * configured as early as possible, as this allows for other resources to glean more information from them. A resource + * that keeps its cards close to its chest only hurts the resources around it!

+ * + * @see com.microsoft.aspire.resources.Resource + * @see IntrospectiveResource + */ +public interface ResourceWithLifecycle { + + /** + * This method is called immediately after the resource is added to the distributed application. It is the earliest + * time that the resource can be configured. + */ + default void onResourceAdded() { + + } + + /** + * This method is called immediately after the resource is removed from the distributed application. + */ + default void onResourceRemoved() { + + } + + /** + * Prior to writing the resource to the aspire manifest, onResourcePrecommit() is called on all resources in the + * order they were added to the distributed application. + */ + default void onResourcePrecommit() { + + } +}