Stage 1: TerdutServer full lifecycle (Deployment, Service, both database
CI / test (push) Successful in 1m46s

paths, self-registration bootstrap)

Replaces the bring-your-own-only Stage 1 (commit 1be7cf2) wholesale, per
the redesign in the previous two commits: the operator creates every
server it manages, so self-registration (DESIGN.md §6) is the only
bootstrap path, and Deployment/Service/database management builds
together with it (ROADMAP.md Stage 1) rather than behind a separate
later stage.

Grounded in terdut-server's actual chart (charts/terdut-server/templates/
deployment.yaml, values.yaml), not reconstructed from DESIGN.md's
illustrative YAML alone -- env var names, the password-via-PGPASSWORD
convention, the Recreate deployment strategy, /healthz probes, and the
TERDUT_OPERATOR_MODE=true decision (always on here, unlike the chart's
default-off: every write this operator's own future controllers make
goes through a service account already) all match that source exactly.

- api/v1alpha1: full TerdutServerSpec (image, replicas, networking,
  database, sweeper, deadman, notify, oidc, passwordLogin, allowedTeams).
  spec.database is a oneOf (dsn xor postgresClusterRef) via CEL
  XValidation. No spec.credentialsSecretRef -- removed entirely in the
  prior redesign commit, not carried forward.
- internal/controller:
  - terdutserver_deployment.go: Deployment + Service via CreateOrUpdate,
    owned (OwnerReference), env built field-for-field against the chart.
  - terdutserver_database.go: both §8 paths. The Zalando path resolves
    the postgresql.acid.zalan.do CR by convention (database/role both
    "terdut", matching every DESIGN.md example) and only ever confirms
    its generated credentials Secret exists -- never reads the value,
    same "wire a secretKeyRef, don't read it" posture the DSN path takes.
    classifyClusterGetError is its own function specifically so the
    CRD-not-installed case (meta.IsNoMatchError) is unit-testable without
    a real client.
  - terdutserver_bootstrap.go: self-registration, checkpointed against
    both real crash windows (DESIGN.md §6 point 1) -- an admin-key
    checkpoint Secret, and adopt-via-GET+mint-new-key on a 409 from
    creating the service account. BootstrapStateLost is its own error
    type so Reconcile can route it to a condition instead of an infinite
    retry.
  - terdutserver_controller.go: ties it together -- finalizer add, DB
    resolution, Deployment/Service reconcile, wait for a ready replica,
    bootstrap, Ready/Bootstrapped/DatabaseReady conditions. Finalizer on
    delete only removes the generated Secrets: terdut-server's API can't
    delete a user or service account, only revoke keys, so there's
    nothing server-side to undo.
- internal/tdclient: added Bootstrap, CreateInstanceServiceAccount,
  GetServiceAccountByName, CreateServiceAccountKey, matching
  terdut-server's real handlers' request/response shapes (internal/api/
  users.go, service_accounts.go in that repo) field-for-field.
- Tests: envtest suite covering the full DSN-path lifecycle end to end
  (finalizer -> Deployment/Service -> simulated readiness -> real
  bootstrap against an httptest.Server fake), the adopt-on-409 recovery
  path, BootstrapStateLost, both Zalando outcomes (cluster not found;
  cluster + Secret found -> real DSN -> Ready), and deletion. A minimal
  test-only stub of the Zalando CRD (internal/controller/testdata) lets
  envtest create fixture objects without a real postgres-operator
  installed. 74.0%/44.7% coverage, 0 lint issues.
- Two things scoped down from §8's full ambition, called out in code and
  ROADMAP.md rather than silently dropped: no live watch on the
  Zalando-generated Secret for rotation (periodic resync notices
  eventually, not immediately), no Gateway API HTTPRoute creation from
  spec.networking (would add a new dependency; nothing about proving
  bootstrap works depends on external ingress existing). Both are
  near-term follow-ups.

