Skip to content

Add first-class KubernetesPersistentVolumeResource (and fix related YAML serializer bugs)#16929

Merged
mitchdenny merged 15 commits into
mainfrom
mitchdenny/k8s-persistent-volumes-research
Jul 5, 2026
Merged

Add first-class KubernetesPersistentVolumeResource (and fix related YAML serializer bugs)#16929
mitchdenny merged 15 commits into
mainfrom
mitchdenny/k8s-persistent-volumes-research

Conversation

@mitchdenny

@mitchdenny mitchdenny commented May 11, 2026

Copy link
Copy Markdown
Member

Tracking issue: #16999

Summary

Adds first-class app-model support for Kubernetes persistent volumes, plus the underlying YAML serializer bug fixes that the new code path exposes. The change is best read as two layers stacked on top of each other:

  1. YAML serializer fixes — foundation work that makes emitted PV / PVC / StatefulSet manifests valid.
  2. KubernetesPersistentVolumeResource — a new first-class resource for describing a volume once in the app model and binding it to workloads.

Together they let AppHost authors say "here's a durable disk with this shape, mount it here" once, in real C# / TypeScript, and get correct, cluster-friendly Kubernetes output.

Layer 1 — YAML serializer bug fixes

The serializer is configured with OmitNull, but several V1 spec types eagerly initialized complex sub-properties to new(). Non-null empty objects don't get omitted, so we shipped invalid manifests like:

spec:
  updateStrategy: {}          # StatefulSetSpecV1.UpdateStrategy
  volumeClaimRetentionPolicy: {}
  dataSource: {}              # PersistentVolumeClaimSpecV1
  dataSourceRef: {}
  selector: {}

The fix, applied uniformly to PersistentVolumeSpecV1, PersistentVolumeClaimSpecV1, StatefulSetSpecV1, StatefulSetUpdateStrategyV1, and VolumeNodeAffinityV1, is to make those complex sub-properties nullable so the existing OmitNull filters do the right thing.

This matters far more after Layer 2, because auto-promoting bound workloads to StatefulSet (below) triggers the previously-rare stateful-set code paths on nearly every PV scenario.

Closes:

Layer 2 — First-class KubernetesPersistentVolumeResource

Today's WithVolume("name", "/path") on a container resource leans entirely on the environment-wide DefaultStorageType / DefaultStorageClassName / DefaultStorageSize knobs. Every volume in the env is forced to share one shape, which doesn't match real clusters where different workloads want different storage classes, sizes, or CSI parameters.

A first-class KubernetesPersistentVolumeResource mirrors how KubernetesIngressResource and KubernetesGatewayResource already work: the volume is a real IResource you configure once, then bind to workloads.

New public API surface

All new APIs are marked [Experimental("ASPIRECOMPUTE002")], matching the package's existing experimental surface.

Resource

KubernetesPersistentVolumeResource

Carries the storage class, capacity, access modes, and metadata.annotations needed to render a single v1.PersistentVolumeClaim at publish time.

Environment builder

kubernetesEnvironment.AddPersistentVolume(string name)

Fluent configuration

.WithStorageClass(string | IManifestExpressionProvider)
.WithCapacity(string | IManifestExpressionProvider)
.WithAccessMode(PersistentVolumeAccessMode)         // ReadWriteOnce | ReadOnlyMany | ReadWriteMany | ReadWriteOncePod
.WithVolumeAnnotation(string key, string | ReferenceExpression value)

Binding workloads to a volume

// Name-match overload — pairs with an existing ContainerMountAnnotation on the workload.
IResourceBuilder<T>.WithPersistentVolume(IResourceBuilder<KubernetesPersistentVolumeResource>)

// Mount-path overload — works for projects and any IComputeResource.
IResourceBuilder<T>.WithPersistentVolume(
    IResourceBuilder<KubernetesPersistentVolumeResource> volume,
    string mountPath)

End-to-end example

var k8s = builder.AddKubernetesEnvironment("k8s");

var pgData = k8s.AddPersistentVolume("pg-data")
    .WithStorageClass("managed-csi")
    .WithCapacity("20Gi")
    .WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce)
    .WithVolumeAnnotation("disk.csi.azure.com/skuName", "Premium_LRS");

// Name-match overload — Postgres' WithDataVolume() emits a ContainerMountAnnotation
// with source "pg-data"; the binding picks it up and routes the pod's volumes[]
// entry through the generated PVC.
builder.AddPostgres("pg")
    .WithDataVolume()
    .WithPersistentVolume(pgData);

// Mount-path overload — works for projects (closes #9430) and any IComputeResource.
builder.AddProject<Projects.Api>("api")
    .WithPersistentVolume(pgData, "/srv/data");

The same shape works from a TypeScript polyglot AppHost via the auto-generated SDK bindings — addPersistentVolume, withStorageClass, withCapacity, and the mount-path overload projected as withKubernetesPersistentVolumeMount:

