From 9ea325141fe28a1afc63b6c14189eec8446aa7c6 Mon Sep 17 00:00:00 2001 From: Jonathan Giles Date: Tue, 2 Jul 2024 10:01:01 +1200 Subject: [PATCH] Pushing code as-is to allow for concurrent development on the Spring introspection feature --- .../azure/storage/AzureStorageExtension.java | 4 +- .../AzureStorageBlobsResource.java | 3 +- .../{ => resources}/AzureStorageResource.java | 33 +++++------ .../src/main/java/module-info.java | 2 + .../bicep}/storage.module.bicep | 0 .../extensions/spring/SpringExtension.java | 6 ++ .../implementation/SpringIntrospector.java | 5 +- .../resources/EurekaServiceDiscovery.java | 31 +++++++++++ .../spring/{ => resources}/SpringProject.java | 12 ++-- .../src/main/java/module-info.java | 2 + .../resources/templates/eureka/Dockerfile | 7 +++ .../main/resources/templates/eureka/pom.xml | 54 ++++++++++++++++++ .../EurekaServerApplication.java | 13 +++++ .../src/main/resources/application.yaml | 9 +++ .../aspire/DistributedApplication.java | 8 +-- .../microsoft/aspire/ManifestGenerator.java | 55 +++++++++++++------ .../aspire/resources/AzureBicepResource.java | 23 +++++--- .../microsoft/aspire/resources/Container.java | 46 +++++++++------- .../aspire/resources/DockerFile.java | 32 +++++++---- .../aspire/resources/Executable.java | 26 +++++---- .../microsoft/aspire/resources/Project.java | 32 ++++++----- .../microsoft/aspire/resources/Resource.java | 5 +- .../com/microsoft/aspire/resources/Value.java | 11 +++- .../traits/ResourceWithArguments.java | 6 +- .../traits/ResourceWithBindings.java | 4 +- .../traits/ResourceWithEnvironment.java | 4 +- .../traits/ResourceWithParameters.java | 4 +- .../traits/ResourceWithReference.java | 6 +- .../traits/ResourceWithTemplate.java | 21 +++++++ .../StorageExplorerAppHost.java | 8 ++- 30 files changed, 350 insertions(+), 122 deletions(-) rename aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/{ => resources}/AzureStorageBlobsResource.java (88%) rename aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/{ => resources}/AzureStorageResource.java (72%) rename aspire4j/aspire4j-extensions-azure-storage/src/main/resources/{com/microsoft/aspire/extensions/azure/storage => templates/bicep}/storage.module.bicep (100%) create mode 100644 aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/EurekaServiceDiscovery.java rename aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/{ => resources}/SpringProject.java (87%) create mode 100644 aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/Dockerfile create mode 100644 aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/pom.xml create mode 100644 aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/java/com/microsoft/aspire/spring.eureka/EurekaServerApplication.java create mode 100644 aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/resources/application.yaml create mode 100644 aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithTemplate.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 0d9c756..c5788ea 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 @@ -2,6 +2,8 @@ import com.microsoft.aspire.DistributedApplicationHelper; import com.microsoft.aspire.Extension; +import com.microsoft.aspire.extensions.azure.storage.resources.AzureStorageBlobsResource; +import com.microsoft.aspire.extensions.azure.storage.resources.AzureStorageResource; import com.microsoft.aspire.resources.Resource; import com.microsoft.aspire.resources.ResourceType; @@ -9,8 +11,6 @@ public class AzureStorageExtension implements Extension { - static final ResourceType AZURE_STORAGE = ResourceType.fromString("azure.storage.v0"); - @Override public String getName() { return "Azure Storage"; diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageBlobsResource.java b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageBlobsResource.java similarity index 88% rename from aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageBlobsResource.java rename to aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageBlobsResource.java index f7b9f95..6941bab 100644 --- a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageBlobsResource.java +++ b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageBlobsResource.java @@ -1,6 +1,5 @@ -package com.microsoft.aspire.extensions.azure.storage; +package com.microsoft.aspire.extensions.azure.storage.resources; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.microsoft.aspire.resources.Value; import com.microsoft.aspire.resources.traits.ResourceWithConnectionString; diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageResource.java b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageResource.java similarity index 72% rename from aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageResource.java rename to aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageResource.java index 840703b..1f64acf 100644 --- a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/AzureStorageResource.java +++ b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/com/microsoft/aspire/extensions/azure/storage/resources/AzureStorageResource.java @@ -1,9 +1,8 @@ -package com.microsoft.aspire.extensions.azure.storage; +package com.microsoft.aspire.extensions.azure.storage.resources; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.microsoft.aspire.resources.AzureBicepResource; +import com.microsoft.aspire.resources.ResourceType; import com.microsoft.aspire.resources.properties.EndpointReference; -import com.microsoft.aspire.resources.traits.ResourceWithConnectionString; import com.microsoft.aspire.DistributedApplicationHelper; import com.microsoft.aspire.resources.traits.ResourceWithEndpoints; @@ -11,11 +10,11 @@ import java.io.InputStream; import java.util.List; -import static com.microsoft.aspire.extensions.azure.storage.AzureStorageExtension.AZURE_STORAGE; - -public class AzureStorageResource extends AzureBicepResource +public class AzureStorageResource extends AzureBicepResource implements ResourceWithEndpoints { - private static final String BICEP_TEMPLATE_FILE = "storage.module.bicep"; + private static final ResourceType AZURE_STORAGE = ResourceType.fromString("azure.storage.v0"); + + private static final String BICEP_TEMPLATE_FILE = "templates/bicep/storage.module.bicep"; private static final String BICEP_OUTPUT_FILE = "%s.module.bicep"; private final String bicepOutputFilename; @@ -38,24 +37,26 @@ public AzureStorageBlobsResource addBlobs(String name) { } @Override - @JsonIgnore - public List getBicepFiles() { + public List getEndpoints() { + // TODO how do I know which endpoints are available? + return List.of(); + } + + @Override + public List processTemplate() { // read the file from our local resources directory InputStream resourceAsStream = AzureStorageResource.class.getResourceAsStream(BICEP_TEMPLATE_FILE); + if (resourceAsStream == null) { + throw new RuntimeException("Resource file not found: " + BICEP_TEMPLATE_FILE); + } try { String bicepTemplate = new String(resourceAsStream.readAllBytes()); // If necessary, modify template variables - return List.of(new BicepFileOutput(bicepOutputFilename, bicepTemplate)); + return List.of(new TemplateFileOutput(bicepOutputFilename, bicepTemplate)); } catch (IOException e) { throw new RuntimeException(e); } } - - @Override - public List getEndpoints() { - // TODO how do I know which endpoints are available? - return List.of(); - } } diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/module-info.java b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/module-info.java index 5f3ecb4..6dd8d97 100644 --- a/aspire4j/aspire4j-extensions-azure-storage/src/main/java/module-info.java +++ b/aspire4j/aspire4j-extensions-azure-storage/src/main/java/module-info.java @@ -6,6 +6,8 @@ exports com.microsoft.aspire.extensions.azure.storage; opens com.microsoft.aspire.extensions.azure.storage to org.hibernate.validator, com.fasterxml.jackson.databind; + exports com.microsoft.aspire.extensions.azure.storage.resources; + opens com.microsoft.aspire.extensions.azure.storage.resources to com.fasterxml.jackson.databind, org.hibernate.validator; provides com.microsoft.aspire.Extension with AzureStorageExtension; } \ No newline at end of file diff --git a/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/com/microsoft/aspire/extensions/azure/storage/storage.module.bicep b/aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep similarity index 100% rename from aspire4j/aspire4j-extensions-azure-storage/src/main/resources/com/microsoft/aspire/extensions/azure/storage/storage.module.bicep rename to aspire4j/aspire4j-extensions-azure-storage/src/main/resources/templates/bicep/storage.module.bicep 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 6992ca9..7933912 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 @@ -2,6 +2,8 @@ import com.microsoft.aspire.DistributedApplicationHelper; import com.microsoft.aspire.Extension; +import com.microsoft.aspire.extensions.spring.resources.EurekaServiceDiscovery; +import com.microsoft.aspire.extensions.spring.resources.SpringProject; import com.microsoft.aspire.resources.Resource; import java.util.List; @@ -26,4 +28,8 @@ public List> getAvailableResources() { public SpringProject addSpringProject(String name) { return DistributedApplicationHelper.getDistributedApplication().addResource(new SpringProject(name)); } + + public EurekaServiceDiscovery addEurekaServiceDiscovery(String name) { + return DistributedApplicationHelper.getDistributedApplication().addResource(new EurekaServiceDiscovery(name)); + } } diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/implementation/SpringIntrospector.java b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/implementation/SpringIntrospector.java index 93c8c50..52e4753 100644 --- a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/implementation/SpringIntrospector.java +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/implementation/SpringIntrospector.java @@ -7,7 +7,7 @@ import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import com.microsoft.aspire.extensions.spring.SpringProject; +import com.microsoft.aspire.extensions.spring.resources.SpringProject; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -156,7 +156,8 @@ private void introspectJavaFiles(SpringProject project) { .filter(path -> path.toString().endsWith(".java")) .forEach(path -> parseAndVisit(javaParser, path.toFile())); } catch (IOException e) { - e.printStackTrace(); + LOGGER.severe("Failed to walk Spring project path '" + projectPath + "' relative to working directory."); +// e.printStackTrace(); } } diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/EurekaServiceDiscovery.java b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/EurekaServiceDiscovery.java new file mode 100644 index 0000000..9f159d2 --- /dev/null +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/EurekaServiceDiscovery.java @@ -0,0 +1,31 @@ +package com.microsoft.aspire.extensions.spring.resources; + +import com.microsoft.aspire.resources.DockerFile; +import com.microsoft.aspire.resources.ResourceType; +import com.microsoft.aspire.resources.traits.ResourceWithTemplate; + +import java.util.List; + +public class EurekaServiceDiscovery extends DockerFile + implements ResourceWithTemplate { + + public EurekaServiceDiscovery(String name) { + super(ResourceType.fromString("spring.eureka.server.v0"), name); + + // TODO + // We have a template for the eureka server, consisting of a minimal Spring Boot application, as well as + // pom.xml, configuration file, and a Dockerfile to build the image. We need to allow for the template properties + // to be set by the user, such as the port, the name of the service, etc, and then we need to write these out + // to a temporary location, and then build the image from that location. + } + + @Override + public List processTemplate() { + return List.of(); + } + + @Override + public EurekaServiceDiscovery self() { + return this; + } +} diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringProject.java b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java similarity index 87% rename from aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringProject.java rename to aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java index 517f501..e9c60e1 100644 --- a/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/SpringProject.java +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/com/microsoft/aspire/extensions/spring/resources/SpringProject.java @@ -1,4 +1,4 @@ -package com.microsoft.aspire.extensions.spring; +package com.microsoft.aspire.extensions.spring.resources; import com.fasterxml.jackson.annotation.JsonProperty; import com.microsoft.aspire.DistributedApplicationHelper; @@ -11,9 +11,8 @@ import jakarta.validation.Valid; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -public class SpringProject extends Project implements IntrospectiveResource { +public class SpringProject extends Project implements IntrospectiveResource { private static final ResourceType SPRING_PROJECT = ResourceType.fromString("project.spring.v0"); @Valid @@ -36,7 +35,7 @@ public void introspect() { .findFirst().ifPresent(s -> { // we need to set the service name (to the existing spring project name), the path to the Dockerfile, and the // context name (which is the directory containing the Dockerfile) - DockerFile dockerFile = new DockerFile(getName()); + DockerFile dockerFile = new DockerFile<>(getName()); this.copyInto(dockerFile); dockerFile.withPath(s.getCommands().get(0)) @@ -46,4 +45,9 @@ public void introspect() { DistributedApplicationHelper.getDistributedApplication().substituteResource(this, dockerFile); }); } + + @Override + public SpringProject self() { + return this; + } } diff --git a/aspire4j/aspire4j-extensions-spring/src/main/java/module-info.java b/aspire4j/aspire4j-extensions-spring/src/main/java/module-info.java index 6b139f3..abc593e 100644 --- a/aspire4j/aspire4j-extensions-spring/src/main/java/module-info.java +++ b/aspire4j/aspire4j-extensions-spring/src/main/java/module-info.java @@ -9,6 +9,8 @@ exports com.microsoft.aspire.extensions.spring; opens com.microsoft.aspire.extensions.spring to org.hibernate.validator, com.fasterxml.jackson.databind; + exports com.microsoft.aspire.extensions.spring.resources; + opens com.microsoft.aspire.extensions.spring.resources to com.fasterxml.jackson.databind, org.hibernate.validator; provides com.microsoft.aspire.Extension with SpringExtension; } \ No newline at end of file diff --git a/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/Dockerfile b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/Dockerfile new file mode 100644 index 0000000..1938102 --- /dev/null +++ b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/Dockerfile @@ -0,0 +1,7 @@ +FROM maven:3.9.7-eclipse-temurin-21 AS maven +MAINTAINER microsoft.com +COPY src eureka-service/src +COPY pom.xml eureka-service/ +RUN mvn package -f eureka-service/pom.xml +RUN cp eureka-service/target/*.jar /eureka-service.jar +ENTRYPOINT ["java","-jar","/eureka-service.jar"] \ No newline at end of file diff --git a/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/pom.xml b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/pom.xml new file mode 100644 index 0000000..431f2fd --- /dev/null +++ b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.1 + + + + com.microsoft.aspire + eureka-server + 0.0.1-SNAPSHOT + + + 17 + 2023.0.2 + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/java/com/microsoft/aspire/spring.eureka/EurekaServerApplication.java b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/java/com/microsoft/aspire/spring.eureka/EurekaServerApplication.java new file mode 100644 index 0000000..45092d0 --- /dev/null +++ b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/java/com/microsoft/aspire/spring.eureka/EurekaServerApplication.java @@ -0,0 +1,13 @@ +package com.microsoft.aspire.spring.eureka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaServerApplication { + public static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } +} diff --git a/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/resources/application.yaml b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/resources/application.yaml new file mode 100644 index 0000000..b7b3ceb --- /dev/null +++ b/aspire4j/aspire4j-extensions-spring/src/main/resources/templates/eureka/src/main/resources/application.yaml @@ -0,0 +1,9 @@ +spring: + application: + name: eureka-server +server: + port: 8761 +eureka: + client: + register-with-eureka: false + fetch-registry: false \ No newline at end of file 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 52666ef..69129b1 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/DistributedApplication.java @@ -42,7 +42,7 @@ private void loadExtensions() { * @param * @return */ - public T addResource(T r) { + public > T addResource(T r) { return manifest.addResource(r); } @@ -53,7 +53,7 @@ public T addResource(T r) { * @param oldResource The resource to remove. * @param newResources The resource(s) to add in the place of the old resource. */ - public void substituteResource(Resource oldResource, Resource... newResources) { + public void substituteResource(Resource oldResource, Resource... newResources) { manifest.substituteResource(oldResource, newResources); } @@ -170,8 +170,8 @@ public T addExecutable(T executable) { * @param args * @return */ - public Executable addExecutable(String name, String command, String workingDirectory, String... args) { - return manifest.addResource(new Executable(name, command, workingDirectory).withArguments(args)); + public Executable addExecutable(String name, String command, String workingDirectory, String... args) { + return manifest.addResource(new Executable<>(name, command, workingDirectory).withArguments(args)); } 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 3ef1355..4a83b24 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/ManifestGenerator.java @@ -13,6 +13,7 @@ import com.microsoft.aspire.implementation.json.RelativePathSerializer; import com.microsoft.aspire.resources.AzureBicepResource; import com.microsoft.aspire.resources.Resource; +import com.microsoft.aspire.resources.traits.ResourceWithTemplate; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; @@ -30,8 +31,18 @@ void generateManifest(AppHost appHost, Path outputPath) { DistributedApplication app = new DistributedApplication(); appHost.configureApplication(app); app.performResourceIntrospection(); + processTemplates(app); writeManifest(app); - writeBicep(app); + } + + private void processTemplates(DistributedApplication app) { + LOGGER.info("Processing templates..."); + app.manifest.getResources().values().stream() + .filter(r -> r instanceof ResourceWithTemplate) + .map(r -> (ResourceWithTemplate) r) + .map(ResourceWithTemplate::processTemplate) + .forEach(templateFiles -> templateFiles.forEach(this::writeTemplateFile)); + LOGGER.info("Templates processed"); } private void writeManifest(DistributedApplication app) { @@ -80,23 +91,31 @@ private void writeManifest(DistributedApplication app) { LOGGER.info("Manifest written to file"); } - private void writeBicep(DistributedApplication app) { - LOGGER.info("Writing Bicep files..."); - - // iterate through the resources in the app, and for any that are of type AzureBicep, give them the opportunity - // to write their bicep file to the output directory. - for (Resource resource : app.manifest.getResources().values()) { - if (resource instanceof AzureBicepResource azureBicepResource) { - List bicepFiles = azureBicepResource.getBicepFiles(); - - for (AzureBicepResource.BicepFileOutput bicepFile : bicepFiles) { - try { - Files.write(Paths.get(outputPath.toString() + "/" + bicepFile.filename()), bicepFile.content().getBytes()); - } catch (IOException e) { - e.printStackTrace(); - } - } - } + private void writeTemplateFile(ResourceWithTemplate.TemplateFileOutput templateFile) { + try { + Files.write(Paths.get(outputPath.toString() + "/" + templateFile.filename()), templateFile.content().getBytes()); + } catch (IOException e) { + e.printStackTrace(); } } + +// private void writeBicep(DistributedApplication app) { +// LOGGER.info("Writing Bicep files..."); +// +// // iterate through the resources in the app, and for any that are of type AzureBicep, give them the opportunity +// // to write their bicep file to the output directory. +// for (Resource resource : app.manifest.getResources().values()) { +// if (resource instanceof AzureBicepResource azureBicepResource) { +// List bicepFiles = azureBicepResource.getBicepFiles(); +// +// for (ResourceWithTemplate.TemplateFileOutput bicepFile : bicepFiles) { +// try { +// Files.write(Paths.get(outputPath.toString() + "/" + bicepFile.filename()), bicepFile.content().getBytes()); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// } +// } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/AzureBicepResource.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/AzureBicepResource.java index 99ea932..b565dfd 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/AzureBicepResource.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/AzureBicepResource.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.microsoft.aspire.resources.traits.ResourceWithParameters; +import com.microsoft.aspire.resources.traits.ResourceWithTemplate; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -71,7 +72,10 @@ }, */ @JsonPropertyOrder({"type", "path", "connectionString", "params"}) -public abstract class AzureBicepResource extends Resource implements ResourceWithParameters { +public abstract class AzureBicepResource> + extends Resource + implements ResourceWithParameters, + ResourceWithTemplate { @NotNull(message = "AzureBicep.path cannot be null") @NotEmpty(message = "AzureBicep.path cannot be an empty string") @@ -101,15 +105,15 @@ public AzureBicepResource(ResourceType type, String name, String path) { } @JsonIgnore - public AzureBicepResource withPath(String path) { + public T withPath(String path) { this.path = path; - return this; + return self(); } @JsonIgnore - public AzureBicepResource withParameter(String name, String value) { + public T withParameter(String name, String value) { parameters.put(name, value); - return this; + return self(); } @Override @@ -118,7 +122,12 @@ public AzureBicepResource withParameter(String name, String value) { return Collections.unmodifiableMap(parameters); } - public abstract List getBicepFiles(); +// public abstract List getBicepFiles(); +// +// public record BicepFileOutput(String filename, String content) { } - public record BicepFileOutput(String filename, String content) { } + @Override + public T self() { + return (T) this; + } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Container.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Container.java index 03b4216..7a902b6 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Container.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Container.java @@ -90,10 +90,11 @@ */ @JsonPropertyOrder({"type", "image", "entrypoint", "args", "connectionString", "env", "bindings", "bindMounts", "volumes"}) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Container extends Resource implements ResourceWithArguments, - ResourceWithEnvironment, - ResourceWithBindings, - ResourceWithEndpoints { +public class Container> extends Resource + implements ResourceWithArguments>, + ResourceWithEnvironment>, + ResourceWithBindings>, + ResourceWithEndpoints> { @NotNull(message = "Container.image cannot be null") @NotEmpty(message = "Container.image cannot be an empty string") @@ -132,22 +133,22 @@ public Container(String name, String image) { } @JsonIgnore - public Container withImage(String image) { + public T withImage(String image) { this.image = image; - return this; + return self(); } @JsonIgnore - public Container withEntryPoint(String entryPoint) { + public T withEntryPoint(String entryPoint) { this.entryPoint = entryPoint; - return this; + return self(); } @Override @JsonIgnore - public Container withArgument(String argument) { + public T withArgument(String argument) { arguments.add(argument); - return this; + return self(); } @Override @@ -157,24 +158,24 @@ public List getArguments() { } @JsonIgnore - public Container withVolume(Volume volume) { + public T withVolume(Volume volume) { volumes.add(volume); - return this; + return self(); } @JsonIgnore - public Container withDataVolume() { + public T withDataVolume() { // FIXME: hardcoded values // placeholder values from https://github.com/dotnet/aspire/blob/main/playground/TestShop/AppHost/aspire-manifest.json#L38 volumes.add(new Volume("TestShop.AppHost-basketcache-data", "/data", false)); - return this; + return self(); } @Override @JsonIgnore - public Container withEnvironment(String name, String value) { + public T withEnvironment(String name, String value) { environment.put(name, value); - return this; + return self(); } @Override @@ -185,9 +186,9 @@ public Map getEnvironment() { @Override @JsonIgnore - public Container withBinding(Binding binding) { + public T withBinding(Binding binding) { bindings.put(binding.getScheme(), binding); - return this; + return self(); } @Override @@ -198,9 +199,9 @@ public Container withBinding(Binding binding) { // TODO should this be part of ResourceWithBindings? @JsonIgnore - public Container withBindMount(BindMount bindMount) { + public T withBindMount(BindMount bindMount) { bindMounts.add(bindMount); - return this; + return self(); } @Override @@ -208,4 +209,9 @@ public List getEndpoints() { // TODO how do I know which endpoints are available? return List.of(); } + + @Override + public T self() { + return (T) this; + } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/DockerFile.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/DockerFile.java index 05d33b7..ef8fb29 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/DockerFile.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/DockerFile.java @@ -49,8 +49,9 @@ */ @JsonPropertyOrder({"type", "path", "context", "env", "bindings"}) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class DockerFile extends Resource implements ResourceWithEnvironment, - ResourceWithBindings { +public class DockerFile> extends Resource + implements ResourceWithEnvironment>, + ResourceWithBindings> { @NotNull(message = "DockerFile.path cannot be null") @NotEmpty(message = "DockerFile.path cannot be an empty string") @@ -83,26 +84,30 @@ public DockerFile(String name, String path) { } public DockerFile(String name, String path, String context) { - super(ResourceType.DOCKER_FILE, name); + this(ResourceType.DOCKER_FILE, name); this.path = path; this.context = context; } - public DockerFile withPath(String path) { + protected DockerFile(ResourceType type, String name) { + super(type, name); + } + + public T withPath(String path) { this.path = path; - return this; + return self(); } - public DockerFile withContext(String context) { + public T withContext(String context) { this.context = context; - return this; + return self(); } @Override @JsonIgnore - public DockerFile withEnvironment(String name, String value) { + public T withEnvironment(String name, String value) { environment.put(name, value); - return this; + return self(); } @Override @@ -113,9 +118,9 @@ public Map getEnvironment() { @Override @JsonIgnore - public DockerFile withBinding(Binding binding) { + public T withBinding(Binding binding) { bindings.put(binding.getScheme(), binding); - return this; + return self(); } @Override @@ -123,4 +128,9 @@ public DockerFile withBinding(Binding binding) { public @Valid Map getBindings() { return Collections.unmodifiableMap(bindings); } + + @Override + public T self() { + return (T) this; + } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Executable.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Executable.java index d6994f2..b8f7dc0 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Executable.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Executable.java @@ -52,10 +52,11 @@ */ @JsonPropertyOrder({"type", "workingDirectory", "command", "args", "env", "bindings"}) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Executable extends Resource implements ResourceWithArguments, - ResourceWithEnvironment, - ResourceWithBindings, - ResourceWithEndpoints { +public class Executable> extends Resource + implements ResourceWithArguments>, + ResourceWithEnvironment>, + ResourceWithBindings>, + ResourceWithEndpoints> { @NotNull(message = "Executable.workingDirectory cannot be null") @NotEmpty(message = "Executable.workingDirectory cannot be an empty string") @@ -93,19 +94,19 @@ public Executable(String name, String workingDirectory, String command) { this.command = command; } - public Executable withWorkingDirectory(String workingDirectory) { + public T withWorkingDirectory(String workingDirectory) { this.workingDirectory = workingDirectory; - return this; + return self(); } - public Executable withCommand(String command) { + public T withCommand(String command) { this.command = command; - return this; + return self(); } - public Executable withEnvironment(String key, String value) { + public T withEnvironment(String key, String value) { this.env.put(key, value); - return this; + return self(); } @Override @@ -145,4 +146,9 @@ public List getEndpoints() { // TODO how do I know which endpoints are available? return List.of(); } + + @Override + public T self() { + return (T) this; + } } \ No newline at end of file diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Project.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Project.java index 8b9d63c..657be17 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Project.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Project.java @@ -50,10 +50,11 @@ */ @JsonPropertyOrder({"type", "path", "args", "env", "bindings"}) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Project extends Resource implements ResourceWithArguments, - ResourceWithEnvironment, - ResourceWithBindings, - ResourceWithEndpoints { +public class Project> extends Resource + implements ResourceWithArguments, + ResourceWithEnvironment, + ResourceWithBindings, + ResourceWithEndpoints { @NotNull(message = "Project.path cannot be null") @NotEmpty(message = "Project.path cannot be an empty string") @@ -88,30 +89,30 @@ protected Project(ResourceType type, String name) { * @return */ @JsonIgnore - public Project withPath(String path) { + public T withPath(String path) { this.path = path; - return this; + return self(); } @Override @JsonIgnore - public Project withEnvironment(String key, String value) { + public T withEnvironment(String key, String value) { this.env.put(key, value); - return this; + return self(); } @Override @JsonIgnore - public Project withArgument(String argument) { + public T withArgument(String argument) { arguments.add(argument); - return this; + return self(); } @Override @JsonIgnore - public Project withBinding(Binding binding) { + public T withBinding(Binding binding) { bindings.put(binding.getScheme(), binding); - return this; + return self(); } @Override @@ -138,8 +139,13 @@ public final List getArguments() { } @Override - public List getEndpoints() { + public final List getEndpoints() { // TODO how do I know which endpoints are available? return List.of(); } + + @Override + public T self() { + return (T) this; + } } 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 5b80cb2..c134b02 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 @@ -9,7 +9,7 @@ import jakarta.validation.constraints.NotNull; @JsonPropertyOrder({"type", "params"}) -public abstract class Resource { +public abstract class Resource> { @Valid @NotNull(message = "Resource Type cannot be null") @@ -58,4 +58,7 @@ public T copyInto(T newResource) { } return newResource; } + + // This method is used to return "this" as the correct type + protected abstract T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Value.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Value.java index 5b9d861..120a29c 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Value.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/Value.java @@ -26,7 +26,7 @@ */ @JsonPropertyOrder({"connectionString"}) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class Value extends Resource { +public class Value> extends Resource { @NotEmpty(message = "Value.properties cannot be empty") @JsonIgnore @@ -41,13 +41,18 @@ public Value(String name, String key, String value) { properties.put(key, value); } - public Value withProperty(String key, String value) { + public T withProperty(String key, String value) { properties.put(key, value); - return this; + return self(); } @JsonAnyGetter public Map getProperties() { return properties; } + + @Override + protected T self() { + return (T) this; + } } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithArguments.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithArguments.java index 06ba4b6..1603e1b 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithArguments.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithArguments.java @@ -19,13 +19,15 @@ default T withArguments(String... arguments) { withArgument(argument); } - return (T) this; + return self(); } default T withArguments(Iterable arguments) { for (String argument : arguments) { withArgument(argument); } - return (T) this; + return self(); } + + T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithBindings.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithBindings.java index b5d48f9..d6b74ac 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithBindings.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithBindings.java @@ -20,6 +20,8 @@ default T withExternalHttpEndpoints() { // TODO we should probably not have the target port be 8080 withBinding(new Binding(Binding.Scheme.HTTP, Binding.Protocol.TCP, Binding.Transport.HTTP).withTargetPort(8080).withExternal()); withBinding(new Binding(Binding.Scheme.HTTPS, Binding.Protocol.TCP, Binding.Transport.HTTP).withTargetPort(8080).withExternal()); - return (T) this; + return self(); } + + T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithEnvironment.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithEnvironment.java index e5afeff..6ef32eb 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithEnvironment.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithEnvironment.java @@ -19,6 +19,8 @@ default T withEnvironment(Map environment) { for (Map.Entry entry : environment.entrySet()) { withEnvironment(entry.getKey(), entry.getValue()); } - return (T) this; + return self(); } + + T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithParameters.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithParameters.java index cd451ae..5ef7b85 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithParameters.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithParameters.java @@ -18,6 +18,8 @@ default T withParameters(Map parameters) { for (Map.Entry entry : parameters.entrySet()) { withParameter(entry.getKey(), entry.getValue()); } - return (T) this; + return self(); } + + T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithReference.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithReference.java index 60d001f..1f1f20b 100644 --- a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithReference.java +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithReference.java @@ -4,7 +4,7 @@ public interface ResourceWithReference> { - default T withReference(Resource resource) { + default T withReference(Resource resource) { // https://learn.microsoft.com/en-us/dotnet/api/aspire.hosting.resourcebuilderextensions.withreference?view=dotnet-aspire-8.0.1#aspire-hosting-resourcebuilderextensions-withreference-1(aspire-hosting-applicationmodel-iresourcebuilder((-0))-aspire-hosting-applicationmodel-iresourcebuilder((aspire-hosting-applicationmodel-iresourcewithconnectionstring))-system-string-system-boolean) // we are adding references from this resource, to the given resource. We do this by adding appropriate @@ -50,6 +50,8 @@ default T withReference(Resource resource) { // TODO anything to be done here? } - return (T) this; + return self(); } + + T self(); } diff --git a/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithTemplate.java b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithTemplate.java new file mode 100644 index 0000000..498ef83 --- /dev/null +++ b/aspire4j/aspire4j/src/main/java/com/microsoft/aspire/resources/traits/ResourceWithTemplate.java @@ -0,0 +1,21 @@ +package com.microsoft.aspire.resources.traits; + +import java.util.List; + +/** + * A resource that has a template associated with it. This may be one or more bicep files, microservice projects (e.g. + * Spring Boot, Quarkus, etc.), or other types of templates. For each of the template files, we defer to Apache Velocity + * to process the template and generate the final output. Where this output is placed is determined by the resource + * itself - sometimes it is a temporary location (which the app host will tidy up), sometimes it will be the output + * location specified by the app host user. + * @param + */ +public interface ResourceWithTemplate> { + + List processTemplate(); + + T self(); + + // FIXME at some point string content won't be sufficient, and we will want to support binary content too + record TemplateFileOutput(String filename, String content) { } +} diff --git a/samples/storage-explorer/storage-explorer-apphost/src/main/java/com/microsoft/aspire/storageexplorer/StorageExplorerAppHost.java b/samples/storage-explorer/storage-explorer-apphost/src/main/java/com/microsoft/aspire/storageexplorer/StorageExplorerAppHost.java index eebe4c4..402ad7c 100644 --- a/samples/storage-explorer/storage-explorer-apphost/src/main/java/com/microsoft/aspire/storageexplorer/StorageExplorerAppHost.java +++ b/samples/storage-explorer/storage-explorer-apphost/src/main/java/com/microsoft/aspire/storageexplorer/StorageExplorerAppHost.java @@ -15,6 +15,9 @@ public class StorageExplorerAppHost implements AppHost { var blobStorage = azureStorage.addBlobs("storage-explorer-blobs"); + var eurekaServiceDiscovery = app.withExtension(SpringExtension.class) + .addEurekaServiceDiscovery("eureka"); + var dateService = app.withExtension(SpringExtension.class) .addSpringProject("date-service-spring") .withPath("date-service") @@ -25,10 +28,11 @@ public class StorageExplorerAppHost implements AppHost { .withPath("storage-explorer") .withExternalHttpEndpoints() .withReference(blobStorage) - .withReference(dateService); + .withReference(dateService) + .withReference(eurekaServiceDiscovery); // Old style, with direct reference to dockerfiles -// var dateService = app.addDockerFile("dateservice", "date-service/Dockerfile", "date-service") +// var dateService2 = app.addDockerFile("dateservice", "samples/storage-explorer/date-service/Dockerfile", "date-service") // .withExternalHttpEndpoints(); // // var storageExplorer = app.addDockerFile("storage-explorer", "storage-explorer/Dockerfile", "storage-explorer")