Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java app build extension #348

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,4 @@
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.Hosting.Java\CommunityToolkit.Aspire.Hosting.Java.csproj" IsAspireProjectResource="false" />
</ItemGroup>

<Target Name="PublishRunMaven" AfterTargets="Build">
<!-- As part of publishing, ensure the Java app is freshly built -->
<Exec WorkingDirectory="$(JavaAppRoot)" Command="./mvnw --quiet clean package" />
</Target>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Port = 8085,
OtelAgentPath = "../../../agents",
})
.WithMavenBuild()
justinyoo marked this conversation as resolved.
Show resolved Hide resolved
.PublishAsDockerFile(
[
new DockerBuildArg("JAR_NAME", "spring-maven-0.0.1-SNAPSHOT.jar"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
<PackageReference Include="Aspire.Hosting" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="CommunityToolkit.Aspire.Hosting.Java.Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Utils;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Java applications to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static partial class JavaAppHostingExtension
{
/// <summary>
/// Adds a Java application to the application model. Executes the containerized Java app.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="options">The <see cref="JavaAppContainerResourceOptions"/> to configure the Java application.</param>"
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<JavaAppContainerResource> AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(options, nameof(options));
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
ArgumentException.ThrowIfNullOrWhiteSpace(options.ContainerImageName, nameof(options.ContainerImageName));

var resource = new JavaAppContainerResource(name);

var rb = builder.AddResource(resource)
.WithAnnotation(new ContainerImageAnnotation { Image = options.ContainerImageName, Tag = options.ContainerImageTag, Registry = options.ContainerRegistry })
.WithHttpEndpoint(port: options.Port, targetPort: options.TargetPort, name: JavaAppContainerResource.HttpEndpointName)
.WithJavaDefaults(options);

if (options.Args is { Length: > 0 })
{
rb.WithArgs(options.Args);
}

return rb;
}

/// <summary>
/// Adds a Spring application to the application model. Executes the containerized Spring app.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="options">The <see cref="JavaAppContainerResourceOptions"/> to configure the Java application.</param>"
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<JavaAppContainerResource> AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options) =>
builder.AddJavaApp(name, options);

private static IResourceBuilder<JavaAppContainerResource> WithJavaDefaults(
this IResourceBuilder<JavaAppContainerResource> builder,
JavaAppContainerResourceOptions options) =>
builder.WithOtlpExporter()
.WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Java applications to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static partial class JavaAppHostingExtension
{
/// <summary>
/// Adds a Java application to the application model. Executes the executable Java app.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="workingDirectory">The working directory to use for the command. If null, the working directory of the current process is used.</param>
/// <param name="options">The <see cref="JavaAppExecutableResourceOptions"/> to configure the Java application.</param>"
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<JavaAppExecutableResource> AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(options, nameof(options));
ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name));
ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory, nameof(workingDirectory));

#pragma warning disable CS8601 // Possible null reference assignment.
string[] allArgs = options.Args is { Length: > 0 }
? ["-jar", options.ApplicationName, .. options.Args]
: ["-jar", options.ApplicationName];
#pragma warning restore CS8601 // Possible null reference assignment.

workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory));
var resource = new JavaAppExecutableResource(name, "java", workingDirectory);

return builder.AddResource(resource)
.WithJavaDefaults(options)
.WithHttpEndpoint(port: options.Port, name: JavaAppContainerResource.HttpEndpointName, isProxied: false)
.WithArgs(allArgs);
}

/// <summary>
/// Adds a Spring application to the application model. Executes the executable Spring app.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="workingDirectory">The working directory to use for the command. If null, the working directory of the current process is used.</param>
/// <param name="options">The <see cref="JavaAppExecutableResourceOptions"/> to configure the Java application.</param>"
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<JavaAppExecutableResource> AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options) =>
builder.AddJavaApp(name, workingDirectory, options);

/// <summary>
/// Adds a Maven build step to the application model.
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{T}"/> to add the Maven build step to.</param>
/// <param name="mavenOptions">The <see cref="MavenOptions"/> to configure the Maven build step.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// This method adds a Maven build step to the application model. The Maven build step is executed before the Java application is started.
///
/// The Maven build step is added as an executable resource named "maven" with the command "mvnw --quiet clean package".
///
/// The Maven build step is excluded from the manifest file.
/// </remarks>
public static IResourceBuilder<JavaAppExecutableResource> WithMavenBuild(
this IResourceBuilder<JavaAppExecutableResource> builder,
MavenOptions? mavenOptions = null)
{
mavenOptions ??= new MavenOptions();

if (mavenOptions.WorkingDirectory is null)
{
mavenOptions.WorkingDirectory = builder.Resource.WorkingDirectory;
}

var annotation = new MavenBuildAnnotation(mavenOptions);

if (builder.Resource.TryGetLastAnnotation<MavenBuildAnnotation>(out _))
{
// Replace the existing annotation, but don't continue on and subscribe to the event again.
builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace);
return builder;
}