const k8sEnv = await builder.addKubernetesEnvironment("env");

const scratch = await k8sEnv.addPersistentVolume("scratch");
await scratch.withStorageClass("standard");
await scratch.withCapacity("256Mi");

// Mount-path overload — binds the volume and auto-promotes the node app
// from Deployment to StatefulSet, same as the C# path.
await app.withKubernetesPersistentVolumeMount(scratch, "/srv/data");

How it hangs together at publish time

The two paths that touch a bound volume run in different phases of the publish pipeline. To keep them in lockstep, name derivation is centralized on the resource:

  1. Workload compose phase (WithPodSpecVolumes in ResourceExtensions)
    • For each volume on a workload, check for a KubernetesPersistentVolumeBindingAnnotation.
    • If bound, emit a PersistentVolumeClaim volume source on the pod and set claimName = binding.Volume.GetClaimName().
    • If unbound, fall through to the existing environment-wide default storage type behavior (emptydir / hostpath / pvc).
  2. PV resource processing phase (ProcessPersistentVolumeResources in KubernetesEnvironmentResource)
    • For each KubernetesPersistentVolumeResource, build a PersistentVolumeClaim with metadata.name = volumeResource.GetClaimName(), using the resource's own storage class / capacity / access modes (falling back to the env-wide defaults when not set).
    • Assign it to GeneratedClaim so the publishing context can emit it into the Helm chart output.
  3. Emission (KubernetesPublishingContext)
    • Emits one PVC YAML per KubernetesPersistentVolumeResource whose Parent == environment.

Because both the pod's claimName and the PVC's metadata.name route through KubernetesPersistentVolumeResource.GetClaimName(), the two phases can never drift.

Behavior change: workloads with a bound PV are promoted to StatefulSet

Any workload that binds to a KubernetesPersistentVolumeResource is rendered as a StatefulSet regardless of its default workload kind. There is intentionally no opt-out.

Why: a Deployment with a ReadWriteOnce PVC and replicas > 1 is broken by design — the second pod cannot mount the same volume. The whole point of the abstraction is to encode the correct defaults, so binding to a durable volume flips you onto the StatefulSet path automatically. This is also what turns Layer 1's StatefulSet serializer fixes from a rare edge case into a routine one.

Fallthrough behavior for unbound volumes

Volumes on a workload that do not have a binding continue to use the environment's DefaultStorageType (emptydir / hostpath / pvc). This is fully covered by tests, so mixing "one first-class PV + one throwaway ephemeral volume" on the same workload works correctly.

The pvc fallthrough path previously emitted a bare PersistentVolume alongside its PersistentVolumeClaim, which was invalid (no PersistentVolumeSource). It now emits only the PVC and lets the cluster's StorageClass dynamically provision the backing PV, matching the documented Kubernetes model.

Issues closed

Related work

Tests

Aspire.Hosting.Kubernetes.Tests (unit / snapshot):

  • Name-match container binding with StatefulSet promotion.
  • ProjectResource mount-path binding.
  • Fallthrough-to-env-default behavior for unbound volumes on a workload that also has a bound volume.
  • Regression coverage for the original DefaultStorageType = "pvc" and hostpath paths.

Aspire.Cli.EndToEnd.Tests (KinD durability E2E):

  • KubernetesDeployWithPersistentVolumeTests — C# AppHost, container workload, verify data survives pod recreation.
  • KubernetesDeployWithProjectPersistentVolumeTests — C# AppHost, project workload, mount-path overload.
  • KubernetesDeployTypeScriptWithPersistentVolumeTests — TypeScript polyglot AppHost, verifying the polyglot binding path emits equivalent output.

All 261 Aspire.Hosting.Kubernetes.Tests pass locally; full PR CI is green.

Copilot AI review requested due to automatic review settings May 11, 2026 01:54
@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16929

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16929"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to make the Kubernetes publisher emit deployable YAML by preventing empty {} mappings in generated PersistentVolume / PersistentVolumeClaim manifests (notably for DefaultStorageType = "pvc"), and adds regression tests/snapshots to cover the affected paths.

Changes:

  • Make several V1 spec sub-properties nullable (removing eager = new()) so YAML serialization omits them when empty.
  • Add regression tests and Verify snapshots for pvc and hostpath storage type publishing, including an explicit {}-substring invariant.
  • Update XML docs/remarks on the affected spec properties to explain the null-default behavior and why it matters for Kubernetes apply.
