| File: KubernetesPersistentVolumeExtensions.cs | Web Access |
| Project: src\src\Aspire.Hosting.Kubernetes\Aspire.Hosting.Kubernetes.csproj (Aspire.Hosting.Kubernetes) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Kubernetes; using Aspire.Hosting.Kubernetes.Annotations; namespace Aspire.Hosting; /// <summary> /// Provides extension methods for configuring Kubernetes /// <see cref="KubernetesPersistentVolumeResource"/> resources and binding workloads /// to them. /// </summary> [Experimental("ASPIRECOMPUTE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static class KubernetesPersistentVolumeExtensions { /// <summary> /// Adds a Kubernetes PersistentVolumeClaim resource to the application model as a /// child of the specified Kubernetes environment. The resource generates a /// <c>v1.PersistentVolumeClaim</c> manifest in the Helm chart output at publish /// time. /// </summary> /// <ats-summary>Adds a Kubernetes PersistentVolumeClaim resource</ats-summary> /// <param name="builder">The Kubernetes environment resource builder.</param> /// <param name="name">The name of the persistent volume resource. Used as the /// generated PVC's <c>metadata.name</c> after lower-casing. To bind a workload /// using the name-match overload of /// <see cref="WithPersistentVolume{T}(IResourceBuilder{T}, IResourceBuilder{KubernetesPersistentVolumeResource})"/>, /// add a <c>WithVolume("name", "/path")</c> on the workload using the same /// <paramref name="name"/>.</param> /// <returns>A builder for the new <see cref="KubernetesPersistentVolumeResource"/>.</returns> /// <example> /// <code> /// var k8s = builder.AddKubernetesEnvironment("k8s"); /// var data = k8s.AddPersistentVolume("data") /// .WithStorageClass("managed-csi") /// .WithCapacity("20Gi"); /// </code> /// </example> [AspireExport] public static IResourceBuilder<KubernetesPersistentVolumeResource> AddPersistentVolume( this IResourceBuilder<KubernetesEnvironmentResource> builder, [ResourceName] string name) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); var volume = new KubernetesPersistentVolumeResource(name, builder.Resource); if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { // Persistent volumes are publish-only — surface them in the model but skip // manifest generation in run mode (mirrors the ingress and gateway pattern). return builder.ApplicationBuilder.CreateResourceBuilder(volume); } return builder.ApplicationBuilder.AddResource(volume) .ExcludeFromManifest(); } /// <summary> /// Sets the Kubernetes storage class name on the PVC's /// <c>spec.storageClassName</c>. When unset, the cluster's default storage class /// is used. /// </summary> /// <ats-summary>Sets the storage class for a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="storageClassName">The storage class name (e.g. /// <c>"managed-csi"</c>, <c>"gp3"</c>).</param> /// <returns>The same builder for chaining.</returns> [AspireExport] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithStorageClass( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, string storageClassName) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(storageClassName); builder.Resource.StorageClassName = ReferenceExpression.Create($"{storageClassName}"); return builder; } /// <summary> /// Sets the Kubernetes storage class name using a parameter resolved at deploy /// time. /// </summary> /// <ats-summary>Sets a parameterized storage class for a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="storageClassName">A parameter resource builder for the storage /// class name.</param> /// <returns>The same builder for chaining.</returns> [AspireExport("withStorageClassParam")] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithStorageClass( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, IResourceBuilder<ParameterResource> storageClassName) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(storageClassName); builder.Resource.StorageClassName = ReferenceExpression.Create($"{storageClassName.Resource}"); return builder; } /// <summary> /// Sets the requested storage capacity on the PVC's /// <c>spec.resources.requests.storage</c> field. /// </summary> /// <ats-summary>Sets the requested storage capacity for a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="capacity">A Kubernetes quantity string (e.g. <c>"10Gi"</c>, /// <c>"500Mi"</c>).</param> /// <returns>The same builder for chaining.</returns> [AspireExport] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithCapacity( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, string capacity) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(capacity); builder.Resource.Capacity = ReferenceExpression.Create($"{capacity}"); return builder; } /// <summary> /// Sets the requested storage capacity using a parameter resolved at deploy time. /// </summary> /// <ats-summary>Sets a parameterized storage capacity for a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="capacity">A parameter resource builder for the capacity quantity /// string.</param> /// <returns>The same builder for chaining.</returns> [AspireExport("withCapacityParam")] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithCapacity( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, IResourceBuilder<ParameterResource> capacity) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(capacity); builder.Resource.Capacity = ReferenceExpression.Create($"{capacity.Resource}"); return builder; } /// <summary> /// Adds an access mode to the PVC's <c>spec.accessModes</c>. Call multiple times /// to declare more than one mode. When unset, the environment's /// <see cref="KubernetesEnvironmentResource.DefaultStorageReadWritePolicy"/> is /// used. /// </summary> /// <ats-summary>Adds an access mode to a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="accessMode">The access mode to add.</param> /// <returns>The same builder for chaining.</returns> [AspireExport] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithAccessMode( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, PersistentVolumeAccessMode accessMode) { ArgumentNullException.ThrowIfNull(builder); if (!builder.Resource.AccessModes.Contains(accessMode)) { builder.Resource.AccessModes.Add(accessMode); } return builder; } /// <summary> /// Adds a Kubernetes metadata annotation to the generated PVC. These flush to /// <c>metadata.annotations</c> on the rendered Kubernetes resource — not Aspire /// <see cref="ApplicationModel.IResourceAnnotation"/> instances. Common uses: /// CSI driver hints, dynamic provisioner parameters, external-secrets selectors, /// or backup tooling tags. /// </summary> /// <ats-summary>Adds a Kubernetes metadata annotation to a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="key">The annotation key (e.g. /// <c>"volume.beta.kubernetes.io/storage-provisioner"</c>).</param> /// <param name="value">The annotation value.</param> /// <returns>The same builder for chaining.</returns> [AspireExport] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithVolumeAnnotation( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, string key, string value) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(value); builder.Resource.VolumeAnnotations[key] = ReferenceExpression.Create($"{value}"); return builder; } /// <summary> /// Adds a Kubernetes metadata annotation with a parameter value resolved at /// deploy time. /// </summary> /// <ats-summary>Adds a parameterized Kubernetes metadata annotation to a persistent volume</ats-summary> /// <param name="builder">The persistent volume resource builder.</param> /// <param name="key">The annotation key.</param> /// <param name="value">A parameter resource builder for the annotation value.</param> /// <returns>The same builder for chaining.</returns> [AspireExport("withVolumeAnnotationParam")] public static IResourceBuilder<KubernetesPersistentVolumeResource> WithVolumeAnnotation( this IResourceBuilder<KubernetesPersistentVolumeResource> builder, string key, IResourceBuilder<ParameterResource> value) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(key); ArgumentNullException.ThrowIfNull(value); builder.Resource.VolumeAnnotations[key] = ReferenceExpression.Create($"{value.Resource}"); return builder; } /// <summary> /// Binds a workload to a Kubernetes <see cref="KubernetesPersistentVolumeResource"/> /// using name matching. The workload must already declare a volume with /// a matching <c>source</c> name (typically via <c>WithVolume("name", "/path")</c> /// or an integration helper such as Postgres' /// <c>WithDataVolume()</c>). The publisher rewrites that volume's pod-spec entry /// to reference the generated PVC and promotes the workload to a /// <c>StatefulSet</c>. /// </summary> /// <ats-summary>Binds a workload to a Kubernetes persistent volume by matching volume name</ats-summary> /// <typeparam name="T">A compute resource (container, project, executable).</typeparam> /// <param name="builder">The workload resource builder.</param> /// <param name="volume">The persistent volume resource to bind to.</param> /// <returns>The same builder for chaining.</returns> /// <remarks> /// To bind a workload that does not already have a matching named mount (for /// example a <c>ProjectResource</c>), use the overload that accepts a /// <c>mountPath</c> instead. The generated pod uses an Aspire-managed /// <c>fsGroup</c> of <c>2000</c> with an <c>OnRootMismatch</c> change policy so /// non-root containers can access supported volumes without matching the image's /// primary group. Use /// <see cref="KubernetesServiceExtensions.PublishAsKubernetesService{T}(IResourceBuilder{T}, Action{KubernetesResource})"/> /// to customize the pod security context when a different group or policy is required. /// </remarks> /// <example> /// <code> /// var pgData = k8s.AddPersistentVolume("pg-data") /// .WithStorageClass("managed-csi") /// .WithCapacity("20Gi"); /// /// var pg = builder.AddPostgres("pg") /// .WithDataVolume("pg-data") // ContainerMountAnnotation source = "pg-data" /// .WithPersistentVolume(pgData); // matches by name "pg-data" /// </code> /// </example> [AspireExport("withKubernetesPersistentVolume")] public static IResourceBuilder<T> WithPersistentVolume<T>( this IResourceBuilder<T> builder, IResourceBuilder<KubernetesPersistentVolumeResource> volume) where T : IComputeResource { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(volume); builder.WithAnnotation(new KubernetesPersistentVolumeBindingAnnotation(volume.Resource)); return builder; } /// <summary> /// Binds a workload to a Kubernetes <see cref="KubernetesPersistentVolumeResource"/> /// and mounts it at the specified path inside the workload's container. Unlike /// the name-match overload this one creates the underlying mount itself, so it /// works for workloads that don't already declare a named volume — including /// <see cref="ProjectResource"/>. /// </summary> /// <ats-summary>Binds a workload to a Kubernetes persistent volume and mounts it at a path</ats-summary> /// <typeparam name="T">A compute resource (container, project, executable).</typeparam> /// <param name="builder">The workload resource builder.</param> /// <param name="volume">The persistent volume resource to bind to.</param> /// <param name="mountPath">The path inside the container where the volume will /// be mounted (e.g. <c>"/var/lib/postgresql/data"</c>).</param> /// <param name="isReadOnly">When <see langword="true"/>, mounts the volume /// read-only.</param> /// <returns>The same builder for chaining.</returns> /// <remarks> /// The generated pod uses an Aspire-managed <c>fsGroup</c> of <c>2000</c> with /// an <c>OnRootMismatch</c> change policy so non-root containers can access /// supported volumes without matching the image's primary group. Use /// <see cref="KubernetesServiceExtensions.PublishAsKubernetesService{T}(IResourceBuilder{T}, Action{KubernetesResource})"/> /// to customize the pod security context when a different group or policy is required. /// </remarks> /// <example> /// <code> /// var media = k8s.AddPersistentVolume("media") /// .WithStorageClass("azurefile-csi") /// .WithCapacity("100Gi") /// .WithAccessMode(PersistentVolumeAccessMode.ReadWriteMany); /// /// builder.AddProject<MyApi>("api") /// .WithPersistentVolume(media, "/srv/media"); /// </code> /// </example> [AspireExport("withKubernetesPersistentVolumeMount")] public static IResourceBuilder<T> WithPersistentVolume<T>( this IResourceBuilder<T> builder, IResourceBuilder<KubernetesPersistentVolumeResource> volume, string mountPath, bool isReadOnly = false) where T : IComputeResource { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(volume); ArgumentException.ThrowIfNullOrEmpty(mountPath); builder.WithAnnotation(new ContainerMountAnnotation(volume.Resource.Name, mountPath, ContainerMountType.Volume, isReadOnly)); builder.WithAnnotation(new KubernetesPersistentVolumeBindingAnnotation(volume.Resource)); return builder; } /// <summary> /// Converts a <see cref="PersistentVolumeAccessMode"/> enum value to the /// Kubernetes API string representation. /// </summary> internal static string ToKubernetesString(this PersistentVolumeAccessMode accessMode) { return accessMode switch { PersistentVolumeAccessMode.ReadWriteOnce => "ReadWriteOnce", PersistentVolumeAccessMode.ReadOnlyMany => "ReadOnlyMany", PersistentVolumeAccessMode.ReadWriteMany => "ReadWriteMany", PersistentVolumeAccessMode.ReadWriteOncePod => "ReadWriteOncePod", _ => throw new ArgumentOutOfRangeException(nameof(accessMode), accessMode, "Unknown persistent volume access mode."), }; } }