|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Kubernetes.Extensions;
using Aspire.Hosting.Kubernetes.Resources;
using Aspire.Hosting.Kubernetes.Yaml;
using Aspire.Hosting.Yaml;
using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Aspire.Hosting.Kubernetes;
internal sealed class KubernetesPublishingContext(
DistributedApplicationExecutionContext executionContext,
KubernetesPublisherOptions publisherOptions,
ILogger logger,
CancellationToken cancellationToken = default)
{
private readonly Dictionary<string, Dictionary<string, object>> _helmValues = new()
{
[HelmExtensions.ParametersKey] = new Dictionary<string, object>(),
[HelmExtensions.SecretsKey] = new Dictionary<string, object>(),
[HelmExtensions.ConfigKey] = new Dictionary<string, object>(),
};
private readonly ISerializer _serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new ByteArrayStringYamlConverter())
.WithTypeConverter(new IntOrStringYamlConverter())
.WithEventEmitter(nextEmitter => new ForceQuotedStringsEventEmitter(nextEmitter))
.WithEventEmitter(e => new FloatEmitter(e))
.WithEmissionPhaseObjectGraphVisitor(args => new YamlIEnumerableSkipEmptyObjectGraphVisitor(args.InnerVisitor))
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
.WithNewLine("\n")
.WithIndentedSequences()
.Build();
public ILogger Logger => logger;
internal async Task WriteModelAsync(DistributedApplicationModel model)
{
if (!executionContext.IsPublishMode)
{
logger.NotInPublishingMode();
return;
}
logger.StartGeneratingKubernetes();
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(publisherOptions.OutputPath);
if (model.Resources.Count == 0)
{
logger.EmptyModel();
return;
}
await WriteKubernetesOutputAsync(model).ConfigureAwait(false);
logger.FinishGeneratingKubernetes(publisherOptions.OutputPath);
}
private async Task WriteKubernetesOutputAsync(DistributedApplicationModel model)
{
var kubernetesEnvironments = model.Resources.OfType<KubernetesEnvironmentResource>().ToArray();
if (kubernetesEnvironments.Length > 1)
{
throw new NotSupportedException("Multiple Kubernetes environments are not supported.");
}
var environment = kubernetesEnvironments.FirstOrDefault();
if (environment == null)
{
// No Kubernetes environment found
throw new InvalidOperationException($"No Kubernetes environment found. Ensure a Kubernetes environment is registered by calling {nameof(KubernetesEnvironmentExtensions.AddKubernetesEnvironment)}.");
}
foreach (var resource in model.Resources)
{
if (resource.GetDeploymentTargetAnnotation()?.DeploymentTarget is KubernetesResource serviceResource)
{
if (serviceResource.TargetResource.TryGetAnnotationsOfType<KubernetesServiceCustomizationAnnotation>(out var annotations))
{
foreach (var a in annotations)
{
a.Configure(serviceResource);
}
}
await WriteKubernetesTemplatesForResource(resource, serviceResource.GetTemplatedResources()).ConfigureAwait(false);
AppendResourceContextToHelmValues(resource, serviceResource);
}
}
await WriteKubernetesHelmChartAsync(environment).ConfigureAwait(false);
await WriteKubernetesHelmValuesAsync().ConfigureAwait(false);
}
private void AppendResourceContextToHelmValues(IResource resource, KubernetesResource resourceContext)
{
AddValuesToHelmSection(resource, resourceContext.Parameters, HelmExtensions.ParametersKey);
AddValuesToHelmSection(resource, resourceContext.EnvironmentVariables, HelmExtensions.ConfigKey);
AddValuesToHelmSection(resource, resourceContext.Secrets, HelmExtensions.SecretsKey);
}
private void AddValuesToHelmSection(
IResource resource,
Dictionary<string, KubernetesResource.HelmExpressionWithValue> contextItems,
string helmKey)
{
if (contextItems.Count <= 0 || _helmValues[helmKey] is not Dictionary<string, object> helmSection)
{
return;
}
var paramValues = new Dictionary<string, string>();
foreach (var (key, helmExpressionWithValue) in contextItems)
{
if (helmExpressionWithValue.ValueContainsHelmExpression)
{
continue;
}
paramValues[key] = helmExpressionWithValue.Value ?? string.Empty;
}
if (paramValues.Count > 0)
{
helmSection[resource.Name] = paramValues;
}
}
private async Task WriteKubernetesTemplatesForResource(IResource resource, IEnumerable<BaseKubernetesResource> templatedItems)
{
var templatesFolder = Path.Combine(publisherOptions.OutputPath!, "templates", resource.Name);
Directory.CreateDirectory(templatesFolder);
foreach (var templatedItem in templatedItems)
{
var fileName = $"{templatedItem.GetType().Name.ToLowerInvariant()}.yaml";
var outputFile = Path.Combine(templatesFolder, fileName);
var yaml = _serializer.Serialize(templatedItem);
using var writer = new StreamWriter(outputFile);
await writer.WriteLineAsync(HelmExtensions.TemplateFileSeparator).ConfigureAwait(false);
await writer.WriteAsync(yaml).ConfigureAwait(false);
}
}
private async Task WriteKubernetesHelmValuesAsync()
{
var valuesYaml = _serializer.Serialize(_helmValues);
var outputFile = Path.Combine(publisherOptions.OutputPath!, "values.yaml");
Directory.CreateDirectory(publisherOptions.OutputPath!);
await File.WriteAllTextAsync(outputFile, valuesYaml, cancellationToken).ConfigureAwait(false);
}
private async Task WriteKubernetesHelmChartAsync(KubernetesEnvironmentResource environment)
{
var helmChart = new HelmChart
{
Name = environment.HelmChartName,
Version = environment.HelmChartVersion,
AppVersion = environment.HelmChartVersion,
Description = environment.HelmChartDescription,
Type = "application",
ApiVersion = "v2",
Keywords = ["aspire", "kubernetes"],
KubeVersion = ">= 1.18.0-0",
};
var chartYaml = _serializer.Serialize(helmChart);
var outputFile = Path.Combine(publisherOptions.OutputPath!, "Chart.yaml");
Directory.CreateDirectory(publisherOptions.OutputPath!);
await File.WriteAllTextAsync(outputFile, chartYaml, cancellationToken).ConfigureAwait(false);
}
}
|