Verified locally: make fmt lint test build all clean.
This commit is contained in:
Niklas Ye
2026-10-01 09:11:56 +02:00
parent fc68ee7256
commit 8064876cb1
16 changed files with 1872 additions and 306 deletions
+226 -57
View File
@@ -5,13 +5,12 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// SecretKeyRef names one data key inside a Secret. Unlike a typical
// cross-namespace reference, there is deliberately no namespace field here:
// every Secret this type points at (DESIGN.md §6) lives in the operator's
// own namespace, always, by construction — never anywhere else, so there is
// nothing for a namespace field to vary. What does vary is the key: fixed
// ("token") when the controller generated the Secret itself, whatever a
// human chose when it was handed to the controller instead.
// SecretKeyRef names one data key inside a Secret. Every use of this type in
// TerdutServerSpec resolves in the TerdutServer's own namespace (it's wired
// straight into the Deployment's pod spec as a secretKeyRef env source,
// which Kubernetes itself only allows same-namespace) -- unlike the
// generated credentials Secret (DESIGN.md §6), which always lives in the
// operator's own namespace and is never referenced through this type.
type SecretKeyRef struct {
// name is the Secret's name.
// +kubebuilder:validation:MinLength=1
@@ -22,6 +21,148 @@ type SecretKeyRef struct {
Key string `json:"key"`
}
// ImageSpec is the terdut-server image to run.
type ImageSpec struct {
// +kubebuilder:validation:MinLength=1
Repository string `json:"repository"`
// +kubebuilder:validation:MinLength=1
Tag string `json:"tag"`
}
// NetworkingSpec is how this TerdutServer is reached from outside the
// cluster.
//
// hostname/gatewayListener describe the intended Gateway API HTTPRoute
// (matching charts/terdut-server's own templates/httpproxy.yaml, despite its
// name — that chart carries a Gateway API HTTPRoute, not a Contour
// HTTPProxy), but creating that HTTPRoute isn't implemented yet: it needs
// the Gateway API types as a new dependency, and nothing about proving a
// TerdutServer boots and bootstraps a real server depends on external
// ingress existing. Tracked as a near-term follow-up, not deferred to a
// later ROADMAP.md stage the way Deployment/database/bootstrap once were.
type NetworkingSpec struct {
// hostname the HTTPRoute will carry once it exists.
// +optional
Hostname string `json:"hostname,omitempty"`
// servicePort is both the Service's port and the HTTPRoute's backend
// port once it exists. Defaults to 8080, matching the chart's own
// service.port default.
// +kubebuilder:default=8080
// +optional
ServicePort int32 `json:"servicePort,omitempty"`
// gatewayListener is the HTTPRoute's sectionName once it exists. Empty
// attaches to every matching listener, including plaintext HTTP.
// +optional
GatewayListener string `json:"gatewayListener,omitempty"`
}
// PostgresClusterRef names a Zalando postgres-operator `postgresql` CR
// (`acid.zalan.do/v1`) in the same namespace (DESIGN.md §8). By convention
// — matching every example in DESIGN.md and the chart's own — the database
// and role this operator consumes from it are both named "terdut"; this
// isn't configurable in v1 (there is no field for it because the design
// doesn't have one yet, not an oversight here).
type PostgresClusterRef struct {
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
}
// DatabaseSpec is the Postgres connection this TerdutServer uses. Exactly
// one of dsn or postgresClusterRef must be set (DESIGN.md §8) — this
// operator provisions no database either way, only wires up one that
// exists.
// +kubebuilder:validation:XValidation:rule="(has(self.dsn) ? 1 : 0) + (has(self.postgresClusterRef) ? 1 : 0) == 1",message="exactly one of dsn or postgresClusterRef must be set"
type DatabaseSpec struct {
// dsn is a DSN with no password in it, e.g.
// "postgres://terdut@terdut-postgres:5432/terdut?sslmode=require" --
// mutually exclusive with postgresClusterRef.
// +optional
DSN string `json:"dsn,omitempty"`
// passwordSecretRef is where PGPASSWORD comes from for the dsn path.
// pgx falls back to libpq's environment variables for anything the DSN
// omits, so the password never appears in the DSN string itself. Unused
// on the postgresClusterRef path -- the Zalando-generated Secret is
// wired in directly instead.
// +optional
PasswordSecretRef *SecretKeyRef `json:"passwordSecretRef,omitempty"`
// postgresClusterRef names a Zalando postgres-operator CR instead of a
// plain DSN -- mutually exclusive with dsn.
// +optional
PostgresClusterRef *PostgresClusterRef `json:"postgresClusterRef,omitempty"`
}
// SweeperSpec controls incident auto-resolve/archive timing. Values are
// Go duration strings (e.g. "6h"), passed straight through to the
// TERDUT_STALE_AFTER/TERDUT_ARCHIVE_AFTER env vars exactly as written --
// not a structured metav1.Duration, since terdut-server parses them itself
// and a round-trip through a different type would buy nothing.
type SweeperSpec struct {
// +optional
StaleAfter string `json:"staleAfter,omitempty"`
// +optional
ArchiveAfter string `json:"archiveAfter,omitempty"`
}
// DeadmanSpec controls dead man's switch alerts. Matchers/Timeout/Severity
// map straight to TERDUT_DEADMAN_MATCHERS/TERDUT_DEADMAN_TIMEOUT/
// TERDUT_DEADMAN_SEVERITY.
type DeadmanSpec struct {
// +optional
Matchers string `json:"matchers,omitempty"`
// +optional
Timeout string `json:"timeout,omitempty"`
// +optional
Severity string `json:"severity,omitempty"`
}
// NotifySpec controls push notifications via ntfy. Empty ntfyURL disables
// notifications entirely (matches the chart's own default).
type NotifySpec struct {
// +optional
NtfyURL string `json:"ntfyURL,omitempty"`
// +optional
FallbackTopic string `json:"fallbackTopic,omitempty"`
// +optional
RepeatEvery string `json:"repeatEvery,omitempty"`
// tokenSecretRef is an optional bearer token for an access-controlled
// ntfy. Leave unset for an open ntfy.
// +optional
TokenSecretRef *SecretKeyRef `json:"tokenSecretRef,omitempty"`
}
// OIDCSpec controls single sign-on. Fields the chart also exposes but
// DESIGN.md's spec doesn't (usernameClaim, emailClaim, groupsClaim,
// trustEmail) use terdut-server's own defaults
// (preferred_username/email/groups/false) rather than being added here
// speculatively.
type OIDCSpec struct {
// +optional
Enabled bool `json:"enabled,omitempty"`
// +optional
Issuer string `json:"issuer,omitempty"`
// +optional
ClientID string `json:"clientID,omitempty"`
// +optional
ClientSecretRef *SecretKeyRef `json:"clientSecretRef,omitempty"`
// +kubebuilder:default="SSO"
// +optional
Name string `json:"name,omitempty"`
// +kubebuilder:default="openid profile email"
// +optional
Scopes string `json:"scopes,omitempty"`
// +optional
AllowedGroups []string `json:"allowedGroups,omitempty"`
// +optional
AdminGroup string `json:"adminGroup,omitempty"`
// +kubebuilder:default="12h"
// +optional
SessionMaxAge string `json:"sessionMaxAge,omitempty"`
}
// AllowedTeamsNamespaces gates which namespaces a TerdutTeam may resolve a
// cross-namespace serverRef into this TerdutServer from (DESIGN.md §4.6).
// Same-namespace TerdutTeams are always allowed, regardless of this field.
@@ -51,41 +192,49 @@ type AllowedTeams struct {
// TerdutServerSpec defines the desired state of TerdutServer.
//
// Narrowed to Stage 1 (ROADMAP.md): this covers only what the bootstrap/
// credentials flow (DESIGN.md §6) needs against an already-running,
// already-bootstrapped terdut-server. The fields that would have the
// controller manage a Deployment/Service/database — image, replicas,
// networking, database — are deferred to Stage 5 and deliberately absent
// here, not an oversight; adding them later is additive, not a breaking
// change to this shape.
// The operator creates and owns every TerdutServer it manages (DESIGN.md
// §1) -- there is no bring-your-own-install path. This is the full shape
// from DESIGN.md §4.1: Deployment, Service, database, bootstrap and
// credentials are all built together (ROADMAP.md Stage 1), not staged
// separately the way an earlier version of this design did.
type TerdutServerSpec struct {
// endpoint is the base URL of an already-running terdut-server this
// TerdutServer represents, e.g. "http://terdut.oncall.svc:8080". This
// stage never creates or manages a Deployment/Service for it — the
// server is expected to already exist, deployed some other way (its own
// Helm chart, by hand).
// +required
// +kubebuilder:validation:MinLength=1
Endpoint string `json:"endpoint"`
Image ImageSpec `json:"image"`
// credentialsSecretRef names a Secret, in the operator's own namespace,
// that a human has already created by minting an instance-scoped service
// account with their own admin session (POST /api/service-accounts,
// DESIGN.md §6) and placing its raw key under the given key. Set, and
// the Secret found: the controller adopts it outright and skips
// bootstrap entirely — this is the expected path for Stage 1, not a
// fallback (DESIGN.md §6 explains why self-registration cannot complete
// unauthenticated in the setup this stage actually exercises).
//
// Unset: the controller has no way to acquire a credential in this
// stage (self-registration lands in Stage 5) and reports
// Ready: False, reason: CredentialsSecretRefRequired.
// replicas. terdut-server is not horizontally-scale-tested; keep this
// at its default of 1 unless you've verified otherwise -- the sweeper
// and the notifier are unsynchronised singletons.
// +kubebuilder:default=1
// +optional
CredentialsSecretRef *SecretKeyRef `json:"credentialsSecretRef,omitempty"`
Replicas int32 `json:"replicas,omitempty"`
// +required
Networking NetworkingSpec `json:"networking"`
// +required
Database DatabaseSpec `json:"database"`
// +optional
Sweeper SweeperSpec `json:"sweeper,omitempty"`
// +optional
Deadman DeadmanSpec `json:"deadman,omitempty"`
// +optional
Notify NotifySpec `json:"notify,omitempty"`
// +optional
OIDC OIDCSpec `json:"oidc,omitempty"`
// passwordLogin: whether a user may sign in, or sign up, with a
// password.
// +kubebuilder:default=true
// +optional
PasswordLogin bool `json:"passwordLogin,omitempty"`
// allowedTeams gates cross-namespace TerdutTeams (DESIGN.md §4.6).
// Unused until TerdutTeam exists (Stage 2); present now so this CRD's
// schema doesn't need a breaking change to grow it later.
// Unused until TerdutTeam exists (ROADMAP.md Stage 2); present now so
// this CRD's schema doesn't need a breaking change to grow it later.
// +optional
AllowedTeams AllowedTeams `json:"allowedTeams,omitempty"`
}
@@ -95,29 +244,42 @@ const (
// ConditionReady is the standard top-level condition every CRD carries
// (DESIGN.md §7).
ConditionReady = "Ready"
// ConditionDatabaseReady reflects whether the configured database is
// usable -- for postgresClusterRef, whether the Zalando CR and its
// generated credentials Secret both resolved; for a plain dsn, always
// true once set (DESIGN.md §8: "no connectivity check beyond what the
// Deployment's own readiness probe already gives").
ConditionDatabaseReady = "DatabaseReady"
// ConditionBootstrapped reflects whether a working credential has been
// acquired — adopted from spec.credentialsSecretRef in this stage.
// acquired via self-registration (DESIGN.md §6).
ConditionBootstrapped = "Bootstrapped"
)
// Condition reasons this controller sets.
const (
// ReasonCredentialsSecretRefRequired: spec.credentialsSecretRef is
// unset, and self-registration isn't implemented yet (Stage 5) — the
// expected, steady-state reason whenever no bring-your-own credential
// has been provided.
ReasonCredentialsSecretRefRequired = "CredentialsSecretRefRequired"
// ReasonCredentialsSecretNotFound: spec.credentialsSecretRef is set but
// no such Secret exists (yet) in the operator's own namespace.
ReasonCredentialsSecretNotFound = "CredentialsSecretNotFound"
// ReasonCredentialsSecretInvalid: the Secret exists but has no data
// under the given key, or it's empty.
ReasonCredentialsSecretInvalid = "CredentialsSecretInvalid"
// ReasonServerUnreachable: GET /api/version against spec.endpoint
// failed — a bad endpoint, or the server is down.
ReasonServerUnreachable = "ServerUnreachable"
// ReasonAdopted: the happy path. A working credential is in hand and the
// server answered its version probe.
// ReasonWaitingForDeployment: the Deployment this controller created has
// no ready replica yet -- bootstrap can't be attempted until it does.
ReasonWaitingForDeployment = "WaitingForDeployment"
// ReasonPostgresClusterNotFound: spec.database.postgresClusterRef names
// no such postgresql.acid.zalan.do object (yet).
ReasonPostgresClusterNotFound = "PostgresClusterNotFound"
// ReasonPostgresOperatorCRDNotInstalled: spec.database.postgresClusterRef
// is set, but the postgresql.acid.zalan.do CRD itself isn't installed in
// this cluster (DESIGN.md §8, §9: this operator degrades gracefully
// rather than hard-failing when that CRD is absent and BYO DSN is used
// instead -- but a TerdutServer that explicitly asks for it still needs
// to say clearly that it can't be satisfied).
ReasonPostgresOperatorCRDNotInstalled = "PostgresOperatorCRDNotInstalled"
// ReasonBootstrapStateLost: a checkpointed admin credential
// (DESIGN.md §6) was lost after being used but before the lasting
// credential it was for could be persisted -- the one genuinely
// pathological case in the self-registration flow. Fail-closed, same
// recovery as DESIGN.md §5's webhook-Secret-loss rule: delete and
// recreate this TerdutServer.
ReasonBootstrapStateLost = "BootstrapStateLost"
// ReasonAdopted: the happy path. A working credential is in hand, the
// Deployment has a ready replica, and the database (if postgresClusterRef)
// resolved.
ReasonAdopted = "Adopted"
)
@@ -135,16 +297,23 @@ type TerdutServerStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// credentialsSecretRef mirrors spec.credentialsSecretRef once adopted —
// same Secret, same key, always in the operator's own namespace. Set
// only once Bootstrapped is True.
// serviceName is the Service this controller created for the
// Deployment, so other objects can reference it without recomputing the
// naming convention.
// +optional
ServiceName string `json:"serviceName,omitempty"`
// credentialsSecretRef is the generated instance-scoped credential
// (DESIGN.md §6) -- pure output, always in the operator's own
// namespace, under a fixed data key ("token"). Set only once
// Bootstrapped is True.
// +optional
CredentialsSecretRef *SecretKeyRef `json:"credentialsSecretRef,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
+152 -5
View File
@@ -45,6 +45,136 @@ func (in *AllowedTeamsNamespaces) DeepCopy() *AllowedTeamsNamespaces {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DatabaseSpec) DeepCopyInto(out *DatabaseSpec) {
*out = *in
if in.PasswordSecretRef != nil {
in, out := &in.PasswordSecretRef, &out.PasswordSecretRef
*out = new(SecretKeyRef)
**out = **in
}
if in.PostgresClusterRef != nil {
in, out := &in.PostgresClusterRef, &out.PostgresClusterRef
*out = new(PostgresClusterRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatabaseSpec.
func (in *DatabaseSpec) DeepCopy() *DatabaseSpec {
if in == nil {
return nil
}
out := new(DatabaseSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeadmanSpec) DeepCopyInto(out *DeadmanSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeadmanSpec.
func (in *DeadmanSpec) DeepCopy() *DeadmanSpec {
if in == nil {
return nil
}
out := new(DeadmanSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageSpec) DeepCopyInto(out *ImageSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSpec.
func (in *ImageSpec) DeepCopy() *ImageSpec {
if in == nil {
return nil
}
out := new(ImageSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkingSpec) DeepCopyInto(out *NetworkingSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingSpec.
func (in *NetworkingSpec) DeepCopy() *NetworkingSpec {
if in == nil {
return nil
}
out := new(NetworkingSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NotifySpec) DeepCopyInto(out *NotifySpec) {
*out = *in
if in.TokenSecretRef != nil {
in, out := &in.TokenSecretRef, &out.TokenSecretRef
*out = new(SecretKeyRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotifySpec.
func (in *NotifySpec) DeepCopy() *NotifySpec {
if in == nil {
return nil
}
out := new(NotifySpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OIDCSpec) DeepCopyInto(out *OIDCSpec) {
*out = *in
if in.ClientSecretRef != nil {
in, out := &in.ClientSecretRef, &out.ClientSecretRef
*out = new(SecretKeyRef)
**out = **in
}
if in.AllowedGroups != nil {
in, out := &in.AllowedGroups, &out.AllowedGroups
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCSpec.
func (in *OIDCSpec) DeepCopy() *OIDCSpec {
if in == nil {
return nil
}
out := new(OIDCSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PostgresClusterRef) DeepCopyInto(out *PostgresClusterRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgresClusterRef.
func (in *PostgresClusterRef) DeepCopy() *PostgresClusterRef {
if in == nil {
return nil
}
out := new(PostgresClusterRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) {
*out = *in
@@ -60,6 +190,21 @@ func (in *SecretKeyRef) DeepCopy() *SecretKeyRef {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SweeperSpec) DeepCopyInto(out *SweeperSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SweeperSpec.
func (in *SweeperSpec) DeepCopy() *SweeperSpec {
if in == nil {
return nil
}
out := new(SweeperSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TerdutServer) DeepCopyInto(out *TerdutServer) {
*out = *in
@@ -122,11 +267,13 @@ func (in *TerdutServerList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TerdutServerSpec) DeepCopyInto(out *TerdutServerSpec) {
*out = *in
if in.CredentialsSecretRef != nil {
in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef
*out = new(SecretKeyRef)
**out = **in
}
out.Image = in.Image
out.Networking = in.Networking
in.Database.DeepCopyInto(&out.Database)
out.Sweeper = in.Sweeper
out.Deadman = in.Deadman
in.Notify.DeepCopyInto(&out.Notify)
in.OIDC.DeepCopyInto(&out.OIDC)
in.AllowedTeams.DeepCopyInto(&out.AllowedTeams)
}