From c283440d1adea45f4f55cf384566e4723d523dc2 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Mon, 27 Oct 2025 14:18:28 +0000 Subject: [PATCH 1/7] chore: Add definition of ManagedCRL spec Signed-off-by: Teddy Andrieux --- api/v1alpha1/managedcrl_types.go | 150 +++++++++- api/v1alpha1/zz_generated.deepcopy.go | 61 +++- .../crl-operator.scality.com_managedcrls.yaml | 156 +++++++++- go.mod | 127 +++++---- go.sum | 268 +++++++++--------- 5 files changed, 558 insertions(+), 204 deletions(-) diff --git a/api/v1alpha1/managedcrl_types.go b/api/v1alpha1/managedcrl_types.go index ec801a4..ef08de6 100644 --- a/api/v1alpha1/managedcrl_types.go +++ b/api/v1alpha1/managedcrl_types.go @@ -17,29 +17,75 @@ limitations under the License. package v1alpha1 import ( + "crypto/x509" + "fmt" + "math/big" + "strings" + + cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// RevocationSpec defines a certificate to be revoked. +type RevocationSpec struct { + // SerialNumber is the serial number of the certificate to be revoked. + SerialNumber string `json:"serialNumber"` + + // RevocationTime is the time at which the certificate was revoked. + // If not specified, the current time will be used. + // +optional + RevocationTime *metav1.Time `json:"revocationTime,omitempty"` + + // Reason is the reason for revocation (refer to RFC 5280 Section 5.3.1.). + // +optional + ReasonCode *int `json:"reasonCode,omitempty"` +} // ManagedCRLSpec defines the desired state of ManagedCRL. type ManagedCRLSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + // IssuerRef is a reference to the cert-manager Issuer or ClusterIssuer + // that will sign the CRL. + IssuerRef cmmetav1.ObjectReference `json:"issuerRef"` - // Foo is an example field of ManagedCRL. Edit managedcrl_types.go to remove/update - Foo string `json:"foo,omitempty"` + // Duration is the duration for which the CRL is valid. + // (default: 168h = 7 days) + // +optional + Duration *metav1.Duration `json:"duration,omitempty"` + + // Revocations is a list of certificates to be revoked. + // +optional + Revocations []RevocationSpec `json:"revocations,omitempty"` } // ManagedCRLStatus defines the observed state of ManagedCRL. type ManagedCRLStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // SecretReady indicates whether the CRL is built and available in the Secret. + SecretReady *bool `json:"secretReady,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // CRLValidUntil is the time until which the CRL is valid. + CRLValidUntil metav1.Time `json:"crlValidUntil,omitempty"` + + // CRLNumber is the number of the CRL. + CRLNumber int64 `json:"crlNumber,omitempty"` + + // ObservedCASecretRef is a reference to the Secret containing the last + // CA certificate and private key used to sign the CRL. + ObservedCASecretRef *corev1.SecretReference `json:"observedCASecretRef,omitempty"` + // ObservedCASecretVersion is the resource version of the Secret + // containing the last CA certificate and private key used to sign the CRL. + ObservedCASecretVersion string `json:"observedCASecretVersion,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=mcrl +// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuerRef.name` +// +kubebuilder:printcolumn:name="Expires",type=string,JSONPath=`.status.crlValidUntil` +// +kubebuilder:printcolumn:name="CRL Number",type=integer,JSONPath=`.status.crlNumber` // ManagedCRL is the Schema for the managedcrls API. type ManagedCRL struct { @@ -62,3 +108,91 @@ type ManagedCRLList struct { func init() { SchemeBuilder.Register(&ManagedCRL{}, &ManagedCRLList{}) } + +func (mcrl *ManagedCRL) WithDefaults() { + mcrl.Spec.withDefaults() +} + +func (mcrls *ManagedCRLSpec) withDefaults() { + if mcrls.Duration == nil { + mcrls.Duration = &metav1.Duration{Duration: 7 * 24 * 60 * 60 * 1e9} // 7 days + } + + for i := range mcrls.Revocations { + mcrls.Revocations[i].withDefaults() + } +} + +func (rs *RevocationSpec) withDefaults() { + if rs.RevocationTime == nil { + rs.RevocationTime = &metav1.Time{Time: metav1.Now().Time} + } + if rs.ReasonCode == nil { + rs.ReasonCode = ptr.To(0) // Unspecified + } +} + +// ToRevocationListEntry converts a RevocationSpec to an x509.RevocationListEntry. +func (rs RevocationSpec) ToRevocationListEntry() (x509.RevocationListEntry, error) { + cleanSerial := strings.ReplaceAll(rs.SerialNumber, ":", "") + + // First try base 16 if not working try 0 to auto detect + serial, ok := big.NewInt(0).SetString(cleanSerial, 16) + if !ok { + serial, ok = big.NewInt(0).SetString(cleanSerial, 0) + if !ok { + return x509.RevocationListEntry{}, fmt.Errorf("invalid serial number: %s", rs.SerialNumber) + } + } + + return x509.RevocationListEntry{ + SerialNumber: serial, + RevocationTime: rs.RevocationTime.Time, + ReasonCode: *rs.ReasonCode, + }, nil +} + +// GetRevokedListEntries converts the Revocations in ManagedCRLSpec to a slice of x509.RevocationListEntry. +func (mcrls *ManagedCRLSpec) GetRevokedListEntries() ([]x509.RevocationListEntry, error) { + if mcrls.Revocations == nil { + return []x509.RevocationListEntry{}, nil + } + + revokedCerts := make([]x509.RevocationListEntry, 0, len(mcrls.Revocations)) + for _, revocation := range mcrls.Revocations { + revocationEntry, err := revocation.ToRevocationListEntry() + if err != nil { + return nil, fmt.Errorf("invalid revocation entry for serial number %s: %w", revocation.SerialNumber, err) + } + revokedCerts = append(revokedCerts, revocationEntry) + } + return revokedCerts, nil +} + +// SetSecretReady sets the ManagedCRL status to SecretReady. +func (mcrl *ManagedCRL) SetSecretReady() { + condition := metav1.Condition{ + Type: "SecretReady", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "CRLSecretReady", + Message: "The secret containing the CRL is ready", + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.SecretReady = ptr.To(true) +} + +// SetSecretNotReady sets the ManagedCRL status to NotReady with the given reason and message. +func (mcrl *ManagedCRL) SetSecretNotReady(reason, message string) { + condition := metav1.Condition{ + Type: "SecretReady", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.SecretReady = ptr.To(false) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c7fa963..57b1648 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,8 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -29,8 +31,8 @@ func (in *ManagedCRL) DeepCopyInto(out *ManagedCRL) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedCRL. @@ -86,6 +88,19 @@ func (in *ManagedCRLList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedCRLSpec) DeepCopyInto(out *ManagedCRLSpec) { *out = *in + in.IssuerRef.DeepCopyInto(&out.IssuerRef) + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(v1.Duration) + **out = **in + } + if in.Revocations != nil { + in, out := &in.Revocations, &out.Revocations + *out = make([]RevocationSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedCRLSpec. @@ -101,6 +116,24 @@ func (in *ManagedCRLSpec) DeepCopy() *ManagedCRLSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedCRLStatus) DeepCopyInto(out *ManagedCRLStatus) { *out = *in + if in.SecretReady != nil { + in, out := &in.SecretReady, &out.SecretReady + *out = new(bool) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.CRLValidUntil.DeepCopyInto(&out.CRLValidUntil) + if in.ObservedCASecretRef != nil { + in, out := &in.ObservedCASecretRef, &out.ObservedCASecretRef + *out = new(corev1.SecretReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedCRLStatus. @@ -112,3 +145,27 @@ func (in *ManagedCRLStatus) DeepCopy() *ManagedCRLStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RevocationSpec) DeepCopyInto(out *RevocationSpec) { + *out = *in + if in.RevocationTime != nil { + in, out := &in.RevocationTime, &out.RevocationTime + *out = (*in).DeepCopy() + } + if in.ReasonCode != nil { + in, out := &in.ReasonCode, &out.ReasonCode + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RevocationSpec. +func (in *RevocationSpec) DeepCopy() *RevocationSpec { + if in == nil { + return nil + } + out := new(RevocationSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml index 389aac6..eb41d6d 100644 --- a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml +++ b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml @@ -11,10 +11,22 @@ spec: kind: ManagedCRL listKind: ManagedCRLList plural: managedcrls + shortNames: + - mcrl singular: managedcrl scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.issuerRef.name + name: Issuer + type: string + - jsonPath: .status.crlValidUntil + name: Expires + type: string + - jsonPath: .status.crlNumber + name: CRL Number + type: integer + name: v1alpha1 schema: openAPIV3Schema: description: ManagedCRL is the Schema for the managedcrls API. @@ -39,13 +51,149 @@ spec: spec: description: ManagedCRLSpec defines the desired state of ManagedCRL. properties: - foo: - description: Foo is an example field of ManagedCRL. Edit managedcrl_types.go - to remove/update + duration: + description: |- + Duration is the duration for which the CRL is valid. + (default: 168h = 7 days) type: string + issuerRef: + description: |- + IssuerRef is a reference to the cert-manager Issuer or ClusterIssuer + that will sign the CRL. + properties: + group: + description: |- + Group of the issuer being referred to. + Defaults to 'cert-manager.io'. + type: string + kind: + description: |- + Kind of the issuer being referred to. + Defaults to 'Issuer'. + type: string + name: + description: Name of the issuer being referred to. + type: string + required: + - name + type: object + revocations: + description: Revocations is a list of certificates to be revoked. + items: + description: RevocationSpec defines a certificate to be revoked. + properties: + reasonCode: + description: Reason is the reason for revocation (refer to RFC + 5280 Section 5.3.1.). + type: integer + revocationTime: + description: |- + RevocationTime is the time at which the certificate was revoked. + If not specified, the current time will be used. + format: date-time + type: string + serialNumber: + description: SerialNumber is the serial number of the certificate + to be revoked. + type: string + required: + - serialNumber + type: object + type: array + required: + - issuerRef type: object status: description: ManagedCRLStatus defines the observed state of ManagedCRL. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + crlNumber: + description: CRLNumber is the number of the CRL. + format: int64 + type: integer + crlValidUntil: + description: CRLValidUntil is the time until which the CRL is valid. + format: date-time + type: string + observedCASecretRef: + description: |- + ObservedCASecretRef is a reference to the Secret containing the last + CA certificate and private key used to sign the CRL. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + observedCASecretVersion: + description: |- + ObservedCASecretVersion is the resource version of the Secret + containing the last CA certificate and private key used to sign the CRL. + type: string + secretReady: + description: SecretReady indicates whether the CRL is built and available + in the Secret. + type: boolean type: object type: object served: true diff --git a/go.mod b/go.mod index 12dd078..e87cf70 100644 --- a/go.mod +++ b/go.mod @@ -5,93 +5,100 @@ go 1.25.0 require ( github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 - k8s.io/apimachinery v0.33.0 - k8s.io/client-go v0.33.0 - sigs.k8s.io/controller-runtime v0.21.0 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.3 ) require ( - cel.dev/expr v0.19.1 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cert-manager/cert-manager v1.19.1 github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.23.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect - go.opentelemetry.io/otel/sdk v1.33.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.26.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/grpc v1.68.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.36.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.0 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect - k8s.io/apiserver v0.33.0 // indirect - k8s.io/component-base v0.33.0 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apiserver v0.34.1 // indirect + k8s.io/component-base v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 14ef049..cd12c32 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,49 @@ -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cert-manager/cert-manager v1.19.1 h1:Txh8L/nLWTDcb7ZnXuXbTe15BxQnLbLirXmbNk0fGgY= +github.com/cert-manager/cert-manager v1.19.1/go.mod h1:8Ps1VXCQRGKT8zNvLQlhDK1gFKWmYKdIPQFmvTS2JeA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -50,11 +52,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= -github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -64,8 +65,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -76,22 +77,20 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -100,25 +99,27 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -128,127 +129,134 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= -go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= -go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= -go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s= +google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= -k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= -k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= -k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= -k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= -k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= -k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From b77c82e163fc6d6d051bfaf0ba3f9cd52dd82b94 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Tue, 28 Oct 2025 13:53:26 +0000 Subject: [PATCH 2/7] chore: Add logic to handle CRL generation Signed-off-by: Teddy Andrieux --- api/v1alpha1/managedcrl_types.go | 10 + cmd/main.go | 10 +- config/rbac/role.yaml | 28 ++ go.mod | 3 +- go.sum | 2 + internal/controller/managedcrl_controller.go | 361 ++++++++++++++++++- 6 files changed, 407 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/managedcrl_types.go b/api/v1alpha1/managedcrl_types.go index ef08de6..382392b 100644 --- a/api/v1alpha1/managedcrl_types.go +++ b/api/v1alpha1/managedcrl_types.go @@ -109,6 +109,16 @@ func init() { SchemeBuilder.Register(&ManagedCRL{}, &ManagedCRLList{}) } +// GetSecret returns the name of the Secret used to store the CRL. +func (mcrl *ManagedCRL) GetSecret() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-crl", mcrl.Name), + Namespace: mcrl.Namespace, + }, + } +} + func (mcrl *ManagedCRL) WithDefaults() { mcrl.Spec.withDefaults() } diff --git a/cmd/main.go b/cmd/main.go index 53a1341..d67b2c9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,6 +26,7 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -49,6 +50,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(cmv1.AddToScheme(scheme)) utilruntime.Must(crloperatorv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme @@ -64,6 +66,7 @@ func main() { var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) + var certManagerNamespace string flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -81,6 +84,8 @@ func main() { flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.StringVar(&certManagerNamespace, "cert-manager-namespace", "cert-manager", + "The namespace where cert-manager is installed.") opts := zap.Options{ Development: true, } @@ -203,8 +208,9 @@ func main() { } if err := (&controller.ManagedCRLReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CertManagerNamespace: certManagerNamespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ManagedCRL") os.Exit(1) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 91d2968..1a50046 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,34 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cert-manager.io + resources: + - clusterissuers + - issuers + verbs: + - get + - list + - watch - apiGroups: - crl-operator.scality.com resources: diff --git a/go.mod b/go.mod index e87cf70..e19f521 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + sigs.k8s.io/gateway-api v1.4.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) @@ -90,7 +91,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.1 // indirect + k8s.io/api v0.34.1 k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/apiserver v0.34.1 // indirect k8s.io/component-base v0.34.1 // indirect diff --git a/go.sum b/go.sum index cd12c32..7720c92 100644 --- a/go.sum +++ b/go.sum @@ -252,6 +252,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFe sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ= +sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index 8ca28f1..78fc9f4 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -18,25 +18,51 @@ package controller import ( "context" + "crypto" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "math/big" + "time" + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" ) +const ( + // renewBefore is the duration before expiry when the CRL should be renewed. + renewBefore = 1 * time.Hour + // secretCRLKey is the key in the Secret data where the CRL is stored. + secretCRLKey = "ca.crl" +) + // ManagedCRLReconciler reconciles a ManagedCRL object type ManagedCRLReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + CertManagerNamespace string } // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls/status,verbs=get;update;patch // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls/finalizers,verbs=update +// +kubebuilder:rbac:groups=cert-manager.io,resources=issuers;clusterissuers,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch + // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by @@ -46,18 +72,345 @@ type ManagedCRLReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile +// nolint:gocyclo // It's the main reconciliation loop func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = logf.FromContext(ctx) + logger := logf.FromContext(ctx) + logger.Info("reconcile started") + + instance := &crloperatorv1alpha1.ManagedCRL{} + if err := r.Get(ctx, req.NamespacedName, instance); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Apply defaults + instance.WithDefaults() + + needRenewal := false + original := instance.DeepCopy() + + // Ensure we update the status in case of early return + defer func() { + if err := r.Status().Patch(ctx, instance, client.MergeFrom(original)); err != nil { + logger.Error(err, "failed to update ManagedCRL status") + } + }() + + // Simple helper to handle errors and update status + handleError := func(err error, reason, message string) (ctrl.Result, error) { // nolint:unparam // It's clearer + instance.SetSecretNotReady(reason, message) + logger.Error(err, message) + return ctrl.Result{}, fmt.Errorf("%s: %w", message, err) + } + + var nextCRLNumber int64 + if instance.Status.CRLNumber == 0 { + // NOTE: We do not start from 1 to avoid potential conflicts when we delete and re-create the + // exact same ManagedCRL resource. + nextCRLNumber = time.Now().Unix() + needRenewal = true + } else { + nextCRLNumber = instance.Status.CRLNumber + 1 + } + + revokedList, err := instance.Spec.GetRevokedListEntries() + if err != nil { + return handleError( + err, + "FailedToGetRevokedListEntries", + "failed to get revoked list entries from spec", + ) + } + + // Get the Secret containing the CA certificate and private key + caSecret, err := r.getIssuerSecret(ctx, instance.Namespace, instance.Spec.IssuerRef) + if err != nil { + return handleError( + err, + "FailedToGetIssuerSecret", + "failed to get issuer secret", + ) + } + if instance.Status.ObservedCASecretRef == nil || + caSecret.Name != instance.Status.ObservedCASecretRef.Name || + caSecret.Namespace != instance.Status.ObservedCASecretRef.Namespace { + + needRenewal = true + } + + // Extract the CA certificate and private key from the Secret + caCert, caKey, err := r.extractCACertAndKey(caSecret) + if err != nil { + return handleError( + err, + "FailedToExtractCACertAndKey", + "failed to extract CA certificate and key from secret", + ) + } + + // Generate the CRL + crl, err := r.generateCRL(caCert, caKey, revokedList, instance.Spec.Duration.Duration, big.NewInt(nextCRLNumber)) + if err != nil { + return handleError( + err, + "FailedToGenerateCRL", + "failed to generate CRL", + ) + } + + secret := instance.GetSecret() + + // Get the current CRL to check if it needs to be updated + var currentCRL *x509.RevocationList + + // If we still don't need renewal, check the current CRL validity + if !needRenewal { + var isWrong bool + currentCRL, isWrong, err = r.getCurrentCRL(ctx, secret.Namespace, secret.Name) + if isWrong { + needRenewal = true + if err != nil { + logger.Info("current CRL is invalid, will renew", "error", err) + } + } else if err != nil { + return handleError( + err, + "FailedToGetCurrentCRL", + "failed to get current CRL", + ) + } else if r.crlNeedRenewal(currentCRL, revokedList, caCert, instance.Spec.Duration.Duration) { + needRenewal = true + } + } + if currentCRL == nil { + needRenewal = true + } + + // If we do not need renewal, keep the current CRL + if !needRenewal { + crl = currentCRL + } + + op, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error { + labels := secret.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + // Add default labels + labels["app.kubernetes.io/managed-by"] = "crl-operator" + labels["app.kubernetes.io/component"] = "managed-crl" + labels["app.kubernetes.io/name"] = secret.Name + labels["app.kubernetes.io/instance"] = instance.Name + + secret.SetLabels(labels) + + err := controllerutil.SetControllerReference(instance, secret, r.Scheme) + if err != nil { + return fmt.Errorf("failed to set owner reference on Secret: %w", err) + } + + secret.Data = map[string][]byte{ + secretCRLKey: crl.Raw, + } + return nil + }) + if err != nil { + return handleError( + err, + "FailedToCreateOrUpdateCRLSecret", + "failed to create or update Secret for CRL", + ) + } + if op != controllerutil.OperationResultNone { + logger.Info("Secret for CRL reconciled", "operation", op) + } + + // Update status + instance.Status.CRLNumber = crl.Number.Int64() + instance.Status.CRLValidUntil = metav1.Time{Time: crl.NextUpdate.UTC()} + instance.Status.ObservedCASecretRef = &corev1.SecretReference{ + Name: caSecret.Name, + Namespace: caSecret.Namespace, + } + instance.Status.ObservedCASecretVersion = caSecret.ResourceVersion + + instance.SetSecretReady() + + // All good + // We still have to requeue before expiry to renew the CRL + requeueAfter := time.Until(crl.NextUpdate.Add(-renewBefore)) + logger.Info("reconcile completed successfully", "requeueAfter", requeueAfter.String()) + return ctrl.Result{RequeueAfter: requeueAfter}, nil +} + +// getIssuerSecret retrieves the Secret containing the CA certificate and private key +func (r *ManagedCRLReconciler) getIssuerSecret(ctx context.Context, namespace string, issuerRef cmmetav1.IssuerReference) (*corev1.Secret, error) { + var secretRef client.ObjectKey + + switch issuerRef.Kind { + case "Issuer": + issuer := &cmv1.Issuer{} + if err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: issuerRef.Name}, issuer); err != nil { + return nil, err + } + if issuer.Spec.CA == nil { + return nil, errors.New("issuer is not a CA issuer") + } + secretRef = client.ObjectKey{ + Name: issuer.Spec.CA.SecretName, + Namespace: namespace, + } + case "ClusterIssuer": + issuer := &cmv1.ClusterIssuer{} + if err := r.Get(ctx, client.ObjectKey{Name: issuerRef.Name}, issuer); err != nil { + return nil, err + } + if issuer.Spec.CA == nil { + return nil, errors.New("cluster issuer is not a CA issuer") + } + secretRef = client.ObjectKey{ + Name: issuer.Spec.CA.SecretName, + // For ClusterIssuer, the secret is in the cert-manager namespace + Namespace: r.CertManagerNamespace, + } + default: + return nil, errors.New("unsupported issuer kind") + } + + secret := &corev1.Secret{} + if err := r.Get(ctx, secretRef, secret); err != nil { + return nil, fmt.Errorf("failed to get secret %s/%s: %w", secretRef.Namespace, secretRef.Name, err) + } + + return secret, nil +} + +// extractCACertAndKey retrieves the CA certificate and private key from the given Secret +func (r *ManagedCRLReconciler) extractCACertAndKey(secret *corev1.Secret) (*x509.Certificate, crypto.Signer, error) { + caCertPEM, ok := secret.Data[corev1.TLSCertKey] + if !ok { + return nil, nil, fmt.Errorf("secret %s/%s does not contain a certificate", secret.Namespace, secret.Name) + } + caKeyPEM, ok := secret.Data[corev1.TLSPrivateKeyKey] + if !ok { + return nil, nil, fmt.Errorf("secret %s/%s does not contain a private key", secret.Namespace, secret.Name) + } + + certBlock, _ := pem.Decode(caCertPEM) + if certBlock == nil || certBlock.Type != "CERTIFICATE" { + return nil, nil, fmt.Errorf("failed to decode CA certificate PEM") + } + caCert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse CA certificate: %w", err) + } + + if !caCert.IsCA { + return nil, nil, fmt.Errorf("the provided certificate is not a CA certificate") + } + + keyBlock, _ := pem.Decode(caKeyPEM) + if keyBlock == nil { + return nil, nil, fmt.Errorf("failed to decode CA private key PEM") + } + var caKey crypto.Signer + if key, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err == nil { + signer, ok := key.(crypto.Signer) + if !ok { + return nil, nil, fmt.Errorf("CA private key is not a crypto.Signer") + } + caKey = signer + } else if key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err == nil { + caKey = key + } else if key, err := x509.ParseECPrivateKey(keyBlock.Bytes); err == nil { + caKey = key + } else { + return nil, nil, fmt.Errorf("failed to parse CA private key: %w", err) + } + + return caCert, caKey, nil +} + +// generateCRL generates a CRL signed by the given CA certificate and private key +func (r *ManagedCRLReconciler) generateCRL(caCert *x509.Certificate, caKey crypto.Signer, revokedList []x509.RevocationListEntry, duration time.Duration, crlNumber *big.Int) (*x509.RevocationList, error) { + now := time.Now().UTC() + nextUpdate := now.Add(duration).UTC() + + crlTemplate := &x509.RevocationList{ + ThisUpdate: now, + NextUpdate: nextUpdate, + RevokedCertificateEntries: revokedList, + Number: crlNumber, + } + + crlBytes, err := x509.CreateRevocationList(rand.Reader, crlTemplate, caCert, caKey) + if err != nil { + return nil, fmt.Errorf("failed to create CRL: %w", err) + } + return x509.ParseRevocationList(crlBytes) +} + +// getCurrentCRL retrieves the current CRL from the Secret +func (r *ManagedCRLReconciler) getCurrentCRL(ctx context.Context, namespace, name string) (*x509.RevocationList, bool, error) { + secret := &corev1.Secret{} + err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, secret) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, true, nil + } + return nil, false, fmt.Errorf("failed to get Secret %s/%s: %w", namespace, name, err) + } + + crlBytes, ok := secret.Data[secretCRLKey] + if !ok { + return nil, true, fmt.Errorf("secret %s/%s does not contain a CRL", namespace, name) + } + + crl, err := x509.ParseRevocationList(crlBytes) + if err != nil { + return nil, true, fmt.Errorf("failed to parse CRL from Secret %s/%s: %w", namespace, name, err) + } + + return crl, false, nil +} + +// crlNeedRenewal check if the CRL needs to be renewed +func (r *ManagedCRLReconciler) crlNeedRenewal(currentCRL *x509.RevocationList, revokedList []x509.RevocationListEntry, caCert *x509.Certificate, duration time.Duration) bool { + // Check if the CRL is about to expire or if duration is shorter than nextUpdate + // (i.e. the duration has been reduced) + if currentCRL.NextUpdate.Before(time.Now().Add(2*renewBefore)) || currentCRL.NextUpdate.After(time.Now().Add(duration)) { + return true + } + + // Check if the CRL is signed by the current CA + err := currentCRL.CheckSignatureFrom(caCert) + if err != nil { + return true + } + + // Check if the CRL contains all revoked certificates + // NOTE: We manage the full list so we expect a match in the same order + for i, revoked := range revokedList { + if i >= len(currentCRL.RevokedCertificateEntries) { + return true + } + currentRevoked := currentCRL.RevokedCertificateEntries[i] + // NOTE: We do not compare revocation time since it default to now if not set + if revoked.SerialNumber.Cmp(currentRevoked.SerialNumber) != 0 || + revoked.ReasonCode != currentRevoked.ReasonCode { - // TODO(user): your logic here + return true + } + } - return ctrl.Result{}, nil + // Current CRL is valid, no need to renew + return false } // SetupWithManager sets up the controller with the Manager. func (r *ManagedCRLReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&crloperatorv1alpha1.ManagedCRL{}). + Owns(&corev1.Secret{}). Named("managedcrl"). Complete(r) } From 8002578d24857929ae874fbb450d516214013944 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Wed, 29 Oct 2025 08:42:14 +0000 Subject: [PATCH 3/7] chore: Reconcile in case of changes on Issuer/ClusterIssuer or Secret Signed-off-by: Teddy Andrieux --- cmd/main.go | 38 +++++++++++++++ internal/controller/managedcrl_controller.go | 49 ++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/cmd/main.go b/cmd/main.go index d67b2c9..b11db9d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,8 +17,10 @@ limitations under the License. package main import ( + "context" "crypto/tls" "flag" + "fmt" "os" "path/filepath" @@ -32,6 +34,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -217,6 +220,41 @@ func main() { } // +kubebuilder:scaffold:builder + // Create a field index for ClusterIssuer, Issuer and Secret references + // so that our CRL controller react to changes in those resources. + err = mgr.GetFieldIndexer().IndexField( + context.Background(), + &crloperatorv1alpha1.ManagedCRL{}, + "IssuerRef", + func(rawObj client.Object) []string { + mcrl := rawObj.(*crloperatorv1alpha1.ManagedCRL) + var indexKeys []string + switch mcrl.Spec.IssuerRef.Kind { + case "Issuer": + indexKeys = append(indexKeys, fmt.Sprintf("Issuer/%s/%s", mcrl.Namespace, mcrl.Spec.IssuerRef.Name)) + case "ClusterIssuer": + indexKeys = append(indexKeys, fmt.Sprintf("ClusterIssuer/%s", mcrl.Spec.IssuerRef.Name)) + default: + return nil + } + + // Add a reference to the Secret containing the CA certificate and private key + // used to sign the CRL. + if mcrl.Status.ObservedCASecretRef != nil { + indexKeys = append( + indexKeys, + fmt.Sprintf("Secret/%s/%s", mcrl.Status.ObservedCASecretRef.Namespace, mcrl.Status.ObservedCASecretRef.Name), + ) + } + + return indexKeys + }, + ) + if err != nil { + setupLog.Error(err, "unable to create field index for IssuerRef") + os.Exit(1) + } + if metricsCertWatcher != nil { setupLog.Info("Adding metrics certificate watcher to manager") if err := mgr.Add(metricsCertWatcher); err != nil { diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index 78fc9f4..8d58eb9 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -36,6 +36,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/log" crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" @@ -408,9 +409,57 @@ func (r *ManagedCRLReconciler) crlNeedRenewal(currentCRL *x509.RevocationList, r // SetupWithManager sets up the controller with the Manager. func (r *ManagedCRLReconciler) SetupWithManager(mgr ctrl.Manager) error { + mapIssuerToCRL := func(ctx context.Context, obj client.Object) []ctrl.Request { + logger := logf.FromContext(ctx) + var indexKey string + + switch obj := obj.(type) { + case *cmv1.Issuer: + indexKey = fmt.Sprintf("Issuer/%s/%s", obj.Namespace, obj.Name) + case *cmv1.ClusterIssuer: + indexKey = fmt.Sprintf("ClusterIssuer/%s", obj.Name) + case *corev1.Secret: + indexKey = fmt.Sprintf("Secret/%s/%s", obj.Namespace, obj.Name) + default: + logger.Error(nil, "unknown type in mapIssuerToCRL: %T", obj) + return nil + } + + mcrlList := &crloperatorv1alpha1.ManagedCRLList{} + err := r.List(ctx, mcrlList, client.MatchingFields{ + "IssuerRef": indexKey, + }) + if err != nil { + logger.Error(err, "failed to list ManagedCRLs", "IssuerRef", indexKey) + return nil + } + + requests := make([]ctrl.Request, 0, len(mcrlList.Items)) + for _, mcrl := range mcrlList.Items { + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: mcrl.Name, + Namespace: mcrl.Namespace, + }, + }) + } + + if len(requests) > 0 { + logger.Info( + "Issuer/ClusterIssuer change detected, enqueueing ManagedCRLs", + "IssuerRef", indexKey, + "count", len(requests), + ) + } + return requests + } + return ctrl.NewControllerManagedBy(mgr). For(&crloperatorv1alpha1.ManagedCRL{}). Owns(&corev1.Secret{}). + Watches(&cmv1.ClusterIssuer{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). + Watches(&cmv1.Issuer{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). + Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). Named("managedcrl"). Complete(r) } From 32c9c31c1748819f22699272bbfa80cde60a0032 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Wed, 29 Oct 2025 10:25:00 +0000 Subject: [PATCH 4/7] chore: Add version and dedicated func to set labels and owner ref Signed-off-by: Teddy Andrieux --- Dockerfile | 7 ++- internal/controller/managedcrl_controller.go | 51 ++++++++++++++------ internal/version.go | 7 +++ 3 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 internal/version.go diff --git a/Dockerfile b/Dockerfile index 17f3baa..2e8366c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,12 +16,17 @@ COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/ internal/ +# Version of the project +ARG VERSION="dev" + # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager \ + -ldflags "-X 'github.com/scality/crl-operator/internal.Version=${VERSION}'" \ + cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index 8d58eb9..bde4e82 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -40,6 +40,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" + "github.com/scality/crl-operator/internal" ) const ( @@ -47,6 +48,17 @@ const ( renewBefore = 1 * time.Hour // secretCRLKey is the key in the Secret data where the CRL is stored. secretCRLKey = "ca.crl" + + // Common labels + labelManagedByName = "app.kubernetes.io/managed-by" + labelManagedByValue = "crl-operator" + + labelComponentName = "app.kubernetes.io/component" + labelComponentValue = "managed-crl" + labelAppName = "app.kubernetes.io/name" + labelInstanceName = "app.kubernetes.io/instance" + + labelVersionName = "app.kubernetes.io/version" ) // ManagedCRLReconciler reconciles a ManagedCRL object @@ -192,21 +204,9 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) } op, err := controllerutil.CreateOrUpdate(ctx, r.Client, secret, func() error { - labels := secret.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - // Add default labels - labels["app.kubernetes.io/managed-by"] = "crl-operator" - labels["app.kubernetes.io/component"] = "managed-crl" - labels["app.kubernetes.io/name"] = secret.Name - labels["app.kubernetes.io/instance"] = instance.Name - - secret.SetLabels(labels) - - err := controllerutil.SetControllerReference(instance, secret, r.Scheme) + err := r.stdMutate(secret, instance) if err != nil { - return fmt.Errorf("failed to set owner reference on Secret: %w", err) + return err } secret.Data = map[string][]byte{ @@ -407,6 +407,29 @@ func (r *ManagedCRLReconciler) crlNeedRenewal(currentCRL *x509.RevocationList, r return false } +// stdMutate applies the standard mutations to the managed resources +// (The one we manage with `CreateOrUpdate`) +func (r *ManagedCRLReconciler) stdMutate(obj metav1.Object, instance *crloperatorv1alpha1.ManagedCRL) error { + labels := obj.GetLabels() + if labels == nil { + labels = make(map[string]string) + } + // Add default labels + labels[labelManagedByName] = labelManagedByValue + labels[labelComponentName] = labelComponentValue + labels[labelAppName] = obj.GetName() + labels[labelInstanceName] = instance.Name + labels[labelVersionName] = internal.Version + + obj.SetLabels(labels) + + err := controllerutil.SetControllerReference(instance, obj, r.Scheme) + if err != nil { + return fmt.Errorf("failed to set owner reference on Secret: %w", err) + } + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *ManagedCRLReconciler) SetupWithManager(mgr ctrl.Manager) error { mapIssuerToCRL := func(ctx context.Context, obj client.Object) []ctrl.Request { diff --git a/internal/version.go b/internal/version.go new file mode 100644 index 0000000..ded9986 --- /dev/null +++ b/internal/version.go @@ -0,0 +1,7 @@ +package internal + +var ( + // Version is the current version of the operator. + // It is set during build time using -ldflags. + Version = "dev" +) From 3a3f09fa7eeb70aac54283ef97abe74219481828 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Thu, 30 Oct 2025 06:55:18 +0000 Subject: [PATCH 5/7] chore: Handle exposing CRL with nginx Signed-off-by: Teddy Andrieux --- api/v1alpha1/managedcrl_types.go | 142 ++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 89 ++++++- .../crl-operator.scality.com_managedcrls.yaml | 95 ++++++++ config/rbac/role.yaml | 30 ++- internal/controller/managedcrl_controller.go | 224 ++++++++++++++++-- internal/utils/object.go | 19 ++ 6 files changed, 576 insertions(+), 23 deletions(-) create mode 100644 internal/utils/object.go diff --git a/api/v1alpha1/managedcrl_types.go b/api/v1alpha1/managedcrl_types.go index 382392b..8e8fe05 100644 --- a/api/v1alpha1/managedcrl_types.go +++ b/api/v1alpha1/managedcrl_types.go @@ -23,12 +23,51 @@ import ( "strings" cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) +// ImageSpec defines information about the image to expose the CRL. +type ImageSpec struct { + // Repository is the container image repository. + // +optional + Repository *string `json:"repository"` + + // Name is the container image name. + // (default: "nginx") + // +optional + Name *string `json:"name"` + + // Tag is the container image tag. + // (default: "1.29.3-alpine3.22") + // +optional + Tag *string `json:"tag"` + + // PullSecretRef is a reference to a Secret containing the image pull + // credentials. + // +optional + PullSecrets []corev1.LocalObjectReference `json:"pullSecrets,omitempty"` +} + +// CRLExposeSpec defines how the CRL should be exposed. +type CRLExposeSpec struct { + // Enabled indicates whether the CRL should be exposed. + Enabled bool `json:"enabled"` + + // Image specifies the container image to use for exposing the CRL. + // +optional + Image *ImageSpec `json:"image"` + // Node Selector to deploy the CRL server + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // Tolerations to deploy the CRL server + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` +} + // RevocationSpec defines a certificate to be revoked. type RevocationSpec struct { // SerialNumber is the serial number of the certificate to be revoked. @@ -58,13 +97,19 @@ type ManagedCRLSpec struct { // Revocations is a list of certificates to be revoked. // +optional Revocations []RevocationSpec `json:"revocations,omitempty"` + + // Expose specifies how the CRL should be exposed. + // +optional + Expose *CRLExposeSpec `json:"expose,omitempty"` } // ManagedCRLStatus defines the observed state of ManagedCRL. type ManagedCRLStatus struct { // SecretReady indicates whether the CRL is built and available in the Secret. - SecretReady *bool `json:"secretReady,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + SecretReady *bool `json:"secretReady,omitempty"` + // PodExposed indicates whether the CRL expose Pod is running. + PodExposed *bool `json:"podExposed,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` // CRLValidUntil is the time until which the CRL is valid. CRLValidUntil metav1.Time `json:"crlValidUntil,omitempty"` @@ -109,6 +154,11 @@ func init() { SchemeBuilder.Register(&ManagedCRL{}, &ManagedCRLList{}) } +// IsExposed returns true if the CRL is configured to be exposed. +func (mcrl *ManagedCRL) IsExposed() bool { + return mcrl.Spec.Expose != nil && mcrl.Spec.Expose.Enabled +} + // GetSecret returns the name of the Secret used to store the CRL. func (mcrl *ManagedCRL) GetSecret() *corev1.Secret { return &corev1.Secret{ @@ -119,6 +169,37 @@ func (mcrl *ManagedCRL) GetSecret() *corev1.Secret { } } +// GetConfigMap returns the name of the ConfigMap used to configure the CRL expose Pod. +func (mcrl *ManagedCRL) GetConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-server-config", mcrl.Name), + Namespace: mcrl.Namespace, + }, + } +} + +// GetDeployment returns the name of the Deployment used to expose the CRL. +func (mcrl *ManagedCRL) GetDeployment() *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-server", mcrl.Name), + Namespace: mcrl.Namespace, + }, + } +} + +// GetService returns the name of the Service used to expose the CRL. +func (mcrl *ManagedCRL) GetService() *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-server", mcrl.Name), + Namespace: mcrl.Namespace, + }, + } +} + +// WithDefaults sets default values on the ManagedCRL resource. func (mcrl *ManagedCRL) WithDefaults() { mcrl.Spec.withDefaults() } @@ -131,6 +212,10 @@ func (mcrls *ManagedCRLSpec) withDefaults() { for i := range mcrls.Revocations { mcrls.Revocations[i].withDefaults() } + + if mcrls.Expose != nil { + mcrls.Expose.withDefaults() + } } func (rs *RevocationSpec) withDefaults() { @@ -142,6 +227,22 @@ func (rs *RevocationSpec) withDefaults() { } } +func (ces *CRLExposeSpec) withDefaults() { + if ces.Image == nil { + ces.Image = &ImageSpec{} + } + ces.Image.withDefaults() +} + +func (is *ImageSpec) withDefaults() { + if is.Name == nil { + is.Name = ptr.To("nginx") + } + if is.Tag == nil { + is.Tag = ptr.To("1.29.3-alpine3.22") + } +} + // ToRevocationListEntry converts a RevocationSpec to an x509.RevocationListEntry. func (rs RevocationSpec) ToRevocationListEntry() (x509.RevocationListEntry, error) { cleanSerial := strings.ReplaceAll(rs.SerialNumber, ":", "") @@ -179,6 +280,15 @@ func (mcrls *ManagedCRLSpec) GetRevokedListEntries() ([]x509.RevocationListEntry return revokedCerts, nil } +// GetImage returns the full image string in the format "repository/name:tag". +func (is *ImageSpec) GetImage() string { + image := fmt.Sprintf("%s:%s", *is.Name, *is.Tag) + if is.Repository != nil { + image = fmt.Sprintf("%s/%s", *is.Repository, image) + } + return image +} + // SetSecretReady sets the ManagedCRL status to SecretReady. func (mcrl *ManagedCRL) SetSecretReady() { condition := metav1.Condition{ @@ -206,3 +316,31 @@ func (mcrl *ManagedCRL) SetSecretNotReady(reason, message string) { meta.SetStatusCondition(&mcrl.Status.Conditions, condition) mcrl.Status.SecretReady = ptr.To(false) } + +// SetPodExposed sets the ManagedCRL status to PodExposed. +func (mcrl *ManagedCRL) SetPodExposed() { + condition := metav1.Condition{ + Type: "PodExposed", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "CRLPodExposed", + Message: "The pod exposing the CRL is running", + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.PodExposed = ptr.To(true) +} + +// SetPodNotExposed sets the ManagedCRL status to PodNotExposed with the given reason and message. +func (mcrl *ManagedCRL) SetPodNotExposed(reason, message string) { + condition := metav1.Condition{ + Type: "PodExposed", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.PodExposed = ptr.To(false) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 57b1648..8888409 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,11 +21,80 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CRLExposeSpec) DeepCopyInto(out *CRLExposeSpec) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(ImageSpec) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRLExposeSpec. +func (in *CRLExposeSpec) DeepCopy() *CRLExposeSpec { + if in == nil { + return nil + } + out := new(CRLExposeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageSpec) DeepCopyInto(out *ImageSpec) { + *out = *in + if in.Repository != nil { + in, out := &in.Repository, &out.Repository + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Tag != nil { + in, out := &in.Tag, &out.Tag + *out = new(string) + **out = **in + } + if in.PullSecrets != nil { + in, out := &in.PullSecrets, &out.PullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSpec. +func (in *ImageSpec) DeepCopy() *ImageSpec { + if in == nil { + return nil + } + out := new(ImageSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedCRL) DeepCopyInto(out *ManagedCRL) { *out = *in @@ -91,7 +160,7 @@ func (in *ManagedCRLSpec) DeepCopyInto(out *ManagedCRLSpec) { in.IssuerRef.DeepCopyInto(&out.IssuerRef) if in.Duration != nil { in, out := &in.Duration, &out.Duration - *out = new(v1.Duration) + *out = new(metav1.Duration) **out = **in } if in.Revocations != nil { @@ -101,6 +170,11 @@ func (in *ManagedCRLSpec) DeepCopyInto(out *ManagedCRLSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Expose != nil { + in, out := &in.Expose, &out.Expose + *out = new(CRLExposeSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedCRLSpec. @@ -121,9 +195,14 @@ func (in *ManagedCRLStatus) DeepCopyInto(out *ManagedCRLStatus) { *out = new(bool) **out = **in } + if in.PodExposed != nil { + in, out := &in.PodExposed, &out.PodExposed + *out = new(bool) + **out = **in + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -131,7 +210,7 @@ func (in *ManagedCRLStatus) DeepCopyInto(out *ManagedCRLStatus) { in.CRLValidUntil.DeepCopyInto(&out.CRLValidUntil) if in.ObservedCASecretRef != nil { in, out := &in.ObservedCASecretRef, &out.ObservedCASecretRef - *out = new(corev1.SecretReference) + *out = new(v1.SecretReference) **out = **in } } diff --git a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml index eb41d6d..6d6de47 100644 --- a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml +++ b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml @@ -56,6 +56,98 @@ spec: Duration is the duration for which the CRL is valid. (default: 168h = 7 days) type: string + expose: + description: Expose specifies how the CRL should be exposed. + properties: + enabled: + description: Enabled indicates whether the CRL should be exposed. + type: boolean + image: + description: Image specifies the container image to use for exposing + the CRL. + properties: + name: + description: |- + Name is the container image name. + (default: "nginx") + type: string + pullSecrets: + description: |- + PullSecretRef is a reference to a Secret containing the image pull + credentials. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + 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 + type: object + x-kubernetes-map-type: atomic + type: array + repository: + description: Repository is the container image repository. + type: string + tag: + description: |- + Tag is the container image tag. + (default: "1.29.3-alpine3.22") + type: string + type: object + nodeSelector: + additionalProperties: + type: string + description: Node Selector to deploy the CRL server + type: object + tolerations: + description: Tolerations to deploy the CRL server + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - enabled + type: object issuerRef: description: |- IssuerRef is a reference to the cert-manager Issuer or ClusterIssuer @@ -190,6 +282,9 @@ spec: ObservedCASecretVersion is the resource version of the Secret containing the last CA certificate and private key used to sign the CRL. type: string + podExposed: + description: PodExposed indicates whether the CRL expose Pod is running. + type: boolean secretReady: description: SecretReady indicates whether the CRL is built and available in the Secret. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1a50046..ed94295 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,14 +7,28 @@ rules: - apiGroups: - "" resources: - - events + - configmaps + - secrets + - services verbs: - create + - delete + - get + - list - patch + - update + - watch - apiGroups: - "" resources: - - secrets + - events + verbs: + - create + - patch +- apiGroups: + - apps + resources: + - deployments verbs: - create - delete @@ -58,3 +72,15 @@ rules: - get - patch - update +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index bde4e82..0433a3c 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -29,10 +29,13 @@ import ( cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -41,6 +44,7 @@ import ( crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1" "github.com/scality/crl-operator/internal" + "github.com/scality/crl-operator/internal/utils" ) const ( @@ -59,6 +63,24 @@ const ( labelInstanceName = "app.kubernetes.io/instance" labelVersionName = "app.kubernetes.io/version" + + // server configuration + nginxConfig = ` +server { + listen 8080; + server_name _; + + location = /ca.crl { + root /srv; + + types { } + default_type application/pkix-crl; + } + + location / { + return 404; + } +}` ) // ManagedCRLReconciler reconciles a ManagedCRL object @@ -73,7 +95,9 @@ type ManagedCRLReconciler struct { // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls/finalizers,verbs=update // +kubebuilder:rbac:groups=cert-manager.io,resources=issuers;clusterissuers,verbs=get;list;watch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=configmaps;secrets;services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -109,8 +133,23 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) }() // Simple helper to handle errors and update status + // Create variable to track what need to be set unavailable + secretReady := false + podReady := false handleError := func(err error, reason, message string) (ctrl.Result, error) { // nolint:unparam // It's clearer - instance.SetSecretNotReady(reason, message) + nextReason := reason + nextMessage := message + + if !secretReady { + instance.SetSecretNotReady(nextReason, nextMessage) + nextReason = "SecretNotReady" + nextMessage = "secret is not ready" + } + if instance.IsExposed() && !podReady { + instance.SetPodNotExposed(nextReason, nextMessage) + nextReason = "PodNotExposed" + nextMessage = "pod is not exposed" + } logger.Error(err, message) return ctrl.Result{}, fmt.Errorf("%s: %w", message, err) } @@ -235,6 +274,165 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) instance.Status.ObservedCASecretVersion = caSecret.ResourceVersion instance.SetSecretReady() + secretReady = true + + // Handle expose if specified + if instance.IsExposed() { + // Handle server Configuration + cm := instance.GetConfigMap() + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { + err := r.stdMutate(cm, instance) + if err != nil { + return err + } + + cm.Data = map[string]string{ + "default.conf": nginxConfig, + } + return nil + }) + if err != nil { + return handleError( + err, + "FailedToCreateOrUpdateServerConfigMap", + "failed to create or update ConfigMap for the server", + ) + } + if op != controllerutil.OperationResultNone { + logger.Info("ConfigMap for the server reconciled", "operation", op) + } + + // Handle Deployment for the server + selector := map[string]string{ + labelAppName: instance.Name, + labelInstanceName: instance.Name, + } + deploy := instance.GetDeployment() + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error { + err := r.stdMutate(deploy, instance) + if err != nil { + return err + } + + // Add selector labels + utils.UpdateLabels(&deploy.Spec.Template.ObjectMeta, selector) + deploy.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: selector, + } + + // Add replicas + deploy.Spec.Replicas = ptr.To[int32](2) + + // Add NodeSelector and Tolerations and ImagePullSecrets + deploy.Spec.Template.Spec.NodeSelector = instance.Spec.Expose.NodeSelector + deploy.Spec.Template.Spec.Tolerations = instance.Spec.Expose.Tolerations + deploy.Spec.Template.Spec.ImagePullSecrets = instance.Spec.Expose.Image.PullSecrets + + // Add volumes + deploy.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cm.GetName(), + }, + }, + }, + }, { + Name: "crl", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secret.GetName(), + }, + }, + }, + } + + // If we have more than 1 container we clean up the containers list + if len(deploy.Spec.Template.Spec.Containers) != 1 { + deploy.Spec.Template.Spec.Containers = []corev1.Container{{}} + } + container := &deploy.Spec.Template.Spec.Containers[0] + + // Handle container definition + container.Name = "server" + container.Image = instance.Spec.Expose.Image.GetImage() + container.Ports = []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + }, + } + container.VolumeMounts = []corev1.VolumeMount{ + { + Name: "config", + MountPath: "/etc/nginx/conf.d/default.conf", + SubPath: "default.conf", + ReadOnly: true, + }, { + Name: "crl", + MountPath: "/srv/", + ReadOnly: true, + }, + } + + return nil + }) + if err != nil { + return handleError( + err, + "FailedToCreateOrUpdateServerDeployment", + "failed to create or update Deployment for the server", + ) + } + if op != controllerutil.OperationResultNone { + logger.Info("Deployment for the server reconciled", "operation", op) + } + + // Handle Service for the server + svc := instance.GetService() + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, svc, func() error { + err := r.stdMutate(svc, instance) + if err != nil { + return err + } + + svc.Spec.Selector = selector + svc.Spec.Ports = []corev1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + } + svc.Spec.Type = corev1.ServiceTypeClusterIP + + return nil + }) + if err != nil { + return handleError( + err, + "FailedToCreateOrUpdateServerService", + "failed to create or update Service for the server", + ) + } + if op != controllerutil.OperationResultNone { + logger.Info("Service for the server reconciled", "operation", op) + } + + // Check if the Deployment is ready + if deploy.Status.ReadyReplicas != deploy.Status.Replicas || deploy.Status.ReadyReplicas == 0 { + return handleError( + errors.New("deployment not ready"), + "ServerPodNotReady", + "server pod is not ready", + ) + } + + instance.SetPodExposed() + podReady = true + } // All good // We still have to requeue before expiry to renew the CRL @@ -410,18 +608,13 @@ func (r *ManagedCRLReconciler) crlNeedRenewal(currentCRL *x509.RevocationList, r // stdMutate applies the standard mutations to the managed resources // (The one we manage with `CreateOrUpdate`) func (r *ManagedCRLReconciler) stdMutate(obj metav1.Object, instance *crloperatorv1alpha1.ManagedCRL) error { - labels := obj.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - // Add default labels - labels[labelManagedByName] = labelManagedByValue - labels[labelComponentName] = labelComponentValue - labels[labelAppName] = obj.GetName() - labels[labelInstanceName] = instance.Name - labels[labelVersionName] = internal.Version - - obj.SetLabels(labels) + utils.UpdateLabels(obj, map[string]string{ + labelManagedByName: labelManagedByValue, + labelComponentName: labelComponentValue, + labelAppName: obj.GetName(), + labelInstanceName: instance.Name, + labelVersionName: internal.Version, + }) err := controllerutil.SetControllerReference(instance, obj, r.Scheme) if err != nil { @@ -480,6 +673,9 @@ func (r *ManagedCRLReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&crloperatorv1alpha1.ManagedCRL{}). Owns(&corev1.Secret{}). + Owns(&corev1.ConfigMap{}). + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). Watches(&cmv1.ClusterIssuer{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). Watches(&cmv1.Issuer{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(mapIssuerToCRL)). diff --git a/internal/utils/object.go b/internal/utils/object.go new file mode 100644 index 0000000..a2d09b4 --- /dev/null +++ b/internal/utils/object.go @@ -0,0 +1,19 @@ +package utils + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Update labels on an Object +func UpdateLabels(object metav1.Object, labels map[string]string) { + current := object.GetLabels() + if current == nil { + current = make(map[string]string) + } + + for k, v := range labels { + current[k] = v + } + + object.SetLabels(current) +} From a1a09d4194078470195447645abffe41d189a113 Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Thu, 30 Oct 2025 15:05:15 +0000 Subject: [PATCH 6/7] chore: Add some validation of fields Signed-off-by: Teddy Andrieux --- api/v1alpha1/managedcrl_types.go | 69 +++++++++++++++++++ .../crl-operator.scality.com_managedcrls.yaml | 4 ++ internal/controller/managedcrl_controller.go | 3 + 3 files changed, 76 insertions(+) diff --git a/api/v1alpha1/managedcrl_types.go b/api/v1alpha1/managedcrl_types.go index 8e8fe05..06a4a0b 100644 --- a/api/v1alpha1/managedcrl_types.go +++ b/api/v1alpha1/managedcrl_types.go @@ -33,16 +33,19 @@ import ( // ImageSpec defines information about the image to expose the CRL. type ImageSpec struct { // Repository is the container image repository. + // +kubebuilder:validation:MinLength=1 // +optional Repository *string `json:"repository"` // Name is the container image name. // (default: "nginx") + // +kubebuilder:validation:MinLength=1 // +optional Name *string `json:"name"` // Tag is the container image tag. // (default: "1.29.3-alpine3.22") + // +kubebuilder:validation:MinLength=1 // +optional Tag *string `json:"tag"` @@ -71,6 +74,7 @@ type CRLExposeSpec struct { // RevocationSpec defines a certificate to be revoked. type RevocationSpec struct { // SerialNumber is the serial number of the certificate to be revoked. + // +kubebuilder:validation:MinLength=1 SerialNumber string `json:"serialNumber"` // RevocationTime is the time at which the certificate was revoked. @@ -243,6 +247,71 @@ func (is *ImageSpec) withDefaults() { } } +// Validate validates the ManagedCRL resource. +func (mcrl *ManagedCRL) Validate() error { + err := mcrl.Spec.validate() + if err != nil { + return fmt.Errorf("spec validation failed: %w", err) + } + return nil +} + +func (mcrls *ManagedCRLSpec) validate() error { + // IssuerRef kind supported is only ClusterIssuer or Issuer + if mcrls.IssuerRef.Kind != "Issuer" && mcrls.IssuerRef.Kind != "ClusterIssuer" { + return fmt.Errorf("issuerRef kind must be either 'Issuer' or 'ClusterIssuer', got '%s'", mcrls.IssuerRef.Kind) + } + + // Ensure duration is at least a day + if mcrls.Duration.Hours() < 24 { + return fmt.Errorf("duration must be at least 24h") + } + + for i, revocation := range mcrls.Revocations { + err := revocation.validate() + if err != nil { + return fmt.Errorf("invalid revocation at index %d: %w", i, err) + } + } + + // Ensure we can get the revoked list entries + _, err := mcrls.GetRevokedListEntries() + if err != nil { + return fmt.Errorf("failed to get revoked list entries: %w", err) + } + + if mcrls.Expose != nil { + err := mcrls.Expose.validate() + if err != nil { + return fmt.Errorf("invalid expose configuration: %w", err) + } + } + + return nil +} + +func (rs *RevocationSpec) validate() error { + // Nothing to validate for now, it's validated by the GetRevokedListEntries method + return nil +} + +func (ces *CRLExposeSpec) validate() error { + if !ces.Enabled { + return nil + } + + err := ces.Image.validate() + if err != nil { + return fmt.Errorf("invalid image configuration: %w", err) + } + return nil +} + +func (is *ImageSpec) validate() error { + // Nothing to validate for now + return nil +} + // ToRevocationListEntry converts a RevocationSpec to an x509.RevocationListEntry. func (rs RevocationSpec) ToRevocationListEntry() (x509.RevocationListEntry, error) { cleanSerial := strings.ReplaceAll(rs.SerialNumber, ":", "") diff --git a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml index 6d6de47..70b1114 100644 --- a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml +++ b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml @@ -70,6 +70,7 @@ spec: description: |- Name is the container image name. (default: "nginx") + minLength: 1 type: string pullSecrets: description: |- @@ -94,11 +95,13 @@ spec: type: array repository: description: Repository is the container image repository. + minLength: 1 type: string tag: description: |- Tag is the container image tag. (default: "1.29.3-alpine3.22") + minLength: 1 type: string type: object nodeSelector: @@ -187,6 +190,7 @@ spec: serialNumber: description: SerialNumber is the serial number of the certificate to be revoked. + minLength: 1 type: string required: - serialNumber diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index 0433a3c..d0c3a96 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -121,6 +121,9 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Apply defaults instance.WithDefaults() + if err := instance.Validate(); err != nil { + return ctrl.Result{}, fmt.Errorf("validation failed: %w", err) + } needRenewal := false original := instance.DeepCopy() From f91eef180bcd5e2de730f5f8d07b1ca16bdd094c Mon Sep 17 00:00:00 2001 From: Teddy Andrieux Date: Thu, 30 Oct 2025 15:31:04 +0000 Subject: [PATCH 7/7] chore: Handle configuration of CRL Distribution Points Signed-off-by: Teddy Andrieux --- api/v1alpha1/managedcrl_types.go | 198 +++++++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 60 ++++++ .../crl-operator.scality.com_managedcrls.yaml | 49 +++++ config/rbac/role.yaml | 1 + internal/controller/managedcrl_controller.go | 145 ++++++++++++- 5 files changed, 442 insertions(+), 11 deletions(-) diff --git a/api/v1alpha1/managedcrl_types.go b/api/v1alpha1/managedcrl_types.go index 06a4a0b..4456b73 100644 --- a/api/v1alpha1/managedcrl_types.go +++ b/api/v1alpha1/managedcrl_types.go @@ -25,11 +25,15 @@ import ( cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) +// +kubebuilder:validation:Format=ipv4 +type IPAddress string + // ImageSpec defines information about the image to expose the CRL. type ImageSpec struct { // Repository is the container image repository. @@ -55,6 +59,35 @@ type ImageSpec struct { PullSecrets []corev1.LocalObjectReference `json:"pullSecrets,omitempty"` } +// IngressSpec defines the ingress configuration for exposing the CRL. +type IngressSpec struct { + // Enabled indicates whether to create an Ingress resource to expose the CRL. + // (default: true) + // +optional + Enabled *bool `json:"enabled"` + + // Managed indicates whether the operator should manage the Ingress resource. + // If false, the Ingress resource will not be created or updated by the operator. + // (default: true) + // +optional + Managed *bool `json:"managed"` + + // Hostname is the hostname to use for the ingress. + // (One of Hostname or IPAddresses must be specified) + // +kubebuilder:validation:MinLength=1 + // +optional + Hostname *string `json:"hostname,omitempty"` + + // ClassName is the ingress class name to use for the ingress. + // +optional + ClassName *string `json:"className,omitempty"` + + // IPAddresses is a list of IP addresses to use for the ingress. + // (One of Hostname or IPAddresses must be specified) + // +optional + IPAddresses []IPAddress `json:"ipAddresses,omitempty"` +} + // CRLExposeSpec defines how the CRL should be exposed. type CRLExposeSpec struct { // Enabled indicates whether the CRL should be exposed. @@ -69,6 +102,18 @@ type CRLExposeSpec struct { // Tolerations to deploy the CRL server // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // Internal indicates whether the issuer should be configured to reach the + // CRL internally within the cluster. + // (default: true) + // +optional + Internal *bool `json:"internal"` + + // Ingress indicates whether the CRL should be exposed externally outside the cluster + // using an Ingress resource. + // (default: Disabled) + // +optional + Ingress *IngressSpec `json:"ingress"` } // RevocationSpec defines a certificate to be revoked. @@ -112,8 +157,12 @@ type ManagedCRLStatus struct { // SecretReady indicates whether the CRL is built and available in the Secret. SecretReady *bool `json:"secretReady,omitempty"` // PodExposed indicates whether the CRL expose Pod is running. - PodExposed *bool `json:"podExposed,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + PodExposed *bool `json:"podExposed,omitempty"` + // IngressExposed indicates whether the CRL Ingress is available. + IngressExposed *bool `json:"ingressExposed,omitempty"` + // IssuerConfigured indicates whether the Issuer is properly configured. + IssuerConfigured *bool `json:"issuerConfigured,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` // CRLValidUntil is the time until which the CRL is valid. CRLValidUntil metav1.Time `json:"crlValidUntil,omitempty"` @@ -163,6 +212,21 @@ func (mcrl *ManagedCRL) IsExposed() bool { return mcrl.Spec.Expose != nil && mcrl.Spec.Expose.Enabled } +// IsIngressEnabled returns true if the CRL is configured to be exposed via Ingress. +func (mcrl *ManagedCRL) IsIngressEnabled() bool { + return mcrl.IsExposed() && mcrl.Spec.Expose.Ingress != nil && *mcrl.Spec.Expose.Ingress.Enabled +} + +// IsIngressManaged returns true if the Ingress is managed by the operator. +func (mcrl *ManagedCRL) IsIngressManaged() bool { + return mcrl.IsIngressEnabled() && mcrl.Spec.Expose.Ingress.Managed != nil && *mcrl.Spec.Expose.Ingress.Managed +} + +// IsInternalEnabled returns true if the CRL is configured to be exposed internally. +func (mcrl *ManagedCRL) IsInternalEnabled() bool { + return mcrl.IsExposed() && mcrl.Spec.Expose.Internal != nil && *mcrl.Spec.Expose.Internal +} + // GetSecret returns the name of the Secret used to store the CRL. func (mcrl *ManagedCRL) GetSecret() *corev1.Secret { return &corev1.Secret{ @@ -203,6 +267,16 @@ func (mcrl *ManagedCRL) GetService() *corev1.Service { } } +// GetIngress returns the name of the Ingress used to expose the CRL. +func (mcrl *ManagedCRL) GetIngress() *networkingv1.Ingress { + return &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-server", mcrl.Name), + Namespace: mcrl.Namespace, + }, + } +} + // WithDefaults sets default values on the ManagedCRL resource. func (mcrl *ManagedCRL) WithDefaults() { mcrl.Spec.withDefaults() @@ -236,6 +310,14 @@ func (ces *CRLExposeSpec) withDefaults() { ces.Image = &ImageSpec{} } ces.Image.withDefaults() + + if ces.Ingress != nil { + ces.Ingress.withDefaults() + } + + if ces.Internal == nil { + ces.Internal = ptr.To(true) + } } func (is *ImageSpec) withDefaults() { @@ -247,6 +329,15 @@ func (is *ImageSpec) withDefaults() { } } +func (is *IngressSpec) withDefaults() { + if is.Enabled == nil { + is.Enabled = ptr.To(true) + } + if is.Managed == nil { + is.Managed = ptr.To(true) + } +} + // Validate validates the ManagedCRL resource. func (mcrl *ManagedCRL) Validate() error { err := mcrl.Spec.validate() @@ -304,6 +395,14 @@ func (ces *CRLExposeSpec) validate() error { if err != nil { return fmt.Errorf("invalid image configuration: %w", err) } + + if ces.Ingress != nil { + err := ces.Ingress.validate() + if err != nil { + return fmt.Errorf("invalid ingress configuration: %w", err) + } + } + return nil } @@ -312,6 +411,18 @@ func (is *ImageSpec) validate() error { return nil } +func (is *IngressSpec) validate() error { + if !*is.Enabled { + return nil + } + + if is.Hostname == nil && len(is.IPAddresses) == 0 { + return fmt.Errorf("either hostname or ipAddresses must be specified") + } + + return nil +} + // ToRevocationListEntry converts a RevocationSpec to an x509.RevocationListEntry. func (rs RevocationSpec) ToRevocationListEntry() (x509.RevocationListEntry, error) { cleanSerial := strings.ReplaceAll(rs.SerialNumber, ":", "") @@ -358,6 +469,33 @@ func (is *ImageSpec) GetImage() string { return image } +// GetCRLDistributionPoint returns the CRL distribution point URL based on the Ingress configuration. +func (mcrl *ManagedCRL) GetCRLDistributionPoint() []string { + var urls []string + + // Add Ingress URLs if enabled + if mcrl.IsIngressEnabled() { + if mcrl.Spec.Expose.Ingress.Hostname != nil { + urls = append(urls, fmt.Sprintf("http://%s/ca.crl", *mcrl.Spec.Expose.Ingress.Hostname)) + } + for _, ip := range mcrl.Spec.Expose.Ingress.IPAddresses { + urls = append(urls, fmt.Sprintf("http://%s/ca.crl", ip)) + } + } + + // Add internal URL if enabled + if mcrl.IsInternalEnabled() { + urls = append(urls, fmt.Sprintf("http://%s.%s.svc/ca.crl", mcrl.GetName(), mcrl.GetNamespace())) + } + + return urls +} + +// NeedsIssuerConfiguration returns true if the Issuer needs to be configured. +func (mcrl *ManagedCRL) NeedsIssuerConfiguration() bool { + return len(mcrl.GetCRLDistributionPoint()) > 0 +} + // SetSecretReady sets the ManagedCRL status to SecretReady. func (mcrl *ManagedCRL) SetSecretReady() { condition := metav1.Condition{ @@ -413,3 +551,59 @@ func (mcrl *ManagedCRL) SetPodNotExposed(reason, message string) { meta.SetStatusCondition(&mcrl.Status.Conditions, condition) mcrl.Status.PodExposed = ptr.To(false) } + +// SetIngressExposed sets the ManagedCRL status to IngressExposed. +func (mcrl *ManagedCRL) SetIngressExposed() { + condition := metav1.Condition{ + Type: "IngressExposed", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "CRLIngressExposed", + Message: "The ingress exposing the CRL is available", + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.IngressExposed = ptr.To(true) +} + +// SetIngressNotExposed sets the ManagedCRL status to IngressNotExposed with the given reason and message. +func (mcrl *ManagedCRL) SetIngressNotExposed(reason, message string) { + condition := metav1.Condition{ + Type: "IngressExposed", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.IngressExposed = ptr.To(false) +} + +// SetIssuerConfigured sets the ManagedCRL status to IssuerConfigured. +func (mcrl *ManagedCRL) SetIssuerConfigured() { + condition := metav1.Condition{ + Type: "IssuerConfigured", + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: "CRLIssuerConfigured", + Message: "The issuer is properly configured", + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.IssuerConfigured = ptr.To(true) +} + +// SetIssuerNotConfigured sets the ManagedCRL status to IssuerNotConfigured with the given reason and message. +func (mcrl *ManagedCRL) SetIssuerNotConfigured(reason, message string) { + condition := metav1.Condition{ + Type: "IssuerConfigured", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + ObservedGeneration: mcrl.Generation, + } + meta.SetStatusCondition(&mcrl.Status.Conditions, condition) + mcrl.Status.IssuerConfigured = ptr.To(false) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8888409..0cdce52 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -48,6 +48,16 @@ func (in *CRLExposeSpec) DeepCopyInto(out *CRLExposeSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Internal != nil { + in, out := &in.Internal, &out.Internal + *out = new(bool) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRLExposeSpec. @@ -95,6 +105,46 @@ func (in *ImageSpec) DeepCopy() *ImageSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressSpec) DeepCopyInto(out *IngressSpec) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Managed != nil { + in, out := &in.Managed, &out.Managed + *out = new(bool) + **out = **in + } + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.ClassName != nil { + in, out := &in.ClassName, &out.ClassName + *out = new(string) + **out = **in + } + if in.IPAddresses != nil { + in, out := &in.IPAddresses, &out.IPAddresses + *out = make([]IPAddress, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressSpec. +func (in *IngressSpec) DeepCopy() *IngressSpec { + if in == nil { + return nil + } + out := new(IngressSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedCRL) DeepCopyInto(out *ManagedCRL) { *out = *in @@ -200,6 +250,16 @@ func (in *ManagedCRLStatus) DeepCopyInto(out *ManagedCRLStatus) { *out = new(bool) **out = **in } + if in.IngressExposed != nil { + in, out := &in.IngressExposed, &out.IngressExposed + *out = new(bool) + **out = **in + } + if in.IssuerConfigured != nil { + in, out := &in.IssuerConfigured, &out.IssuerConfigured + *out = new(bool) + **out = **in + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) diff --git a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml index 70b1114..d8eadcc 100644 --- a/config/crd/bases/crl-operator.scality.com_managedcrls.yaml +++ b/config/crd/bases/crl-operator.scality.com_managedcrls.yaml @@ -104,6 +104,48 @@ spec: minLength: 1 type: string type: object + ingress: + description: |- + Ingress indicates whether the CRL should be exposed externally outside the cluster + using an Ingress resource. + (default: Disabled) + properties: + className: + description: ClassName is the ingress class name to use for + the ingress. + type: string + enabled: + description: |- + Enabled indicates whether to create an Ingress resource to expose the CRL. + (default: true) + type: boolean + hostname: + description: |- + Hostname is the hostname to use for the ingress. + (One of Hostname or IPAddresses must be specified) + minLength: 1 + type: string + ipAddresses: + description: |- + IPAddresses is a list of IP addresses to use for the ingress. + (One of Hostname or IPAddresses must be specified) + items: + format: ipv4 + type: string + type: array + managed: + description: |- + Managed indicates whether the operator should manage the Ingress resource. + If false, the Ingress resource will not be created or updated by the operator. + (default: true) + type: boolean + type: object + internal: + description: |- + Internal indicates whether the issuer should be configured to reach the + CRL internally within the cluster. + (default: true) + type: boolean nodeSelector: additionalProperties: type: string @@ -266,6 +308,13 @@ spec: description: CRLValidUntil is the time until which the CRL is valid. format: date-time type: string + ingressExposed: + description: IngressExposed indicates whether the CRL Ingress is available. + type: boolean + issuerConfigured: + description: IssuerConfigured indicates whether the Issuer is properly + configured. + type: boolean observedCASecretRef: description: |- ObservedCASecretRef is a reference to the Secret containing the last diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ed94295..ce7135e 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -45,6 +45,7 @@ rules: verbs: - get - list + - patch - watch - apiGroups: - crl-operator.scality.com diff --git a/internal/controller/managedcrl_controller.go b/internal/controller/managedcrl_controller.go index d0c3a96..0cb90df 100644 --- a/internal/controller/managedcrl_controller.go +++ b/internal/controller/managedcrl_controller.go @@ -31,6 +31,7 @@ import ( cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -94,7 +95,7 @@ type ManagedCRLReconciler struct { // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls/status,verbs=get;update;patch // +kubebuilder:rbac:groups=crl-operator.scality.com,resources=managedcrls/finalizers,verbs=update -// +kubebuilder:rbac:groups=cert-manager.io,resources=issuers;clusterissuers,verbs=get;list;watch +// +kubebuilder:rbac:groups=cert-manager.io,resources=issuers;clusterissuers,verbs=get;list;watch;patch // +kubebuilder:rbac:groups="",resources=configmaps;secrets;services,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete @@ -139,6 +140,7 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Create variable to track what need to be set unavailable secretReady := false podReady := false + ingressReady := false handleError := func(err error, reason, message string) (ctrl.Result, error) { // nolint:unparam // It's clearer nextReason := reason nextMessage := message @@ -153,6 +155,15 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) nextReason = "PodNotExposed" nextMessage = "pod is not exposed" } + if instance.IsIngressManaged() && !ingressReady { + instance.SetIngressNotExposed(nextReason, nextMessage) + nextReason = "IngressNotExposed" + nextMessage = "ingress is not exposed" + } + if instance.NeedsIssuerConfiguration() { + instance.SetIssuerNotConfigured(nextReason, nextMessage) + } + logger.Error(err, message) return ctrl.Result{}, fmt.Errorf("%s: %w", message, err) } @@ -177,7 +188,15 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Get the Secret containing the CA certificate and private key - caSecret, err := r.getIssuerSecret(ctx, instance.Namespace, instance.Spec.IssuerRef) + issuer, err := r.getIssuer(ctx, instance.Namespace, instance.Spec.IssuerRef) + if err != nil { + return handleError( + err, + "FailedToGetIssuer", + "failed to get issuer", + ) + } + caSecret, err := r.getIssuerSecret(ctx, instance.Namespace, issuer) if err != nil { return handleError( err, @@ -435,6 +454,96 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) instance.SetPodExposed() podReady = true + + // Handle Ingress if enabled + if instance.IsIngressManaged() { + ingress := instance.GetIngress() + op, err = controllerutil.CreateOrUpdate(ctx, r.Client, ingress, func() error { + err := r.stdMutate(ingress, instance) + if err != nil { + return err + } + + ingress.Spec.IngressClassName = instance.Spec.Expose.Ingress.ClassName + + ingressRule := &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/ca.crl", + PathType: ptr.To(networkingv1.PathTypePrefix), + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: fmt.Sprintf("%s-server", instance.Name), + Port: networkingv1.ServiceBackendPort{ + Number: 80, + }, + }, + }, + }, + }, + } + + ingress.Spec.Rules = []networkingv1.IngressRule{{}} + if instance.Spec.Expose.Ingress.Hostname != nil { + ingress.Spec.Rules[0].Host = *instance.Spec.Expose.Ingress.Hostname + } + ingress.Spec.Rules[0].HTTP = ingressRule + + // If we have some IPAddress let's add an entry without host + // to support direct access + if len(instance.Spec.Expose.Ingress.IPAddresses) > 0 { + ingress.Spec.Rules = append(ingress.Spec.Rules, networkingv1.IngressRule{}) + ingress.Spec.Rules[1].HTTP = ingressRule + } + + return nil + }) + if err != nil { + return handleError( + err, + "FailedToCreateOrUpdateServerIngress", + "failed to create or update Ingress for the server", + ) + } + if op != controllerutil.OperationResultNone { + logger.Info("Ingress for the server reconciled", "operation", op) + } + + instance.SetIngressExposed() + ingressReady = true + } + + // Update the Issuer to add CRL Distribution points + if instance.NeedsIssuerConfiguration() { + var originalIssuer client.Object + desiredDP := instance.GetCRLDistributionPoint() + + switch issuer := issuer.(type) { + case *cmv1.Issuer: + originalIssuer = issuer.DeepCopy() + issuer.Spec.CA.CRLDistributionPoints = desiredDP + case *cmv1.ClusterIssuer: + originalIssuer = issuer.DeepCopy() + issuer.Spec.CA.CRLDistributionPoints = desiredDP + default: + return handleError( + errors.New("unsupported issuer kind for updating CRL Distribution Points"), + "UnsupportedIssuerKind", + "unsupported issuer kind for updating CRL Distribution Points", + ) + } + + err = r.Patch(ctx, issuer, client.MergeFrom(originalIssuer)) + if err != nil { + return handleError( + err, + "FailedToPatchIssuer", + "failed to patch issuer", + ) + } + + instance.SetIssuerConfigured() + } } // All good @@ -444,16 +553,35 @@ func (r *ManagedCRLReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{RequeueAfter: requeueAfter}, nil } -// getIssuerSecret retrieves the Secret containing the CA certificate and private key -func (r *ManagedCRLReconciler) getIssuerSecret(ctx context.Context, namespace string, issuerRef cmmetav1.IssuerReference) (*corev1.Secret, error) { - var secretRef client.ObjectKey - +// getIssuer retireves the Issuer or ClusterIssuer specified in the IssuerReference +func (r *ManagedCRLReconciler) getIssuer(ctx context.Context, namespace string, issuerRef cmmetav1.IssuerReference) (client.Object, error) { switch issuerRef.Kind { case "Issuer": issuer := &cmv1.Issuer{} if err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: issuerRef.Name}, issuer); err != nil { return nil, err } + return issuer, nil + case "ClusterIssuer": + issuer := &cmv1.ClusterIssuer{} + if err := r.Get(ctx, client.ObjectKey{Name: issuerRef.Name}, issuer); err != nil { + return nil, err + } + return issuer, nil + default: + return nil, errors.New("unsupported issuer kind") + } +} + +// getIssuerSecret retrieves the Secret containing the CA certificate and private key +func (r *ManagedCRLReconciler) getIssuerSecret(ctx context.Context, namespace string, issuer client.Object) (*corev1.Secret, error) { + var secretRef client.ObjectKey + + switch issuer := issuer.(type) { + case *cmv1.Issuer: + if err := r.Get(ctx, client.ObjectKey{Namespace: namespace, Name: issuer.Name}, issuer); err != nil { + return nil, err + } if issuer.Spec.CA == nil { return nil, errors.New("issuer is not a CA issuer") } @@ -461,9 +589,8 @@ func (r *ManagedCRLReconciler) getIssuerSecret(ctx context.Context, namespace st Name: issuer.Spec.CA.SecretName, Namespace: namespace, } - case "ClusterIssuer": - issuer := &cmv1.ClusterIssuer{} - if err := r.Get(ctx, client.ObjectKey{Name: issuerRef.Name}, issuer); err != nil { + case *cmv1.ClusterIssuer: + if err := r.Get(ctx, client.ObjectKey{Name: issuer.Name}, issuer); err != nil { return nil, err } if issuer.Spec.CA == nil {