Show a summary per file
File Description
tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs Adds regression tests for pvc and hostpath storage type publishing and asserts no empty {} mappings appear in emitted YAML.
tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithPvcStorageType_GeneratesValidPersistentVolumeAndClaim#00.verified.yaml New snapshot covering the generated Deployment for pvc storage type.
tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithPvcStorageType_GeneratesValidPersistentVolumeAndClaim#01.verified.yaml New snapshot covering the generated PersistentVolume for pvc storage type.
tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithPvcStorageType_GeneratesValidPersistentVolumeAndClaim#02.verified.yaml New snapshot covering the generated PersistentVolumeClaim for pvc storage type.
tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithHostPathStorageType_GeneratesValidPersistentVolumeAndClaim.verified.yaml New snapshot covering the generated Deployment for hostpath storage type.
src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeSpecV1.cs Makes ClaimRef, HostPath, Local, and NodeAffinity nullable to avoid emitting empty mappings.
src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeClaimSpecV1.cs Makes DataSource, DataSourceRef, and Selector nullable to avoid emitting empty mappings.
src/Aspire.Hosting.Kubernetes/Resources/VolumeNodeAffinityV1.cs Makes Required nullable to avoid emitting required: {} when unset.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Comment thread tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs Outdated
@mitchdenny mitchdenny changed the title Fix invalid empty mappings in generated Kubernetes PV/PVC YAML Add first-class KubernetesPersistentVolumeResource (and fix related YAML serializer bugs) May 11, 2026
@mitchdenny

Copy link
Copy Markdown
Member Author

Tracking issue: #16999 (created retroactively to capture the design decisions and context for this work). Now in the 13.4 milestone alongside the future-work items called out in that issue.

@github-actions

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 4 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

Matched test failure patterns (1 test)
  • Aspire.Cli.EndToEnd.Tests.KubernetesDeployWithRedisTests.DeployK8sWithRedis — Unable to access container registry during publish

@mitchdenny
mitchdenny force-pushed the mitchdenny/k8s-persistent-volumes-research branch from 4746841 to d830d5b Compare May 18, 2026 10:14
@mitchdenny
mitchdenny force-pushed the mitchdenny/k8s-persistent-volumes-research branch from d830d5b to f1c4555 Compare May 24, 2026 04:22
@github-actions

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

@github-actions

Copy link
Copy Markdown
Contributor

CLI E2E Tests failed — 98 passed, 1 failed, 5 unknown (commit f1c4555)

Failed Tests

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View recording
AddPackageWhileAppHostRunningDetached ▶️ View recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View recording
AgentInitCommand_DefaultSelection_InstallsDefaultSkills ▶️ View recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View recording
AgentMcpListStructuredLogsFromStarterAppCore ▶️ View recording
AllPublishMethodsBuildDockerImages ▶️ View recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View recording
AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAndPreservesFiles ▶️ View recording
AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstChannelHive ▶️ View recording
AspireStartUpdatesStaleTypeScriptAppHostPath ▶️ View recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View recording
AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCurrent ▶️ View recording
Banner_DisplayedOnFirstRun ▶️ View recording
Banner_DisplayedWithExplicitFlag ▶️ View recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View recording
CertificatesClean_RemovesCertificates ▶️ View recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View recording
CreateAndRunAspireStarterProject ▶️ View recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View recording
CreateAndRunEmptyAppHostProject ▶️ View recording
CreateAndRunJavaEmptyAppHostProject ▶️ View recording
CreateAndRunJsReactProject ▶️ View recording
CreateAndRunPythonReactProject ▶️ View recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View recording
CreateAndRunTypeScriptStarterProject ▶️ View recording
CreateJavaAppHostWithViteApp ▶️ View recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View recording
DashboardRunWithAgentMcpCore ▶️ View recording
DashboardRunWithOtelTracesReturnsNoTracesCore ▶️ View recording
DeployK8sBasicApiService ▶️ View recording
DeployK8sWithExternalHelmChart ▶️ View recording
DeployK8sWithGarnet ▶️ View recording
DeployK8sWithMongoDB ▶️ View recording
DeployK8sWithMySql ▶️ View recording
DeployK8sWithPostgres ▶️ View recording
DeployK8sWithPostgresPersistentVolumeSurvivesPodRestart ▶️ View recording
DeployK8sWithProjectPersistentVolumeSurvivesPodRestart ▶️ View recording
DeployK8sWithRabbitMQ ▶️ View recording
DeployK8sWithRedis ▶️ View recording
DeployK8sWithSqlServer ▶️ View recording
DeployK8sWithValkey ▶️ View recording
DeployTypeScriptAppToKubernetes ▶️ View recording
DeployTypeScriptK8sWithPersistentVolumeSurvivesPodRestart ▶️ View failure recording
DescribeCommandResolvesReplicaNames ▶️ View recording
DescribeCommandShowsRunningResources ▶️ View recording
DetachFormatJsonProducesValidJson ▶️ View recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View recording
DoListStepsShowsPipelineSteps ▶️ View recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View recording
GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolchain ▶️ View recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View recording
GlobalMigration_PreservesAllValueTypes ▶️ View recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View recording
JavaScriptHostingApisRunFromTypeScriptAppHost ▶️ View recording
LatestCliCanStartStableChannelAppHost ▶️ View recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View recording
LogLevelTrace_ProducesTraceEntriesInCliLogFile ▶️ View recording
LogsCommandShowsResourceLogs ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterApp ▶️ View recording
OtelLogsReturnsStructuredLogsFromStarterAppIsolated ▶️ View recording
PsCommandListsRunningAppHost ▶️ View recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View recording
PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifacts ▶️ View recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View recording
ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogContainsEntries ▶️ View recording
ResourceCommand_FailsWhenInteractionServiceIsRequired ▶️ View recording
ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput ▶️ View recording
RestoreGeneratesSdkFiles ▶️ View recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View recording
RunPublishFailureScenarioAsync ▶️ View recording
RunReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
RunReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
SecretCrudOnDotNetAppHost ▶️ View recording
SecretCrudOnTypeScriptAppHost ▶️ View recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View recording
StartReportsSyntaxErrorsForDotNetAppHost ▶️ View recording
StartReportsSyntaxErrorsForTypeScriptAppHost ▶️ View recording
StopAllAppHostsFromAppHostDirectory ▶️ View recording
StopJavaPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopNonInteractiveSingleAppHost ▶️ View recording
StopTypeScriptPolyglotAppHostUsingApphostDirectory ▶️ View recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View recording
UpdateProjectChannelToStable_TypeScript_PicksUpStablePackages ▶️ View recording

