Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,73 @@ ghcr.io/appthrust/dns-api:<version>

For local development or source installs, use the chart in [deploy/charts/dns-api](./deploy/charts/dns-api).

### Customer-managed DNS (external provider)

The external provider is opt-in. It verifies customer-managed `A`, `AAAA`, and
`CNAME` records without creating, updating, or deleting DNS records and needs no
identity or provider credentials. Install cert-manager first, then save these
values as `external-values.yaml` and install from this checkout:

```yaml
providers:
route53:
enabled: false
cloudflare:
enabled: false
external:
enabled: true
controllerName: external.dns.appthrust.io/controller
providerName: external.dns.appthrust.io
providerVersion: v1alpha1
dnsQPS: 40
dnsBurst: 80
webhook:
enabled: true
certManager:
enabled: true
```

```sh
helm install dns-api ./deploy/charts/dns-api \
--namespace dns-api-system \
--create-namespace \
--values external-values.yaml
```

Use a controller image built from the same revision (or a release containing the
external provider). The chart rejects external enablement without both webhook
and cert-manager support: endpoint conversion uses the webhook Service with
cert-manager CA injection, never a TLS-verification bypass. External-only installs
grant no cloud identity or Secret API permissions; serving TLS certificates are
mounted by Kubernetes. External ZoneClasses must omit `identityRef`.

The chart values map to `--external-controller-enabled`,
`--external-controller-name`, `--external-provider-name`,
`--external-provider-version`, `--external-dns-qps`, and `--external-dns-burst`.
When running the manager directly, the first four accept environment defaults
`EXTERNAL_CONTROLLER_ENABLED`, `EXTERNAL_CONTROLLER_NAME`,
`EXTERNAL_PROVIDER_NAME`, and `EXTERNAL_PROVIDER_VERSION`; explicit flags win.
The defaults are the values above except enablement, which defaults to `false`.
DNS query limits apply per controller process.

For Kustomize, `app/operator/config/default` remains external-disabled.
`app/operator/config/external` is the opt-in overlay that adds external controllers,
Provider, capability, TLS-backed APIService, and conversion-create permission to
the default Route 53/Cloudflare installation. Disabling cloud providers in a
custom overlay must also remove their identity admission entries and RBAC (and
Cloudflare's Secret permission).

For a dedicated-member platform with base domain `B` and **M Projects**, customers
maintain **M+2 DNS record names**: `B` and `*.B` point at the PMC public edge, and
each `*.<Project>.B` points at that Project's actual serving Gateway addresses.
Publish the required A/AAAA sets (or a supported CNAME) for each name; do not point
a shared wildcard at the union of reserved member VIPs. Verification requires
exact target sets, including the absence of unexpected address-family answers.
Wildcard sampling does not replace probing each exact application hostname.
Cross-namespace wildcard claims also require an explicit `allowedRecordSets.records`
grant. See the [external provider design](./docs/design/external-provider.md) for
resolver policy, readiness, and wildcard limits.

## First Resources

A typical setup has two sides.
Expand Down
205 changes: 143 additions & 62 deletions app/operator/cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"flag"
"os"
"strconv"

endpointrecordsetcontroller "github.com/appthrust/dns-api/internal/go/apps/endpoint/controllers/endpointrecordset"
gatewayendpointcontroller "github.com/appthrust/dns-api/internal/go/apps/gatewayendpoint/controllers/gateway"
Expand All @@ -13,6 +14,10 @@ import (
cloudflarezoneclass "github.com/appthrust/dns-api/internal/go/providers/cloudflare/controllers/zoneclass"
cloudflarezoneunit "github.com/appthrust/dns-api/internal/go/providers/cloudflare/controllers/zoneunit"
cloudflarewebhook "github.com/appthrust/dns-api/internal/go/providers/cloudflare/webhook"
externalzoneclass "github.com/appthrust/dns-api/internal/go/providers/external/controllers/zoneclass"
externalzoneunit "github.com/appthrust/dns-api/internal/go/providers/external/controllers/zoneunit"
externalconversion "github.com/appthrust/dns-api/internal/go/providers/external/conversion"
externalprobe "github.com/appthrust/dns-api/internal/go/providers/external/probe"
route53identity "github.com/appthrust/dns-api/internal/go/providers/route53/controllers/identity"
route53zoneclass "github.com/appthrust/dns-api/internal/go/providers/route53/controllers/zoneclass"
route53zoneunit "github.com/appthrust/dns-api/internal/go/providers/route53/controllers/zoneunit"
Expand All @@ -22,6 +27,7 @@ import (
dnsv1alpha1 "github.com/appthrust/dns-api/pkg/go/api/dns/v1alpha1"
endpointconversionv1alpha1 "github.com/appthrust/dns-api/pkg/go/api/endpoint/conversion/v1alpha1"
endpointv1alpha1 "github.com/appthrust/dns-api/pkg/go/api/endpoint/v1alpha1"
externalv1alpha1 "github.com/appthrust/dns-api/pkg/go/api/external/v1alpha1"
route53v1alpha1 "github.com/appthrust/dns-api/pkg/go/api/route53/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
Expand Down Expand Up @@ -72,6 +78,7 @@ func init() {
utilruntime.Must(dnsv1alpha1.AddToScheme(scheme))
utilruntime.Must(endpointv1alpha1.AddToScheme(scheme))
utilruntime.Must(endpointconversionv1alpha1.AddToScheme(scheme))
utilruntime.Must(externalv1alpha1.AddToScheme(scheme))
utilruntime.Must(gatewayv1.Install(scheme))
utilruntime.Must(route53v1alpha1.AddToScheme(scheme))
}
Expand All @@ -82,26 +89,44 @@ func main() {
var leaderElection bool
var gatewayEndpointControllerEnabled bool
var serviceEndpointControllerEnabled bool
route53ControllerEnabled := envBoolOrDefault("ROUTE53_CONTROLLER_ENABLED", true)
cloudflareControllerEnabled := envBoolOrDefault("CLOUDFLARE_CONTROLLER_ENABLED", true)
externalControllerEnabled := envBoolOrDefault("EXTERNAL_CONTROLLER_ENABLED", false)
route53EndpointConversionEnabled := envBoolOrDefault("ROUTE53_ENDPOINT_CONVERSION_ENABLED", true)
endpointRecordSetNamespace := envOrDefault("ENDPOINT_RECORDSET_NAMESPACE", "")
route53ControllerName := envOrDefault("ROUTE53_CONTROLLER_NAME", route53zoneunit.DefaultControllerName)
route53ProviderName := envOrDefault("ROUTE53_PROVIDER_NAME", route53v1alpha1.ProviderName)
route53ProviderVersion := envOrDefault("ROUTE53_PROVIDER_VERSION", route53v1alpha1.ProviderVersion)
cloudflareControllerName := envOrDefault("CLOUDFLARE_CONTROLLER_NAME", cloudflarezoneunit.DefaultControllerName)
cloudflareProviderName := envOrDefault("CLOUDFLARE_PROVIDER_NAME", cloudflarev1alpha1.ProviderName)
cloudflareProviderVersion := envOrDefault("CLOUDFLARE_PROVIDER_VERSION", cloudflarev1alpha1.ProviderVersion)
externalControllerName := envOrDefault("EXTERNAL_CONTROLLER_NAME", externalv1alpha1.ControllerName)
externalProviderName := envOrDefault("EXTERNAL_PROVIDER_NAME", externalv1alpha1.ProviderName)
externalProviderVersion := envOrDefault("EXTERNAL_PROVIDER_VERSION", externalv1alpha1.ProviderVersion)
var externalDNSQPS float64
var externalDNSBurst int

flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&leaderElection, "leader-elect", false, "Enable leader election for controller manager.")
flag.BoolVar(&gatewayEndpointControllerEnabled, "gateway-endpoint-controller-enabled", true, "Watch Gateway API resources and synthesize EndpointRecordSets from HTTPRoutes.")
flag.BoolVar(&serviceEndpointControllerEnabled, "service-endpoint-controller-enabled", true, "Watch Services and synthesize EndpointRecordSets from their endpoint annotations.")
flag.StringVar(&endpointRecordSetNamespace, "endpoint-recordset-namespace", endpointRecordSetNamespace, "Namespace where generated EndpointRecordSet and RecordSet resources are stored. Defaults to the source namespace.")
flag.BoolVar(&route53ControllerEnabled, "route53-controller-enabled", route53ControllerEnabled, "Enable Route 53 controllers and identity admission.")
flag.BoolVar(&cloudflareControllerEnabled, "cloudflare-controller-enabled", cloudflareControllerEnabled, "Enable Cloudflare controllers and identity admission.")
flag.BoolVar(&externalControllerEnabled, "external-controller-enabled", externalControllerEnabled, "Enable external DNS verification controllers and endpoint conversion.")
flag.BoolVar(&route53EndpointConversionEnabled, "route53-endpoint-conversion-enabled", route53EndpointConversionEnabled, "Serve endpoint conversion when the Route 53 provider is enabled.")
flag.StringVar(&route53ControllerName, "route53-controller-name", route53ControllerName, "ZoneClass.spec.controllerName handled by the Route 53 controller.")
flag.StringVar(&route53ProviderName, "route53-provider-name", route53ProviderName, "Provider.metadata.name handled by the Route 53 controller.")
flag.StringVar(&route53ProviderVersion, "route53-provider-version", route53ProviderVersion, "Provider version handled by the Route 53 controller.")
flag.StringVar(&cloudflareControllerName, "cloudflare-controller-name", cloudflareControllerName, "ZoneClass.spec.controllerName handled by the Cloudflare controller.")
flag.StringVar(&cloudflareProviderName, "cloudflare-provider-name", cloudflareProviderName, "Provider.metadata.name handled by the Cloudflare controller.")
flag.StringVar(&cloudflareProviderVersion, "cloudflare-provider-version", cloudflareProviderVersion, "Provider version handled by the Cloudflare controller.")
flag.StringVar(&externalControllerName, "external-controller-name", externalControllerName, "ZoneClass.spec.controllerName handled by the external controller.")
flag.StringVar(&externalProviderName, "external-provider-name", externalProviderName, "Provider.metadata.name handled by the external controller.")
flag.StringVar(&externalProviderVersion, "external-provider-version", externalProviderVersion, "Provider version handled by the external controller.")
flag.Float64Var(&externalDNSQPS, "external-dns-qps", externalprobe.DefaultQPS, "Maximum external DNS query rate per controller process.")
flag.IntVar(&externalDNSBurst, "external-dns-burst", externalprobe.DefaultBurst, "Maximum external DNS query burst per controller process.")

zapOpts := zap.Options{Development: true}
zapOpts.BindFlags(flag.CommandLine)
Expand All @@ -123,68 +148,102 @@ func main() {
os.Exit(1)
}

if err := (&route53identity.IdentityReconciler{
Client: mgr.GetClient(),
Resolver: route53identity.NewAWSIdentityResolver(),
Recorder: mgr.GetEventRecorderFor("route53-identity-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 Identity controller")
os.Exit(1)
}
if route53ControllerEnabled {
if err := (&route53identity.IdentityReconciler{
Client: mgr.GetClient(),
Resolver: route53identity.NewAWSIdentityResolver(),
Recorder: mgr.GetEventRecorderFor("route53-identity-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 Identity controller")
os.Exit(1)
}

if err := (&cloudflareidentity.IdentityReconciler{
Client: mgr.GetClient(),
Resolver: cloudflareidentity.NewCloudflareAPIClient(),
Recorder: mgr.GetEventRecorderFor("cloudflare-identity-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare Identity controller")
os.Exit(1)
}
if err := (&route53zoneclass.ZoneClassReconciler{
ControllerName: route53ControllerName,
Client: mgr.GetClient(),
ProviderName: route53ProviderName,
ProviderVersion: route53ProviderVersion,
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up ZoneClass controller")
os.Exit(1)
}

if err := (&route53zoneclass.ZoneClassReconciler{
ControllerName: route53ControllerName,
Client: mgr.GetClient(),
ProviderName: route53ProviderName,
ProviderVersion: route53ProviderVersion,
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up ZoneClass controller")
os.Exit(1)
if err := (&route53zoneunit.ZoneReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ProviderFactory: route53zoneunit.NewAWSProviderFactory(),
ControllerName: route53ControllerName,
ProviderName: route53ProviderName,
ProviderVersion: route53ProviderVersion,
Recorder: mgr.GetEventRecorderFor("route53-zoneunit-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 ZoneUnit controller")
os.Exit(1)
}
}

if err := (&cloudflarezoneclass.ZoneClassReconciler{
ControllerName: cloudflareControllerName,
Client: mgr.GetClient(),
ProviderName: cloudflareProviderName,
ProviderVersion: cloudflareProviderVersion,
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare ZoneClass controller")
os.Exit(1)
}
if cloudflareControllerEnabled {
if err := (&cloudflareidentity.IdentityReconciler{
Client: mgr.GetClient(),
Resolver: cloudflareidentity.NewCloudflareAPIClient(),
Recorder: mgr.GetEventRecorderFor("cloudflare-identity-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare Identity controller")
os.Exit(1)
}

if err := (&route53zoneunit.ZoneReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ProviderFactory: route53zoneunit.NewAWSProviderFactory(),
ControllerName: route53ControllerName,
ProviderName: route53ProviderName,
ProviderVersion: route53ProviderVersion,
Recorder: mgr.GetEventRecorderFor("route53-zoneunit-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 ZoneUnit controller")
os.Exit(1)
if err := (&cloudflarezoneclass.ZoneClassReconciler{
ControllerName: cloudflareControllerName,
Client: mgr.GetClient(),
ProviderName: cloudflareProviderName,
ProviderVersion: cloudflareProviderVersion,
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare ZoneClass controller")
os.Exit(1)
}

if err := (&cloudflarezoneunit.ZoneReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ProviderFactory: cloudflarezoneunit.NewCloudflareAPIClient(),
ControllerName: cloudflareControllerName,
ProviderName: cloudflareProviderName,
ProviderVersion: cloudflareProviderVersion,
Recorder: mgr.GetEventRecorderFor("cloudflare-zoneunit-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare ZoneUnit controller")
os.Exit(1)
}
}

if err := (&cloudflarezoneunit.ZoneReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ProviderFactory: cloudflarezoneunit.NewCloudflareAPIClient(),
ControllerName: cloudflareControllerName,
ProviderName: cloudflareProviderName,
ProviderVersion: cloudflareProviderVersion,
Recorder: mgr.GetEventRecorderFor("cloudflare-zoneunit-controller"),
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare ZoneUnit controller")
os.Exit(1)
if externalControllerEnabled {
if err := (&externalzoneclass.ZoneClassReconciler{
Client: mgr.GetClient(),
ControllerName: externalControllerName,
ProviderName: externalProviderName,
ProviderVersion: externalProviderVersion,
}).SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up external ZoneClass controller")
os.Exit(1)
}

// NewReconciler supplies the wire resolver, its effective pod DNS policy,
// time.Now, and random jitter when their injection hooks are unset.
externalReconciler, err := externalzoneunit.NewReconciler(mgr.GetClient(), externalzoneunit.Options{
ControllerName: externalControllerName,
ProviderName: externalProviderName,
ProviderVersion: externalProviderVersion,
QPS: externalDNSQPS,
Burst: externalDNSBurst,
})
if err != nil {
ctrl.Log.Error(err, "unable to configure external DNS resolver")
os.Exit(1)
}
if err := externalReconciler.SetupWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up external ZoneUnit controller")
os.Exit(1)
}
}

if err := (&zoneunitcontroller.ZoneUnitCompositionReconciler{
Expand Down Expand Up @@ -230,15 +289,24 @@ func main() {
ctrl.Log.Error(err, "unable to set up core validation webhook")
os.Exit(1)
}
if err := route53webhook.SetupValidationWebhookWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 validation webhook")
os.Exit(1)
if route53ControllerEnabled {
if err := route53webhook.SetupValidationWebhookWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Route 53 validation webhook")
os.Exit(1)
}
if route53EndpointConversionEnabled {
route53conversion.NewHandler().Register(mgr.GetWebhookServer())
}
}
if err := cloudflarewebhook.SetupValidationWebhookWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare validation webhook")
os.Exit(1)
if cloudflareControllerEnabled {
if err := cloudflarewebhook.SetupValidationWebhookWithManager(mgr); err != nil {
ctrl.Log.Error(err, "unable to set up Cloudflare validation webhook")
os.Exit(1)
}
}
if externalControllerEnabled {
externalconversion.NewHandler().Register(mgr.GetWebhookServer())
}
route53conversion.NewHandler().Register(mgr.GetWebhookServer())

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
ctrl.Log.Error(err, "unable to set up health check")
Expand All @@ -262,3 +330,16 @@ func envOrDefault(name, fallback string) string {
}
return fallback
}

func envBoolOrDefault(name string, fallback bool) bool {
value := os.Getenv(name)
if value == "" {
return fallback
}
enabled, err := strconv.ParseBool(value)
if err != nil {
ctrl.Log.Error(err, "invalid boolean environment variable", "name", name)
os.Exit(1)
}
return enabled
}
Loading
Loading