diff --git a/KubeOps.slnx b/KubeOps.slnx index 91dd83b7c..73939c5a5 100644 --- a/KubeOps.slnx +++ b/KubeOps.slnx @@ -1,5 +1,6 @@  + @@ -29,6 +30,8 @@ + + @@ -41,6 +44,8 @@ + + diff --git a/README.md b/README.md index 5474e0a80..d93916456 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ The SDK is designed to be modular. You can include only the packages you need: | Package | Description | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [KubeOps.Abstractions](./src/KubeOps.Abstractions/README.md) | Defines core interfaces, attributes (like `[KubernetesEntity]`), and base classes used across the SDK. Essential for defining your custom resources and controllers. | +| [KubeOps.Aspire](./src/KubeOps.Aspire/README.md) | [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) service defaults for an operator: a single `AddKubeOpsServiceDefaults()` call wiring up OpenTelemetry, service discovery, HTTP resilience, and health checks. | +| [KubeOps.Aspire.Hosting](./src/KubeOps.Aspire.Hosting/README.md) | [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) hosting integration. Adds `AddKubeOps(...)` so a KubeOps operator can be orchestrated as a resource inside an Aspire AppHost. | | [KubeOps.Cli](./src/KubeOps.Cli/README.md) | A [.NET Tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) providing commands for scaffolding projects, generating [Custom Resource Definitions (CRDs)](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/), and more. | | [KubeOps.Generator](./src/KubeOps.Generator/README.md) | Contains [Roslyn Source Generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) to automate boilerplate code generation for CRDs and controllers based on your definitions. | | [KubeOps.KubernetesClient](./src/KubeOps.KubernetesClient/README.md) | Provides an enhanced client for interacting with the [Kubernetes API](https://kubernetes.io/docs/reference/kubernetes-api/), built on top of the official `KubernetesClient` library. Offers convenience methods for common operator tasks. | diff --git a/docs/docs/operator/aspire.mdx b/docs/docs/operator/aspire.mdx new file mode 100644 index 000000000..300af30e1 --- /dev/null +++ b/docs/docs/operator/aspire.mdx @@ -0,0 +1,122 @@ +--- +title: .NET Aspire +description: Orchestrate and observe a KubeOps operator with .NET Aspire +sidebar_position: 9.5 +--- + +# .NET Aspire Integration + +[.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) is an opinionated stack for building observable, production-ready distributed applications. KubeOps ships two packages that let you treat an operator as a first-class Aspire resource: + +| Package | Side | Purpose | +| --- | --- | --- | +| [`KubeOps.Aspire.Hosting`](../packages/aspire-hosting) | AppHost | Adds `AddKubeOps(...)` so the operator is orchestrated as a resource. | +| [`KubeOps.Aspire`](../packages/aspire) | Operator | Adds `AddKubeOpsServiceDefaults()` for OpenTelemetry, service discovery, resilience and health checks. | + +Together they give you a local dashboard with the operator's logs, traces and metrics, automatic service discovery to other resources, and a single entry point to run the whole stack. + +## The two halves of an Aspire integration + +Aspire integrations always come in two parts, and KubeOps follows that convention: + +1. **Hosting integration** — code that runs in the *AppHost* project and describes the application model. +2. **Service defaults** — a small amount of wiring inside the *operator* project itself. + +The service-defaults call is the one piece you add to the operator. This is the idiomatic Aspire pattern (`AddServiceDefaults()`); the hosting side then injects the configuration (telemetry endpoint, service discovery variables) automatically. + +## Operator project + +Install `KubeOps.Aspire` and call `AddKubeOpsServiceDefaults()` on the host builder, **after** `AddKubernetesOperator()`: + +```csharp +using KubeOps.Aspire; +using KubeOps.Operator; + +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services + .AddKubernetesOperator() + .RegisterComponents(); + +builder.AddKubeOpsServiceDefaults(); + +using var host = builder.Build(); +await host.RunAsync(); +``` + +`AddKubeOpsServiceDefaults()` configures: + +- **OpenTelemetry** logging (with scopes and formatted messages), metrics (runtime + `HttpClient`) and tracing. It subscribes to the operator's `ActivitySource`, which KubeOps registers under the operator name (see [Logging, Tracing, and OpenTelemetry](./logging)). +- **OTLP export** — enabled automatically when the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable is present. Aspire sets this for you, so traces and metrics show up in the dashboard with no extra code. +- **Service discovery** — `HttpClient` instances resolve logical Aspire resource names (e.g. `https://apiservice`). +- **HTTP resilience** — the standard resilience handler (retries, circuit breaker, timeouts) is applied to all `HttpClient` instances. +- **Health checks** — a default `self` liveness check tagged `live`. + +:::tip Operator name +The OpenTelemetry service name and the tracing source name must match `OperatorSettings.Name` — otherwise the operator's reconciliation traces are never captured. Call `AddKubeOpsServiceDefaults()` **after** `AddKubernetesOperator()` so KubeOps can resolve the configured name automatically. If you must call it earlier, pass the name explicitly (and keep it in sync with `OperatorSettings.Name`): + +```csharp +builder.AddKubeOpsServiceDefaults("my-operator"); +``` +::: + +## AppHost project + +Create an [Aspire AppHost](https://learn.microsoft.com/dotnet/aspire/fundamentals/app-host-overview) project, reference `KubeOps.Aspire.Hosting`, and add the operator with `AddKubeOps`: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var apiService = builder.AddProject("apiservice"); + +builder.AddKubeOps("operator") + .WithReference(apiService); + +builder.Build().Run(); +``` + +`AddKubeOps` is a thin, KubeOps-flavoured wrapper around the built-in `AddProject`. It returns the standard `IResourceBuilder`, so every Aspire extension works as usual: + +- `WithReference(apiService)` injects the service-discovery configuration so the operator can call `apiservice` by name. +- `WithEnvironment(...)`, `WaitFor(...)`, `WithReplicas(...)` and friends all apply unchanged. + +:::note Project reference +When referencing `KubeOps.Aspire.Hosting` from an AppHost, mark it as a normal code reference so the AppHost SDK does not treat it as a resource: + +```xml + +``` +::: + +## Running locally + +Run the AppHost; the Aspire dashboard opens and starts the operator alongside any other resources: + +```bash +dotnet run --project examples/AspireAppHost +``` + +The operator connects to your current cluster using the standard Kubernetes configuration resolution (`KUBECONFIG`, in-cluster config). The dashboard shows structured logs, the reconciliation traces emitted from the operator's `ActivitySource`, and runtime/HTTP metrics. + +## Health endpoints + +`KubeOps.Aspire` is usable from a plain console operator and therefore does **not** force an ASP.NET Core dependency. The default health checks are registered in the service collection, but exposing them over HTTP requires an HTTP server. + +If your operator already hosts webhooks via [`KubeOps.Operator.Web`](../packages/operator-web), map the endpoints on the `WebApplication`: + +```csharp +app.MapHealthChecks("/health"); +app.MapHealthChecks("/alive", new HealthCheckOptions +{ + Predicate = registration => registration.Tags.Contains("live"), +}); +``` + +These line up with the standard Kubernetes liveness/readiness probe conventions and the Aspire dashboard's health reporting. + +## Deployment + +Because the operator is an ordinary project resource in the Aspire application model, you can publish the whole stack with the [Aspire CLI](https://learn.microsoft.com/dotnet/aspire/cli/overview) or community tooling such as [Aspir8](https://github.com/prom3theu5/aspirational-manifests) to generate Kubernetes manifests. For hand-rolled manifests and RBAC, see [Deployment](./deployment) and [RBAC](./rbac). diff --git a/docs/docs/packages/aspire-hosting.mdx b/docs/docs/packages/aspire-hosting.mdx new file mode 100644 index 000000000..6a4c28769 --- /dev/null +++ b/docs/docs/packages/aspire-hosting.mdx @@ -0,0 +1,17 @@ +--- +title: KubeOps.Aspire.Hosting +description: .NET Aspire hosting integration for KubeOps operators +sidebar_position: 4.4 +hide_title: true +--- + +import AspireHosting from "../../../src/KubeOps.Aspire.Hosting/README.md"; + + + +## Related Packages + +- [KubeOps.Aspire](./aspire) - Service defaults for the operator project +- [KubeOps.Operator](./operator) - Main operator engine + +See the [.NET Aspire guide](../operator/aspire) for the full walkthrough. diff --git a/docs/docs/packages/aspire.mdx b/docs/docs/packages/aspire.mdx new file mode 100644 index 000000000..84ad74a66 --- /dev/null +++ b/docs/docs/packages/aspire.mdx @@ -0,0 +1,17 @@ +--- +title: KubeOps.Aspire +description: .NET Aspire service defaults for KubeOps operators +sidebar_position: 4.3 +hide_title: true +--- + +import Aspire from "../../../src/KubeOps.Aspire/README.md"; + + + +## Related Packages + +- [KubeOps.Aspire.Hosting](./aspire-hosting) - AppHost integration for orchestrating the operator +- [KubeOps.Operator](./operator) - Main operator engine + +See the [.NET Aspire guide](../operator/aspire) for the full walkthrough. diff --git a/docs/docs/packages/index.mdx b/docs/docs/packages/index.mdx index dd1e20d17..7f52b92e9 100644 --- a/docs/docs/packages/index.mdx +++ b/docs/docs/packages/index.mdx @@ -14,6 +14,11 @@ KubeOps is designed to be modular, allowing you to include only the packages you - [KubeOps.Operator](./operator) - Main operator engine - [KubeOps.Operator.Web](./operator-web) - ASP.NET Core integration +## Hosting & Orchestration + +- [KubeOps.Aspire.Hosting](./aspire-hosting) - .NET Aspire AppHost integration +- [KubeOps.Aspire](./aspire) - .NET Aspire service defaults for the operator + ## Development Tools - [KubeOps.Cli](./cli) - Command-line tools diff --git a/examples/AspireAppHost/AspireAppHost.csproj b/examples/AspireAppHost/AspireAppHost.csproj new file mode 100644 index 000000000..85801acdb --- /dev/null +++ b/examples/AspireAppHost/AspireAppHost.csproj @@ -0,0 +1,26 @@ + + + + + + Exe + net9.0 + enable + enable + true + false + false + kubeops-aspire-apphost + + + + + + + + + + + + diff --git a/examples/AspireAppHost/Program.cs b/examples/AspireAppHost/Program.cs new file mode 100644 index 000000000..9c5e442c1 --- /dev/null +++ b/examples/AspireAppHost/Program.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddKubeOps("operator"); + +builder.Build().Run(); diff --git a/examples/Operator/Program.cs b/examples/Operator/Program.cs index 17b2a7699..8615a6a55 100644 --- a/examples/Operator/Program.cs +++ b/examples/Operator/Program.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +#if DEBUG using KubeOps.Abstractions.Crds; +#endif using KubeOps.Operator; using Microsoft.Extensions.Hosting; diff --git a/src/KubeOps.Aspire.Hosting/KubeOps.Aspire.Hosting.csproj b/src/KubeOps.Aspire.Hosting/KubeOps.Aspire.Hosting.csproj new file mode 100644 index 000000000..beef4fce8 --- /dev/null +++ b/src/KubeOps.Aspire.Hosting/KubeOps.Aspire.Hosting.csproj @@ -0,0 +1,22 @@ + + + + net8.0;net9.0;net10.0 + + + + KubeOps.Aspire.Hosting + Kubernetes Operator SDK Aspire Hosting AppHost + + .NET Aspire hosting integration for KubeOps operators. Adds an + AddKubeOps extension to the distributed application builder so a KubeOps + operator project can be orchestrated as a resource in a .NET Aspire + AppHost, wired up with references, environment variables and telemetry. + + + + + + + + diff --git a/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs b/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs new file mode 100644 index 000000000..c3dde11c4 --- /dev/null +++ b/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Extension methods for adding KubeOps operator projects to a +/// . +/// +public static class KubeOpsHostingExtensions +{ + /// + /// Adds a KubeOps operator project to the distributed application model. + /// + /// + /// This is a KubeOps-flavoured wrapper around + /// . + /// It returns the standard builder, so all the usual + /// Aspire extensions (WithReference, WithEnvironment, WaitFor, ...) + /// apply unchanged. Pair it with AddKubeOpsServiceDefaults() from the + /// KubeOps.Aspire package in the operator project to enable telemetry and + /// service discovery. + /// + /// + /// The operator project metadata type generated by the Aspire AppHost SDK + /// (e.g. Projects.MyOperator). + /// + /// The distributed application builder. + /// The resource name of the operator. + /// An for further configuration. + public static IResourceBuilder AddKubeOps( + this IDistributedApplicationBuilder builder, + [ResourceName] string name) + where TProject : IProjectMetadata, new() + => builder.AddProject(name); +} diff --git a/src/KubeOps.Aspire.Hosting/README.md b/src/KubeOps.Aspire.Hosting/README.md new file mode 100644 index 000000000..9de7a794e --- /dev/null +++ b/src/KubeOps.Aspire.Hosting/README.md @@ -0,0 +1,22 @@ +# KubeOps.Aspire.Hosting + +`KubeOps.Aspire.Hosting` is the [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) **hosting** integration for KubeOps operators. It lets you orchestrate a KubeOps operator project as a resource inside a .NET Aspire AppHost. + +It is the AppHost-side counterpart to the [`KubeOps.Aspire`](https://www.nuget.org/packages/KubeOps.Aspire) service-defaults integration. + +## Usage + +In your Aspire AppHost project: + +```csharp +var apiService = builder.AddProject("apiservice"); + +builder.AddKubeOps("operator") + .WithReference(apiService); +``` + +`AddKubeOps` is a thin, KubeOps-flavoured wrapper around the built-in `AddProject` and returns the standard `IResourceBuilder`, so every Aspire extension (`WithReference`, `WithEnvironment`, `WaitFor`, ...) works as usual. + +To complete the loop, add `AddKubeOpsServiceDefaults()` from the `KubeOps.Aspire` package in the operator project. This enables OpenTelemetry export to the Aspire dashboard and service discovery for the references wired up above. + +See the [.NET Aspire guide](https://dotnet.github.io/dotnet-operator-sdk/docs/operator/aspire) for the full picture. diff --git a/src/KubeOps.Aspire/KubeOps.Aspire.csproj b/src/KubeOps.Aspire/KubeOps.Aspire.csproj new file mode 100644 index 000000000..454afd7ab --- /dev/null +++ b/src/KubeOps.Aspire/KubeOps.Aspire.csproj @@ -0,0 +1,34 @@ + + + + net8.0;net9.0;net10.0 + + + + KubeOps.Aspire + Kubernetes Operator SDK Aspire OpenTelemetry ServiceDiscovery + + .NET Aspire service defaults for KubeOps operators. Provides a single + AddKubeOpsServiceDefaults extension that wires up OpenTelemetry (logging, + metrics and tracing with OTLP export), service discovery, HTTP resilience + and default health checks so an operator integrates cleanly with a + .NET Aspire AppHost. + + + + + + + + + + + + + + + + + + + diff --git a/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs b/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs new file mode 100644 index 000000000..d69d6739f --- /dev/null +++ b/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using KubeOps.Abstractions.Builder; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace KubeOps.Aspire; + +/// +/// Adds .NET Aspire "service defaults" to a KubeOps operator: OpenTelemetry +/// (logging, metrics and tracing), OTLP export, service discovery, HTTP +/// resilience and default health checks. +/// +public static class KubeOpsServiceDefaultsExtensions +{ + /// + /// Wires up the standard Aspire service defaults for a KubeOps operator. + /// Call this once on the host builder, after AddKubernetesOperator(), + /// so the configured can be resolved and + /// the tracing source matches the operator's . + /// If you call it earlier, pass explicitly with + /// the same value as . + /// + /// The host application builder type. + /// The host application builder. + /// + /// Optional name used as the OpenTelemetry service and tracing + /// name. When null, the name is + /// taken from the registered (if + /// AddKubernetesOperator() ran first) and otherwise falls back to + /// . + /// + /// The same for chaining. + public static TBuilder AddKubeOpsServiceDefaults(this TBuilder builder, string? operatorName = null) + where TBuilder : IHostApplicationBuilder + { + var serviceName = ResolveOperatorName(builder, operatorName); + + builder.ConfigureKubeOpsOpenTelemetry(serviceName); + + builder.Services.AddHealthChecks() + + // Liveness check: the operator process is up and the host has started. + .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default, so calls to referenced Aspire services are retried. + http.AddStandardResilienceHandler(); + + // Resolve logical Aspire service names (e.g. "https+http://apiservice") via service discovery. + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// Configures OpenTelemetry logging, metrics and tracing for the operator. + /// When the OTEL_EXPORTER_OTLP_ENDPOINT environment variable (or + /// configuration key) is present, the OTLP exporter is enabled for all signals. + /// + /// The host application builder type. + /// The host application builder. + /// + /// The OpenTelemetry service name and the tracing source name to subscribe to. + /// This must match the operator name (), since + /// KubeOps registers its under that name. + /// + /// The same for chaining. + public static TBuilder ConfigureKubeOpsOpenTelemetry(this TBuilder builder, string serviceName) + where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource.AddService(serviceName)) + .WithMetrics(metrics => metrics + .AddRuntimeInstrumentation() + .AddHttpClientInstrumentation()) + .WithTracing(tracing => tracing + .AddHttpClientInstrumentation() + + // KubeOps registers an ActivitySource named after the operator (OperatorSettings.Name). + .AddSource(serviceName)); + + if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"])) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + private static string ResolveOperatorName(IHostApplicationBuilder builder, string? operatorName) + { + if (!string.IsNullOrWhiteSpace(operatorName)) + { + return operatorName; + } + + var settings = builder.Services + .FirstOrDefault(descriptor => descriptor.ServiceType == typeof(OperatorSettings))? + .ImplementationInstance as OperatorSettings; + + return settings?.Name + ?? builder.Configuration["KubeOps:OperatorName"] + ?? builder.Environment.ApplicationName; + } +} diff --git a/src/KubeOps.Aspire/README.md b/src/KubeOps.Aspire/README.md new file mode 100644 index 000000000..2bcf28946 --- /dev/null +++ b/src/KubeOps.Aspire/README.md @@ -0,0 +1,36 @@ +# KubeOps.Aspire + +`KubeOps.Aspire` is the [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) **service defaults** integration for KubeOps operators. It is the operator-side counterpart to the [`KubeOps.Aspire.Hosting`](https://www.nuget.org/packages/KubeOps.Aspire.Hosting) AppHost integration. + +A single call wires up the cross-cutting concerns that Aspire expects from a well-behaved resource: + +- **OpenTelemetry** — logging, metrics and tracing, including the operator's `ActivitySource`. +- **OTLP export** — enabled automatically when `OTEL_EXPORTER_OTLP_ENDPOINT` is set (Aspire injects this). +- **Service discovery** — so the operator can call other Aspire resources by their logical name. +- **HTTP resilience** — a standard resilience handler on all `HttpClient` instances. +- **Health checks** — a default `self` liveness check. + +## Usage + +```csharp +using KubeOps.Aspire; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services + .AddKubernetesOperator() + .RegisterComponents(); + +builder.AddKubeOpsServiceDefaults(); + +using var host = builder.Build(); +await host.RunAsync(); +``` + +Call `AddKubeOpsServiceDefaults()` **after** `AddKubernetesOperator()` so the OpenTelemetry service and tracing source names match `OperatorSettings.Name` and the operator's reconciliation traces are captured. If you must call it earlier, pass the name explicitly (and keep it in sync with `OperatorSettings.Name`): + +```csharp +builder.AddKubeOpsServiceDefaults("my-operator"); +``` + +See the [.NET Aspire guide](https://dotnet.github.io/dotnet-operator-sdk/docs/operator/aspire) for the full picture. diff --git a/test/KubeOps.Aspire.Hosting.Test/KubeOps.Aspire.Hosting.Test.csproj b/test/KubeOps.Aspire.Hosting.Test/KubeOps.Aspire.Hosting.Test.csproj new file mode 100644 index 000000000..c9c2fa21e --- /dev/null +++ b/test/KubeOps.Aspire.Hosting.Test/KubeOps.Aspire.Hosting.Test.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs b/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs new file mode 100644 index 000000000..bbb96318f --- /dev/null +++ b/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; + +using FluentAssertions; + +namespace KubeOps.Aspire.Hosting.Test; + +public class KubeOpsHostingTest +{ + [Fact] + public void AddKubeOps_Adds_Named_Project_Resource() + { + var builder = DistributedApplication.CreateBuilder([]); + + builder.AddKubeOps("operator"); + + builder.Resources.OfType() + .Should().ContainSingle(resource => resource.Name == "operator"); + } + + [Fact] + public void AddKubeOps_Returns_Builder_For_Chaining() + { + var builder = DistributedApplication.CreateBuilder([]); + + var resourceBuilder = builder.AddKubeOps("operator"); + + resourceBuilder.Should().NotBeNull(); + resourceBuilder.Resource.Name.Should().Be("operator"); + } + + private sealed class TestProjectMetadata : IProjectMetadata + { + public string ProjectPath => typeof(TestProjectMetadata).Assembly.Location; + } +} diff --git a/test/KubeOps.Aspire.Test/KubeOps.Aspire.Test.csproj b/test/KubeOps.Aspire.Test/KubeOps.Aspire.Test.csproj new file mode 100644 index 000000000..5aee074fb --- /dev/null +++ b/test/KubeOps.Aspire.Test/KubeOps.Aspire.Test.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs b/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs new file mode 100644 index 000000000..e173bc739 --- /dev/null +++ b/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using FluentAssertions; + +using KubeOps.Aspire; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace KubeOps.Aspire.Test; + +public class KubeOpsServiceDefaultsTest +{ + [Fact] + public void Should_Register_OpenTelemetry_Providers() + { + var builder = Host.CreateApplicationBuilder(); + builder.AddKubeOpsServiceDefaults("test-operator"); + + using var provider = builder.Services.BuildServiceProvider(); + + provider.GetService().Should().NotBeNull(); + provider.GetService().Should().NotBeNull(); + } + + [Fact] + public void Should_Register_Self_Liveness_Health_Check() + { + var builder = Host.CreateApplicationBuilder(); + builder.AddKubeOpsServiceDefaults("test-operator"); + + using var provider = builder.Services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + options.Value.Registrations.Should().Contain(r => r.Name == "self" && r.Tags.Contains("live")); + } + + [Fact] + public void Should_Register_Service_Discovery() + { + var builder = Host.CreateApplicationBuilder(); + builder.AddKubeOpsServiceDefaults("test-operator"); + + builder.Services.Should().Contain(descriptor => + descriptor.ServiceType.Namespace != null && + descriptor.ServiceType.Namespace.StartsWith("Microsoft.Extensions.ServiceDiscovery")); + } +}