Skip to content

Commit

Permalink
Pushing template engine prototype as-is, so work can be picked up in …
Browse files Browse the repository at this point in the history
…other time zones.
  • Loading branch information
JonathanGiles committed Jul 2, 2024
1 parent 9ea3251 commit a6e4e87
Show file tree
Hide file tree
Showing 18 changed files with 253 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,22 @@
import com.microsoft.aspire.resources.properties.EndpointReference;
import com.microsoft.aspire.DistributedApplicationHelper;
import com.microsoft.aspire.resources.traits.ResourceWithEndpoints;
import com.microsoft.aspire.utils.templates.TemplateEngine;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

public class AzureStorageResource extends AzureBicepResource<AzureStorageResource>
implements ResourceWithEndpoints<AzureStorageResource> {
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;

public AzureStorageResource(String name) {
super(AZURE_STORAGE, name);

// This is the path to the output bicep file
bicepOutputFilename = String.format(BICEP_OUTPUT_FILE, name);
withPath(bicepOutputFilename);
// bicepOutputFilename = String.format(BICEP_OUTPUT_FILE, name);
// withPath(bicepOutputFilename);

// FIXME just here because I saw other samples with it
// withParameter("principalId", "");
Expand All @@ -43,20 +39,18 @@ public List<EndpointReference> getEndpoints() {
}

@Override
public List<TemplateFileOutput> 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 TemplateFileOutput(bicepOutputFilename, bicepTemplate));
} catch (IOException e) {
throw new RuntimeException(e);
}
public List<TemplateFileOutput> processTemplate(Path outputPath) {
List<TemplateDescriptor> templateFiles = List.of(
new TemplateDescriptor("/templates/bicep/storage.module.bicep", "${name}.module.bicep")
);

List<TemplateFileOutput> templateOutput = TemplateEngine.process(AzureStorageResource.class, templateFiles, Map.of(
"name", getName()
));

// we know that we need to get the output filename from the first element, and set that as the path
withPath(outputPath.toString());

return templateOutput;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
requires transitive com.microsoft.aspire;

exports com.microsoft.aspire.extensions.azure.storage;
exports com.microsoft.aspire.extensions.azure.storage.resources;

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;

// We conditionally open up the template files to the apphost, so it can write them out
opens templates.bicep to com.microsoft.aspire;

provides com.microsoft.aspire.Extension with AzureStorageExtension;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ param principalType string


resource storageAccount_1XR3Um8QY 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
name: toLower(take('storage${r"${uniqueString(resourceGroup().id)"}}', 24))
location: location
tags: {
'aspire-resource-name': 'storage'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
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 com.microsoft.aspire.utils.templates.TemplateEngine;

import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EurekaServiceDiscovery extends DockerFile<EurekaServiceDiscovery>
implements ResourceWithTemplate<EurekaServiceDiscovery> {
public final class EurekaServiceDiscovery extends DockerFile<EurekaServiceDiscovery>
implements ResourceWithTemplate<EurekaServiceDiscovery> {

private final String PROPERTY_NAME = "name";
private final String PROPERTY_PORT = "port";
private final String PROPERTY_REGISTER_WITH_EUREKA = "registerWithEureka";
private final String PROPERTY_FETCH_REGISTRY = "fetchRegistry";

private final Map<String, Object> properties;

public EurekaServiceDiscovery(String name) {
super(ResourceType.fromString("spring.eureka.server.v0"), name);
super(name);

this.properties = new HashMap<>();
this.properties.put(PROPERTY_NAME, name);
this.properties.put(PROPERTY_PORT, 8761);
this.properties.put(PROPERTY_REGISTER_WITH_EUREKA, false);
this.properties.put(PROPERTY_FETCH_REGISTRY, false);
}

// 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.
public EurekaServiceDiscovery withPort(int port) {
this.properties.put(PROPERTY_PORT, port);
return this;
}

@Override
public List<TemplateFileOutput> processTemplate() {
return List.of();
public List<TemplateFileOutput> processTemplate(Path outputPath) {
final String inPrefix = "/templates/eureka/";
final String outPrefix = "eureka/";
List<TemplateDescriptor> templateFiles = List.of(
new TemplateDescriptor(inPrefix+"pom.xml", outPrefix+"pom.xml"),
new TemplateDescriptor(inPrefix+"Dockerfile", outPrefix+"Dockerfile"),
new TemplateDescriptor(inPrefix+"EurekaServerApplication.java", outPrefix+"src/main/java/com/microsoft/aspire/spring/eureka/EurekaServerApplication.java"),
new TemplateDescriptor(inPrefix+"application.yaml", outPrefix+"src/main/resources/application.yaml")
);

List<TemplateFileOutput> templateOutput = TemplateEngine.process(EurekaServiceDiscovery.class, templateFiles, properties);

// TODO we know that we need to get the output filename from the first element, and set that as the path
withPath(outputPath.resolve(templateOutput.get(0).filename()).toString());
withContext(outputPath.resolve(templateOutput.get(0).filename()).getParent().toString());

return templateOutput;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
requires java.logging;

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 to org.hibernate.validator, com.fasterxml.jackson.databind;
opens com.microsoft.aspire.extensions.spring.resources to com.fasterxml.jackson.databind, org.hibernate.validator;

// We conditionally open up the template files to the apphost, so it can write them out
opens templates.eureka to com.microsoft.aspire;

provides com.microsoft.aspire.Extension with SpringExtension;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
spring:
application:
name: ${name}
server:
port: ${port?c}
eureka:
client:
register-with-eureka: ${registerWithEureka?string("true", "false")}
fetch-registry: ${fetchRegistry?string("true", "false")}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<version>${r"${spring-cloud.version}"}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down

This file was deleted.

12 changes: 12 additions & 0 deletions aspire4j/aspire4j/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
<artifactId>expressly</artifactId>
<version>5.0.0</version>
</dependency>

<!-- Template engine -->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>2.3</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.33</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

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;
Expand All @@ -28,19 +25,24 @@ class ManifestGenerator {
void generateManifest(AppHost appHost, Path outputPath) {
this.outputPath = outputPath;

File outputDir = outputPath.toFile();
if (!outputDir.exists()) {
outputDir.mkdirs();
}

DistributedApplication app = new DistributedApplication();
appHost.configureApplication(app);
app.performResourceIntrospection();
processTemplates(app);
processTemplates(app, outputPath);
writeManifest(app);
}

private void processTemplates(DistributedApplication app) {
private void processTemplates(DistributedApplication app, Path outputPath) {
LOGGER.info("Processing templates...");
app.manifest.getResources().values().stream()
.filter(r -> r instanceof ResourceWithTemplate<?>)
.map(r -> (ResourceWithTemplate<?>) r)
.map(ResourceWithTemplate::processTemplate)
.map(r -> r.processTemplate(outputPath))
.forEach(templateFiles -> templateFiles.forEach(this::writeTemplateFile));
LOGGER.info("Templates processed");
}
Expand Down Expand Up @@ -70,11 +72,6 @@ private void writeManifest(DistributedApplication app) {
LOGGER.info("Models validated...Writing manifest to file");
}

File outputDir = outputPath.toFile();
if (!outputDir.exists()) {
outputDir.mkdirs();
}

// Set the outputDir in the RelativePathSerializer
RelativePathSerializer.setOutputPath(outputPath);

Expand All @@ -84,7 +81,7 @@ private void writeManifest(DistributedApplication app) {

try {
objectMapper.writerWithDefaultPrettyPrinter()
.writeValue(new File(outputDir, "aspire-manifest.json"), app.manifest);
.writeValue(new File(outputPath.toFile(), "aspire-manifest.json"), app.manifest);
} catch (IOException e) {
e.printStackTrace();
}
Expand All @@ -93,29 +90,13 @@ private void writeManifest(DistributedApplication app) {

private void writeTemplateFile(ResourceWithTemplate.TemplateFileOutput templateFile) {
try {
Files.write(Paths.get(outputPath.toString() + "/" + templateFile.filename()), templateFile.content().getBytes());
Path path = Paths.get(outputPath.toString() + "/" + templateFile.filename());

// ensure the parent directories exist
Files.createDirectories(path.getParent());
Files.write(path, 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<ResourceWithTemplate.TemplateFileOutput> 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();
// }
// }
// }
// }
// }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.microsoft.aspire.resources.traits;

import java.nio.file.Path;
import java.util.List;

/**
Expand All @@ -12,10 +13,12 @@
*/
public interface ResourceWithTemplate<T extends ResourceWithTemplate<T>> {

List<TemplateFileOutput> processTemplate();
List<TemplateFileOutput> processTemplate(Path outputPath);

T self();

record TemplateDescriptor(String inputFilename, String outputFilename) { }

// 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) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.microsoft.aspire.utils.templates;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.Version;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;

public class FreeMarkerTemplateProcessor implements TemplateEngine {

@Override
public String processTemplate(String templateContent, Map<String, Object> context) {
Configuration cfg = new Configuration(new Version("2.3.31"));
StringWriter out = new StringWriter();

try {
Template template = new Template("template", new StringReader(templateContent), cfg);
template.process(context, out);
} catch (IOException | TemplateException e) {
throw new RuntimeException(e);
}

return out.toString();
}
}
Loading

0 comments on commit a6e4e87

Please sign in to comment.