📹 Recordings uploaded automatically from CI run #26351772900

@davidfowl davidfowl removed this from the 13.4 milestone May 27, 2026
@davidfowl

Copy link
Copy Markdown
Contributor

Not for 13.4, moving.

Copilot AI review requested due to automatic review settings June 18, 2026 02:51
@mitchdenny
mitchdenny force-pushed the mitchdenny/k8s-persistent-volumes-research branch from f1c4555 to e886f5b Compare June 18, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 26/26 changed files
  • Comments generated: 1

Comment thread tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@davidfowl
davidfowl marked this pull request as draft June 18, 2026 07:24
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing July 2, 2026 00:43 Failure
@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #16929...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 26/26 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeExtensions.cs Outdated
The <em> HTML tag isn't supported in C# XML documentation and won't render
in IntelliSense or the published API docs. Only <c>, <code>, and <para>
are permitted for inline emphasis.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@mitchdenny

Copy link
Copy Markdown
Member Author

PR Testing Report

PR Information

  • PR: #16929 — Add first-class KubernetesPersistentVolumeResource (and fix related YAML serializer bugs)
  • Head Commit: 8a4ce42e43ada116cfa843d42184e889e52b6f6e
  • Tested At: 2026-07-03 (local macOS, PR CLI in temp dir)

Artifact Version Verification

  • Expected Commit: 8a4ce42e43…
  • Installed CLI version: 13.5.0-pr.16929.g8a4ce42e
  • Status: ✅ Verified (short SHA g8a4ce42e matches PR head)

Changes Analyzed

Kubernetes hosting integration only:

  • New KubernetesPersistentVolumeResource + WithPersistentVolume(...) extensions and AddPersistentVolume(...) on IResourceBuilder<KubernetesEnvironmentResource>.
  • Serializer fixes for PersistentVolumeClaimSpecV1, PersistentVolumeSpecV1, StatefulSetSpecV1, StatefulSetUpdateStrategyV1, VolumeNodeAffinityV1 to suppress emission of empty {} blocks.
  • HelmExtensions / KubernetesResource cleanup (dead PersistentVolumes collection, ToPvName helper, PvKey constant).
  • New Kubernetes publisher unit tests + snapshots and three CLI E2E tests.

Test Scenarios Executed

Scenario 1 — Name-match PV binding + StatefulSet auto-promotion

Objective: AddPersistentVolume + WithPersistentVolume on a container workload produces a valid PVC and promotes the workload to StatefulSet.
Coverage: Happy path.
Status: ✅ Passed.

AppHost:

var k8s = builder.AddKubernetesEnvironment("k8s");
var pgData = k8s.AddPersistentVolume("pg-data")
    .WithStorageClass("fast-ssd")
    .WithCapacity("20Gi");

builder.AddPostgres("pg")
    .WithDataVolume("pg-data")
    .WithPersistentVolume(pgData);

Emitted:

  • templates/pg-data/pg-data.yaml — PVC with storageClassName: "fast-ssd", resources.requests.storage: "20Gi", no empty dataSource/dataSourceRef/selector blocks.
  • templates/pg/statefulset.yamlkind: StatefulSet (promoted), volumes[0].persistentVolumeClaim.claimName: "pg-data" matches PVC metadata.name.
  • No bare PersistentVolume file is emitted (only the PVC).
  • No empty updateStrategy: {} or volumeClaimRetentionPolicy: {} blocks in StatefulSet spec.

