From 9d356081cecb26b33140ded75d2870e782634d8f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 14 May 2026 09:42:23 +0000 Subject: [PATCH 01/20] update targetsource --- api/v1alpha1/targetsource_types.go | 168 +++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 179 +++++++++++-- .../operator.gnmic.dev_targetsources.yaml | 249 +++++++++++++++++- 3 files changed, 556 insertions(+), 40 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 3d69743..143da3c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -17,37 +17,191 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // TargetSourceSpec defines the desired state of TargetSource // +kubebuilder:validation:Required type TargetSourceSpec struct { + // Provider defines the source of targets for this TargetSource + // Only one provider can be specified per TargetSource + // +kubebuilder:validation:Required Provider *ProviderSpec `json:"provider"` + // TODO: implement in message processor + // Optional port to use for discovered targets if not specified by the provider + // +kubebuilder:validation:Optional + TargetPort int32 `json:"targetPort,omitempty"` + + // Optional labels to apply to all targets discovered by this TargetSource // +kubebuilder:validation:Optional TargetLabels map[string]string `json:"targetLabels,omitempty"` + // The TargetProfile to use for targets discovered by this TargetSource + // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 TargetProfile string `json:"targetProfile"` } -// +kubebuilder:validation:ExactlyOneOf=http;consul +// ProviderSpec defines the source of targets for a TargetSource +// Only one provider can be specified per TargetSource +// +kubebuilder:validation:ExactlyOneOf=http type ProviderSpec struct { - HTTP *HTTPConfig `json:"http,omitempty"` - Consul *ConsulConfig `json:"consul,omitempty"` + // HTTP defines the configuration for a HTTP provider + HTTP *HTTPConfig `json:"http,omitempty"` } +// HTTPConfig defines the configuration for the HTTP provider +// +kubebuilder:validation:AtLeastOneOf=url;acceptPush type HTTPConfig struct { - // +kubebuilder:validation:MinLength=1 - URL string `json:"url"` + // URL of the HTTP endpoint to pull targets from + // If defined, the loader will periodically poll this endpoint for targets + // +kubebuilder:validation:Optional + URL string `json:"url,omitempty"` + + // If true, the loader will accept pushed target updates to the controller endpoint + // The endpoint will be /{namespace}/{targetsource}/ + // +kubebuilder:default=false // +kubebuilder:validation:Optional AcceptPush bool `json:"acceptPush,omitempty"` + + // Optional authorization configuration for accessing the HTTP endpoint + // +kubebuilder:validation:Optional + Authorization *AuthorizationSpec `json:"authorization,omitempty"` + + // Optional interval for polling the HTTP endpoint for targets + // TODO: increase default value + // +kubebuilder:default="30s" + // +kubebuilder:validation:Optional + PollInterval *metav1.Duration `json:"interval,omitempty"` + + // Optional timeout for HTTP requests to the endpoint + // +kubebuilder:default="10s" + // +kubebuilder:validation:Optional + Timeout *metav1.Duration `json:"timeout,omitempty"` + + // Optional TLS configuration for connecting to the HTTP endpoint + // +kubebuilder:validation:Optional + TLS *ClientTLSConfig `json:"tls,omitempty"` + + // Optional pagination configuration for parsing responses from the HTTP endpoint + // +kubebuilder:validation:Optional + Pagination *PaginationSpec `json:"pagination,omitempty"` + + // Optional mapping configuration for parsing responses from the HTTP endpoint + // +kubebuilder:validation:Optional + ResponseMapping *ResponseMappingSpec `json:"mapping,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="!(has(self.caBundle) && has(self.caBundleSecretRef))",message="caBundle and caBundleSecretRef are mutually exclusive" +type ClientTLSConfig struct { + // Skip TLS verification of the Provider's certificate. + // +kubebuilder:default:=false + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + + // Base64-encoded bundle of PEM CAs which will be used to validate the certificate + // chain presented by the Provider. Only used if using HTTPS to connect to Provider and + // ignored for HTTP connections. + // Mutually exclusive with CABundleSecretRef. + // +optional + CABundle []byte `json:"caBundle,omitempty"` + + // Reference to a Secret containing a bundle of PEM-encoded CAs to use when + // verifying the certificate chain presented by the Provider when using HTTPS. + // Mutually exclusive with CABundle. + CABundleSecretRef *corev1.SecretKeySelector `json:"caBundleSecretRef,omitempty"` +} + +// AuthorizationSpec defines the configuration for authentication +// +kubebuilder:validation:ExactlyOneOf=basic;token +type AuthorizationSpec struct { + // Basic authentication configuration + Basic *BasicAuthSpec `json:"basic,omitempty"` + // Token-based authentication configuration + Token *TokenAuthSpec `json:"token,omitempty"` + // JWT *JWTAuthSpec `json:"jwt,omitempty"` + // MTLS } -type ConsulConfig struct { +// BasicAuthSpec defines the configuration for basic authentication +// Enforce EITHER inline creds OR secret ref +// +kubebuilder:validation:XValidation:rule="(has(self.credentialsSecretRef) && !has(self.username) && !has(self.password)) || (!has(self.credentialsSecretRef) && has(self.username) && has(self.password))",message="either credentialsSecretRef OR both username and password must be set, but not a mix" +type BasicAuthSpec struct { + // Username for basic auth + // Mutually exclusive with CredentialsSecretRef. + Username string `json:"username,omitempty"` + // Password for basic auth + // Mutually exclusive with CredentialsSecretRef. + Password string `json:"password,omitempty"` + + // Reference to a Secret containing "username" and "password" keys to use for + // basic authentication when connecting to the Provider. + // Mutually exclusive with Username and Password. + CredentialsSecretRef *corev1.SecretKeySelector `json:"credentialsSecretRef,omitempty"` +} + +// TokenAuthSpec defines the configuration for token-based authentication +// +kubebuilder:validation:XValidation:rule="has(self.token) != has(self.tokenSecretRef)",message="either token or tokenSecretRef must be set, but not both" +type TokenAuthSpec struct { + // Scheme for the token, e.g. "Bearer" // +kubebuilder:validation:MinLength=1 - URL string `json:"url,omitempty"` + Scheme string `json:"scheme"` + // Token value for authentication + // Mutually exclusive with TokenSecretRef. + Token string `json:"token,omitempty"` + // Reference to a Secret containing a key with the token value to use for + // authentication when connecting to the Provider. + // Mutually exclusive with Token. + TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="!((has(self.token) || has(self.tokenSecretRef)) && ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))",message="static JWT token and generated JWT configuration cannot be combined" +// +kubebuilder:validation:XValidation:rule="!has(self.signingKeySecretRef) || self.algorithm != \"\"",message="algorithm must be specified when generating a JWT" +// type JWTAuthSpec struct { +// // Static pre-generated JWT +// Token string `json:"token,omitempty"` +// TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` +// // Optional: generate JWT dynamically +// Claims map[string]string `json:"claims,omitempty"` +// Key string `json:"key,omitempty"` +// SigningKeySecretRef *corev1.SecretKeySelector `json:"signingKeySecretRef,omitempty"` +// // HS256, RS256, ES256, etc. +// Algorithm string `json:"algorithm,omitempty"` +// TTL *metav1.Duration `json:"ttl,omitempty"` +// } + +// PaginationSpec defines the configuration for paginating through responses from providers +type PaginationSpec struct { + // JSONPath-style expression to extract the list of targets from the response + // Example: "results" + ItemsField string `json:"itemsField,omitempty"` + + // JSONPath-style expression to extract the next page token or URL from the response for pagination + // Example: "next" + NextField string `json:"nextField,omitempty"` +} + +// JSONPath-style expressions to extract target fields from the response +// and map them to the corresponding Target fields. +type ResponseMappingSpec struct { + // JSONPath expression to extract the target name from the response + // +kubebuilder:validation:Required + Name string `json:"name"` + + // JSONPath expression to extract the target address from the response + // +kubebuilder:validation:Required + Address string `json:"address"` + + // JSONPath expression to extract the target port from the response + // +kubebuilder:validation:Optional + Port string `json:"port,omitempty"` + + // JSONPath expression to extract the target labels from the response + // The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, + // with values from the response taking precedence in case of conflicts. + // +kubebuilder:validation:Optional + Labels map[string]string `json:"labels,omitempty"` } // TargetSourceStatus defines the observed state of TargetSource diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 61e81fd..dc4b784 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -46,6 +46,76 @@ func (in *APIConfig) DeepCopy() *APIConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationSpec) DeepCopyInto(out *AuthorizationSpec) { + *out = *in + if in.Basic != nil { + in, out := &in.Basic, &out.Basic + *out = new(BasicAuthSpec) + (*in).DeepCopyInto(*out) + } + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(TokenAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationSpec. +func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { + if in == nil { + return nil + } + out := new(AuthorizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthSpec) DeepCopyInto(out *BasicAuthSpec) { + *out = *in + if in.CredentialsSecretRef != nil { + in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthSpec. +func (in *BasicAuthSpec) DeepCopy() *BasicAuthSpec { + if in == nil { + return nil + } + out := new(BasicAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientTLSConfig) DeepCopyInto(out *ClientTLSConfig) { + *out = *in + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.CABundleSecretRef != nil { + in, out := &in.CABundleSecretRef, &out.CABundleSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLSConfig. +func (in *ClientTLSConfig) DeepCopy() *ClientTLSConfig { + if in == nil { + return nil + } + out := new(ClientTLSConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in @@ -213,21 +283,6 @@ func (in *ClusterTargetState) DeepCopy() *ClusterTargetState { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConsulConfig) DeepCopyInto(out *ConsulConfig) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConsulConfig. -func (in *ConsulConfig) DeepCopy() *ConsulConfig { - if in == nil { - return nil - } - out := new(ConsulConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GRPCKeepAliveConfig) DeepCopyInto(out *GRPCKeepAliveConfig) { *out = *in @@ -273,6 +328,36 @@ func (in *GRPCTunnelConfig) DeepCopy() *GRPCTunnelConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPConfig) DeepCopyInto(out *HTTPConfig) { *out = *in + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(AuthorizationSpec) + (*in).DeepCopyInto(*out) + } + if in.PollInterval != nil { + in, out := &in.PollInterval, &out.PollInterval + *out = new(metav1.Duration) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(metav1.Duration) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ClientTLSConfig) + (*in).DeepCopyInto(*out) + } + if in.Pagination != nil { + in, out := &in.Pagination, &out.Pagination + *out = new(PaginationSpec) + **out = **in + } + if in.ResponseMapping != nil { + in, out := &in.ResponseMapping, &out.ResponseMapping + *out = new(ResponseMappingSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPConfig. @@ -587,6 +672,21 @@ func (in *OutputStatus) DeepCopy() *OutputStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PaginationSpec) DeepCopyInto(out *PaginationSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PaginationSpec. +func (in *PaginationSpec) DeepCopy() *PaginationSpec { + if in == nil { + return nil + } + out := new(PaginationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Pipeline) DeepCopyInto(out *Pipeline) { *out = *in @@ -824,12 +924,7 @@ func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) { if in.HTTP != nil { in, out := &in.HTTP, &out.HTTP *out = new(HTTPConfig) - **out = **in - } - if in.Consul != nil { - in, out := &in.Consul, &out.Consul - *out = new(ConsulConfig) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -843,6 +938,28 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseMappingSpec) DeepCopyInto(out *ResponseMappingSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseMappingSpec. +func (in *ResponseMappingSpec) DeepCopy() *ResponseMappingSpec { + if in == nil { + return nil + } + out := new(ResponseMappingSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) { *out = *in @@ -1384,6 +1501,26 @@ func (in *TargetTLSConfig) DeepCopy() *TargetTLSConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenAuthSpec) DeepCopyInto(out *TokenAuthSpec) { + *out = *in + if in.TokenSecretRef != nil { + in, out := &in.TokenSecretRef, &out.TokenSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenAuthSpec. +func (in *TokenAuthSpec) DeepCopy() *TokenAuthSpec { + if in == nil { + return nil + } + out := new(TokenAuthSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TunnelTargetPolicy) DeepCopyInto(out *TunnelTargetPolicy) { *out = *in diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 37d6919..d603546 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -40,33 +40,258 @@ spec: description: TargetSourceSpec defines the desired state of TargetSource properties: provider: + description: |- + Provider defines the source of targets for this TargetSource + Only one provider can be specified per TargetSource properties: - consul: - properties: - url: - minLength: 1 - type: string - type: object http: + description: HTTP defines the configuration for a HTTP provider properties: acceptPush: + default: false + description: |- + If true, the loader will accept pushed target updates to the controller endpoint + The endpoint will be /{namespace}/{targetsource}/ type: boolean + authorization: + description: Optional authorization configuration for accessing + the HTTP endpoint + properties: + basic: + description: Basic authentication configuration + properties: + credentialsSecretRef: + description: |- + Reference to a Secret containing "username" and "password" keys to use for + basic authentication when connecting to the Provider. + Mutually exclusive with Username and Password. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + description: |- + Password for basic auth + Mutually exclusive with CredentialsSecretRef. + type: string + username: + description: |- + Username for basic auth + Mutually exclusive with CredentialsSecretRef. + type: string + type: object + x-kubernetes-validations: + - message: either credentialsSecretRef OR both username + and password must be set, but not a mix + rule: (has(self.credentialsSecretRef) && !has(self.username) + && !has(self.password)) || (!has(self.credentialsSecretRef) + && has(self.username) && has(self.password)) + token: + description: Token-based authentication configuration + properties: + scheme: + description: Scheme for the token, e.g. "Bearer" + minLength: 1 + type: string + token: + description: |- + Token value for authentication + Mutually exclusive with TokenSecretRef. + type: string + tokenSecretRef: + description: |- + Reference to a Secret containing a key with the token value to use for + authentication when connecting to the Provider. + Mutually exclusive with Token. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - scheme + type: object + x-kubernetes-validations: + - message: either token or tokenSecretRef must be set, + but not both + rule: has(self.token) != has(self.tokenSecretRef) + type: object + x-kubernetes-validations: + - message: exactly one of the fields in [basic token] must + be set + rule: '[has(self.basic),has(self.token)].filter(x,x==true).size() + == 1' + interval: + default: 30s + description: Optional interval for polling the HTTP endpoint + for targets + type: string + mapping: + description: Optional mapping configuration for parsing responses + from the HTTP endpoint + properties: + address: + description: JSONPath expression to extract the target + address from the response + type: string + labels: + additionalProperties: + type: string + description: |- + JSONPath expression to extract the target labels from the response + The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, + with values from the response taking precedence in case of conflicts. + type: object + name: + description: JSONPath expression to extract the target + name from the response + type: string + port: + description: JSONPath expression to extract the target + port from the response + type: string + required: + - address + - name + type: object + pagination: + description: Optional pagination configuration for parsing + responses from the HTTP endpoint + properties: + itemsField: + description: |- + JSONPath-style expression to extract the list of targets from the response + Example: "results" + type: string + nextField: + description: |- + JSONPath-style expression to extract the next page token or URL from the response for pagination + Example: "next" + type: string + type: object + x-kubernetes-validations: + - message: static JWT token and generated JWT configuration + cannot be combined + rule: '!((has(self.token) || has(self.tokenSecretRef)) && + ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))' + - message: algorithm must be specified when generating a JWT + rule: '!has(self.signingKeySecretRef) || self.algorithm + != ""' + timeout: + default: 10s + description: Optional timeout for HTTP requests to the endpoint + type: string + tls: + description: Optional TLS configuration for connecting to + the HTTP endpoint + properties: + caBundle: + description: |- + Base64-encoded bundle of PEM CAs which will be used to validate the certificate + chain presented by the Provider. Only used if using HTTPS to connect to Provider and + ignored for HTTP connections. + Mutually exclusive with CABundleSecretRef. + format: byte + type: string + caBundleSecretRef: + description: |- + Reference to a Secret containing a bundle of PEM-encoded CAs to use when + verifying the certificate chain presented by the Provider when using HTTPS. + Mutually exclusive with CABundle. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + insecureSkipVerify: + default: false + description: Skip TLS verification of the Provider's certificate. + type: boolean + type: object + x-kubernetes-validations: + - message: caBundle and caBundleSecretRef are mutually exclusive + rule: '!(has(self.caBundle) && has(self.caBundleSecretRef))' url: - minLength: 1 + description: |- + URL of the HTTP endpoint to pull targets from + If defined, the loader will periodically poll this endpoint for targets type: string - required: - - url type: object + x-kubernetes-validations: + - message: at least one of the fields in [url acceptPush] must + be set + rule: '[has(self.url),has(self.acceptPush)].filter(x,x==true).size() + >= 1' type: object x-kubernetes-validations: - - message: exactly one of the fields in [http consul] must be set - rule: '[has(self.http),has(self.consul)].filter(x,x==true).size() - == 1' + - message: exactly one of the fields in [http] must be set + rule: '[has(self.http)].filter(x,x==true).size() == 1' targetLabels: additionalProperties: type: string + description: Optional labels to apply to all targets discovered by + this TargetSource type: object + targetPort: + description: Optional port to use for discovered targets if not specified + by the provider + format: int32 + type: integer targetProfile: + description: The TargetProfile to use for targets discovered by this + TargetSource minLength: 1 type: string required: From bcc0b4f993526db5ffd7b9f45dc6fd47c05c4d00 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 20 May 2026 13:30:54 -0600 Subject: [PATCH 02/20] cherry-pick 5d95c90: DiscoveredTarget type changes --- api/v1alpha1/targetsource_types.go | 4 ++-- .../bases/operator.gnmic.dev_targetsources.yaml | 6 +++--- go.mod | 2 ++ go.sum | 5 +++++ internal/controller/discovery/core/types.go | 7 ++++--- internal/controller/discovery/loaders.go | 2 -- .../controller/discovery/loaders/http/loader.go | 14 ++++++++------ internal/controller/discovery/message_processor.go | 3 ++- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 143da3c..666805d 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -189,9 +189,9 @@ type ResponseMappingSpec struct { // +kubebuilder:validation:Required Name string `json:"name"` - // JSONPath expression to extract the target address from the response + // JSONPath expression to extract the target IP from the response // +kubebuilder:validation:Required - Address string `json:"address"` + IP string `json:"ip"` // JSONPath expression to extract the target port from the response // +kubebuilder:validation:Optional diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index d603546..1b71922 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -165,9 +165,9 @@ spec: description: Optional mapping configuration for parsing responses from the HTTP endpoint properties: - address: + ip: description: JSONPath expression to extract the target - address from the response + IP from the response type: string labels: additionalProperties: @@ -186,7 +186,7 @@ spec: port from the response type: string required: - - address + - ip - name type: object pagination: diff --git a/go.mod b/go.mod index 9dc2b78..c877a7b 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,8 @@ require ( require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/PaesslerAG/gval v1.0.0 // indirect + github.com/PaesslerAG/jsonpath v0.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 45485f1..d900003 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,11 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= +github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= +github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cert-manager/cert-manager v1.19.3 h1:3d0Nk/HO3BOmAdBJNaBh+6YgaO3Ciey3xCpOjiX5Obs= diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 99605b9..51a3477 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -37,9 +37,10 @@ const ( // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { - Name string - Address string - Labels map[string]string + Name string + IP string + Port int32 + Labels map[string]string } type DiscoveryEvent struct { diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index c888c27..c4ebe78 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -15,8 +15,6 @@ func NewLoader(cfg *core.CommonLoaderConfig, spec gnmicv1alpha1.TargetSourceSpec case spec.Provider.HTTP != nil: cfg.AcceptPush = spec.Provider.HTTP.AcceptPush return http.New(*cfg), nil - case spec.Provider.Consul != nil: - return nil, fmt.Errorf("Unimplemented targetsource provider, check TargetSource CRD for %s", cfg.TargetsourceNN) default: return nil, fmt.Errorf("unknown targetsource provider, check TargetSource CRD for %s", cfg.TargetsourceNN) } diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index 3325adb..a2bfa0e 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -53,14 +53,16 @@ func (l *Loader) Run(ctx context.Context, out chan<- []core.DiscoveryMessage) er snapshotID := fmt.Sprintf("%s-%s-%s", l.commonCfg.TargetsourceNN.Namespace, l.commonCfg.TargetsourceNN.Name, uuid.NewString()) targets := []core.DiscoveredTarget{ { - Name: "ceos1", - Address: "clab-3-nodes-ceos1:6030", - Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, + Name: "ceos1", + IP: "clab-3-nodes-ceos1", + Port: 57400, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, { - Name: "leaf1", - Address: "clab-3-nodes-leaf1:57400", - Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, + Name: "leaf1", + IP: "clab-3-nodes-leaf1", + Port: 57400, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, } diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index f7aafb1..cb1e068 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -283,7 +283,8 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE logger.Info( "Applying Target", "target", event.Target.Name, - "address", event.Target.Address, + "port", event.Target.Port, + "ip", event.Target.IP, "labels", event.Target.Labels, "targetsource", m.targetSource.Name, ) From d523adb2653a9fc6abaa174b90a6589a04007b58 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 20 May 2026 13:33:18 -0600 Subject: [PATCH 03/20] renamed IP to Address --- api/v1alpha1/targetsource_types.go | 4 ++-- .../bases/operator.gnmic.dev_targetsources.yaml | 6 +++--- internal/controller/discovery/core/types.go | 8 ++++---- .../controller/discovery/loaders/http/loader.go | 16 ++++++++-------- .../controller/discovery/message_processor.go | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 666805d..143da3c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -189,9 +189,9 @@ type ResponseMappingSpec struct { // +kubebuilder:validation:Required Name string `json:"name"` - // JSONPath expression to extract the target IP from the response + // JSONPath expression to extract the target address from the response // +kubebuilder:validation:Required - IP string `json:"ip"` + Address string `json:"address"` // JSONPath expression to extract the target port from the response // +kubebuilder:validation:Optional diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 1b71922..d603546 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -165,9 +165,9 @@ spec: description: Optional mapping configuration for parsing responses from the HTTP endpoint properties: - ip: + address: description: JSONPath expression to extract the target - IP from the response + address from the response type: string labels: additionalProperties: @@ -186,7 +186,7 @@ spec: port from the response type: string required: - - ip + - address - name type: object pagination: diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 51a3477..66bbe50 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -37,10 +37,10 @@ const ( // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { - Name string - IP string - Port int32 - Labels map[string]string + Name string + Address string + Port int32 + Labels map[string]string } type DiscoveryEvent struct { diff --git a/internal/controller/discovery/loaders/http/loader.go b/internal/controller/discovery/loaders/http/loader.go index a2bfa0e..aace9cd 100644 --- a/internal/controller/discovery/loaders/http/loader.go +++ b/internal/controller/discovery/loaders/http/loader.go @@ -53,16 +53,16 @@ func (l *Loader) Run(ctx context.Context, out chan<- []core.DiscoveryMessage) er snapshotID := fmt.Sprintf("%s-%s-%s", l.commonCfg.TargetsourceNN.Namespace, l.commonCfg.TargetsourceNN.Name, uuid.NewString()) targets := []core.DiscoveredTarget{ { - Name: "ceos1", - IP: "clab-3-nodes-ceos1", - Port: 57400, - Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, + Name: "ceos1", + Address: "clab-3-nodes-ceos1", + Port: 57400, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, { - Name: "leaf1", - IP: "clab-3-nodes-leaf1", - Port: 57400, - Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, + Name: "leaf1", + Address: "clab-3-nodes-leaf1", + Port: 57400, + Labels: map[string]string{"TargetSource": l.commonCfg.TargetsourceNN.String()}, }, } diff --git a/internal/controller/discovery/message_processor.go b/internal/controller/discovery/message_processor.go index cb1e068..930635d 100644 --- a/internal/controller/discovery/message_processor.go +++ b/internal/controller/discovery/message_processor.go @@ -284,7 +284,7 @@ func (m *MessageProcessor) applyEvent(ctx context.Context, event core.DiscoveryE "Applying Target", "target", event.Target.Name, "port", event.Target.Port, - "ip", event.Target.IP, + "ip", event.Target.Address, "labels", event.Target.Labels, "targetsource", m.targetSource.Name, ) From 2c0d7cb2d69c7852ea0fbab9262013931bf3a8e2 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 20 May 2026 14:24:27 -0600 Subject: [PATCH 04/20] disabled JWTAuthSpec validations --- api/v1alpha1/targetsource_types.go | 4 ++-- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 143da3c..5bbfcec 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -156,8 +156,8 @@ type TokenAuthSpec struct { TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } -// +kubebuilder:validation:XValidation:rule="!((has(self.token) || has(self.tokenSecretRef)) && ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))",message="static JWT token and generated JWT configuration cannot be combined" -// +kubebuilder:validation:XValidation:rule="!has(self.signingKeySecretRef) || self.algorithm != \"\"",message="algorithm must be specified when generating a JWT" +// disabled: +kubebuilder:validation:XValidation:rule="!((has(self.token) || has(self.tokenSecretRef)) && ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))",message="static JWT token and generated JWT configuration cannot be combined" +// disabled: +kubebuilder:validation:XValidation:rule="!has(self.signingKeySecretRef) || self.algorithm != \"\"",message="algorithm must be specified when generating a JWT" // type JWTAuthSpec struct { // // Static pre-generated JWT // Token string `json:"token,omitempty"` diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index d603546..6851ad7 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -204,14 +204,6 @@ spec: Example: "next" type: string type: object - x-kubernetes-validations: - - message: static JWT token and generated JWT configuration - cannot be combined - rule: '!((has(self.token) || has(self.tokenSecretRef)) && - ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))' - - message: algorithm must be specified when generating a JWT - rule: '!has(self.signingKeySecretRef) || self.algorithm - != ""' timeout: default: 10s description: Optional timeout for HTTP requests to the endpoint From 77a5bd4277eba07fb623ef7b4b21c6e2a2fc1bf7 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Wed, 20 May 2026 19:04:22 -0600 Subject: [PATCH 05/20] cherry-pick 6a83f49: added TargetProfile variable to DiscoveredTarget --- api/v1alpha1/targetsource_types.go | 4 ++++ config/crd/bases/operator.gnmic.dev_targetsources.yaml | 4 ++++ internal/controller/discovery/core/types.go | 9 +++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 5bbfcec..5b0f8e5 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -202,6 +202,10 @@ type ResponseMappingSpec struct { // with values from the response taking precedence in case of conflicts. // +kubebuilder:validation:Optional Labels map[string]string `json:"labels,omitempty"` + + // JSONPath expression to extract the target profile from the response + // +kubebuilder:validation:Optional + TargetProfile string `json:"targetProfile,omitempty"` } // TargetSourceStatus defines the observed state of TargetSource diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 6851ad7..5816ecd 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -185,6 +185,10 @@ spec: description: JSONPath expression to extract the target port from the response type: string + targetProfile: + description: JSONPath expression to extract the target + profile from the response + type: string required: - address - name diff --git a/internal/controller/discovery/core/types.go b/internal/controller/discovery/core/types.go index 66bbe50..27c4774 100644 --- a/internal/controller/discovery/core/types.go +++ b/internal/controller/discovery/core/types.go @@ -37,10 +37,11 @@ const ( // DiscoveredTarget represents a target discovered from an external source // before it is materialized as a Kubernetes Target CR type DiscoveredTarget struct { - Name string - Address string - Port int32 - Labels map[string]string + Name string + Address string + Port int32 + Labels map[string]string + TargetProfile string } type DiscoveryEvent struct { From 0d744ba61e263f1b8ad08470ca3b4e42b1d17b33 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 13:47:23 -0600 Subject: [PATCH 06/20] added port + targetProfile handling to mapper --- internal/controller/discovery/mapper.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/controller/discovery/mapper.go b/internal/controller/discovery/mapper.go index bc42531..4690fd1 100644 --- a/internal/controller/discovery/mapper.go +++ b/internal/controller/discovery/mapper.go @@ -1,6 +1,7 @@ package discovery import ( + "fmt" "maps" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,10 +21,19 @@ func generateTargetResource(d core.DiscoveredTarget, ts *gnmicv1alpha1.TargetSou }, } - // Add Address from DiscoveredTarget - t.Spec.Address = d.Address - // Add default Target Profile from the TargetSource Spec TargetProfile - t.Spec.Profile = ts.Spec.TargetProfile + // Add Address + Port from DiscoveredTarget or use TargetSource.spec.targetPort + targetPort := ts.Spec.TargetPort + if d.Port != 0 { + targetPort = d.Port + } + t.Spec.Address = fmt.Sprintf("%s:%d", d.Address, targetPort) + + // Add discovered Target Profile or use TargetSource.spec.targetProfile + targetProfile := ts.Spec.TargetProfile + if d.TargetProfile != "" { + targetProfile = d.TargetProfile + } + t.Spec.Profile = targetProfile // Copy TargetLabels from TargetSource Spec & DiscoveredTarget. Discovered labels take precedence over TargetSource labels. maps.Copy(t.Labels, ts.Spec.TargetLabels) From 5b612f2d6ea32ebfd8b3c4567a23a48c47fe1c24 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 14:09:55 -0600 Subject: [PATCH 07/20] added webhook spec for authorization --- api/v1alpha1/targetsource_types.go | 40 ++++++-- api/v1alpha1/zz_generated.deepcopy.go | 90 ++++++++++++++++++ .../operator.gnmic.dev_targetsources.yaml | 91 +++++++++++++++++-- 3 files changed, 209 insertions(+), 12 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index e8b4c4c..7d96c0d 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -60,12 +60,6 @@ type HTTPConfig struct { // +kubebuilder:validation:Optional URL string `json:"url,omitempty"` - // If true, the loader will accept pushed target updates to the controller endpoint - // The endpoint will be /{namespace}/{targetsource}/ - // +kubebuilder:default=false - // +kubebuilder:validation:Optional - AcceptPush bool `json:"acceptPush,omitempty"` - // Optional authorization configuration for accessing the HTTP endpoint // +kubebuilder:validation:Optional Authorization *AuthorizationSpec `json:"authorization,omitempty"` @@ -92,6 +86,10 @@ type HTTPConfig struct { // Optional mapping configuration for parsing responses from the HTTP endpoint // +kubebuilder:validation:Optional ResponseMapping *ResponseMappingSpec `json:"mapping,omitempty"` + + // Optional configuration to enable webhooks + // +kubebuilder:validation:Optional + Webhook *WebhookSpec `json:"webhook,omitempty"` } // +kubebuilder:validation:XValidation:rule="!(has(self.caBundle) && has(self.caBundleSecretRef))",message="caBundle and caBundleSecretRef are mutually exclusive" @@ -208,6 +206,36 @@ type ResponseMappingSpec struct { TargetProfile string `json:"targetProfile,omitempty"` } +// WebhookSpec defines the settings for event-based update mechanism (i.e. push-based) +type WebhookSpec struct { + // +kubebuilder:default=false + Enabled bool `json:"enabled"` + + // +kubebuilder:validation:Optional + Auth *WebhookAuthSpec `json:"auth,omitempty"` +} + +// +kubebuilder:validation:ExactlyOneOf=bearer;signature +type WebhookAuthSpec struct { + Bearer *WebhookBearerAuthSpec `json:"bearer,omitempty"` + Signature *WebhookSignatureAuthSpec `json:"signature,omitempty"` +} + +type WebhookBearerAuthSpec struct { + TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` +} + +type WebhookSignatureAuthSpec struct { + SecretRef *corev1.SecretKeySelector `json:"secretRef"` + + // Header containing the signature + Header string `json:"header,omitempty"` + + // +kubebuilder:default="sha256" + // +kubebuilder:validation:Enum=sha1;sha256;sha512 + Algorithm string `json:"algorithm,omitempty"` +} + // TargetSourceStatus defines the observed state of TargetSource type TargetSourceStatus struct { Status string `json:"status,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index dc4b784..9d59d3c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -358,6 +358,11 @@ func (in *HTTPConfig) DeepCopyInto(out *HTTPConfig) { *out = new(ResponseMappingSpec) (*in).DeepCopyInto(*out) } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(WebhookSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPConfig. @@ -1614,3 +1619,88 @@ func (in *TunnelTargetPolicyStatus) DeepCopy() *TunnelTargetPolicyStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookAuthSpec) DeepCopyInto(out *WebhookAuthSpec) { + *out = *in + if in.Bearer != nil { + in, out := &in.Bearer, &out.Bearer + *out = new(WebhookBearerAuthSpec) + (*in).DeepCopyInto(*out) + } + if in.Signature != nil { + in, out := &in.Signature, &out.Signature + *out = new(WebhookSignatureAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthSpec. +func (in *WebhookAuthSpec) DeepCopy() *WebhookAuthSpec { + if in == nil { + return nil + } + out := new(WebhookAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookBearerAuthSpec) DeepCopyInto(out *WebhookBearerAuthSpec) { + *out = *in + if in.TokenSecretRef != nil { + in, out := &in.TokenSecretRef, &out.TokenSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookBearerAuthSpec. +func (in *WebhookBearerAuthSpec) DeepCopy() *WebhookBearerAuthSpec { + if in == nil { + return nil + } + out := new(WebhookBearerAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookSignatureAuthSpec) DeepCopyInto(out *WebhookSignatureAuthSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSignatureAuthSpec. +func (in *WebhookSignatureAuthSpec) DeepCopy() *WebhookSignatureAuthSpec { + if in == nil { + return nil + } + out := new(WebhookSignatureAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) { + *out = *in + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(WebhookAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec. +func (in *WebhookSpec) DeepCopy() *WebhookSpec { + if in == nil { + return nil + } + out := new(WebhookSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index d6def2b..81c8e20 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -47,12 +47,6 @@ spec: http: description: HTTP defines the configuration for a HTTP provider properties: - acceptPush: - default: false - description: |- - If true, the loader will accept pushed target updates to the controller endpoint - The endpoint will be /{namespace}/{targetsource}/ - type: boolean authorization: description: Optional authorization configuration for accessing the HTTP endpoint @@ -264,6 +258,91 @@ spec: URL of the HTTP endpoint to pull targets from If defined, the loader will periodically poll this endpoint for targets type: string + webhook: + description: Optional configuration to enable webhooks + properties: + auth: + properties: + bearer: + properties: + tokenSecretRef: + description: SecretKeySelector selects a key of + a Secret. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + signature: + properties: + algorithm: + default: sha256 + enum: + - sha1 + - sha256 + - sha512 + type: string + header: + description: Header containing the signature + type: string + secretRef: + description: SecretKeySelector selects a key of + a Secret. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + type: object + x-kubernetes-validations: + - message: exactly one of the fields in [bearer signature] + must be set + rule: '[has(self.bearer),has(self.signature)].filter(x,x==true).size() + == 1' + enabled: + default: false + type: boolean + required: + - enabled + type: object type: object x-kubernetes-validations: - message: at least one of the fields in [url acceptPush] must From 58b77d4929be55d594894c1f7fbaf648605cf1c2 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 14:11:23 -0600 Subject: [PATCH 08/20] removed inline credential fields --- api/v1alpha1/targetsource_types.go | 15 ----------- .../operator.gnmic.dev_targetsources.yaml | 27 ------------------- 2 files changed, 42 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 7d96c0d..6e72439 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -123,34 +123,19 @@ type AuthorizationSpec struct { } // BasicAuthSpec defines the configuration for basic authentication -// Enforce EITHER inline creds OR secret ref -// +kubebuilder:validation:XValidation:rule="(has(self.credentialsSecretRef) && !has(self.username) && !has(self.password)) || (!has(self.credentialsSecretRef) && has(self.username) && has(self.password))",message="either credentialsSecretRef OR both username and password must be set, but not a mix" type BasicAuthSpec struct { - // Username for basic auth - // Mutually exclusive with CredentialsSecretRef. - Username string `json:"username,omitempty"` - // Password for basic auth - // Mutually exclusive with CredentialsSecretRef. - Password string `json:"password,omitempty"` - // Reference to a Secret containing "username" and "password" keys to use for // basic authentication when connecting to the Provider. - // Mutually exclusive with Username and Password. CredentialsSecretRef *corev1.SecretKeySelector `json:"credentialsSecretRef,omitempty"` } // TokenAuthSpec defines the configuration for token-based authentication -// +kubebuilder:validation:XValidation:rule="has(self.token) != has(self.tokenSecretRef)",message="either token or tokenSecretRef must be set, but not both" type TokenAuthSpec struct { // Scheme for the token, e.g. "Bearer" // +kubebuilder:validation:MinLength=1 Scheme string `json:"scheme"` - // Token value for authentication - // Mutually exclusive with TokenSecretRef. - Token string `json:"token,omitempty"` // Reference to a Secret containing a key with the token value to use for // authentication when connecting to the Provider. - // Mutually exclusive with Token. TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 81c8e20..d333a9e 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -58,7 +58,6 @@ spec: description: |- Reference to a Secret containing "username" and "password" keys to use for basic authentication when connecting to the Provider. - Mutually exclusive with Username and Password. properties: key: description: The key of the secret to select from. Must @@ -81,23 +80,7 @@ spec: - key type: object x-kubernetes-map-type: atomic - password: - description: |- - Password for basic auth - Mutually exclusive with CredentialsSecretRef. - type: string - username: - description: |- - Username for basic auth - Mutually exclusive with CredentialsSecretRef. - type: string type: object - x-kubernetes-validations: - - message: either credentialsSecretRef OR both username - and password must be set, but not a mix - rule: (has(self.credentialsSecretRef) && !has(self.username) - && !has(self.password)) || (!has(self.credentialsSecretRef) - && has(self.username) && has(self.password)) token: description: Token-based authentication configuration properties: @@ -105,16 +88,10 @@ spec: description: Scheme for the token, e.g. "Bearer" minLength: 1 type: string - token: - description: |- - Token value for authentication - Mutually exclusive with TokenSecretRef. - type: string tokenSecretRef: description: |- Reference to a Secret containing a key with the token value to use for authentication when connecting to the Provider. - Mutually exclusive with Token. properties: key: description: The key of the secret to select from. Must @@ -140,10 +117,6 @@ spec: required: - scheme type: object - x-kubernetes-validations: - - message: either token or tokenSecretRef must be set, - but not both - rule: has(self.token) != has(self.tokenSecretRef) type: object x-kubernetes-validations: - message: exactly one of the fields in [basic token] must From aa023abdd11feb748b9d91c5627f116613fa2cb3 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 21 May 2026 09:25:46 +0000 Subject: [PATCH 09/20] use itemsField independent of pagination --- api/v1alpha1/targetsource_types.go | 21 +++++++++++++------ .../operator.gnmic.dev_targetsources.yaml | 17 ++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 6e72439..63bd535 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -76,9 +76,17 @@ type HTTPConfig struct { Timeout *metav1.Duration `json:"timeout,omitempty"` // Optional TLS configuration for connecting to the HTTP endpoint + // If it is an HTTP endpoint, this will be ignored // +kubebuilder:validation:Optional TLS *ClientTLSConfig `json:"tls,omitempty"` + // Field name in the JSON response that contains the list of items (targets). + // Must refer to a top-level key in the response object. + // If not specified, the entire response is expected to be a list of items. + // Example: "results" + // +kubebuilder:validation:Optional + ItemsField string `json:"itemsField,omitempty"` + // Optional pagination configuration for parsing responses from the HTTP endpoint // +kubebuilder:validation:Optional Pagination *PaginationSpec `json:"pagination,omitempty"` @@ -156,12 +164,13 @@ type TokenAuthSpec struct { // PaginationSpec defines the configuration for paginating through responses from providers type PaginationSpec struct { - // JSONPath-style expression to extract the list of targets from the response - // Example: "results" - ItemsField string `json:"itemsField,omitempty"` - - // JSONPath-style expression to extract the next page token or URL from the response for pagination - // Example: "next" + // Field name in the JSON response that contains the next page reference. + // The value can be either: + // - a full URL (used directly for the next request), or + // - a pagination token (appended as a query parameter using this field name as the key). + // + // Must refer to a top-level key in the response object. + // Example: "next" or "nextToken" NextField string `json:"nextField,omitempty"` } diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index d333a9e..f3a66f2 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -128,6 +128,13 @@ spec: description: Optional interval for polling the HTTP endpoint for targets type: string + itemsField: + description: |- + Field name in the JSON response that contains the list of items (targets). + Must refer to a top-level key in the response object. + If not specified, the entire response is expected to be a list of items. + Example: "results" + type: string mapping: description: Optional mapping configuration for parsing responses from the HTTP endpoint @@ -164,11 +171,6 @@ spec: description: Optional pagination configuration for parsing responses from the HTTP endpoint properties: - itemsField: - description: |- - JSONPath-style expression to extract the list of targets from the response - Example: "results" - type: string nextField: description: |- JSONPath-style expression to extract the next page token or URL from the response for pagination @@ -180,8 +182,9 @@ spec: description: Optional timeout for HTTP requests to the endpoint type: string tls: - description: Optional TLS configuration for connecting to - the HTTP endpoint + description: |- + Optional TLS configuration for connecting to the HTTP endpoint + If it is an HTTP endpoint, this will be ignored properties: caBundle: description: |- From fc6008da9ded3f1a5edff8150519bbaf557bf26d Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Thu, 21 May 2026 09:27:00 +0000 Subject: [PATCH 10/20] rename ItemsField to TargetsField --- api/v1alpha1/targetsource_types.go | 2 +- .../bases/operator.gnmic.dev_targetsources.yaml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 63bd535..d0045be 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -85,7 +85,7 @@ type HTTPConfig struct { // If not specified, the entire response is expected to be a list of items. // Example: "results" // +kubebuilder:validation:Optional - ItemsField string `json:"itemsField,omitempty"` + TargetsField string `json:"targetsField,omitempty"` // Optional pagination configuration for parsing responses from the HTTP endpoint // +kubebuilder:validation:Optional diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index f3a66f2..08615c8 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -128,13 +128,6 @@ spec: description: Optional interval for polling the HTTP endpoint for targets type: string - itemsField: - description: |- - Field name in the JSON response that contains the list of items (targets). - Must refer to a top-level key in the response object. - If not specified, the entire response is expected to be a list of items. - Example: "results" - type: string mapping: description: Optional mapping configuration for parsing responses from the HTTP endpoint @@ -177,6 +170,13 @@ spec: Example: "next" type: string type: object + targetsField: + description: |- + Field name in the JSON response that contains the list of items (targets). + Must refer to a top-level key in the response object. + If not specified, the entire response is expected to be a list of items. + Example: "results" + type: string timeout: default: 10s description: Optional timeout for HTTP requests to the endpoint From 0c1b414b609d5b2047b10fa125d0f2fd2658acd6 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 14:16:01 -0600 Subject: [PATCH 11/20] changed assertion to new webhook spec --- internal/controller/discovery/loaders.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index c4ebe78..dce3928 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -13,7 +13,7 @@ func NewLoader(cfg *core.CommonLoaderConfig, spec gnmicv1alpha1.TargetSourceSpec switch { case spec.Provider.HTTP != nil: - cfg.AcceptPush = spec.Provider.HTTP.AcceptPush + cfg.AcceptPush = spec.Provider.HTTP.Webhook.Enabled return http.New(*cfg), nil default: return nil, fmt.Errorf("unknown targetsource provider, check TargetSource CRD for %s", cfg.TargetsourceNN) From 7e06e146f98b266638da1a3f034bf2b41e4bb962 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 14:18:04 -0600 Subject: [PATCH 12/20] moved TargetsField to ResponseMappingSpec --- api/v1alpha1/targetsource_types.go | 26 ++++++------- .../operator.gnmic.dev_targetsources.yaml | 37 ++++++++++--------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index d0045be..0ca4181 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -80,13 +80,6 @@ type HTTPConfig struct { // +kubebuilder:validation:Optional TLS *ClientTLSConfig `json:"tls,omitempty"` - // Field name in the JSON response that contains the list of items (targets). - // Must refer to a top-level key in the response object. - // If not specified, the entire response is expected to be a list of items. - // Example: "results" - // +kubebuilder:validation:Optional - TargetsField string `json:"targetsField,omitempty"` - // Optional pagination configuration for parsing responses from the HTTP endpoint // +kubebuilder:validation:Optional Pagination *PaginationSpec `json:"pagination,omitempty"` @@ -177,25 +170,30 @@ type PaginationSpec struct { // JSONPath-style expressions to extract target fields from the response // and map them to the corresponding Target fields. type ResponseMappingSpec struct { - // JSONPath expression to extract the target name from the response + // Field name in the JSON response that contains the list of items (targets). + // If not specified, the entire response is expected to be a list of items. + // All subsequent fields are specified relative to this field + // Example: "results" + // +kubebuilder:validation:Optional + TargetsField string `json:"targetsField,omitempty"` + + // JSONPath expression to extract the target name from the response list // +kubebuilder:validation:Required Name string `json:"name"` - // JSONPath expression to extract the target address from the response + // JSONPath expression to extract the target address from the response list // +kubebuilder:validation:Required Address string `json:"address"` - // JSONPath expression to extract the target port from the response + // JSONPath expression to extract the target port from the response list // +kubebuilder:validation:Optional Port string `json:"port,omitempty"` - // JSONPath expression to extract the target labels from the response - // The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, - // with values from the response taking precedence in case of conflicts. + // JSONPath expression to extract the target labels from the response list // +kubebuilder:validation:Optional Labels map[string]string `json:"labels,omitempty"` - // JSONPath expression to extract the target profile from the response + // JSONPath expression to extract the target profile from the response list // +kubebuilder:validation:Optional TargetProfile string `json:"targetProfile,omitempty"` } diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 08615c8..e68385d 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -134,27 +134,32 @@ spec: properties: address: description: JSONPath expression to extract the target - address from the response + address from the response list type: string labels: additionalProperties: type: string - description: |- - JSONPath expression to extract the target labels from the response - The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, - with values from the response taking precedence in case of conflicts. + description: JSONPath expression to extract the target + labels from the response list type: object name: description: JSONPath expression to extract the target - name from the response + name from the response list type: string port: description: JSONPath expression to extract the target - port from the response + port from the response list type: string targetProfile: description: JSONPath expression to extract the target - profile from the response + profile from the response list + type: string + targetsField: + description: |- + Field name in the JSON response that contains the list of items (targets). + If not specified, the entire response is expected to be a list of items. + All subsequent fields are specified relative to this field + Example: "results" type: string required: - address @@ -166,17 +171,15 @@ spec: properties: nextField: description: |- - JSONPath-style expression to extract the next page token or URL from the response for pagination - Example: "next" + Field name in the JSON response that contains the next page reference. + The value can be either: + - a full URL (used directly for the next request), or + - a pagination token (appended as a query parameter using this field name as the key). + + Must refer to a top-level key in the response object. + Example: "next" or "nextToken" type: string type: object - targetsField: - description: |- - Field name in the JSON response that contains the list of items (targets). - Must refer to a top-level key in the response object. - If not specified, the entire response is expected to be a list of items. - Example: "results" - type: string timeout: default: 10s description: Optional timeout for HTTP requests to the endpoint From 421d40ff3baf3c8c178000769beb968f76fcc03e Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 14:22:38 -0600 Subject: [PATCH 13/20] changed validation rule for url or webhook --- api/v1alpha1/targetsource_types.go | 2 +- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 0ca4181..e5776ca 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -53,7 +53,7 @@ type ProviderSpec struct { } // HTTPConfig defines the configuration for the HTTP provider -// +kubebuilder:validation:AtLeastOneOf=url;acceptPush +// +kubebuilder:validation:AtLeastOneOf=url;webhook type HTTPConfig struct { // URL of the HTTP endpoint to pull targets from // If defined, the loader will periodically poll this endpoint for targets diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index e68385d..0aae289 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -324,9 +324,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: at least one of the fields in [url acceptPush] must - be set - rule: '[has(self.url),has(self.acceptPush)].filter(x,x==true).size() + - message: at least one of the fields in [url webhook] must be + set + rule: '[has(self.url),has(self.webhook)].filter(x,x==true).size() >= 1' type: object x-kubernetes-validations: From 85c46b5ec6e7ae599262968b58ade8795baca4c0 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 16:25:49 -0600 Subject: [PATCH 14/20] reworked kubebuilder validations --- api/v1alpha1/targetsource_types.go | 13 ++++++++----- .../crd/bases/operator.gnmic.dev_targetsources.yaml | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index e5776ca..9febc06 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -53,7 +53,7 @@ type ProviderSpec struct { } // HTTPConfig defines the configuration for the HTTP provider -// +kubebuilder:validation:AtLeastOneOf=url;webhook +// +kubebuilder:validation:AtLeastOneOf:=url;webhook type HTTPConfig struct { // URL of the HTTP endpoint to pull targets from // If defined, the loader will periodically poll this endpoint for targets @@ -207,25 +207,28 @@ type WebhookSpec struct { Auth *WebhookAuthSpec `json:"auth,omitempty"` } -// +kubebuilder:validation:ExactlyOneOf=bearer;signature +// +kubebuilder:validation:ExactlyOneOf:=bearer;signature type WebhookAuthSpec struct { Bearer *WebhookBearerAuthSpec `json:"bearer,omitempty"` Signature *WebhookSignatureAuthSpec `json:"signature,omitempty"` } +// +kubebuilder:validation:Required type WebhookBearerAuthSpec struct { TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } +// +kubebuilder:validation:Required type WebhookSignatureAuthSpec struct { SecretRef *corev1.SecretKeySelector `json:"secretRef"` // Header containing the signature - Header string `json:"header,omitempty"` + // +kubebuilder:validation:MinLength=1 + Header string `json:"header"` - // +kubebuilder:default="sha256" + // +kubebuilder:default="sha512" // +kubebuilder:validation:Enum=sha1;sha256;sha512 - Algorithm string `json:"algorithm,omitempty"` + Algorithm string `json:"algorithm"` } // TargetSourceStatus defines the observed state of TargetSource diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 0aae289..63ae646 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -281,6 +281,7 @@ spec: type: string header: description: Header containing the signature + minLength: 1 type: string secretRef: description: SecretKeySelector selects a key of @@ -308,6 +309,7 @@ spec: type: object x-kubernetes-map-type: atomic required: + - header - secretRef type: object type: object From e7a62dff98fd03d420af63defa146e38540cc6a4 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Thu, 21 May 2026 16:33:20 -0600 Subject: [PATCH 15/20] generated manifests --- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 63ae646..7816afc 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -273,7 +273,7 @@ spec: signature: properties: algorithm: - default: sha256 + default: sha512 enum: - sha1 - sha256 @@ -309,6 +309,7 @@ spec: type: object x-kubernetes-map-type: atomic required: + - algorithm - header - secretRef type: object From 022ed92641fb6298958db72dc3db213026e5067f Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 22 May 2026 14:10:49 +0000 Subject: [PATCH 16/20] refactor --- api/v1alpha1/targetsource_types.go | 66 ++++---- api/v1alpha1/zz_generated.deepcopy.go | 57 ++++--- .../operator.gnmic.dev_targetsources.yaml | 141 +++++++++--------- internal/controller/discovery/loaders.go | 2 +- 4 files changed, 120 insertions(+), 146 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 9febc06..8cfb6ad 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -88,28 +88,20 @@ type HTTPConfig struct { // +kubebuilder:validation:Optional ResponseMapping *ResponseMappingSpec `json:"mapping,omitempty"` - // Optional configuration to enable webhooks + // Optional configuration to enable push // +kubebuilder:validation:Optional - Webhook *WebhookSpec `json:"webhook,omitempty"` + Push *PushSpec `json:"push,omitempty"` } -// +kubebuilder:validation:XValidation:rule="!(has(self.caBundle) && has(self.caBundleSecretRef))",message="caBundle and caBundleSecretRef are mutually exclusive" type ClientTLSConfig struct { // Skip TLS verification of the Provider's certificate. // +kubebuilder:default:=false InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` - // Base64-encoded bundle of PEM CAs which will be used to validate the certificate - // chain presented by the Provider. Only used if using HTTPS to connect to Provider and - // ignored for HTTP connections. - // Mutually exclusive with CABundleSecretRef. - // +optional - CABundle []byte `json:"caBundle,omitempty"` - - // Reference to a Secret containing a bundle of PEM-encoded CAs to use when + // Reference to a ConfigMap containing a bundle of PEM-encoded CAs to use when // verifying the certificate chain presented by the Provider when using HTTPS. // Mutually exclusive with CABundle. - CABundleSecretRef *corev1.SecretKeySelector `json:"caBundleSecretRef,omitempty"` + CABundleRef *corev1.ConfigMapKeySelector `json:"caBundleSecretRef,omitempty"` } // AuthorizationSpec defines the configuration for authentication @@ -119,15 +111,14 @@ type AuthorizationSpec struct { Basic *BasicAuthSpec `json:"basic,omitempty"` // Token-based authentication configuration Token *TokenAuthSpec `json:"token,omitempty"` - // JWT *JWTAuthSpec `json:"jwt,omitempty"` - // MTLS } // BasicAuthSpec defines the configuration for basic authentication type BasicAuthSpec struct { // Reference to a Secret containing "username" and "password" keys to use for // basic authentication when connecting to the Provider. - CredentialsSecretRef *corev1.SecretKeySelector `json:"credentialsSecretRef,omitempty"` + // +kubebuilder:validation:Required + CredentialsSecretRef *corev1.SecretKeySelector `json:"credentialsSecretRef"` } // TokenAuthSpec defines the configuration for token-based authentication @@ -137,24 +128,11 @@ type TokenAuthSpec struct { Scheme string `json:"scheme"` // Reference to a Secret containing a key with the token value to use for // authentication when connecting to the Provider. + // Mutually exclusive with Token. + // +kubebuilder:validation:Required TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } -// disabled: +kubebuilder:validation:XValidation:rule="!((has(self.token) || has(self.tokenSecretRef)) && ((has(self.key) || has(self.signingKeySecretRef) || has(self.claims)))",message="static JWT token and generated JWT configuration cannot be combined" -// disabled: +kubebuilder:validation:XValidation:rule="!has(self.signingKeySecretRef) || self.algorithm != \"\"",message="algorithm must be specified when generating a JWT" -// type JWTAuthSpec struct { -// // Static pre-generated JWT -// Token string `json:"token,omitempty"` -// TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` -// // Optional: generate JWT dynamically -// Claims map[string]string `json:"claims,omitempty"` -// Key string `json:"key,omitempty"` -// SigningKeySecretRef *corev1.SecretKeySelector `json:"signingKeySecretRef,omitempty"` -// // HS256, RS256, ES256, etc. -// Algorithm string `json:"algorithm,omitempty"` -// TTL *metav1.Duration `json:"ttl,omitempty"` -// } - // PaginationSpec defines the configuration for paginating through responses from providers type PaginationSpec struct { // Field name in the JSON response that contains the next page reference. @@ -167,39 +145,45 @@ type PaginationSpec struct { NextField string `json:"nextField,omitempty"` } -// JSONPath-style expressions to extract target fields from the response +// CEL expressions to extract target fields from the response // and map them to the corresponding Target fields. type ResponseMappingSpec struct { // Field name in the JSON response that contains the list of items (targets). // If not specified, the entire response is expected to be a list of items. // All subsequent fields are specified relative to this field - // Example: "results" + // Example: "results" if the response is of the form {"results": [ ... list of items ... ]} // +kubebuilder:validation:Optional TargetsField string `json:"targetsField,omitempty"` - // JSONPath expression to extract the target name from the response list - // +kubebuilder:validation:Required + // CEL expression to extract the target name from the response + // If TargetsField is specified, this should be relative to TargetsField + // +kubebuilder:validation:Optional Name string `json:"name"` - // JSONPath expression to extract the target address from the response list - // +kubebuilder:validation:Required + // CEL expression to extract the target Address from the response + // If TargetsField is specified, this should be relative to TargetsField + // +kubebuilder:validation:Optional Address string `json:"address"` - // JSONPath expression to extract the target port from the response list + // CEL expression to extract the target port from the response + // If TargetsField is specified, this should be relative to TargetsField // +kubebuilder:validation:Optional Port string `json:"port,omitempty"` - // JSONPath expression to extract the target labels from the response list + // CEL expression to extract the target labels from the response + // The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, + // with values from the response taking precedence in case of conflicts. // +kubebuilder:validation:Optional Labels map[string]string `json:"labels,omitempty"` - // JSONPath expression to extract the target profile from the response list + // CEL expression to extract the target profile from the response + // If TargetsField is specified, this should be relative to TargetsField // +kubebuilder:validation:Optional TargetProfile string `json:"targetProfile,omitempty"` } -// WebhookSpec defines the settings for event-based update mechanism (i.e. push-based) -type WebhookSpec struct { +// PushSpec defines the settings for event-based update mechanism (i.e. push-based) +type PushSpec struct { // +kubebuilder:default=false Enabled bool `json:"enabled"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9d59d3c..c9a7235 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -94,14 +94,9 @@ func (in *BasicAuthSpec) DeepCopy() *BasicAuthSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientTLSConfig) DeepCopyInto(out *ClientTLSConfig) { *out = *in - if in.CABundle != nil { - in, out := &in.CABundle, &out.CABundle - *out = make([]byte, len(*in)) - copy(*out, *in) - } - if in.CABundleSecretRef != nil { - in, out := &in.CABundleSecretRef, &out.CABundleSecretRef - *out = new(v1.SecretKeySelector) + if in.CABundleRef != nil { + in, out := &in.CABundleRef, &out.CABundleRef + *out = new(v1.ConfigMapKeySelector) (*in).DeepCopyInto(*out) } } @@ -358,9 +353,9 @@ func (in *HTTPConfig) DeepCopyInto(out *HTTPConfig) { *out = new(ResponseMappingSpec) (*in).DeepCopyInto(*out) } - if in.Webhook != nil { - in, out := &in.Webhook, &out.Webhook - *out = new(WebhookSpec) + if in.Push != nil { + in, out := &in.Push, &out.Push + *out = new(PushSpec) (*in).DeepCopyInto(*out) } } @@ -943,6 +938,26 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushSpec) DeepCopyInto(out *PushSpec) { + *out = *in + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(WebhookAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSpec. +func (in *PushSpec) DeepCopy() *PushSpec { + if in == nil { + return nil + } + out := new(PushSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResponseMappingSpec) DeepCopyInto(out *ResponseMappingSpec) { *out = *in @@ -1684,23 +1699,3 @@ func (in *WebhookSignatureAuthSpec) DeepCopy() *WebhookSignatureAuthSpec { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookSpec) DeepCopyInto(out *WebhookSpec) { - *out = *in - if in.Auth != nil { - in, out := &in.Auth, &out.Auth - *out = new(WebhookAuthSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSpec. -func (in *WebhookSpec) DeepCopy() *WebhookSpec { - if in == nil { - return nil - } - out := new(WebhookSpec) - in.DeepCopyInto(out) - return out -} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 7816afc..b3eaa7d 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -80,6 +80,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + required: + - credentialsSecretRef type: object token: description: Token-based authentication configuration @@ -92,6 +94,7 @@ spec: description: |- Reference to a Secret containing a key with the token value to use for authentication when connecting to the Provider. + Mutually exclusive with Token. properties: key: description: The key of the secret to select from. Must @@ -116,6 +119,7 @@ spec: x-kubernetes-map-type: atomic required: - scheme + - tokenSecretRef type: object type: object x-kubernetes-validations: @@ -133,37 +137,40 @@ spec: from the HTTP endpoint properties: address: - description: JSONPath expression to extract the target - address from the response list + description: |- + CEL expression to extract the target Address from the response + If TargetsField is specified, this should be relative to TargetsField type: string labels: additionalProperties: type: string - description: JSONPath expression to extract the target - labels from the response list + description: |- + CEL expression to extract the target labels from the response + The extracted labels will be merged with the static TargetLabels defined in the TargetSourceSpec, + with values from the response taking precedence in case of conflicts. type: object name: - description: JSONPath expression to extract the target - name from the response list + description: |- + CEL expression to extract the target name from the response + If TargetsField is specified, this should be relative to TargetsField type: string port: - description: JSONPath expression to extract the target - port from the response list + description: |- + CEL expression to extract the target port from the response + If TargetsField is specified, this should be relative to TargetsField type: string targetProfile: - description: JSONPath expression to extract the target - profile from the response list + description: |- + CEL expression to extract the target profile from the response + If TargetsField is specified, this should be relative to TargetsField type: string targetsField: description: |- Field name in the JSON response that contains the list of items (targets). If not specified, the entire response is expected to be a list of items. All subsequent fields are specified relative to this field - Example: "results" + Example: "results" if the response is of the form {"results": [ ... list of items ... ]} type: string - required: - - address - - name type: object pagination: description: Optional pagination configuration for parsing @@ -180,65 +187,8 @@ spec: Example: "next" or "nextToken" type: string type: object - timeout: - default: 10s - description: Optional timeout for HTTP requests to the endpoint - type: string - tls: - description: |- - Optional TLS configuration for connecting to the HTTP endpoint - If it is an HTTP endpoint, this will be ignored - properties: - caBundle: - description: |- - Base64-encoded bundle of PEM CAs which will be used to validate the certificate - chain presented by the Provider. Only used if using HTTPS to connect to Provider and - ignored for HTTP connections. - Mutually exclusive with CABundleSecretRef. - format: byte - type: string - caBundleSecretRef: - description: |- - Reference to a Secret containing a bundle of PEM-encoded CAs to use when - verifying the certificate chain presented by the Provider when using HTTPS. - Mutually exclusive with CABundle. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key - must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - insecureSkipVerify: - default: false - description: Skip TLS verification of the Provider's certificate. - type: boolean - type: object - x-kubernetes-validations: - - message: caBundle and caBundleSecretRef are mutually exclusive - rule: '!(has(self.caBundle) && has(self.caBundleSecretRef))' - url: - description: |- - URL of the HTTP endpoint to pull targets from - If defined, the loader will periodically poll this endpoint for targets - type: string - webhook: - description: Optional configuration to enable webhooks + push: + description: Optional configuration to enable push properties: auth: properties: @@ -325,6 +275,51 @@ spec: required: - enabled type: object + timeout: + default: 10s + description: Optional timeout for HTTP requests to the endpoint + type: string + tls: + description: |- + Optional TLS configuration for connecting to the HTTP endpoint + If it is an HTTP endpoint, this will be ignored + properties: + caBundleSecretRef: + description: |- + Reference to a ConfigMap containing a bundle of PEM-encoded CAs to use when + verifying the certificate chain presented by the Provider when using HTTPS. + Mutually exclusive with CABundle. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + insecureSkipVerify: + default: false + description: Skip TLS verification of the Provider's certificate. + type: boolean + type: object + url: + description: |- + URL of the HTTP endpoint to pull targets from + If defined, the loader will periodically poll this endpoint for targets + type: string type: object x-kubernetes-validations: - message: at least one of the fields in [url webhook] must be diff --git a/internal/controller/discovery/loaders.go b/internal/controller/discovery/loaders.go index dce3928..af014a5 100644 --- a/internal/controller/discovery/loaders.go +++ b/internal/controller/discovery/loaders.go @@ -13,7 +13,7 @@ func NewLoader(cfg *core.CommonLoaderConfig, spec gnmicv1alpha1.TargetSourceSpec switch { case spec.Provider.HTTP != nil: - cfg.AcceptPush = spec.Provider.HTTP.Webhook.Enabled + cfg.AcceptPush = spec.Provider.HTTP.Push.Enabled return http.New(*cfg), nil default: return nil, fmt.Errorf("unknown targetsource provider, check TargetSource CRD for %s", cfg.TargetsourceNN) From 214810c45c6d5361651958354710a6f4e445dd8a Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 22 May 2026 08:16:35 -0600 Subject: [PATCH 17/20] removed old webhook statements --- api/v1alpha1/targetsource_types.go | 14 +- api/v1alpha1/zz_generated.deepcopy.go | 132 +++++++++--------- .../operator.gnmic.dev_targetsources.yaml | 5 +- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 8cfb6ad..56afe46 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -53,7 +53,7 @@ type ProviderSpec struct { } // HTTPConfig defines the configuration for the HTTP provider -// +kubebuilder:validation:AtLeastOneOf:=url;webhook +// +kubebuilder:validation:AtLeastOneOf:=url;push type HTTPConfig struct { // URL of the HTTP endpoint to pull targets from // If defined, the loader will periodically poll this endpoint for targets @@ -188,22 +188,22 @@ type PushSpec struct { Enabled bool `json:"enabled"` // +kubebuilder:validation:Optional - Auth *WebhookAuthSpec `json:"auth,omitempty"` + Auth *PushAuthSpec `json:"auth,omitempty"` } // +kubebuilder:validation:ExactlyOneOf:=bearer;signature -type WebhookAuthSpec struct { - Bearer *WebhookBearerAuthSpec `json:"bearer,omitempty"` - Signature *WebhookSignatureAuthSpec `json:"signature,omitempty"` +type PushAuthSpec struct { + Bearer *PushBearerAuthSpec `json:"bearer,omitempty"` + Signature *PushSignatureAuthSpec `json:"signature,omitempty"` } // +kubebuilder:validation:Required -type WebhookBearerAuthSpec struct { +type PushBearerAuthSpec struct { TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } // +kubebuilder:validation:Required -type WebhookSignatureAuthSpec struct { +type PushSignatureAuthSpec struct { SecretRef *corev1.SecretKeySelector `json:"secretRef"` // Header containing the signature diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c9a7235..5567a56 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -938,12 +938,77 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushAuthSpec) DeepCopyInto(out *PushAuthSpec) { + *out = *in + if in.Bearer != nil { + in, out := &in.Bearer, &out.Bearer + *out = new(PushBearerAuthSpec) + (*in).DeepCopyInto(*out) + } + if in.Signature != nil { + in, out := &in.Signature, &out.Signature + *out = new(PushSignatureAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushAuthSpec. +func (in *PushAuthSpec) DeepCopy() *PushAuthSpec { + if in == nil { + return nil + } + out := new(PushAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushBearerAuthSpec) DeepCopyInto(out *PushBearerAuthSpec) { + *out = *in + if in.TokenSecretRef != nil { + in, out := &in.TokenSecretRef, &out.TokenSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushBearerAuthSpec. +func (in *PushBearerAuthSpec) DeepCopy() *PushBearerAuthSpec { + if in == nil { + return nil + } + out := new(PushBearerAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushSignatureAuthSpec) DeepCopyInto(out *PushSignatureAuthSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSignatureAuthSpec. +func (in *PushSignatureAuthSpec) DeepCopy() *PushSignatureAuthSpec { + if in == nil { + return nil + } + out := new(PushSignatureAuthSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PushSpec) DeepCopyInto(out *PushSpec) { *out = *in if in.Auth != nil { in, out := &in.Auth, &out.Auth - *out = new(WebhookAuthSpec) + *out = new(PushAuthSpec) (*in).DeepCopyInto(*out) } } @@ -1634,68 +1699,3 @@ func (in *TunnelTargetPolicyStatus) DeepCopy() *TunnelTargetPolicyStatus { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookAuthSpec) DeepCopyInto(out *WebhookAuthSpec) { - *out = *in - if in.Bearer != nil { - in, out := &in.Bearer, &out.Bearer - *out = new(WebhookBearerAuthSpec) - (*in).DeepCopyInto(*out) - } - if in.Signature != nil { - in, out := &in.Signature, &out.Signature - *out = new(WebhookSignatureAuthSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthSpec. -func (in *WebhookAuthSpec) DeepCopy() *WebhookAuthSpec { - if in == nil { - return nil - } - out := new(WebhookAuthSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookBearerAuthSpec) DeepCopyInto(out *WebhookBearerAuthSpec) { - *out = *in - if in.TokenSecretRef != nil { - in, out := &in.TokenSecretRef, &out.TokenSecretRef - *out = new(v1.SecretKeySelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookBearerAuthSpec. -func (in *WebhookBearerAuthSpec) DeepCopy() *WebhookBearerAuthSpec { - if in == nil { - return nil - } - out := new(WebhookBearerAuthSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookSignatureAuthSpec) DeepCopyInto(out *WebhookSignatureAuthSpec) { - *out = *in - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.SecretKeySelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSignatureAuthSpec. -func (in *WebhookSignatureAuthSpec) DeepCopy() *WebhookSignatureAuthSpec { - if in == nil { - return nil - } - out := new(WebhookSignatureAuthSpec) - in.DeepCopyInto(out) - return out -} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index b3eaa7d..5ee51af 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -322,9 +322,8 @@ spec: type: string type: object x-kubernetes-validations: - - message: at least one of the fields in [url webhook] must be - set - rule: '[has(self.url),has(self.webhook)].filter(x,x==true).size() + - message: at least one of the fields in [url push] must be set + rule: '[has(self.url),has(self.push)].filter(x,x==true).size() >= 1' type: object x-kubernetes-validations: From 89bfbf54d76b6d593b003a051cee24a2932b4b26 Mon Sep 17 00:00:00 2001 From: Valentino Diller Date: Fri, 22 May 2026 08:25:50 -0600 Subject: [PATCH 18/20] changed pushSpec comment --- api/v1alpha1/targetsource_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 56afe46..320482c 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -182,7 +182,7 @@ type ResponseMappingSpec struct { TargetProfile string `json:"targetProfile,omitempty"` } -// PushSpec defines the settings for event-based update mechanism (i.e. push-based) +// PushSpec defines the settings for event-based update mechanism (i.e. webhooks sent from the server) type PushSpec struct { // +kubebuilder:default=false Enabled bool `json:"enabled"` From 0e147516f06d5745c54c45855c011e01bc5be6d8 Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 22 May 2026 14:34:19 +0000 Subject: [PATCH 19/20] update webhook to push --- api/v1alpha1/targetsource_types.go | 14 +- api/v1alpha1/zz_generated.deepcopy.go | 132 +++++++++--------- .../operator.gnmic.dev_targetsources.yaml | 5 +- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 8cfb6ad..56afe46 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -53,7 +53,7 @@ type ProviderSpec struct { } // HTTPConfig defines the configuration for the HTTP provider -// +kubebuilder:validation:AtLeastOneOf:=url;webhook +// +kubebuilder:validation:AtLeastOneOf:=url;push type HTTPConfig struct { // URL of the HTTP endpoint to pull targets from // If defined, the loader will periodically poll this endpoint for targets @@ -188,22 +188,22 @@ type PushSpec struct { Enabled bool `json:"enabled"` // +kubebuilder:validation:Optional - Auth *WebhookAuthSpec `json:"auth,omitempty"` + Auth *PushAuthSpec `json:"auth,omitempty"` } // +kubebuilder:validation:ExactlyOneOf:=bearer;signature -type WebhookAuthSpec struct { - Bearer *WebhookBearerAuthSpec `json:"bearer,omitempty"` - Signature *WebhookSignatureAuthSpec `json:"signature,omitempty"` +type PushAuthSpec struct { + Bearer *PushBearerAuthSpec `json:"bearer,omitempty"` + Signature *PushSignatureAuthSpec `json:"signature,omitempty"` } // +kubebuilder:validation:Required -type WebhookBearerAuthSpec struct { +type PushBearerAuthSpec struct { TokenSecretRef *corev1.SecretKeySelector `json:"tokenSecretRef,omitempty"` } // +kubebuilder:validation:Required -type WebhookSignatureAuthSpec struct { +type PushSignatureAuthSpec struct { SecretRef *corev1.SecretKeySelector `json:"secretRef"` // Header containing the signature diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c9a7235..5567a56 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -938,12 +938,77 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushAuthSpec) DeepCopyInto(out *PushAuthSpec) { + *out = *in + if in.Bearer != nil { + in, out := &in.Bearer, &out.Bearer + *out = new(PushBearerAuthSpec) + (*in).DeepCopyInto(*out) + } + if in.Signature != nil { + in, out := &in.Signature, &out.Signature + *out = new(PushSignatureAuthSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushAuthSpec. +func (in *PushAuthSpec) DeepCopy() *PushAuthSpec { + if in == nil { + return nil + } + out := new(PushAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushBearerAuthSpec) DeepCopyInto(out *PushBearerAuthSpec) { + *out = *in + if in.TokenSecretRef != nil { + in, out := &in.TokenSecretRef, &out.TokenSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushBearerAuthSpec. +func (in *PushBearerAuthSpec) DeepCopy() *PushBearerAuthSpec { + if in == nil { + return nil + } + out := new(PushBearerAuthSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PushSignatureAuthSpec) DeepCopyInto(out *PushSignatureAuthSpec) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSignatureAuthSpec. +func (in *PushSignatureAuthSpec) DeepCopy() *PushSignatureAuthSpec { + if in == nil { + return nil + } + out := new(PushSignatureAuthSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PushSpec) DeepCopyInto(out *PushSpec) { *out = *in if in.Auth != nil { in, out := &in.Auth, &out.Auth - *out = new(WebhookAuthSpec) + *out = new(PushAuthSpec) (*in).DeepCopyInto(*out) } } @@ -1634,68 +1699,3 @@ func (in *TunnelTargetPolicyStatus) DeepCopy() *TunnelTargetPolicyStatus { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookAuthSpec) DeepCopyInto(out *WebhookAuthSpec) { - *out = *in - if in.Bearer != nil { - in, out := &in.Bearer, &out.Bearer - *out = new(WebhookBearerAuthSpec) - (*in).DeepCopyInto(*out) - } - if in.Signature != nil { - in, out := &in.Signature, &out.Signature - *out = new(WebhookSignatureAuthSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAuthSpec. -func (in *WebhookAuthSpec) DeepCopy() *WebhookAuthSpec { - if in == nil { - return nil - } - out := new(WebhookAuthSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookBearerAuthSpec) DeepCopyInto(out *WebhookBearerAuthSpec) { - *out = *in - if in.TokenSecretRef != nil { - in, out := &in.TokenSecretRef, &out.TokenSecretRef - *out = new(v1.SecretKeySelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookBearerAuthSpec. -func (in *WebhookBearerAuthSpec) DeepCopy() *WebhookBearerAuthSpec { - if in == nil { - return nil - } - out := new(WebhookBearerAuthSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookSignatureAuthSpec) DeepCopyInto(out *WebhookSignatureAuthSpec) { - *out = *in - if in.SecretRef != nil { - in, out := &in.SecretRef, &out.SecretRef - *out = new(v1.SecretKeySelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSignatureAuthSpec. -func (in *WebhookSignatureAuthSpec) DeepCopy() *WebhookSignatureAuthSpec { - if in == nil { - return nil - } - out := new(WebhookSignatureAuthSpec) - in.DeepCopyInto(out) - return out -} diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index b3eaa7d..5ee51af 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -322,9 +322,8 @@ spec: type: string type: object x-kubernetes-validations: - - message: at least one of the fields in [url webhook] must be - set - rule: '[has(self.url),has(self.webhook)].filter(x,x==true).size() + - message: at least one of the fields in [url push] must be set + rule: '[has(self.url),has(self.push)].filter(x,x==true).size() >= 1' type: object x-kubernetes-validations: From b62f3f5a635eae07060f7b43b3d611337ea001cd Mon Sep 17 00:00:00 2001 From: Daniel Schatzmann Date: Fri, 22 May 2026 15:01:05 +0000 Subject: [PATCH 20/20] refactor CRD --- api/v1alpha1/targetsource_types.go | 9 +++++---- api/v1alpha1/zz_generated.deepcopy.go | 4 ++-- config/crd/bases/operator.gnmic.dev_targetsources.yaml | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/targetsource_types.go b/api/v1alpha1/targetsource_types.go index 320482c..b34b096 100644 --- a/api/v1alpha1/targetsource_types.go +++ b/api/v1alpha1/targetsource_types.go @@ -65,10 +65,10 @@ type HTTPConfig struct { Authorization *AuthorizationSpec `json:"authorization,omitempty"` // Optional interval for polling the HTTP endpoint for targets - // TODO: increase default value - // +kubebuilder:default="30s" + // TODO: document about default value + // +kubebuilder:default="6h" // +kubebuilder:validation:Optional - PollInterval *metav1.Duration `json:"interval,omitempty"` + Interval *metav1.Duration `json:"interval,omitempty"` // Optional timeout for HTTP requests to the endpoint // +kubebuilder:default="10s" @@ -101,7 +101,8 @@ type ClientTLSConfig struct { // Reference to a ConfigMap containing a bundle of PEM-encoded CAs to use when // verifying the certificate chain presented by the Provider when using HTTPS. // Mutually exclusive with CABundle. - CABundleRef *corev1.ConfigMapKeySelector `json:"caBundleSecretRef,omitempty"` + // +kubebuilder:validation:Optional + CABundleRef *corev1.ConfigMapKeySelector `json:"caBundleRef,omitempty"` } // AuthorizationSpec defines the configuration for authentication diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5567a56..9051fc7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -328,8 +328,8 @@ func (in *HTTPConfig) DeepCopyInto(out *HTTPConfig) { *out = new(AuthorizationSpec) (*in).DeepCopyInto(*out) } - if in.PollInterval != nil { - in, out := &in.PollInterval, &out.PollInterval + if in.Interval != nil { + in, out := &in.Interval, &out.Interval *out = new(metav1.Duration) **out = **in } diff --git a/config/crd/bases/operator.gnmic.dev_targetsources.yaml b/config/crd/bases/operator.gnmic.dev_targetsources.yaml index 5ee51af..d9c9184 100644 --- a/config/crd/bases/operator.gnmic.dev_targetsources.yaml +++ b/config/crd/bases/operator.gnmic.dev_targetsources.yaml @@ -128,7 +128,7 @@ spec: rule: '[has(self.basic),has(self.token)].filter(x,x==true).size() == 1' interval: - default: 30s + default: 6h description: Optional interval for polling the HTTP endpoint for targets type: string @@ -284,7 +284,7 @@ spec: Optional TLS configuration for connecting to the HTTP endpoint If it is an HTTP endpoint, this will be ignored properties: - caBundleSecretRef: + caBundleRef: description: |- Reference to a ConfigMap containing a bundle of PEM-encoded CAs to use when verifying the certificate chain presented by the Provider when using HTTPS.