microsoft/aspire · error · InvalidOperationException

ClusterIssuer ' ' has no spec. Configure it with…

Error message

ClusterIssuer '{issuer.Name}' has no spec. Configure it with WithLetsEncryptProduction(), WithLetsEncryptStaging(), or WithAcmeServer().

What it means

During publishing, ApplyClusterIssuerAsync renders the ClusterIssuer manifest. If issuer.Spec is null — no ACME configuration was ever applied — there is nothing to serialize, so the publisher throws InvalidOperationException telling you which configuration methods to call.

Solutions

  1. Chain WithLetsEncryptProduction() on the ClusterIssuer builder for production Let's Encrypt.
  2. Use WithLetsEncryptStaging() while testing to avoid ACME rate limits.
  3. Use WithAcmeServer() for a custom ACME directory endpoint.
  4. Remove the issuer from the model if it is intentionally unconfigured.

Example fix

// before
var issuer = certManager.AddClusterIssuer("letsencrypt");

// after
var issuer = certManager.AddClusterIssuer("letsencrypt")
    .WithLetsEncryptProduction("admin@example.com");
Defensive patterns

Strategy: validation

Validate before calling

if (issuer.Spec is null) issuerBuilder.WithLetsEncryptProduction(email); // configure before publish

Prevention

When it happens

Trigger: Publishing a model where AddClusterIssuer was called but neither WithLetsEncryptProduction(), WithLetsEncryptStaging(), nor WithAcmeServer() was invoked on the resulting issuer.

Common situations: Creating a ClusterIssuer placeholder and deferring configuration, or calling AddClusterIssuer without chaining any configuration extension.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/913cbc4445cdd1c2. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs:426

            // not found) or fails. Deleting while cert-manager is still alive also lets the
            // controller clean up the ACME account secret it created in the cert-manager
            // namespace.
            step.RequiredBy($"helm-uninstall-{chartName}");
            step.RequiredBy(WellKnownPipelineSteps.Destroy);
            steps.Add(step);
        }

        return Task.FromResult<IEnumerable<PipelineStep>>(steps);
    }

    private static async Task ApplyClusterIssuerAsync(
        PipelineStepContext context,
        CertManagerResource certManager,
        CertManagerIssuerResource issuer)
    {
        if (issuer.Spec is null)
        {
            throw new InvalidOperationException(
                $"ClusterIssuer '{issuer.Name}' has no spec. Configure it with WithLetsEncryptProduction(), " +
                "WithLetsEncryptStaging(), or WithAcmeServer().");
        }

        if (issuer.Solvers.Count == 0)
        {
            throw new InvalidOperationException(
                $"ClusterIssuer '{issuer.Name}' has no solvers configured. Add at least one " +
                "solver via WithHttp01Solver().");
        }

        var environment = certManager.Parent;

        var manifest = await BuildClusterIssuerManifestAsync(context.Model, certManager, issuer, context.Logger, context.CancellationToken)
            .ConfigureAwait(false);

        context.Logger.LogInformation(
            "Applying cert-manager ClusterIssuer '{IssuerName}'.", issuer.Name);

View on GitHub (pinned to 25830f84bd)