Scenario 2 — Project mount-path overload (#9430)

Objective: WithPersistentVolume(pv, "/srv/data") on a ProjectResource binds a PVC and mounts it at the given path.
Coverage: Happy path.
Status: ✅ Passed.

AppHost (starter template + PV binding on the apiservice project):

var k8s = builder.AddKubernetesEnvironment("k8s");
var apiData = k8s.AddPersistentVolume("api-data")
    .WithStorageClass("standard").WithCapacity("5Gi");

var apiService = builder.AddProject<Projects.PvProj_ApiService>("apiservice")
    .WithHttpHealthCheck("/health")
    .WithPersistentVolume(apiData, "/srv/data");

Emitted:

  • apiservice-statefulset.yaml (promoted from Deployment) with volumeMounts[0].mountPath: "/srv/data" and claimName: "api-data".
  • webfrontend/deployment.yaml stays a Deployment (correctly no promotion for the unbound project).
  • PVC file templates/api-data/api-data.yaml with correct spec, no empty blocks.

Scenario 3 — Fallthrough for unbound volume with env DefaultStorageType = "pvc" (#14096)

Objective: When a workload has WithVolume but no WithPersistentVolume, and the env's DefaultStorageType = "pvc", the publisher must emit a valid PVC (not a bare PV that Kubernetes will reject).
Coverage: Regression/fallthrough happy path.
Status: ✅ Passed.

AppHost:

var k8s = builder.AddKubernetesEnvironment("k8s");
k8s.Resource.DefaultStorageType = "pvc";
builder.AddPostgres("pg").WithDataVolume("pg-data");

Emitted:

  • templates/pg/pg-data-pvc.yaml — valid PVC (kind: PersistentVolumeClaim), no empty dataSource: {} / selector: {} blocks. No bare PV emitted.
  • Workload correctly promoted to StatefulSet.

Scenario 4 — Duplicate WithPersistentVolume on same workload

Objective: Calling .WithPersistentVolume(pv) twice on the same workload should not produce duplicate volume mounts or duplicate PVCs.
Coverage: Unhappy path (misuse).
Status: ✅ Passed (silent dedupe).

Behavior observed: aspire publish succeeds, exactly one PVC file (pg-data.yaml) is emitted, and the StatefulSet has exactly one volumeMounts entry and one volumes entry for the PVC. No warning is emitted, but the output is safe.

Note: Silent dedupe is acceptable given the annotation-based binding model, but a diagnostic could be helpful to catch user typos. Not required for merge.


Scenario 5 — Cross-environment binding

Objective: Bind a PV from environment A to a workload assigned to environment B.
Coverage: Unhappy path (misuse across compute environments).
Status: ⚠️ Silently produces broken output — worth follow-up.

AppHost:

var envA = builder.AddKubernetesEnvironment("envA");
var envB = builder.AddKubernetesEnvironment("envB");
var pgData = envA.AddPersistentVolume("pg-data").WithCapacity("5Gi");
builder.AddPostgres("pg")
    .WithComputeEnvironment(envB)
    .WithDataVolume("pg-data")
    .WithPersistentVolume(pgData);

Behavior observed: aspire publish succeeds with no warning. Two charts are emitted:

  • envA/templates/pg-data/pg-data.yaml — the PVC
  • envB/templates/pg/statefulset.yaml — the workload, referencing claimName: "pg-data" — but no PVC of that name exists in envB's chart.

If the two charts are deployed to different namespaces/clusters (the normal reason to have two envs), the pg StatefulSet will fail to schedule with persistentvolumeclaim "pg-data" not found.

Recommendation: Add a validation step in the KubernetesPersistentVolumeBindingAnnotation processing (or during prepare-deployment-targets-*) that fails with a clear message when a workload binds a PV owned by a different environment. This is a latent gap the new first-class API opens up — it's not strictly a regression from this PR, but the PR is what makes AddPersistentVolume a first-class shared resource that can be referenced from anywhere in the model. Not merge-blocking, but should be tracked as a follow-up issue.


Summary

Scenario Status Notes
1. Name-match binding + StatefulSet promotion ✅ Passed All invariants hold, no empty YAML blocks
2. Project mount-path overload ✅ Passed #9430 fixed
3. Fallthrough with DefaultStorageType = "pvc" ✅ Passed #14096 fixed, valid PVC emitted
4. Duplicate binding ✅ Passed Safe silent dedupe
5. Cross-environment binding ⚠️ Silent broken output Latent gap, not merge-blocking, follow-up recommended

Overall Result

✅ PR VERIFIED for its stated scope.

The three named issues (#9430 project mount-path, #14096 PVC fallthrough, and the serializer emit-empty-blocks bug) are all fixed by this PR against a real emitted Helm chart. StatefulSet auto-promotion works cleanly for both container and project workloads, PVC names match consistently, and no invalid empty spec blocks appear anywhere in the emitted YAML.

Follow-up recommendation (not blocking)

  • Add publish-time validation that a KubernetesPersistentVolumeBindingAnnotation refers to a PV owned by the same KubernetesEnvironmentResource as the bound workload. Today this misuse silently produces YAML that won't apply.

A workload assigned to Kubernetes environment B cannot bind a persistent
volume declared on environment A: the two charts render into separate
namespaces/clusters, so the workload's claimName reference would resolve
to a PVC that does not exist alongside it and Kubernetes would fail to
schedule the pod with 'persistentvolumeclaim not found'.

Validate at publish time in the per-env compute-resource loop, where
both the resolved workload compute environment and the annotated PV are
known, and throw with a clear message pointing at the offending
resource, both environments, and the fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 26/26 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment thread tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs Outdated
Comment thread tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPersistentVolumeTests.cs Outdated
@github-actions

This comment has been minimized.

@mitchdenny

Copy link
Copy Markdown
Member Author

PR #16929 Testing Report (follow-up: cross-env validation)

PR Information

Artifact Version Verification

  • Expected Commit: b8c21ba6
  • Installed CLI Version: 13.5.0-pr.16929.gb8c21ba6
  • Status: ✅ Verified — CLI version's git-hash suffix matches PR head SHA

What Changed Since the Previous Test Run (8a4ce42e)

The previous full 5-scenario run against 8a4ce42e identified a silent-broken-output bug in Scenario 5: binding a workload in one environment to a PV declared on another environment produced a chart that referenced a non-existent PVC (the PV landed in envA's chart, the workload in envB's chart, and neither chart contained the corresponding PVC + workload pair).

Commit b8c21ba6 closes that gap by adding publish-time validation in
KubernetesEnvironmentResource.PrepareDeploymentTargetsAsync that throws
InvalidOperationException when a workload's
KubernetesPersistentVolumeBindingAnnotation refers to a PV whose
Parent is a different KubernetesEnvironmentResource. Also added unit test
PublishAsync_WithFirstClassPersistentVolume_ThrowsWhenBoundAcrossEnvironments.

All 5 scenarios were re-run end-to-end against the dogfood CLI to confirm:

  • Scenarios 1–4 still emit correct output (no regression to the emission path).
  • Scenario 5 now fails fast with a clear, actionable error instead of silently producing broken YAML.

Test Scenarios Executed

Scenario 1: Name-match binding + StatefulSet promotion

Objective: WithDataVolume("pg-data") + WithPersistentVolume(pgData) produces one PVC file, pod-spec volume is a persistentVolumeClaim (not emptyDir), and workload is promoted from Deployment to StatefulSet.
Coverage Type: Happy path
Status: ✅ Passed

Evidence ($out/templates):

  • pg-data/pg-data.yamlkind: PersistentVolumeClaim, name: pg-data, resources.requests.storage: 5Gi
  • pg/statefulset.yamlkind: StatefulSet, volumes: [{ name: pg-data, persistentVolumeClaim: { claimName: pg-data } }]

Scenario 2: Mount-path overload (WithPersistentVolume(vol, mountPath))

Objective: The mount-path overload works for compute resources that don't already declare a named volume (e.g. ProjectResource / plain container). Confirms fix for #9430.
Coverage Type: Happy path
Status: ✅ Passed

Evidence:

  • media/media.yamlkind: PersistentVolumeClaim, name: media
  • api/statefulset.yamlvolumeMounts: [{ name: media, mountPath: /srv/media }] and volumes: [{ name: media, persistentVolumeClaim: { claimName: media } }]

Scenario 3: Fallthrough with DefaultStorageType = "pvc"

Objective: When no first-class PV is bound, the environment's DefaultStorageType = "pvc" still emits a per-workload PVC and wires the pod to it. Confirms fix for #14096.
Coverage Type: Happy path (default-storage fallthrough)
Status: ✅ Passed

Evidence:

  • pg/pg-data-pvc.yamlkind: PersistentVolumeClaim, name: pg-pg-data-pvc
  • pg/statefulset.yamlvolumes: [{ name: pg-data, persistentVolumeClaim: { claimName: pg-pg-data-pvc } }]
  • No orphan first-class PV in the chart (correctly falls through to per-workload PVC).

Scenario 4: Duplicate WithPersistentVolume(pgData) binding

Objective: Calling WithPersistentVolume(pgData) twice does not produce duplicate PVC files or duplicated pod-spec volume entries.
Coverage Type: Unhappy path / boundary
Status: ✅ Passed

Evidence:

  • Exactly one PVC file (pg-data/pg-data.yaml).
  • Pod spec has exactly one volumes entry for pg-data with the correct claimName.

Scenario 5: Cross-environment binding (the new fix)

Objective: Binding a workload in envB to a PV declared on envA must fail fast at publish time with a clear error, instead of silently producing a chart that references a non-existent PVC.
Coverage Type: Unhappy path (validation)
Status: ✅ Passed (fix works end-to-end)

AppHost:

var envA = builder.AddKubernetesEnvironment("envA");
var envB = builder.AddKubernetesEnvironment("envB");
var pgData = envA.AddPersistentVolume("pg-data").WithCapacity("5Gi");
builder.AddPostgres("pg")
    .WithComputeEnvironment(envB)
    .WithDataVolume("pg-data")
    .WithPersistentVolume(pgData);

Result: aspire publish fails with a wrapped exception. The relevant inner exception:

System.InvalidOperationException: Resource 'pg' is assigned to Kubernetes environment 'envB' but binds persistent volume 'pg-data' which belongs to environment 'envA'. A workload can only bind persistent volumes declared on its own environment. Move the AddPersistentVolume call to 'envB', or assign the workload to 'envA' with WithComputeEnvironment.

Stack trace confirms the throw originates in KubernetesEnvironmentResource.PrepareDeploymentTargetsAsync (src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs:474), as designed. The failing pipeline step is prepare-deployment-targets-envB — the throw fires exactly once from the environment actually assigned to the workload.

The error message names:

  • The workload resource (pg)
  • The wrongly-referenced PV (pg-data)
  • Both environments (envA, envB)
  • Both valid fixes (move AddPersistentVolume to envB, or reassign workload to envA)

Summary

Scenario Status Notes
S1: Name-match + StatefulSet promotion ✅ Passed Correct PVC + claimName
S2: Mount-path overload (#9430) ✅ Passed Works for ProjectResource-style compute
S3: Fallthrough with DefaultStorageType=pvc (#14096) ✅ Passed Emits per-workload PVC
S4: Duplicate binding ✅ Passed Safe silent dedupe
S5: Cross-env binding (new validation) ✅ Passed Throws InvalidOperationException with clear message

Overall Result

✅ PR VERIFIED — all 5 scenarios pass on b8c21ba6, including the new cross-environment validation.

Recommendations

  • No blocking issues found.
  • The error message in S5 is developer-friendly and self-explanatory.
  • Ready for merge once required CI checks complete.

- KubernetesPublisherTests.cs: replace bare Assert.DoesNotContain("{}") with
  targeted AssertNoBuggyEmptyMappings helper. The bare check would false-positive
  on valid Kubernetes YAML shorthand (emptyDir: {}, resources: {}).
- KubernetesPersistentVolumeExtensions.cs: XML doc example now calls
  WithDataVolume("pg-data") explicitly. The parameterless form auto-generates
  a name via VolumeNameGenerator.Generate, which would not match a PV named
  "pg-data", so the documented name-match binding would not fire.
- KubernetesDeployWithPersistentVolumeTests.cs: rewrite the DNS_LABEL comment
  to explain the constraint (and cite the Kubernetes docs) instead of implying
  a tracking issue exists.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@davidfowl

Copy link
Copy Markdown
Contributor

TS code sample?

@mitchdenny

Copy link
Copy Markdown
Member Author

@davidfowl good call — added a TypeScript example to the PR description right after the C# one, showing the same shape via the auto-generated SDK bindings (addPersistentVolume, withStorageClass, withCapacity, withKubernetesPersistentVolumeMount). The full end-to-end TS scenario (with aspire add + aspire restore + aspire deploy to KinD) is covered by KubernetesDeployTypeScriptWithPersistentVolumeTests.

Covers WithStorageClass(ParameterResource), WithCapacity(ParameterResource),
and WithVolumeAnnotation(key, ParameterResource) — the overloads that go
through the Helm placeholder / value-capture path. Verifies each parameter
renders as a per-PV-scoped Helm template reference in the PVC manifest
({{ .Values.parameters.<pv-name>.<param> }}) and lands in values.yaml so
consumers can supply the value via --set / values overrides.

The literal-value overloads never exercise the placeholder path, so a
regression there could silently break generated PVC YAML or values.yaml
without any other existing test failing.

Addresses PR review feedback from @davidfowl.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 26/26 changed files
  • Comments generated: 1
  • Review effort level: Medium

The mount-path overload of WithPersistentVolume accepted an isReadOnly
parameter but silently dropped it: WithContainerVolumes constructed a
new VolumeMountV1 with only Name/MountPath (ignoring the source
KubernetesResource.Volumes[i].ReadOnly), and the bound-volume branch
in WithPodSpecVolumes set PersistentVolumeClaim without ReadOnly. Net
result: isReadOnly: true never reached the rendered manifest, so the
XML doc promise ("When true, mounts the volume read-only") was unfulfilled.

Now propagate ReadOnly on both emission paths, gated on == true so
existing manifests that never set the flag don't gain a redundant
readOnly: false and churn every snapshot. Cover both the container
volumeMount side and the pod-volume-source side (belt-and-braces:
if a co-scheduled container in the same pod forgets its mount flag,
the PVC source still forces read-only).

Adds PublishAsync_WithFirstClassPersistentVolume_ReadOnlyFlag_PropagatesToVolumeMountAndPodVolume
which YAML-parses the emitted StatefulSet and asserts the exact fields
(not substring matches, so an unrelated readOnly: on a securityContext
can't satisfy the check).

Addresses PR review feedback from copilot-pull-request-reviewer.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Tests selector (audit mode)

The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement.

4 / 99 test projects · 4 jobs, from 26 changed files.

Selected test projects (4 / 99)

Aspire.Cli.EndToEnd.Tests, Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Docker.Tests, Aspire.Hosting.Kubernetes.Tests

Selected jobs (4)

cli-starter, deployment-e2e, extension-e2e, typescript-api-compat


How these were chosen — grouped by what changed

🔧 src/Aspire.Hosting.Kubernetes/Annotations/KubernetesPersistentVolumeBindingAnnotation.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests
2 via the project graph: Aspire.Hosting.Azure.Kubernetes.Tests (2 hops), Aspire.Hosting.Docker.Tests

🔧 src/Aspire.Hosting.Kubernetes/Extensions/HelmExtensions.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Extensions/ResourceExtensions.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeExtensions.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeResource.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/KubernetesResource.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeClaimSpecV1.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeSpecV1.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Resources/StatefulSetSpecV1.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Resources/StatefulSetUpdateStrategyV1.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🔧 src/Aspire.Hosting.Kubernetes/Resources/VolumeNodeAffinityV1.cs (changed source)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptWithPersistentVolumeTests.cs (changed test)
1 directly: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPersistentVolumeTests.cs (changed test)
1 directly: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithProjectPersistentVolumeTests.cs (changed test)
1 directly: Aspire.Cli.EndToEnd.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_BindsByName_PromotesToStatefulSet#00.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_BindsByName_PromotesToStatefulSet#01.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_FallsThroughForUnboundVolumes#00.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_FallsThroughForUnboundVolumes#01.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_OnProject_BindsViaMountPathOverload#00.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_OnProject_BindsViaMountPathOverload#01.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithHostPathStorageType_EmitsHostPathVolumeOnPodSpec.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithPvcStorageType_GeneratesValidPersistentVolumeClaim#00.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithPvcStorageType_GeneratesValidPersistentVolumeClaim#01.verified.yaml (changed test)
1 directly: Aspire.Hosting.Kubernetes.Tests

Job reasons

Job Triggered by
cli-starter selected test Aspire.Cli.EndToEnd.Tests
deployment-e2e affected project Aspire.Hosting.Azure.Kubernetes
extension-e2e src/Aspire.Hosting.Kubernetes/Annotations/KubernetesPersistentVolumeBindingAnnotation.cs, src/Aspire.Hosting.Kubernetes/Extensions/HelmExtensions.cs, src/Aspire.Hosting.Kubernetes/Extensions/ResourceExtensions.cs, src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs, src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeExtensions.cs, src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeResource.cs, src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs, src/Aspire.Hosting.Kubernetes/KubernetesResource.cs, src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeClaimSpecV1.cs, src/Aspire.Hosting.Kubernetes/Resources/PersistentVolumeSpecV1.cs, src/Aspire.Hosting.Kubernetes/Resources/StatefulSetSpecV1.cs, src/Aspire.Hosting.Kubernetes/Resources/StatefulSetUpdateStrategyV1.cs, src/Aspire.Hosting.Kubernetes/Resources/VolumeNodeAffinityV1.cs, tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptWithPersistentVolumeTests.cs, tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPersistentVolumeTests.cs, tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithProjectPersistentVolumeTests.cs
• affected project Aspire.Hosting.Kubernetes
typescript-api-compat affected project Aspire.Hosting.Kubernetes

Selection computed for commit 7a3e9e4.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

⚠️ Documentation was required for this change, but a docs PR could not be drafted automatically.

Documentation was authored and committed locally (docs/kubernetes-persistent-volumes branch, commit ba192e7) but the create_pull_request safe-output tool failed twice with "Pinned SHA failed to generate patch: Git command failed with status 1", likely due to the shallow clone (fetch-depth=1) preventing the tool from computing a diff against the remote base branch.

Triggered signals: experimental_attribute_added (new [Experimental("ASPIRECOMPUTE002")] on KubernetesPersistentVolumeExtensions and KubernetesPersistentVolumeResource), new_public_type (KubernetesPersistentVolumeExtensions, KubernetesPersistentVolumeResource).

Documentation written but not pushed:

  • New page: src/frontend/src/content/docs/deployment/kubernetes-persistent-volumes.mdx — full AddPersistentVolume / WithStorageClass / WithCapacity / WithAccessMode / WithVolumeAnnotation / WithPersistentVolume reference with C# and TypeScript examples
  • Updated: src/frontend/src/content/docs/integrations/compute/kubernetes.mdx — new "Persistent volumes" section
  • Updated: src/frontend/src/content/docs/deployment/kubernetes.mdx — updated resource-mapping table and "Continue learning" cards
  • Updated: src/frontend/config/sidebar/deployment.topics.ts — new "Persistent volumes" sidebar entry

See the workflow run for details: https://github.com/microsoft/aspire/actions/runs/28758907542

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants