diff --git a/src/Aspirate.Cli/Aspirate.Cli.csproj b/src/Aspirate.Cli/Aspirate.Cli.csproj
index 26f5979..b4eeb40 100644
--- a/src/Aspirate.Cli/Aspirate.Cli.csproj
+++ b/src/Aspirate.Cli/Aspirate.Cli.csproj
@@ -42,13 +42,7 @@
-
- Always
-
-
- Always
-
-
+
Always
diff --git a/src/Aspirate.Cli/Properties/launchSettings.json b/src/Aspirate.Cli/Properties/launchSettings.json
index 0cfff82..d5142af 100644
--- a/src/Aspirate.Cli/Properties/launchSettings.json
+++ b/src/Aspirate.Cli/Properties/launchSettings.json
@@ -15,7 +15,7 @@
"generate": {
"commandName": "Project",
"commandLineArgs": "generate",
- "workingDirectory": "$(ProjectDir)/../../../aspire/samples/eShopLite/AppHost",
+ "workingDirectory": "/Users/prom3theu5/git/test-volumes/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
"generate-non-interactive": {
@@ -39,7 +39,7 @@
"apply": {
"commandName": "Project",
"commandLineArgs": "apply",
- "workingDirectory": "$(ProjectDir)/../../../aspire/samples/eShopLite/AppHost",
+ "workingDirectory": "/Users/prom3theu5/git/test-volumes/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
"apply-non-interactive": {
@@ -51,7 +51,7 @@
"destroy": {
"commandName": "Project",
"commandLineArgs": "destroy",
- "workingDirectory": "$(ProjectDir)/../../../aspire/samples/eShopLite/AppHost",
+ "workingDirectory": "/Users/prom3theu5/git/test-volumes/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
"destroy-non-interactive": {
diff --git a/src/Aspirate.Cli/Templates/mongo-server.hbs b/src/Aspirate.Cli/Templates/mongo-server.hbs
deleted file mode 100644
index a365f7b..0000000
--- a/src/Aspirate.Cli/Templates/mongo-server.hbs
+++ /dev/null
@@ -1,42 +0,0 @@
----
-apiVersion: apps/v1
-kind: StatefulSet
-metadata:
- name: mongo-statefulset
- labels:
- app: mongo
-spec:
- serviceName: "mongo"
- replicas: 1
- selector:
- matchLabels:
- app: mongo
- template:
- metadata:
- labels:
- app: mongo
- spec:
- {{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
- {{/if}}
- containers:
- - name: mongo
- image: mongo:latest
- ports:
- - containerPort: 27017
- name: mongodb
----
-apiVersion: v1
-kind: Service
-metadata:
- name: mongo-service
- labels:
- app: mongo
-spec:
- type: ClusterIP
- ports:
- - port: 27017
- name: mongo
- selector:
- app: mongo
\ No newline at end of file
diff --git a/src/Aspirate.Cli/Templates/mysql.hbs b/src/Aspirate.Cli/Templates/mysql.hbs
deleted file mode 100644
index fc3fd5f..0000000
--- a/src/Aspirate.Cli/Templates/mysql.hbs
+++ /dev/null
@@ -1,53 +0,0 @@
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: mysql-secrets
-type: Opaque
-data:
- MYSQL_ROOT_PASSWORD: {{RootPassword}}
----
-apiVersion: apps/v1
-kind: StatefulSet
-metadata:
- name: mysql-statefulset
- labels:
- app: mysql
-spec:
- serviceName: "mysql"
- replicas: 1
- selector:
- matchLabels:
- app: mysql
- template:
- metadata:
- labels:
- app: mysql
- spec:
- {{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
- {{/if}}
- containers:
- - name: mysql
- image: mysql:latest
- envFrom:
- - secretRef:
- name: mysql-secrets
- ports:
- - containerPort: 3306
- name: mysqldb
----
-apiVersion: v1
-kind: Service
-metadata:
- name: mysql-service
- labels:
- app: mysql
-spec:
- type: ClusterIP
- ports:
- - port: 3306
- name: mysql
- selector:
- app: mysql
\ No newline at end of file
diff --git a/src/Aspirate.Cli/Templates/postgres-server.hbs b/src/Aspirate.Cli/Templates/postgres-server.hbs
deleted file mode 100644
index 9d3f27e..0000000
--- a/src/Aspirate.Cli/Templates/postgres-server.hbs
+++ /dev/null
@@ -1,56 +0,0 @@
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: postgres-configuration
- labels:
- app: postgres
-data:
- POSTGRES_DB: "postgres"
- POSTGRES_USER: "postgres"
- POSTGRES_PASSWORD: "postgres"
----
-apiVersion: apps/v1
-kind: StatefulSet
-metadata:
- name: postgres-statefulset
- labels:
- app: postgres
-spec:
- serviceName: "postgres"
- replicas: 1
- selector:
- matchLabels:
- app: postgres
- template:
- metadata:
- labels:
- app: postgres
- spec:
- {{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
- {{/if}}
- containers:
- - name: postgres
- image: postgres:12
- envFrom:
- - configMapRef:
- name: postgres-configuration
- ports:
- - containerPort: 5432
- name: postgresdb
----
-apiVersion: v1
-kind: Service
-metadata:
- name: postgres-service
- labels:
- app: postgres
-spec:
- type: ClusterIP
- ports:
- - port: 5432
- name: postgres
- selector:
- app: postgres
\ No newline at end of file
diff --git a/src/Aspirate.Cli/Templates/rabbitmq.hbs b/src/Aspirate.Cli/Templates/rabbitmq.hbs
deleted file mode 100644
index d72ccc5..0000000
--- a/src/Aspirate.Cli/Templates/rabbitmq.hbs
+++ /dev/null
@@ -1,55 +0,0 @@
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: rabbitmq-configuration
- labels:
- app: rabbitmq
-data:
- RABBITMQ_DEFAULT_USER: "guest"
- RABBITMQ_DEFAULT_PASS: "guest"
----
-apiVersion: apps/v1
-kind: StatefulSet
-metadata:
- name: rabbitmq-statefulset
- labels:
- app: rabbitmq
-spec:
- serviceName: "rabbitmq"
- replicas: 1
- selector:
- matchLabels:
- app: rabbitmq
- template:
- metadata:
- labels:
- app: rabbitmq
- spec:
- {{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
- {{/if}}
- containers:
- - name: rabbitmq
- image: rabbitmq:3.8-management
- envFrom:
- - configMapRef:
- name: rabbitmq-configuration
- ports:
- - containerPort: 5672
- name: rabbitmq
----
-apiVersion: v1
-kind: Service
-metadata:
- name: rabbitmq-service
- labels:
- app: rabbitmq
-spec:
- type: ClusterIP
- ports:
- - port: 5672
- name: rabbitmq
- selector:
- app: rabbitmq
diff --git a/src/Aspirate.Cli/Templates/redis.hbs b/src/Aspirate.Cli/Templates/redis.hbs
deleted file mode 100644
index 949ac74..0000000
--- a/src/Aspirate.Cli/Templates/redis.hbs
+++ /dev/null
@@ -1,46 +0,0 @@
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: redis
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: redis
- template:
- metadata:
- labels:
- app: redis
- spec:
-{{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
-{{/if}}
- containers:
- - name: redis
- image: bitnami/redis:latest
- env:
- - name: ALLOW_EMPTY_PASSWORD
- value: "yes"
- resources:
- requests:
- cpu: 100m
- memory: 128Mi
- limits:
- cpu: 250m
- memory: 256Mi
- ports:
- - containerPort: 6379
- name: redis
----
-apiVersion: v1
-kind: Service
-metadata:
- name: redis
-spec:
- ports:
- - port: 6379
- selector:
- app: redis
----
\ No newline at end of file
diff --git a/src/Aspirate.Cli/Templates/sqlserver.hbs b/src/Aspirate.Cli/Templates/sqlserver.hbs
deleted file mode 100644
index e881739..0000000
--- a/src/Aspirate.Cli/Templates/sqlserver.hbs
+++ /dev/null
@@ -1,64 +0,0 @@
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: sqlserver-configuration
- labels:
- app: sqlserver
-data:
- ACCEPT_EULA: "Y"
----
-apiVersion: v1
-kind: Secret
-metadata:
- name: sqlserver-secrets
-type: Opaque
-data:
- MSSQL_SA_PASSWORD: {{saPassword}}
----
-apiVersion: apps/v1
-kind: StatefulSet
-metadata:
- name: sqlserver-statefulset
- labels:
- app: sqlserver
-spec:
- serviceName: "sqlserver"
- replicas: 1
- selector:
- matchLabels:
- app: sqlserver
- template:
- metadata:
- labels:
- app: sqlserver
- spec:
-{{#if withPrivateRegistry}}
- imagePullSecrets:
- - name: image-pull-secret
-{{/if}}
- containers:
- - name: sqlserver
- image: mcr.microsoft.com/mssql/server:2022-latest
- envFrom:
- - configMapRef:
- name: sqlserver-configuration
- - secretRef:
- name: sqlserver-secrets
- ports:
- - containerPort: 1433
- name: sqlserverdb
----
-apiVersion: v1
-kind: Service
-metadata:
- name: sqlserver-service
- labels:
- app: sqlserver
-spec:
- type: ClusterIP
- ports:
- - port: 1433
- name: sqlserver
- selector:
- app: sqlserver
\ No newline at end of file
diff --git a/src/Aspirate.Cli/Templates/statefulset.hbs b/src/Aspirate.Cli/Templates/statefulset.hbs
new file mode 100644
index 0000000..824bb15
--- /dev/null
+++ b/src/Aspirate.Cli/Templates/statefulset.hbs
@@ -0,0 +1,68 @@
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: {{name}}
+ {{#if hasAnyAnnotations}}
+ annotations:
+ {{#each annotations}}
+ {{@key}}: {{this}}
+ {{/each}}
+ {{/if}}
+spec:
+ serviceName: "{{name}}"
+ replicas: 1
+ selector:
+ matchLabels:
+ app: {{name}}
+ template:
+ metadata:
+ labels:
+ app: {{name}}
+ spec:
+ {{#if withPrivateRegistry}}
+ imagePullSecrets:
+ - name: image-pull-secret
+ {{/if}}
+ containers:
+ - name: {{name}}
+ image: {{containerImage}}
+ imagePullPolicy: {{imagePullPolicy}}
+ {{#if hasArgs}}
+ args:
+ {{#each args}}
+ - '{{this}}'
+ {{/each}}
+ {{/if}}
+ {{#if hasPorts}}
+ ports:
+ {{#each ports}}
+ - containerPort: {{port}}
+ {{/each}}
+ {{/if}}
+ envFrom:
+ - configMapRef:
+ name: {{name}}-env
+ {{#if hasAnySecrets}}
+ - secretRef:
+ name: {{name}}-secrets
+ {{/if}}
+ {{#if hasVolumes}}
+ volumeMounts:
+ {{#each volumes}}
+ - name: {{this.name}}
+ mountPath: {{this.target}}
+ {{/each}}
+ {{/if}}
+ {{#if hasVolumes}}
+ volumeClaimTemplates:
+ {{#each volumes}}
+ - metadata:
+ name: {{this.name}}
+ spec:
+ accessModes: ["ReadWriteOnce"]
+ resources:
+ requests:
+ storage: 1Gi
+ {{/each}}
+ {{/if}}
\ No newline at end of file
diff --git a/src/Aspirate.Commands/Actions/Manifests/GenerateDockerComposeManifestAction.cs b/src/Aspirate.Commands/Actions/Manifests/GenerateDockerComposeManifestAction.cs
index 978a6b5..7e64985 100644
--- a/src/Aspirate.Commands/Actions/Manifests/GenerateDockerComposeManifestAction.cs
+++ b/src/Aspirate.Commands/Actions/Manifests/GenerateDockerComposeManifestAction.cs
@@ -1,3 +1,5 @@
+using Volume = Aspirate.DockerCompose.Models.Volume;
+
namespace Aspirate.Commands.Actions.Manifests;
public sealed class GenerateDockerComposeManifestAction(IServiceProvider serviceProvider, IFileSystem fileSystem) : BaseAction(serviceProvider)
@@ -35,8 +37,11 @@ public override Task ExecuteAsync()
private void WriteFile(List services, string outputFile)
{
+ var volumes = CreateVolumes(services);
+
var composeFile = Builder.MakeCompose()
.WithServices(services.ToArray())
+ .WithVolumes(volumes.ToArray())
.Build();
var composeFileString = composeFile.Serialize();
@@ -49,14 +54,23 @@ private void WriteFile(List services, string outputFile)
fileSystem.File.WriteAllText(outputFile, composeFileString);
}
- private void ProcessIndividualComponent(KeyValuePair resource, List services)
+ private static List CreateVolumes(List services)
{
- if (resource.Value.Type is null)
+ var volumes = new List();
+
+ foreach (var service in services)
{
- Logger.MarkupLine($"[yellow]Skipping resource '{resource.Key}' as its type is unknown.[/]");
- return;
+ if (service.Volumes is not null)
+ {
+ volumes.AddRange(service.Volumes.Select(volume => new Volume { Name = volume.Split(':')[0] }));
+ }
}
+ return volumes;
+ }
+
+ private void ProcessIndividualComponent(KeyValuePair resource, List services)
+ {
if (AspirateState.IsNotDeployable(resource.Value))
{
return;
diff --git a/src/Aspirate.Processors/KubernetesDeploymentTemplateData.cs b/src/Aspirate.Processors/KubernetesDeploymentTemplateData.cs
index 4ff2edc..bc64812 100644
--- a/src/Aspirate.Processors/KubernetesDeploymentTemplateData.cs
+++ b/src/Aspirate.Processors/KubernetesDeploymentTemplateData.cs
@@ -8,6 +8,7 @@ public class KubernetesDeploymentTemplateData
public Dictionary? Env {get; private set;}
public Dictionary? Secrets {get; private set;}
public Dictionary? Annotations {get; private set;}
+ public List? Volumes {get; private set;}
public IReadOnlyCollection? Manifests {get; private set;}
public IReadOnlyCollection? Args {get; private set;}
public bool? IsService { get; private set; } = true;
@@ -73,6 +74,12 @@ public KubernetesDeploymentTemplateData SetIsProject(bool project)
return this;
}
+ public KubernetesDeploymentTemplateData SetVolumes(List? volumes)
+ {
+ Volumes = volumes ?? [];
+ return this;
+ }
+
public KubernetesDeploymentTemplateData SetWithPrivateRegistry(bool isPrivateRegistry)
{
WithPrivateRegistry = isPrivateRegistry;
@@ -133,6 +140,7 @@ public KubernetesDeploymentTemplateData SetSecretsFromSecretState(KeyValuePair Ports?.Any() == true;
+ public bool HasVolumes => Volumes?.Any() == true;
public bool HasAnySecrets => Secrets?.Any() == true;
public bool HasAnyAnnotations => Annotations?.Any() == true;
public bool HasArgs => Args?.Any() == true;
diff --git a/src/Aspirate.Processors/Resources/AbstractProcessors/ContainerProcessor.cs b/src/Aspirate.Processors/Resources/AbstractProcessors/ContainerProcessor.cs
index d890583..689d376 100644
--- a/src/Aspirate.Processors/Resources/AbstractProcessors/ContainerProcessor.cs
+++ b/src/Aspirate.Processors/Resources/AbstractProcessors/ContainerProcessor.cs
@@ -1,3 +1,6 @@
+using Aspirate.DockerCompose.Models;
+using Volume = Aspirate.Shared.Models.AspireManifests.Components.V0.Volume;
+
namespace Aspirate.Processors.Resources.AbstractProcessors;
///
@@ -16,12 +19,6 @@ public class ContainerProcessor(
///
public override string ResourceType => AspireComponentLiterals.Container;
- private readonly IReadOnlyCollection _manifests =
- [
- $"{TemplateLiterals.DeploymentType}.yml",
- $"{TemplateLiterals.ServiceType}.yml",
- ];
-
///
public override Resource? Deserialize(ref Utf8JsonReader reader) =>
JsonSerializer.Deserialize(ref reader);
@@ -39,6 +36,16 @@ public override Task CreateManifests(KeyValuePair resour
var container = resource.Value as ContainerResource;
+ var manifests = new List
+ {
+ container.Volumes.Count > 0
+ ? $"{TemplateLiterals.StatefulSetType}.yml"
+ : $"{TemplateLiterals.DeploymentType}.yml",
+ $"{TemplateLiterals.ServiceType}.yml",
+ };
+
+ KuberizeVolumeNames(container.Volumes);
+
var containerPorts = container.Bindings?.Select(b => new Ports { Name = b.Key, Port = b.Value.TargetPort.GetValueOrDefault() }).ToList() ?? [];
var data = new KubernetesDeploymentTemplateData()
@@ -47,15 +54,24 @@ public override Task CreateManifests(KeyValuePair resour
.SetImagePullPolicy(imagePullPolicy)
.SetEnv(GetFilteredEnvironmentalVariables(resource.Value, disableSecrets))
.SetAnnotations(container.Annotations)
+ .SetVolumes(container.Volumes)
.SetSecrets(GetSecretEnvironmentalVariables(resource.Value, disableSecrets))
.SetSecretsFromSecretState(resource, secretProvider, disableSecrets)
.SetPorts(containerPorts)
.SetArgs(container.Args)
- .SetManifests(_manifests)
+ .SetManifests(manifests)
.SetWithPrivateRegistry(withPrivateRegistry.GetValueOrDefault())
.Validate();
- _manifestWriter.CreateDeployment(resourceOutputPath, data, templatePath);
+ if (container.Volumes.Count > 0)
+ {
+ _manifestWriter.CreateStatefulSet(resourceOutputPath, data, templatePath);
+ }
+ else
+ {
+ _manifestWriter.CreateDeployment(resourceOutputPath, data, templatePath);
+ }
+
_manifestWriter.CreateService(resourceOutputPath, data, templatePath);
_manifestWriter.CreateComponentKustomizeManifest(resourceOutputPath, data, templatePath);
@@ -64,6 +80,19 @@ public override Task CreateManifests(KeyValuePair resour
return Task.FromResult(true);
}
+ private static void KuberizeVolumeNames(List containerVolumes)
+ {
+ if (containerVolumes.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var volume in containerVolumes)
+ {
+ volume.Name = volume.Name.Replace("/", "-").Replace(".", "-").ToLowerInvariant();
+ }
+ }
+
public override ComposeService CreateComposeEntry(KeyValuePair resource)
{
var response = new ComposeService();
@@ -76,7 +105,7 @@ public override ComposeService CreateComposeEntry(KeyValuePair
if (resource.Value is IResourceWithEnvironmentalVariables { Env: not null } resourceWithEnv)
{
- foreach (var entry in resourceWithEnv.Env)
+ foreach (var entry in resourceWithEnv.Env.Where(entry => !string.IsNullOrEmpty(entry.Value)))
{
environment.Add(entry.Key, entry.Value);
}
@@ -90,10 +119,20 @@ public override ComposeService CreateComposeEntry(KeyValuePair
service.WithCommands(container.Args.ToArray());
}
+ var composeVolumes = new List();
+
+ if (container.Volumes.Count > 0)
+ {
+ KuberizeVolumeNames(container.Volumes);
+
+ composeVolumes.AddRange(container.Volumes.Where(x=>!string.IsNullOrWhiteSpace(x.Name)).Select(volume => $"{volume.Name}:{volume.Target}"));
+ }
+
response.Service = service
.WithEnvironment(environment)
.WithContainerName(resource.Key)
.WithRestartPolicy(RestartMode.UnlessStopped)
+ .WithVolumes(composeVolumes.ToArray())
.WithPortMappings(containerPorts.Select(x=> new Port
{
Target = x.Port,
diff --git a/src/Aspirate.Processors/Resources/Project/ProjectProcessor.cs b/src/Aspirate.Processors/Resources/Project/ProjectProcessor.cs
index 2da8fc1..4b25e42 100644
--- a/src/Aspirate.Processors/Resources/Project/ProjectProcessor.cs
+++ b/src/Aspirate.Processors/Resources/Project/ProjectProcessor.cs
@@ -148,12 +148,12 @@ protected override void PreSubstitutePlaceholders(Resource resource, Dictionary<
return;
}
- if (resourceWithBinding.Bindings.TryGetValue("http", out var httpBinding) && httpBinding.TargetPort == 0 || httpBinding.TargetPort is null)
+ if (resourceWithBinding.Bindings.TryGetValue("http", out var httpBinding) && httpBinding.TargetPort is 0 or null)
{
httpBinding.TargetPort = 8080;
}
- if (resourceWithBinding.Bindings.TryGetValue("https", out var httpsBinding) && httpsBinding.TargetPort == 0 || httpsBinding.TargetPort is null)
+ if (resourceWithBinding.Bindings.TryGetValue("https", out var httpsBinding) && httpsBinding.TargetPort is 0 or null)
{
httpsBinding.TargetPort = 8443;
}
diff --git a/src/Aspirate.Services/Implementations/ContainerCompositionService.cs b/src/Aspirate.Services/Implementations/ContainerCompositionService.cs
index 1961f29..0722978 100644
--- a/src/Aspirate.Services/Implementations/ContainerCompositionService.cs
+++ b/src/Aspirate.Services/Implementations/ContainerCompositionService.cs
@@ -1,3 +1,5 @@
+using System.Runtime.InteropServices;
+
namespace Aspirate.Services.Implementations;
public sealed class ContainerCompositionService(
@@ -157,9 +159,10 @@ private bool AskIfShouldRetryHandlingDuplicateFiles(bool nonInteractive)
"\r\n[red bold]Implicitly, dotnet publish does not allow duplicate filenames to be output to the artefact directory at build time.\r\nWould you like to retry the build explicitly allowing them?[/]\r\n");
}
- private async Task AddProjectPublishArguments(ArgumentsBuilder argumentsBuilder, string fullProjectPath,
- string? runtimeIdentifier)
+ private async Task AddProjectPublishArguments(ArgumentsBuilder argumentsBuilder, string fullProjectPath, string? runtimeIdentifier)
{
+ var defaultRuntimeIdentifier = GetRuntimeIdentifier();
+
var propertiesJson = await projectPropertyService.GetProjectPropertiesAsync(
fullProjectPath,
MsBuildPropertiesLiterals.PublishSingleFileArgument,
@@ -182,9 +185,11 @@ private async Task AddProjectPublishArguments(ArgumentsBuilder argumentsBuilder,
.AppendArgument(DotNetSdkLiterals.PublishProfileArgument, DotNetSdkLiterals.ContainerPublishProfile)
.AppendArgument(DotNetSdkLiterals.PublishSingleFileArgument, msbuildProperties.Properties.PublishSingleFile)
.AppendArgument(DotNetSdkLiterals.PublishTrimmedArgument, msbuildProperties.Properties.PublishTrimmed)
- .AppendArgument(DotNetSdkLiterals.SelfContainedArgument, DotNetSdkLiterals.DefaultSelfContained);
+ .AppendArgument(DotNetSdkLiterals.SelfContainedArgument, DotNetSdkLiterals.DefaultSelfContained)
+ .AppendArgument(DotNetSdkLiterals.VerbosityArgument, DotNetSdkLiterals.DefaultVerbosity)
+ .AppendArgument(DotNetSdkLiterals.NoLogoArgument, string.Empty, quoteValue: false);
- argumentsBuilder.AppendArgument(DotNetSdkLiterals.RuntimeIdentifierArgument, string.IsNullOrEmpty(runtimeIdentifier) ? DotNetSdkLiterals.DefaultRuntimeIdentifier : runtimeIdentifier);
+ argumentsBuilder.AppendArgument(DotNetSdkLiterals.RuntimeIdentifierArgument, string.IsNullOrEmpty(runtimeIdentifier) ? defaultRuntimeIdentifier : runtimeIdentifier);
}
private static void AddContainerDetailsToArguments(ArgumentsBuilder argumentsBuilder,
@@ -267,4 +272,11 @@ private static void CheckSuccess(ShellCommandResult result)
ActionCausesExitException.ExitNow(result.ExitCode);
}
}
+
+ private static string GetRuntimeIdentifier()
+ {
+ var architecture = RuntimeInformation.OSArchitecture;
+
+ return architecture == Architecture.Arm64 ? "linux-arm64" : "linux-x64";
+ }
}
diff --git a/src/Aspirate.Services/Implementations/ManifestWriter.cs b/src/Aspirate.Services/Implementations/ManifestWriter.cs
index 0d91811..c9ecde1 100644
--- a/src/Aspirate.Services/Implementations/ManifestWriter.cs
+++ b/src/Aspirate.Services/Implementations/ManifestWriter.cs
@@ -8,15 +8,10 @@ public class ManifestWriter(IFileSystem fileSystem) : IManifestWriter
private readonly Dictionary _templateFileMapping = new()
{
[TemplateLiterals.DeploymentType] = $"{TemplateLiterals.DeploymentType}.hbs",
+ [TemplateLiterals.StatefulSetType] = $"{TemplateLiterals.StatefulSetType}.hbs",
[TemplateLiterals.DaprComponentType] = $"{TemplateLiterals.DaprComponentType}.hbs",
[TemplateLiterals.ServiceType] = $"{TemplateLiterals.ServiceType}.hbs",
[TemplateLiterals.ComponentKustomizeType] = $"{TemplateLiterals.ComponentKustomizeType}.hbs",
- [TemplateLiterals.RedisType] = $"{TemplateLiterals.RedisType}.hbs",
- [TemplateLiterals.SqlServerType] = $"{TemplateLiterals.SqlServerType}.hbs",
- [TemplateLiterals.MysqlServerType] = $"{TemplateLiterals.MysqlServerType}.hbs",
- [TemplateLiterals.RabbitMqType] = $"{TemplateLiterals.RabbitMqType}.hbs",
- [TemplateLiterals.MongoDbServerType] = $"{TemplateLiterals.MongoDbServerType}.hbs",
- [TemplateLiterals.PostgresServerType] = $"{TemplateLiterals.PostgresServerType}.hbs",
[TemplateLiterals.NamespaceType] = $"{TemplateLiterals.NamespaceType}.hbs",
};
@@ -45,6 +40,14 @@ public void CreateDeployment(string outputPath, TTemplateData dat
CreateFile(templateFile, deploymentOutputPath, data, templatePath);
}
+ public void CreateStatefulSet(string outputPath, TTemplateData data, string? templatePath)
+ {
+ _templateFileMapping.TryGetValue(TemplateLiterals.StatefulSetType, out var templateFile);
+ var deploymentOutputPath = Path.Combine(outputPath, $"{TemplateLiterals.StatefulSetType}.yml");
+
+ CreateFile(templateFile, deploymentOutputPath, data, templatePath);
+ }
+
public void CreateDaprManifest(string outputPath, TTemplateData data, string name, string? templatePath)
{
var daprOutputPath = Path.Combine(outputPath, "dapr");
diff --git a/src/Aspirate.Services/Interfaces/IManifestWriter.cs b/src/Aspirate.Services/Interfaces/IManifestWriter.cs
index 82594b0..fde1b22 100644
--- a/src/Aspirate.Services/Interfaces/IManifestWriter.cs
+++ b/src/Aspirate.Services/Interfaces/IManifestWriter.cs
@@ -17,6 +17,14 @@ public interface IManifestWriter
/// The path of the template file (optional).
void CreateDeployment(string outputPath, TTemplateData data, string? templatePath);
+ ///
+ /// Create a statefulset file using the specified template file and template data.
+ ///
+ /// The path where the statefulset file will be created.
+ /// The template data.
+ /// The path of the template file (optional).
+ void CreateStatefulSet(string outputPath, TTemplateData data, string? templatePath);
+
///
/// Creates a Dapr manifest file based on the provided template data and saves it to the specified output path.
///
diff --git a/src/Aspirate.Shared/Literals/DotNetSdkLiterals.cs b/src/Aspirate.Shared/Literals/DotNetSdkLiterals.cs
index 5c53d3c..53cb6ea 100644
--- a/src/Aspirate.Shared/Literals/DotNetSdkLiterals.cs
+++ b/src/Aspirate.Shared/Literals/DotNetSdkLiterals.cs
@@ -11,11 +11,13 @@ public static class DotNetSdkLiterals
public const string RunArgument = "run";
public const string PublishArgument = "publish";
public const string ArgumentDelimiter = "--";
+ public const string VerbosityArgument = "--verbosity";
public const string ProjectArgument = "--project";
public const string PublisherArgument = "--publisher";
public const string OutputPathArgument = "--output-path";
public const string SelfContainedArgument = "--self-contained";
+ public const string NoLogoArgument = "--nologo";
public const string RuntimeIdentifierArgument = "-r";
public const string OsArgument = "--os";
public const string ArchArgument = "--arch";
@@ -37,6 +39,7 @@ public static class DotNetSdkLiterals
public const string DefaultRuntimeIdentifier = "linux-x64";
public const string DefaultOs = "linux";
public const string DefaultArch = "x64";
+ public const string DefaultVerbosity = "quiet";
public const string DotNetCommand = "dotnet";
diff --git a/src/Aspirate.Shared/Literals/TemplateLiterals.cs b/src/Aspirate.Shared/Literals/TemplateLiterals.cs
index 4d23c59..351ebf6 100644
--- a/src/Aspirate.Shared/Literals/TemplateLiterals.cs
+++ b/src/Aspirate.Shared/Literals/TemplateLiterals.cs
@@ -5,14 +5,9 @@ public static class TemplateLiterals
{
public const string TemplatesFolder = "Templates";
public const string DeploymentType = "deployment";
+ public const string StatefulSetType = "statefulset";
public const string DaprComponentType = "dapr-component";
public const string ServiceType = "service";
- public const string RedisType = "redis";
- public const string SqlServerType = "sqlserver";
- public const string MysqlServerType = "mysql";
- public const string RabbitMqType = "rabbitmq";
- public const string PostgresServerType = "postgres-server";
- public const string MongoDbServerType = "mongo-server";
public const string ComponentKustomizeType = "kustomization";
public const string NamespaceType = "namespace";
public const string ImagePullSecretType = "image-pull-secret";
diff --git a/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Container/ContainerResource.cs b/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Container/ContainerResource.cs
index 20f6c63..2245def 100644
--- a/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Container/ContainerResource.cs
+++ b/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Container/ContainerResource.cs
@@ -1,6 +1,12 @@
namespace Aspirate.Shared.Models.AspireManifests.Components.V0.Container;
-public class ContainerResource : Resource, IResourceWithBinding, IResourceWithConnectionString, IResourceWithArgs, IResourceWithAnnotations, IResourceWithEnvironmentalVariables
+public class ContainerResource : Resource,
+ IResourceWithBinding,
+ IResourceWithConnectionString,
+ IResourceWithArgs,
+ IResourceWithAnnotations,
+ IResourceWithEnvironmentalVariables,
+ IResourceWithVolumes
{
[JsonPropertyName("image")]
public required string Image { get; set; }
@@ -19,4 +25,7 @@ public class ContainerResource : Resource, IResourceWithBinding, IResourceWithCo
[JsonPropertyName("env")]
public Dictionary? Env { get; set; } = [];
+
+ [JsonPropertyName("volumes")]
+ public List? Volumes { get; set; } = [];
}
diff --git a/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Volume.cs b/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Volume.cs
new file mode 100644
index 0000000..f974c26
--- /dev/null
+++ b/src/Aspirate.Shared/Models/AspireManifests/Components/V0/Volume.cs
@@ -0,0 +1,13 @@
+namespace Aspirate.Shared.Models.AspireManifests.Components.V0;
+
+public class Volume
+{
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("target")]
+ public string? Target { get; set; }
+
+ [JsonPropertyName("readOnly")]
+ public bool ReadOnly { get; set; }
+}
diff --git a/src/Aspirate.Shared/Models/AspireManifests/Interfaces/IResourceWithVolumes.cs b/src/Aspirate.Shared/Models/AspireManifests/Interfaces/IResourceWithVolumes.cs
new file mode 100644
index 0000000..21a357e
--- /dev/null
+++ b/src/Aspirate.Shared/Models/AspireManifests/Interfaces/IResourceWithVolumes.cs
@@ -0,0 +1,8 @@
+using Volume = Aspirate.Shared.Models.AspireManifests.Components.V0.Volume;
+
+namespace Aspirate.Shared.Models.AspireManifests.Interfaces;
+
+public interface IResourceWithVolumes
+{
+ List? Volumes { get; set; }
+}
diff --git a/tests/Aspirate.Tests/Aspirate.Tests.csproj b/tests/Aspirate.Tests/Aspirate.Tests.csproj
index 984827f..93421a0 100644
--- a/tests/Aspirate.Tests/Aspirate.Tests.csproj
+++ b/tests/Aspirate.Tests/Aspirate.Tests.csproj
@@ -39,6 +39,9 @@
+
+ Always
+
diff --git a/tests/Aspirate.Tests/ServiceTests/ManifestFileParserServiceTests.cs b/tests/Aspirate.Tests/ServiceTests/ManifestFileParserServiceTests.cs
index cc718a4..738d793 100644
--- a/tests/Aspirate.Tests/ServiceTests/ManifestFileParserServiceTests.cs
+++ b/tests/Aspirate.Tests/ServiceTests/ManifestFileParserServiceTests.cs
@@ -114,6 +114,7 @@ public void LoadAndParseAspireManifest_ReturnsResource_WhenResourceTypeIsSupport
[InlineData("pg-endtoend.json", 22)]
[InlineData("sqlserver-endtoend.json", 4)]
[InlineData("starter-with-redis.json", 3)]
+ [InlineData("project-no-binding.json", 1)]
public async Task EndToEnd_ParsesSuccessfully(string manifestFile, int expectedCount)
{
// Arrange
@@ -149,7 +150,28 @@ public async Task EndToEndWithManualEntry_ParsesSuccessfully()
await PerformEndToEndTests(manifestFile, 8, serviceProvider, service, inputPopulator, valueSubstitutor);
}
- private static async Task PerformEndToEndTests(string manifestFile, int expectedCount, IServiceProvider serviceProvider, IManifestFileParserService service, IAction inputPopulator, IAction valueSubstitutor)
+ [Fact]
+ public async Task EndToEndShop_ParsesSuccessfully()
+ {
+ // Arrange
+ var fileSystem = new MockFileSystem();
+ var manifestFile = "shop.json";
+ var testData = Path.Combine(AppContext.BaseDirectory, "TestData", manifestFile);
+ fileSystem.AddFile(manifestFile, new(await File.ReadAllTextAsync(testData)));
+ var serviceProvider = CreateServiceProvider(fileSystem);
+
+ var service = serviceProvider.GetRequiredService();
+ var inputPopulator = serviceProvider.GetRequiredKeyedService(nameof(PopulateInputsAction));
+ var valueSubstitutor = serviceProvider.GetRequiredKeyedService(nameof(SubstituteValuesAspireManifestAction));
+
+ var results = await PerformEndToEndTests(manifestFile, 12, serviceProvider, service, inputPopulator, valueSubstitutor);
+
+ var shopResource = results["basketcache"] as ContainerResource;
+ shopResource.Volumes.Should().HaveCount(1);
+ shopResource.Volumes[0].Name.Should().Be("basketcache-data");
+ }
+
+ private static async Task> PerformEndToEndTests(string manifestFile, int expectedCount, IServiceProvider serviceProvider, IManifestFileParserService service, IAction inputPopulator, IAction valueSubstitutor)
{
// Act
var state = serviceProvider.GetRequiredService();
@@ -190,6 +212,8 @@ private static async Task PerformEndToEndTests(string manifestFile, int expected
envVar.Value.Should().NotContain("}");
}
}
+
+ return result;
}
private static IServiceProvider CreateServiceProvider(IFileSystem? fileSystem = null, IAnsiConsole? console = null)
diff --git a/tests/Aspirate.Tests/TestData/project-no-binding.json b/tests/Aspirate.Tests/TestData/project-no-binding.json
new file mode 100644
index 0000000..609ca13
--- /dev/null
+++ b/tests/Aspirate.Tests/TestData/project-no-binding.json
@@ -0,0 +1,13 @@
+{
+ "resources": {
+ "no-bindings": {
+ "type": "project.v0",
+ "path": "../ProjectWithoutBindings/ProjectWithoutBindings.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory"
+ }
+ }
+ }
+}
diff --git a/tests/Aspirate.Tests/TestData/shop.json b/tests/Aspirate.Tests/TestData/shop.json
new file mode 100644
index 0000000..514f3eb
--- /dev/null
+++ b/tests/Aspirate.Tests/TestData/shop.json
@@ -0,0 +1,246 @@
+{
+ "resources": {
+ "postgres": {
+ "type": "container.v0",
+ "connectionString": "Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres-password.value}",
+ "image": "postgres:16.2",
+ "env": {
+ "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256",
+ "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256",
+ "POSTGRES_USER": "postgres",
+ "POSTGRES_PASSWORD": "{postgres-password.value}"
+ },
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 5432
+ }
+ }
+ },
+ "catalogdb": {
+ "type": "value.v0",
+ "connectionString": "{postgres.connectionString};Database=catalogdb"
+ },
+ "basketcache": {
+ "type": "container.v0",
+ "connectionString": "{basketcache.bindings.tcp.host}:{basketcache.bindings.tcp.port}",
+ "image": "redis:7.2.4",
+ "args": [
+ "--save",
+ "60",
+ "1"
+ ],
+ "volumes": [
+ {
+ "name": "basketcache-data",
+ "target": "/data",
+ "readOnly": false
+ }
+ ],
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 6379
+ }
+ }
+ },
+ "catalogservice": {
+ "type": "project.v0",
+ "path": "../CatalogService/CatalogService.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__catalogdb": "{catalogdb.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "rabbitmq-password": {
+ "type": "parameter.v0",
+ "value": "{rabbitmq-password.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true,
+ "default": {
+ "generate": {
+ "minLength": 22
+ }
+ }
+ }
+ }
+ },
+ "messaging": {
+ "type": "container.v0",
+ "connectionString": "amqp://guest:{rabbitmq-password.value}@{messaging.bindings.tcp.host}:{messaging.bindings.tcp.port}",
+ "image": "rabbitmq:3-management",
+ "volumes": [
+ {
+ "name": "TestShop.AppHost-messaging-data",
+ "target": "/var/lib/rabbitmq",
+ "readOnly": false
+ }
+ ],
+ "env": {
+ "RABBITMQ_DEFAULT_USER": "guest",
+ "RABBITMQ_DEFAULT_PASS": "{rabbitmq-password.value}"
+ },
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 5672
+ },
+ "management": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 15672
+ }
+ }
+ },
+ "basketservice": {
+ "type": "project.v0",
+ "path": "../BasketService/BasketService.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__basketcache": "{basketcache.connectionString}",
+ "ConnectionStrings__messaging": "{messaging.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http2"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http2"
+ }
+ }
+ },
+ "frontend": {
+ "type": "project.v0",
+ "path": "../MyFrontend/MyFrontend.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "services__basketservice__http__0": "{basketservice.bindings.http.url}",
+ "services__basketservice__https__0": "{basketservice.bindings.https.url}",
+ "services__catalogservice__http__0": "{catalogservice.bindings.http.url}",
+ "services__catalogservice__https__0": "{catalogservice.bindings.https.url}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "external": true
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http",
+ "external": true
+ }
+ }
+ },
+ "orderprocessor": {
+ "type": "project.v0",
+ "path": "../OrderProcessor/OrderProcessor.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ConnectionStrings__messaging": "{messaging.connectionString}"
+ }
+ },
+ "apigateway": {
+ "type": "project.v0",
+ "path": "../ApiGateway/ApiGateway.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "services__basketservice__http__0": "{basketservice.bindings.http.url}",
+ "services__basketservice__https__0": "{basketservice.bindings.https.url}",
+ "services__catalogservice__http__0": "{catalogservice.bindings.http.url}",
+ "services__catalogservice__https__0": "{catalogservice.bindings.https.url}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "catalogdbapp": {
+ "type": "project.v0",
+ "path": "../CatalogDb/CatalogDb.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__catalogdb": "{catalogdb.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "postgres-password": {
+ "type": "parameter.v0",
+ "value": "{postgres-password.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true,
+ "default": {
+ "generate": {
+ "minLength": 22
+ }
+ }
+ }
+ }
+ }
+ }
+}