builder.WithAnnotation(annotation);

builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (e, ct) =>
{
if (e.Resource is not JavaAppExecutableResource javaAppResource)
{
return;
}

await BuildWithMaven(javaAppResource, e.Services, ct).ConfigureAwait(false);
});

builder.WithCommand(
"build-with-maven",
"Build with Maven",
async (context) =>
await BuildWithMaven(builder.Resource, context.ServiceProvider, context.CancellationToken, false).ConfigureAwait(false) ?
new ExecuteCommandResult { Success = true } :
new ExecuteCommandResult { Success = false, ErrorMessage = "Failed to build with Maven" },
(context) => context.ResourceSnapshot.State switch
{
{ Text: "Stopped" } or
{ Text: "Exited" } or
{ Text: "Finished" } or
{ Text: "FailedToStart" } => ResourceCommandState.Enabled,
_ => ResourceCommandState.Disabled
},
iconName: "build"
);

return builder;

static async Task<bool> BuildWithMaven(JavaAppExecutableResource javaAppResource, IServiceProvider services, CancellationToken ct, bool useNotificationService = true)
{
if (!javaAppResource.TryGetLastAnnotation<MavenBuildAnnotation>(out var mavenOptionsAnnotation))
{
return false;
}

var mavenOptions = mavenOptionsAnnotation.MavenOptions;
var logger = services.GetRequiredService<ResourceLoggerService>().GetLogger(javaAppResource);
var notificationService = services.GetRequiredService<ResourceNotificationService>();

if (useNotificationService)
{
await notificationService.PublishUpdateAsync(javaAppResource, state => state with
{
State = new("Building Maven project", KnownResourceStates.Starting)
}).ConfigureAwait(false);
}

logger.LogInformation("Building Maven project");

var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

var mvnw = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = isWindows ? "cmd" : "sh",
Arguments = isWindows ? $"/c {mavenOptions.Command} {string.Join(" ", mavenOptions.Args)}" : $"./{mavenOptions.Command} {string.Join(" ", mavenOptions.Args)}",
WorkingDirectory = mavenOptions.WorkingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
UseShellExecute = false,
}
};

mvnw.OutputDataReceived += async (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
if (useNotificationService)
{
await notificationService.PublishUpdateAsync(javaAppResource, state => state with
{
State = new(args.Data, KnownResourceStates.Starting)
}).ConfigureAwait(false);
}

logger.LogInformation("{Data}", args.Data);
}
};

mvnw.ErrorDataReceived += async (sender, args) =>
{
if (!string.IsNullOrWhiteSpace(args.Data))
{
if (useNotificationService)
{
await notificationService.PublishUpdateAsync(javaAppResource, state => state with
{
State = new(args.Data, KnownResourceStates.FailedToStart)
}).ConfigureAwait(false);
}

logger.LogError("{Data}", args.Data);
}
};

mvnw.Start();
mvnw.BeginOutputReadLine();
mvnw.BeginErrorReadLine();

await mvnw.WaitForExitAsync(ct).ConfigureAwait(false);

if (mvnw.ExitCode != 0)
{
// always use notification service to push out errors in the maven build
await notificationService.PublishUpdateAsync(javaAppResource, state => state with
{
State = new($"mvnw exited with {mvnw.ExitCode}", KnownResourceStates.FailedToStart)
}).ConfigureAwait(false);

return false;
}
return true;
}
}

private static IResourceBuilder<JavaAppExecutableResource> WithJavaDefaults(
this IResourceBuilder<JavaAppExecutableResource> builder,
JavaAppExecutableResourceOptions options) =>
builder.WithOtlpExporter()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work without making https work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without HTTPS on the dashboard?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea does this work right now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume so 🤣

.WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar")
.WithEnvironment("SERVER_PORT", options.Port.ToString(CultureInfo.InvariantCulture));
}
Loading
Loading