From 854d484482768fff92566a89922e2cc60dd7a52f Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Fri, 3 May 2024 12:03:10 +0200 Subject: [PATCH 01/43] feat: add flagd crd and controller to deploy flagd Signed-off-by: Florian Bacher --- PROJECT | 13 ++ apis/core/v1beta1/flagd_types.go | 108 +++++++++ apis/core/v1beta1/zz_generated.deepcopy.go | 131 +++++++++++ common/common.go | 7 + common/flagdproxy/flagdproxy.go | 18 +- common/flagdproxy/flagdproxy_test.go | 21 +- .../bases/core.openfeature.dev_flagds.yaml | 166 +++++++++++++ config/crd/kustomization.yaml | 3 + .../patches/cainjection_in_core_flagds.yaml | 7 + .../crd/patches/webhook_in_core_flagds.yaml | 16 ++ config/rbac/core_flagd_editor_role.yaml | 31 +++ config/rbac/core_flagd_viewer_role.yaml | 27 +++ config/rbac/role.yaml | 52 +++++ config/samples/core_v1beta1_flagd.yaml | 12 + controllers/core/flagd/controller.go | 218 ++++++++++++++++++ go.mod | 7 +- go.sum | 1 + main.go | 23 +- 18 files changed, 830 insertions(+), 31 deletions(-) create mode 100644 apis/core/v1beta1/flagd_types.go create mode 100644 config/crd/bases/core.openfeature.dev_flagds.yaml create mode 100644 config/crd/patches/cainjection_in_core_flagds.yaml create mode 100644 config/crd/patches/webhook_in_core_flagds.yaml create mode 100644 config/rbac/core_flagd_editor_role.yaml create mode 100644 config/rbac/core_flagd_viewer_role.yaml create mode 100644 config/samples/core_v1beta1_flagd.yaml create mode 100644 controllers/core/flagd/controller.go diff --git a/PROJECT b/PROJECT index face2ff62..6f02aac63 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: openfeature.dev layout: - go.kubebuilder.io/v3 @@ -59,4 +63,13 @@ resources: kind: FeatureFlagSource path: github.com/open-feature/open-feature-operator/apis/core/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openfeature.dev + group: core + kind: Flagd + path: github.com/open-feature/open-feature-operator/apis/core/v1beta1 + version: v1beta1 version: "3" diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go new file mode 100644 index 000000000..8651f6544 --- /dev/null +++ b/apis/core/v1beta1/flagd_types.go @@ -0,0 +1,108 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + v1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// 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. + +// FlagdSpec defines the desired state of Flagd +type FlagdSpec struct { + // Replicas defines the number of replicas to create for the service + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // ServiceType represents the type of Service to create. + // Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName. + // Default: ClusterIP + // +optional + // +kubebuilder:default=ClusterIP + ServiceType v1.ServiceType `json:"serviceType,omitempty"` + + // NodePort represents the port at which the NodePort service to allocate + // +optional + NodePort int32 `json:"nodePort,omitempty"` + + // ServiceAccountName the service account name for the flagd deployment + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // OtelCollectorUri defines the OpenTelemetry collector URI to enable OpenTelemetry Tracing in flagd. + // +optional + OtelCollectorUri string `json:"otelCollectorUri"` + + // FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves + // the feature flag configurations + FeatureFlagSourceRef v1.ObjectReference `json:"featureFlagSourceRef"` + + // Ingress + // +optional + Ingress IngressSpec `json:"ingress"` +} + +// IngressSpec defines the options to be used when deploying the ingress for flagd +type IngressSpec struct { + // Enabled enables/disables the ingress for flagd + Enabled bool `json:"enabled,omitempty"` + + // Annotations the annotations to be added to the ingress + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + + // Hosts list of hosts to be added to the ingress + // +optional + Hosts []string `json:"hosts,omitempty"` + + // TLS configuration for the ingress + TLS []networkingv1.IngressTLS `json:"tls,omitempty"` +} + +// FlagdStatus defines the observed state of Flagd +type FlagdStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Flagd is the Schema for the flagds API +type Flagd struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FlagdSpec `json:"spec,omitempty"` + Status FlagdStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// FlagdList contains a list of Flagd +type FlagdList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Flagd `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Flagd{}, &FlagdList{}) +} diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index 96f2831df..520856d57 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1beta1 import ( "encoding/json" "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -290,6 +291,136 @@ func (in *FlagSpec) DeepCopy() *FlagSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flagd) DeepCopyInto(out *Flagd) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagd. +func (in *Flagd) DeepCopy() *Flagd { + if in == nil { + return nil + } + out := new(Flagd) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Flagd) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdList) DeepCopyInto(out *FlagdList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Flagd, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdList. +func (in *FlagdList) DeepCopy() *FlagdList { + if in == nil { + return nil + } + out := new(FlagdList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlagdList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdSpec) DeepCopyInto(out *FlagdSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + out.FeatureFlagSourceRef = in.FeatureFlagSourceRef + in.Ingress.DeepCopyInto(&out.Ingress) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdSpec. +func (in *FlagdSpec) DeepCopy() *FlagdSpec { + if in == nil { + return nil + } + out := new(FlagdSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdStatus) DeepCopyInto(out *FlagdStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdStatus. +func (in *FlagdStatus) DeepCopy() *FlagdStatus { + if in == nil { + return nil + } + out := new(FlagdStatus) + in.DeepCopyInto(out) + 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.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = make([]networkingv1.IngressTLS, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// 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 *Source) DeepCopyInto(out *Source) { *out = *in diff --git a/common/common.go b/common/common.go index 42ec9d462..364b69ac3 100644 --- a/common/common.go +++ b/common/common.go @@ -30,6 +30,8 @@ const ( ProbeInitialDelay = 5 FeatureFlagSourceAnnotation = "featureflagsource" EnabledAnnotation = "enabled" + ManagedByAnnotationValue = "open-feature-operator" + OperatorDeploymentName = "open-feature-operator-controller-manager" ) var ErrFlagdProxyNotReady = errors.New("flagd-proxy is not ready, deferring pod admission") @@ -77,3 +79,8 @@ func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference) } return false } + +func IsManagedByOFO(d *appsV1.Deployment) bool { + val, ok := d.Labels["app.kubernetes.io/managed-by"] + return ok && val == ManagedByAnnotationValue +} diff --git a/common/flagdproxy/flagdproxy.go b/common/flagdproxy/flagdproxy.go index 056c61d65..e4ade7a5c 100644 --- a/common/flagdproxy/flagdproxy.go +++ b/common/flagdproxy/flagdproxy.go @@ -3,6 +3,7 @@ package flagdproxy import ( "context" "fmt" + "github.com/open-feature/open-feature-operator/common" "reflect" "github.com/go-logr/logr" @@ -16,11 +17,9 @@ import ( ) const ( - ManagedByAnnotationValue = "open-feature-operator" FlagdProxyDeploymentName = "flagd-proxy" FlagdProxyServiceAccountName = "open-feature-operator-flagd-proxy" FlagdProxyServiceName = "flagd-proxy-svc" - operatorDeploymentName = "open-feature-operator-controller-manager" ) type FlagdProxyHandler struct { @@ -46,7 +45,7 @@ func NewFlagdProxyConfiguration(env types.EnvConfig) *FlagdProxyConfiguration { Image: env.FlagdProxyImage, Tag: env.FlagdProxyTag, Namespace: env.PodNamespace, - OperatorDeploymentName: operatorDeploymentName, + OperatorDeploymentName: common.OperatorDeploymentName, Port: env.FlagdProxyPort, ManagementPort: env.FlagdProxyManagementPort, DebugLogging: env.FlagdProxyDebugLogging, @@ -121,7 +120,7 @@ func (f *FlagdProxyHandler) newFlagdProxyServiceManifest(ownerReference *metav1. Spec: corev1.ServiceSpec{ Selector: map[string]string{ "app.kubernetes.io/name": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, }, Ports: []corev1.ServicePort{ { @@ -150,7 +149,7 @@ func (f *FlagdProxyHandler) newFlagdProxyManifest(ownerReference *metav1.OwnerRe Namespace: f.config.Namespace, Labels: map[string]string{ "app": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, "app.kubernetes.io/version": f.config.Tag, }, OwnerReferences: []metav1.OwnerReference{*ownerReference}, @@ -167,7 +166,7 @@ func (f *FlagdProxyHandler) newFlagdProxyManifest(ownerReference *metav1.OwnerRe Labels: map[string]string{ "app": FlagdProxyDeploymentName, "app.kubernetes.io/name": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, "app.kubernetes.io/version": f.config.Tag, }, }, @@ -211,7 +210,7 @@ func (f *FlagdProxyHandler) doesFlagdProxyExist(ctx context.Context) (bool, *app } func (f *FlagdProxyHandler) shouldUpdateFlagdProxy(old, new *appsV1.Deployment) bool { - if !isDeployedByOFO(old) { + if !common.IsManagedByOFO(old) { f.Log.Info("flagd-proxy Deployment not managed by OFO") return false } @@ -241,8 +240,3 @@ func (f *FlagdProxyHandler) getOwnerReference(ctx context.Context) (*metav1.Owne Kind: operatorDeployment.Kind, }, nil } - -func isDeployedByOFO(d *appsV1.Deployment) bool { - val, ok := d.Labels["app.kubernetes.io/managed-by"] - return ok && val == ManagedByAnnotationValue -} diff --git a/common/flagdproxy/flagdproxy_test.go b/common/flagdproxy/flagdproxy_test.go index 1991a25a5..dd0232a67 100644 --- a/common/flagdproxy/flagdproxy_test.go +++ b/common/flagdproxy/flagdproxy_test.go @@ -3,6 +3,7 @@ package flagdproxy import ( "context" "fmt" + "github.com/open-feature/open-feature-operator/common" "testing" "github.com/go-logr/logr/testr" @@ -29,7 +30,7 @@ func TestNewFlagdProxyConfiguration(t *testing.T) { Port: 8015, ManagementPort: 8016, DebugLogging: false, - OperatorDeploymentName: operatorDeploymentName, + OperatorDeploymentName: common.OperatorDeploymentName, }, kpConfig) } @@ -53,7 +54,7 @@ func TestNewFlagdProxyConfiguration_OverrideEnvVars(t *testing.T) { Image: "my-image", Tag: "my-tag", Namespace: "my-namespace", - OperatorDeploymentName: operatorDeploymentName, + OperatorDeploymentName: common.OperatorDeploymentName, }, kpConfig) } @@ -137,7 +138,7 @@ func TestFlagdProxyHandler_HandleFlagdProxy_ProxyExistsWithBadVersion(t *testing Name: FlagdProxyDeploymentName, OwnerReferences: []metav1.OwnerReference{*ownerRef}, Labels: map[string]string{ - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, }, }, Spec: v1.DeploymentSpec{ @@ -325,7 +326,7 @@ func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { Namespace: "ns", Labels: map[string]string{ "app": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, "app.kubernetes.io/version": "tag", }, ResourceVersion: "1", @@ -333,7 +334,7 @@ func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { { APIVersion: "apps/v1", Kind: "Deployment", - Name: operatorDeploymentName, + Name: common.OperatorDeploymentName, }, }, }, @@ -349,7 +350,7 @@ func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { Labels: map[string]string{ "app": FlagdProxyDeploymentName, "app.kubernetes.io/name": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, "app.kubernetes.io/version": "tag", }, }, @@ -401,14 +402,14 @@ func TestFlagdProxyHandler_HandleFlagdProxy_CreateProxy(t *testing.T) { { APIVersion: "apps/v1", Kind: "Deployment", - Name: operatorDeploymentName, + Name: common.OperatorDeploymentName, }, }, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "app.kubernetes.io/name": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": ManagedByAnnotationValue, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, }, Ports: []corev1.ServicePort{ { @@ -427,14 +428,14 @@ func createOFOTestDeployment(ns string) *v1.Deployment { return &v1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Namespace: ns, - Name: operatorDeploymentName, + Name: common.OperatorDeploymentName, }, } } func getTestOFODeploymentOwnerRef(c client.Client, ns string) (*metav1.OwnerReference, error) { d := &appsV1.Deployment{} - if err := c.Get(context.TODO(), client.ObjectKey{Name: operatorDeploymentName, Namespace: ns}, d); err != nil { + if err := c.Get(context.TODO(), client.ObjectKey{Name: common.OperatorDeploymentName, Namespace: ns}, d); err != nil { return nil, err } return &metav1.OwnerReference{ diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml new file mode 100644 index 000000000..b049fc644 --- /dev/null +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -0,0 +1,166 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: flagds.core.openfeature.dev +spec: + group: core.openfeature.dev + names: + kind: Flagd + listKind: FlagdList + plural: flagds + singular: flagd + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: Flagd is the Schema for the flagds API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FlagdSpec defines the desired state of Flagd + properties: + featureFlagSourceRef: + description: |- + FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves + the feature flag configurations + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + TODO: this design is not final and this field is subject to change in the future. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + ingress: + description: Ingress + properties: + annotations: + additionalProperties: + type: string + description: Annotations the annotations to be added to the ingress + type: object + enabled: + description: Enabled enables/disables the ingress for flagd + type: boolean + hosts: + description: Hosts list of hosts to be added to the ingress + items: + type: string + type: array + tls: + description: TLS configuration for the ingress + items: + description: IngressTLS describes the transport layer security + associated with an Ingress. + properties: + hosts: + description: |- + Hosts are a list of hosts included in the TLS certificate. The values in + this list must match the name/s used in the tlsSecret. Defaults to the + wildcard host setting for the loadbalancer controller fulfilling this + Ingress, if left unspecified. + items: + type: string + type: array + x-kubernetes-list-type: atomic + secretName: + description: |- + SecretName is the name of the secret used to terminate TLS traffic on + port 443. Field is left optional to allow TLS routing based on SNI + hostname alone. If the SNI host in a listener conflicts with the "Host" + header field used by an IngressRule, the SNI host is used for termination + and value of the Host header is used for routing. + type: string + type: object + type: array + type: object + nodePort: + description: NodePort represents the port at which the NodePort service + to allocate + format: int32 + type: integer + otelCollectorUri: + description: OtelCollectorUri defines the OpenTelemetry collector + URI to enable OpenTelemetry Tracing in flagd. + type: string + replicas: + description: Replicas defines the number of replicas to create for + the service + format: int32 + type: integer + serviceAccountName: + description: ServiceAccountName the service account name for the flagd + deployment + type: string + serviceType: + default: ClusterIP + description: |- + ServiceType represents the type of Service to create. + Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName. + Default: ClusterIP + type: string + required: + - featureFlagSourceRef + type: object + status: + description: FlagdStatus defines the observed state of Flagd + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b7902f7e0..877f4444c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,6 +4,7 @@ resources: - bases/core.openfeature.dev_featureflags.yaml - bases/core.openfeature.dev_featureflagsources.yaml +- bases/core.openfeature.dev_flagds.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -11,12 +12,14 @@ patchesStrategicMerge: # patches here are for enabling the conversion webhook for each CRD #- patches/webhook_in_featureflags.yaml #- patches/webhook_in_featureflagsources.yaml +#- patches/webhook_in_flagds.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- patches/cainjection_in_featureflags.yaml #- patches/cainjection_in_featureflagsources.yaml +#- patches/cainjection_in_flagds.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_core_flagds.yaml b/config/crd/patches/cainjection_in_core_flagds.yaml new file mode 100644 index 000000000..a1b2f05d1 --- /dev/null +++ b/config/crd/patches/cainjection_in_core_flagds.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: flagds.core.openfeature.dev diff --git a/config/crd/patches/webhook_in_core_flagds.yaml b/config/crd/patches/webhook_in_core_flagds.yaml new file mode 100644 index 000000000..3c992a318 --- /dev/null +++ b/config/crd/patches/webhook_in_core_flagds.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: flagds.core.openfeature.dev +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/core_flagd_editor_role.yaml b/config/rbac/core_flagd_editor_role.yaml new file mode 100644 index 000000000..5e6329e89 --- /dev/null +++ b/config/rbac/core_flagd_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit flagds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flagd-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: open-feature-operator + app.kubernetes.io/part-of: open-feature-operator + app.kubernetes.io/managed-by: kustomize + name: flagd-editor-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - flagds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.openfeature.dev + resources: + - flagds/status + verbs: + - get diff --git a/config/rbac/core_flagd_viewer_role.yaml b/config/rbac/core_flagd_viewer_role.yaml new file mode 100644 index 000000000..efa24e29a --- /dev/null +++ b/config/rbac/core_flagd_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view flagds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: flagd-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: open-feature-operator + app.kubernetes.io/part-of: open-feature-operator + app.kubernetes.io/managed-by: kustomize + name: flagd-viewer-role +rules: +- apiGroups: + - core.openfeature.dev + resources: + - flagds + verbs: + - get + - list + - watch +- apiGroups: + - core.openfeature.dev + resources: + - flagds/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3f811c918..8ab09dad2 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -56,6 +56,19 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - services + - services/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - core.openfeature.dev resources: @@ -73,6 +86,7 @@ rules: resources: - featureflagsources/finalizers verbs: + - get - update - apiGroups: - core.openfeature.dev @@ -82,6 +96,44 @@ rules: - get - patch - update +- apiGroups: + - core.openfeature.dev + resources: + - flagds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.openfeature.dev + resources: + - flagds/finalizers + verbs: + - update +- apiGroups: + - core.openfeature.dev + resources: + - flagds/status + verbs: + - get + - patch + - update +- apiGroups: + - extensions + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resourceNames: diff --git a/config/samples/core_v1beta1_flagd.yaml b/config/samples/core_v1beta1_flagd.yaml new file mode 100644 index 000000000..13cfa0198 --- /dev/null +++ b/config/samples/core_v1beta1_flagd.yaml @@ -0,0 +1,12 @@ +apiVersion: core.openfeature.dev/v1beta1 +kind: Flagd +metadata: + labels: + app.kubernetes.io/name: flagd + app.kubernetes.io/instance: flagd-sample + app.kubernetes.io/part-of: open-feature-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: open-feature-operator + name: flagd-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go new file mode 100644 index 000000000..c981e1c08 --- /dev/null +++ b/controllers/core/flagd/controller.go @@ -0,0 +1,218 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flagd + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdinjector" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var operatorOwnerReference *metav1.OwnerReference + +// FlagdReconciler reconciles a Flagd object +type FlagdReconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger + + FlagdInjector flagdinjector.IFlagdContainerInjector + FlagdConfig FlagdConfiguration + + operatorOwnerReference *metav1.OwnerReference +} + +type FlagdConfiguration struct { + Port int + ManagementPort int + DebugLogging bool + Image string + Tag string + + OperatorNamespace string + OperatorDeploymentName string +} + +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/finalizers,verbs=update +//+kubebuilder:rbac:groups=extensions,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=services;services/finalizers,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=core.openfeature.dev,resources=featureflagsources/finalizers,verbs=get + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log.Info("Searching for FeatureFlagSource") + + // Fetch the Flagd resource + flagd := &api.Flagd{} + if err := r.Client.Get(ctx, req.NamespacedName, flagd); err != nil { + if errors.IsNotFound(err) { + // taking down all associated K8s resources is handled by K8s + r.Log.Info(fmt.Sprintf("Flagd resource '%s' not found. Ignoring since object must be deleted", req.NamespacedName)) + return ctrl.Result{Requeue: false}, nil + } + r.Log.Error(err, fmt.Sprintf("Failed to get Flagd resource '%s'", req.NamespacedName)) + return ctrl.Result{}, err + } + + result, err := r.reconcileFlagdDeployment(ctx, req, flagd) + if err != nil || result != nil { + return *result, err + } + + return ctrl.Result{}, nil +} + +func (r *FlagdReconciler) reconcileFlagdDeployment(ctx context.Context, req ctrl.Request, flagd *api.Flagd) (*ctrl.Result, error) { + exists := false + existingDeployment := &v1.Deployment{} + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: flagd.Namespace, + Name: flagd.Name, + }, existingDeployment) + + if err == nil { + exists = true + } else if err != nil && !errors.IsNotFound(err) { + r.Log.Error(err, fmt.Sprintf("Failed to get Flagd deployment '%s'", req.NamespacedName)) + return &ctrl.Result{}, err + } + + // check if the deployment is managed by the operator. + // if not, do not continue to not mess with anything user generated + if !common.IsManagedByOFO(existingDeployment) { + r.Log.Info(fmt.Sprintf("Found existing deployment '%s' that is not managed by OFO. Will not proceed with deployment", req.NamespacedName)) + return &ctrl.Result{}, nil + } + + newDeployment, err := r.getFlagdDeployment(ctx, flagd) + + if exists && !reflect.DeepEqual(existingDeployment, newDeployment) { + if err := r.Client.Update(ctx, newDeployment); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to update Flagd deployment '%s'", req.NamespacedName)) + return &ctrl.Result{}, err + } + } else { + if err := r.Client.Create(ctx, newDeployment); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to create Flagd deployment '%s'", req.NamespacedName)) + return &ctrl.Result{}, err + } + } + return nil, nil +} + +func shouldUpdate(old *v1.Deployment, new *v1.Deployment) bool { + return !reflect.DeepEqual(old, new) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FlagdReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&api.Flagd{}). + Complete(r) +} + +func (r *FlagdReconciler) getFlagdDeployment(ctx context.Context, flagd *api.Flagd) (*v1.Deployment, error) { + + ownerRef, err := r.getOwnerReference(ctx) + if err != nil { + return nil, err + } + labels := map[string]string{ + "app": flagd.Name, + "app.kubernetes.io/name": flagd.Name, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, + "app.kubernetes.io/version": r.FlagdConfig.Tag, + } + deployment := &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: flagd.Name, + Namespace: flagd.Namespace, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + }, + Spec: v1.DeploymentSpec{ + Replicas: flagd.Spec.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": flagd.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: flagd.Spec.ServiceAccountName, + }, + }, + }, + } + + featureFlagSource := &api.FeatureFlagSource{} + + if err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: flagd.Spec.FeatureFlagSourceRef.Namespace, + Name: flagd.Spec.FeatureFlagSourceRef.Name, + }, featureFlagSource); err != nil { + return nil, fmt.Errorf("could not look up feature flag source for flagd: %v", err) + } + + err = r.FlagdInjector.InjectFlagd(ctx, &deployment.ObjectMeta, &deployment.Spec.Template.Spec, &featureFlagSource.Spec) + if err != nil { + return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) + } + + return deployment, nil +} + +func (r *FlagdReconciler) getOwnerReference(ctx context.Context) (*metav1.OwnerReference, error) { + if r.operatorOwnerReference != nil { + return r.operatorOwnerReference, nil + } + + operatorDeployment := &v1.Deployment{} + if err := r.Client.Get(ctx, client.ObjectKey{Name: r.FlagdConfig.OperatorDeploymentName, Namespace: r.FlagdConfig.OperatorNamespace}, operatorDeployment); err != nil { + return nil, fmt.Errorf("unable to fetch operator deployment: %w", err) + } + + r.operatorOwnerReference = &metav1.OwnerReference{ + UID: operatorDeployment.GetUID(), + Name: operatorDeployment.GetName(), + APIVersion: operatorDeployment.APIVersion, + Kind: operatorDeployment.Kind, + } + + return r.operatorOwnerReference, nil +} diff --git a/go.mod b/go.mod index 7db73716b..04c8260c8 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/go-logr/logr v1.2.4 github.com/golang/mock v1.4.4 github.com/kelseyhightower/envconfig v1.4.0 + github.com/onsi/ginkgo/v2 v2.9.2 + github.com/onsi/gomega v1.27.6 github.com/open-feature/open-feature-operator/apis v0.2.40 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.24.0 @@ -27,12 +29,14 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.1.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -42,8 +46,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.9.2 // indirect - github.com/onsi/gomega v1.27.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect @@ -59,6 +61,7 @@ require ( golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.7.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 7d2534e74..ff8f1f5c3 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/main.go b/main.go index 3b8e8fc15..59099e090 100644 --- a/main.go +++ b/main.go @@ -24,13 +24,6 @@ import ( "os" "github.com/kelseyhightower/envconfig" - corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" - "github.com/open-feature/open-feature-operator/common" - "github.com/open-feature/open-feature-operator/common/flagdinjector" - "github.com/open-feature/open-feature-operator/common/flagdproxy" - "github.com/open-feature/open-feature-operator/common/types" - "github.com/open-feature/open-feature-operator/controllers/core/featureflagsource" - webhooks "github.com/open-feature/open-feature-operator/webhooks" "go.uber.org/zap/zapcore" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -45,6 +38,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/webhook" + + corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdinjector" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/open-feature/open-feature-operator/controllers/core/featureflagsource" + "github.com/open-feature/open-feature-operator/controllers/core/flagd" + webhooks "github.com/open-feature/open-feature-operator/webhooks" ) const ( @@ -181,6 +183,13 @@ func main() { os.Exit(1) } + if err = (&flagd.FlagdReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Flagd") + os.Exit(1) + } //+kubebuilder:scaffold:builder hookServer := mgr.GetWebhookServer() podMutator := &webhooks.PodMutator{ From 0dda2099fb49832b98008b018b1dc45efe8c8e70 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Fri, 3 May 2024 12:18:06 +0200 Subject: [PATCH 02/43] extract deployment reconciler into separate component Signed-off-by: Florian Bacher --- controllers/core/flagd/controller.go | 115 +++----------------------- controllers/core/flagd/deployment.go | 118 +++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 105 deletions(-) create mode 100644 controllers/core/flagd/deployment.go diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index c981e1c08..34007bf29 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -21,28 +21,23 @@ import ( "fmt" "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" - "github.com/open-feature/open-feature-operator/common" - "github.com/open-feature/open-feature-operator/common/flagdinjector" v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) -var operatorOwnerReference *metav1.OwnerReference - // FlagdReconciler reconciles a Flagd object type FlagdReconciler struct { client.Client Scheme *runtime.Scheme Log logr.Logger - FlagdInjector flagdinjector.IFlagdContainerInjector - FlagdConfig FlagdConfiguration + FlagdConfig FlagdConfiguration + + FlagdDeployment IFlagdDeployment operatorOwnerReference *metav1.OwnerReference } @@ -86,7 +81,13 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - result, err := r.reconcileFlagdDeployment(ctx, req, flagd) + owner, err := r.getOwnerReference(ctx) + if err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to get owner reference for Flagd resource '%s'", req.NamespacedName)) + return ctrl.Result{}, err + } + + result, err := r.FlagdDeployment.Reconcile(ctx, flagd, *owner) if err != nil || result != nil { return *result, err } @@ -94,48 +95,6 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, nil } -func (r *FlagdReconciler) reconcileFlagdDeployment(ctx context.Context, req ctrl.Request, flagd *api.Flagd) (*ctrl.Result, error) { - exists := false - existingDeployment := &v1.Deployment{} - err := r.Client.Get(ctx, client.ObjectKey{ - Namespace: flagd.Namespace, - Name: flagd.Name, - }, existingDeployment) - - if err == nil { - exists = true - } else if err != nil && !errors.IsNotFound(err) { - r.Log.Error(err, fmt.Sprintf("Failed to get Flagd deployment '%s'", req.NamespacedName)) - return &ctrl.Result{}, err - } - - // check if the deployment is managed by the operator. - // if not, do not continue to not mess with anything user generated - if !common.IsManagedByOFO(existingDeployment) { - r.Log.Info(fmt.Sprintf("Found existing deployment '%s' that is not managed by OFO. Will not proceed with deployment", req.NamespacedName)) - return &ctrl.Result{}, nil - } - - newDeployment, err := r.getFlagdDeployment(ctx, flagd) - - if exists && !reflect.DeepEqual(existingDeployment, newDeployment) { - if err := r.Client.Update(ctx, newDeployment); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to update Flagd deployment '%s'", req.NamespacedName)) - return &ctrl.Result{}, err - } - } else { - if err := r.Client.Create(ctx, newDeployment); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to create Flagd deployment '%s'", req.NamespacedName)) - return &ctrl.Result{}, err - } - } - return nil, nil -} - -func shouldUpdate(old *v1.Deployment, new *v1.Deployment) bool { - return !reflect.DeepEqual(old, new) -} - // SetupWithManager sets up the controller with the Manager. func (r *FlagdReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -143,60 +102,6 @@ func (r *FlagdReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *FlagdReconciler) getFlagdDeployment(ctx context.Context, flagd *api.Flagd) (*v1.Deployment, error) { - - ownerRef, err := r.getOwnerReference(ctx) - if err != nil { - return nil, err - } - labels := map[string]string{ - "app": flagd.Name, - "app.kubernetes.io/name": flagd.Name, - "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, - "app.kubernetes.io/version": r.FlagdConfig.Tag, - } - deployment := &v1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: flagd.Name, - Namespace: flagd.Namespace, - Labels: labels, - OwnerReferences: []metav1.OwnerReference{*ownerRef}, - }, - Spec: v1.DeploymentSpec{ - Replicas: flagd.Spec.Replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": flagd.Name, - }, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: flagd.Spec.ServiceAccountName, - }, - }, - }, - } - - featureFlagSource := &api.FeatureFlagSource{} - - if err := r.Client.Get(ctx, client.ObjectKey{ - Namespace: flagd.Spec.FeatureFlagSourceRef.Namespace, - Name: flagd.Spec.FeatureFlagSourceRef.Name, - }, featureFlagSource); err != nil { - return nil, fmt.Errorf("could not look up feature flag source for flagd: %v", err) - } - - err = r.FlagdInjector.InjectFlagd(ctx, &deployment.ObjectMeta, &deployment.Spec.Template.Spec, &featureFlagSource.Spec) - if err != nil { - return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) - } - - return deployment, nil -} - func (r *FlagdReconciler) getOwnerReference(ctx context.Context) (*metav1.OwnerReference, error) { if r.operatorOwnerReference != nil { return r.operatorOwnerReference, nil diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go new file mode 100644 index 000000000..bf8b7e570 --- /dev/null +++ b/controllers/core/flagd/deployment.go @@ -0,0 +1,118 @@ +package flagd + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdinjector" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type IFlagdDeployment interface { + Reconcile(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*ctrl.Result, error) +} + +type FlagdDeployment struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger + + FlagdInjector flagdinjector.IFlagdContainerInjector + FlagdConfig FlagdConfiguration +} + +func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*ctrl.Result, error) { + exists := false + existingDeployment := &appsv1.Deployment{} + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: flagd.Namespace, + Name: flagd.Name, + }, existingDeployment) + + if err == nil { + exists = true + } else if err != nil && !errors.IsNotFound(err) { + r.Log.Error(err, fmt.Sprintf("Failed to get Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + + // check if the deployment is managed by the operator. + // if not, do not continue to not mess with anything user generated + if !common.IsManagedByOFO(existingDeployment) { + r.Log.Info(fmt.Sprintf("Found existing deployment '%s/%s' that is not managed by OFO. Will not proceed with deployment", flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, nil + } + + newDeployment, err := r.getFlagdDeployment(ctx, flagd, owner) + + if exists && !reflect.DeepEqual(existingDeployment, newDeployment) { + if err := r.Client.Update(ctx, newDeployment); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to update Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + } else { + if err := r.Client.Create(ctx, newDeployment); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to create Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + } + return nil, nil +} + +func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*appsv1.Deployment, error) { + labels := map[string]string{ + "app": flagd.Name, + "app.kubernetes.io/name": flagd.Name, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, + "app.kubernetes.io/version": r.FlagdConfig.Tag, + } + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: flagd.Name, + Namespace: flagd.Namespace, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{owner}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: flagd.Spec.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": flagd.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: flagd.Spec.ServiceAccountName, + }, + }, + }, + } + + featureFlagSource := &api.FeatureFlagSource{} + + if err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: flagd.Spec.FeatureFlagSourceRef.Namespace, + Name: flagd.Spec.FeatureFlagSourceRef.Name, + }, featureFlagSource); err != nil { + return nil, fmt.Errorf("could not look up feature flag source for flagd: %v", err) + } + + err := r.FlagdInjector.InjectFlagd(ctx, &deployment.ObjectMeta, &deployment.Spec.Template.Spec, &featureFlagSource.Spec) + if err != nil { + return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) + } + + return deployment, nil +} From cdc2658194f13b23fb5e02432946b38b3085bc99 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 10:08:40 +0200 Subject: [PATCH 03/43] implement flagd deployment/service/ingress creation Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 13 ++++ common/common.go | 4 +- common/types/envconfig.go | 8 ++ controllers/core/flagd/config.go | 31 ++++++++ controllers/core/flagd/controller.go | 49 +++--------- controllers/core/flagd/deployment.go | 80 ++++++++----------- controllers/core/flagd/ingress.go | 112 +++++++++++++++++++++++++++ controllers/core/flagd/resource.go | 59 ++++++++++++++ controllers/core/flagd/service.go | 98 +++++++++++++++++++++++ main.go | 45 +++++++++-- 10 files changed, 405 insertions(+), 94 deletions(-) create mode 100644 controllers/core/flagd/config.go create mode 100644 controllers/core/flagd/ingress.go create mode 100644 controllers/core/flagd/resource.go create mode 100644 controllers/core/flagd/service.go diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 8651f6544..f2f257218 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + "fmt" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -74,6 +75,10 @@ type IngressSpec struct { // TLS configuration for the ingress TLS []networkingv1.IngressTLS `json:"tls,omitempty"` + + // IngressClassName defines the name if the ingress class to be used for flagd + // +optional + IngressClassName *string `json:"ingressClassName,omitempty"` } // FlagdStatus defines the observed state of Flagd @@ -94,6 +99,14 @@ type Flagd struct { Status FlagdStatus `json:"status,omitempty"` } +func (f *Flagd) GetServiceName() string { + namespace := "default" + if f.Namespace != "" { + namespace = f.Namespace + } + return fmt.Sprintf("%s.%s", f.Name, namespace) +} + //+kubebuilder:object:root=true // FlagdList contains a list of Flagd diff --git a/common/common.go b/common/common.go index 364b69ac3..bb08ac6e7 100644 --- a/common/common.go +++ b/common/common.go @@ -80,7 +80,7 @@ func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference) return false } -func IsManagedByOFO(d *appsV1.Deployment) bool { - val, ok := d.Labels["app.kubernetes.io/managed-by"] +func IsManagedByOFO(obj client.Object) bool { + val, ok := obj.GetLabels()["app.kubernetes.io/managed-by"] return ok && val == ManagedByAnnotationValue } diff --git a/common/types/envconfig.go b/common/types/envconfig.go index 5619c6bc7..29c9e1e6d 100644 --- a/common/types/envconfig.go +++ b/common/types/envconfig.go @@ -9,6 +9,14 @@ type EnvConfig struct { FlagdProxyManagementPort int `envconfig:"FLAGD_PROXY_MANAGEMENT_PORT" default:"8016"` FlagdProxyDebugLogging bool `envconfig:"FLAGD_PROXY_DEBUG_LOGGING" default:"false"` + FlagdImage string `envconfig:"FLAGD_IMAGE" default:"ghcr.io/open-feature/flagd"` + // renovate: datasource=github-tags depName=open-feature/flagd/flagd + FlagdTag string `envconfig:"FLAGD_TAG" default:"v0.5.0"` + FlagdPort int `envconfig:"FLAGD_PORT" default:"8013"` + FlagdOFREPPort int `envconfig:"FLAGD_OFREP_PORT" default:"8016"` + FlagdManagementPort int `envconfig:"FLAGD_MANAGEMENT_PORT" default:"8014"` + FlagdDebugLogging bool `envconfig:"FLAGD_DEBUG_LOGGING" default:"false"` + SidecarEnvVarPrefix string `envconfig:"SIDECAR_ENV_VAR_PREFIX" default:"FLAGD"` SidecarManagementPort int `envconfig:"SIDECAR_MANAGEMENT_PORT" default:"8014"` SidecarPort int `envconfig:"SIDECAR_PORT" default:"8013"` diff --git a/controllers/core/flagd/config.go b/controllers/core/flagd/config.go new file mode 100644 index 000000000..2fe59b8bd --- /dev/null +++ b/controllers/core/flagd/config.go @@ -0,0 +1,31 @@ +package flagd + +import ( + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/types" +) + +type FlagdConfiguration struct { + FlagdPort int + OFREPPort int + MetricsPort int + ManagementPort int + DebugLogging bool + Image string + Tag string + + OperatorNamespace string + OperatorDeploymentName string +} + +func NewFlagdConfiguration(env types.EnvConfig) FlagdConfiguration { + return FlagdConfiguration{ + Image: env.FlagdImage, + Tag: env.FlagdTag, + OperatorDeploymentName: common.OperatorDeploymentName, + FlagdPort: env.FlagdPort, + OFREPPort: env.FlagdOFREPPort, + ManagementPort: env.FlagdManagementPort, + DebugLogging: env.FlagdDebugLogging, + } +} diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index 34007bf29..f8d44d274 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -21,7 +21,6 @@ import ( "fmt" "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" - v1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -37,20 +36,15 @@ type FlagdReconciler struct { FlagdConfig FlagdConfiguration - FlagdDeployment IFlagdDeployment + FlagdDeployment IFlagdResource + FlagdService IFlagdResource + FlagdIngress IFlagdResource operatorOwnerReference *metav1.OwnerReference } -type FlagdConfiguration struct { - Port int - ManagementPort int - DebugLogging bool - Image string - Tag string - - OperatorNamespace string - OperatorDeploymentName string +type IFlagdResource interface { + Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) } //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete @@ -81,14 +75,15 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - owner, err := r.getOwnerReference(ctx) - if err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to get owner reference for Flagd resource '%s'", req.NamespacedName)) - return ctrl.Result{}, err + if result, err := r.FlagdDeployment.Reconcile(ctx, flagd); err != nil || result != nil { + return *result, err } - result, err := r.FlagdDeployment.Reconcile(ctx, flagd, *owner) - if err != nil || result != nil { + if result, err := r.FlagdService.Reconcile(ctx, flagd); err != nil || result != nil { + return *result, err + } + + if result, err := r.FlagdIngress.Reconcile(ctx, flagd); err != nil || result != nil { return *result, err } @@ -101,23 +96,3 @@ func (r *FlagdReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&api.Flagd{}). Complete(r) } - -func (r *FlagdReconciler) getOwnerReference(ctx context.Context) (*metav1.OwnerReference, error) { - if r.operatorOwnerReference != nil { - return r.operatorOwnerReference, nil - } - - operatorDeployment := &v1.Deployment{} - if err := r.Client.Get(ctx, client.ObjectKey{Name: r.FlagdConfig.OperatorDeploymentName, Namespace: r.FlagdConfig.OperatorNamespace}, operatorDeployment); err != nil { - return nil, fmt.Errorf("unable to fetch operator deployment: %w", err) - } - - r.operatorOwnerReference = &metav1.OwnerReference{ - UID: operatorDeployment.GetUID(), - Name: operatorDeployment.GetName(), - APIVersion: operatorDeployment.APIVersion, - Kind: operatorDeployment.Kind, - } - - return r.operatorOwnerReference, nil -} diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go index bf8b7e570..203dee863 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/deployment.go @@ -9,66 +9,47 @@ import ( "github.com/open-feature/open-feature-operator/common/flagdinjector" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) -type IFlagdDeployment interface { - Reconcile(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*ctrl.Result, error) -} - type FlagdDeployment struct { client.Client - Scheme *runtime.Scheme - Log logr.Logger + Log logr.Logger FlagdInjector flagdinjector.IFlagdContainerInjector FlagdConfig FlagdConfiguration -} -func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*ctrl.Result, error) { - exists := false - existingDeployment := &appsv1.Deployment{} - err := r.Client.Get(ctx, client.ObjectKey{ - Namespace: flagd.Namespace, - Name: flagd.Name, - }, existingDeployment) - - if err == nil { - exists = true - } else if err != nil && !errors.IsNotFound(err) { - r.Log.Error(err, fmt.Sprintf("Failed to get Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err - } + ResourceReconciler *ResourceReconciler +} - // check if the deployment is managed by the operator. - // if not, do not continue to not mess with anything user generated - if !common.IsManagedByOFO(existingDeployment) { - r.Log.Info(fmt.Sprintf("Found existing deployment '%s/%s' that is not managed by OFO. Will not proceed with deployment", flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, nil - } +func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { + return r.ResourceReconciler.Reconcile( + ctx, + flagd, + &appsv1.Deployment{}, + func() (client.Object, error) { + return r.getFlagdDeployment(ctx, flagd) + }, + func(old client.Object, new client.Object) bool { + oldDeployment, ok := old.(*appsv1.Deployment) + if !ok { + return false + } - newDeployment, err := r.getFlagdDeployment(ctx, flagd, owner) + newDeployment, ok := new.(*appsv1.Deployment) + if !ok { + return false + } - if exists && !reflect.DeepEqual(existingDeployment, newDeployment) { - if err := r.Client.Update(ctx, newDeployment); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to update Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err - } - } else { - if err := r.Client.Create(ctx, newDeployment); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to create Flagd deployment '%s/%s'", flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err - } - } - return nil, nil + return reflect.DeepEqual(oldDeployment.Spec, newDeployment.Spec) + }, + ) } -func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Flagd, owner metav1.OwnerReference) (*appsv1.Deployment, error) { +func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Flagd) (*appsv1.Deployment, error) { labels := map[string]string{ "app": flagd.Name, "app.kubernetes.io/name": flagd.Name, @@ -77,10 +58,15 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla } deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: flagd.Name, - Namespace: flagd.Namespace, - Labels: labels, - OwnerReferences: []metav1.OwnerReference{owner}, + Name: flagd.Name, + Namespace: flagd.Namespace, + Labels: labels, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: flagd.APIVersion, + Kind: flagd.Kind, + Name: flagd.Kind, + UID: flagd.UID, + }}, }, Spec: appsv1.DeploymentSpec{ Replicas: flagd.Spec.Replicas, diff --git a/controllers/core/flagd/ingress.go b/controllers/core/flagd/ingress.go new file mode 100644 index 000000000..1b0a09e6d --- /dev/null +++ b/controllers/core/flagd/ingress.go @@ -0,0 +1,112 @@ +package flagd + +import ( + "context" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FlagdIngress struct { + client.Client + + Log logr.Logger + FlagdConfig FlagdConfiguration + + ResourceReconciler *ResourceReconciler +} + +func (r FlagdIngress) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { + return r.ResourceReconciler.Reconcile( + ctx, + flagd, + &networkingv1.Ingress{}, + func() (client.Object, error) { + return r.getIngress(flagd), nil + }, + func(old client.Object, new client.Object) bool { + oldIngress, ok := old.(*networkingv1.Ingress) + if !ok { + return false + } + + newIngress, ok := new.(*networkingv1.Ingress) + if !ok { + return false + } + + return reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) + }, + ) +} + +func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { + return &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: flagd.Name, + Namespace: flagd.Namespace, + Labels: map[string]string{ + "app": flagd.Name, + "app.kubernetes.io/name": flagd.Name, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, + "app.kubernetes.io/version": r.FlagdConfig.Tag, + }, + Annotations: flagd.Spec.Ingress.Annotations, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: flagd.APIVersion, + Kind: flagd.Kind, + Name: flagd.Kind, + UID: flagd.UID, + }}, + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: flagd.Spec.Ingress.IngressClassName, + DefaultBackend: &networkingv1.IngressBackend{ + Service: nil, + Resource: nil, + }, + TLS: flagd.Spec.Ingress.TLS, + Rules: r.getRules(flagd), + }, + } +} + +func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule { + rules := make([]networkingv1.IngressRule, 2*len(flagd.Spec.Ingress.Hosts)) + for i, host := range flagd.Spec.Ingress.Hosts { + rules[2*i] = r.getRule(flagd, host, "flagd", int32(r.FlagdConfig.FlagdPort)) + rules[2*i+1] = r.getRule(flagd, host, "ofrep", int32(r.FlagdConfig.OFREPPort)) + } + return rules +} + +func (r FlagdIngress) getRule(flagd *api.Flagd, host, path string, port int32) networkingv1.IngressRule { + pathType := networkingv1.PathTypePrefix + return networkingv1.IngressRule{ + Host: host, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: path, + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: flagd.GetServiceName(), + Port: networkingv1.ServiceBackendPort{ + Number: port, + }, + }, + Resource: nil, + }, + }, + }, + }, + }, + } +} diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go new file mode 100644 index 000000000..0523c3075 --- /dev/null +++ b/controllers/core/flagd/resource.go @@ -0,0 +1,59 @@ +package flagd + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ResourceReconciler struct { + client.Client + Log logr.Logger +} + +func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, newObjFunc func() (client.Object, error), equalFunc func(client.Object, client.Object) bool) (*ctrl.Result, error) { + exists := false + existingObj := obj + err := r.Client.Get(ctx, client.ObjectKey{ + Namespace: flagd.Namespace, + Name: flagd.Name, + }, existingObj) + + if err == nil { + exists = true + } else if err != nil && !errors.IsNotFound(err) { + r.Log.Error(err, fmt.Sprintf("Failed to get flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + + // check if the resource is managed by the operator. + // if not, do not continue to not mess with anything user generated + if !common.IsManagedByOFO(existingObj) { + r.Log.Info(fmt.Sprintf("Found existing %s '%s/%s' that is not managed by OFO. Will not proceed.", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, nil + } + + newObj, err := newObjFunc() + if err != nil { + r.Log.Error(err, fmt.Sprintf("Could not create new flagd %s resource '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + + if exists && !equalFunc(existingObj, newObj) { + if err := r.Client.Update(ctx, newObj); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + } else { + if err := r.Client.Create(ctx, newObj); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to create Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return &ctrl.Result{}, err + } + } + return nil, nil +} diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/service.go new file mode 100644 index 000000000..6a251c14f --- /dev/null +++ b/controllers/core/flagd/service.go @@ -0,0 +1,98 @@ +package flagd + +import ( + "context" + "github.com/go-logr/logr" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FlagdService struct { + client.Client + + Log logr.Logger + FlagdConfig FlagdConfiguration + + ResourceReconciler *ResourceReconciler +} + +func (r FlagdService) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { + return r.ResourceReconciler.Reconcile( + ctx, + flagd, + &v1.Service{}, + func() (client.Object, error) { + return r.getService(flagd), nil + }, + func(old client.Object, new client.Object) bool { + oldService, ok := old.(*v1.Service) + if !ok { + return false + } + + newService, ok := new.(*v1.Service) + if !ok { + return false + } + + return reflect.DeepEqual(oldService.Spec, newService.Spec) + }, + ) +} + +func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: flagd.Name, + Namespace: flagd.Namespace, + Labels: map[string]string{ + "app": flagd.Name, + "app.kubernetes.io/name": flagd.Name, + "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, + "app.kubernetes.io/version": r.FlagdConfig.Tag, + }, + Annotations: flagd.Annotations, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: flagd.APIVersion, + Kind: flagd.Kind, + Name: flagd.Kind, + UID: flagd.UID, + }}, + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": flagd.Name, + }, + Ports: []v1.ServicePort{ + { + Name: "flagd", + Port: int32(r.FlagdConfig.FlagdPort), + TargetPort: intstr.IntOrString{ + IntVal: int32(r.FlagdConfig.FlagdPort), + }, + }, + { + Name: "ofrep", + Port: int32(r.FlagdConfig.OFREPPort), + TargetPort: intstr.IntOrString{ + IntVal: int32(r.FlagdConfig.OFREPPort), + }, + }, + { + Name: "metrics", + Port: int32(r.FlagdConfig.MetricsPort), + TargetPort: intstr.IntOrString{ + IntVal: int32(r.FlagdConfig.MetricsPort), + }, + }, + }, + Type: flagd.Spec.ServiceType, + }, + } +} diff --git a/main.go b/main.go index 59099e090..da5fdf7d4 100644 --- a/main.go +++ b/main.go @@ -183,9 +183,45 @@ func main() { os.Exit(1) } + flagdContainerInjector := &flagdinjector.FlagdContainerInjector{ + Client: mgr.GetClient(), + Logger: ctrl.Log.WithName("flagd-container injector"), + FlagdProxyConfig: kph.Config(), + FlagdResourceRequirements: *resources, + Image: env.SidecarImage, + Tag: env.SidecarTag, + } + + flagdControllerLogger := ctrl.Log.WithName("Flagd Controller") + + flagdResourceReconciler := &flagd.ResourceReconciler{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + } + flagdConfig := flagd.NewFlagdConfiguration(env) + if err = (&flagd.FlagdReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + FlagdDeployment: &flagd.FlagdDeployment{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + FlagdInjector: flagdContainerInjector, + FlagdConfig: flagdConfig, + ResourceReconciler: flagdResourceReconciler, + }, + FlagdService: &flagd.FlagdService{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + FlagdConfig: flagdConfig, + ResourceReconciler: flagdResourceReconciler, + }, + FlagdIngress: &flagd.FlagdIngress{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + FlagdConfig: flagdConfig, + ResourceReconciler: flagdResourceReconciler, + }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Flagd") os.Exit(1) @@ -197,14 +233,7 @@ func main() { Log: ctrl.Log.WithName("mutating-pod-webhook"), FlagdProxyConfig: kph.Config(), Env: env, - FlagdInjector: &flagdinjector.FlagdContainerInjector{ - Client: mgr.GetClient(), - Logger: ctrl.Log.WithName("flagd-container injector"), - FlagdProxyConfig: kph.Config(), - FlagdResourceRequirements: *resources, - Image: env.SidecarImage, - Tag: env.SidecarTag, - }, + FlagdInjector: flagdContainerInjector, } hookServer.Register("/mutate-v1-pod", &webhook.Admission{Handler: podMutator}) From 7c7cd90e24ee2d4fbff6a43462a3cf6ad156f287 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 10:18:56 +0200 Subject: [PATCH 04/43] update crd definition Signed-off-by: Florian Bacher --- apis/core/v1beta1/zz_generated.deepcopy.go | 5 +++++ config/crd/bases/core.openfeature.dev_flagds.yaml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index 520856d57..f5f4a1fdc 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -409,6 +409,11 @@ func (in *IngressSpec) DeepCopyInto(out *IngressSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.IngressClassName != nil { + in, out := &in.IngressClassName, &out.IngressClassName + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressSpec. diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index b049fc644..a17ddaac9 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -101,6 +101,10 @@ spec: items: type: string type: array + ingressClassName: + description: IngressClassName defines the name if the ingress + class to be used for flagd + type: string tls: description: TLS configuration for the ingress items: From 434c2bd1488b86dc11af547c89b520612a0dc96b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 12:46:01 +0200 Subject: [PATCH 05/43] fix creation of resources Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 9 --------- config/rbac/role.yaml | 12 ++++++++++++ config/samples/core_v1beta1_flagd.yaml | 13 ++++++++++++- controllers/core/flagd/config.go | 1 - controllers/core/flagd/controller.go | 2 +- controllers/core/flagd/deployment.go | 17 ++++++++++++++++- controllers/core/flagd/ingress.go | 16 ++++++---------- controllers/core/flagd/resource.go | 8 ++++++-- controllers/core/flagd/service.go | 6 +++--- main.go | 20 ++++++++++---------- 10 files changed, 66 insertions(+), 38 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index f2f257218..335b198e2 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1beta1 import ( - "fmt" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -99,14 +98,6 @@ type Flagd struct { Status FlagdStatus `json:"status,omitempty"` } -func (f *Flagd) GetServiceName() string { - namespace := "default" - if f.Namespace != "" { - namespace = f.Namespace - } - return fmt.Sprintf("%s.%s", f.Name, namespace) -} - //+kubebuilder:object:root=true // FlagdList contains a list of Flagd diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8ab09dad2..c65314e95 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -134,6 +134,18 @@ rules: - patch - update - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resourceNames: diff --git a/config/samples/core_v1beta1_flagd.yaml b/config/samples/core_v1beta1_flagd.yaml index 13cfa0198..592ed6fbe 100644 --- a/config/samples/core_v1beta1_flagd.yaml +++ b/config/samples/core_v1beta1_flagd.yaml @@ -9,4 +9,15 @@ metadata: app.kubernetes.io/created-by: open-feature-operator name: flagd-sample spec: - # TODO(user): Add fields here + replicas: 1 + serviceType: ClusterIP + serviceAccountName: default + featureFlagSourceRef: + name: end-to-end + namespace: default + ingress: + enabled: true + hosts: + - flagd-sample + ingressClassName: nginx + diff --git a/controllers/core/flagd/config.go b/controllers/core/flagd/config.go index 2fe59b8bd..20c7a93b6 100644 --- a/controllers/core/flagd/config.go +++ b/controllers/core/flagd/config.go @@ -8,7 +8,6 @@ import ( type FlagdConfiguration struct { FlagdPort int OFREPPort int - MetricsPort int ManagementPort int DebugLogging bool Image string diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index f8d44d274..304d499be 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -50,7 +50,7 @@ type IFlagdResource interface { //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/status,verbs=get;update;patch //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/finalizers,verbs=update -//+kubebuilder:rbac:groups=extensions,resources=ingresses,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=core,resources=services;services/finalizers,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=core.openfeature.dev,resources=featureflagsources/finalizers,verbs=get diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go index 203dee863..c3e6516d7 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/deployment.go @@ -64,7 +64,7 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla OwnerReferences: []metav1.OwnerReference{{ APIVersion: flagd.APIVersion, Kind: flagd.Kind, - Name: flagd.Kind, + Name: flagd.Name, UID: flagd.UID, }}, }, @@ -100,5 +100,20 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) } + deployment.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{ + { + Name: "management", + ContainerPort: int32(r.FlagdConfig.ManagementPort), + }, + { + Name: "flagd", + ContainerPort: int32(r.FlagdConfig.FlagdPort), + }, + { + Name: "ofrep", + ContainerPort: int32(r.FlagdConfig.OFREPPort), + }, + } + return deployment, nil } diff --git a/controllers/core/flagd/ingress.go b/controllers/core/flagd/ingress.go index 1b0a09e6d..e05210c99 100644 --- a/controllers/core/flagd/ingress.go +++ b/controllers/core/flagd/ingress.go @@ -60,18 +60,14 @@ func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { OwnerReferences: []metav1.OwnerReference{{ APIVersion: flagd.APIVersion, Kind: flagd.Kind, - Name: flagd.Kind, + Name: flagd.Name, UID: flagd.UID, }}, }, Spec: networkingv1.IngressSpec{ IngressClassName: flagd.Spec.Ingress.IngressClassName, - DefaultBackend: &networkingv1.IngressBackend{ - Service: nil, - Resource: nil, - }, - TLS: flagd.Spec.Ingress.TLS, - Rules: r.getRules(flagd), + TLS: flagd.Spec.Ingress.TLS, + Rules: r.getRules(flagd), }, } } @@ -79,8 +75,8 @@ func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule { rules := make([]networkingv1.IngressRule, 2*len(flagd.Spec.Ingress.Hosts)) for i, host := range flagd.Spec.Ingress.Hosts { - rules[2*i] = r.getRule(flagd, host, "flagd", int32(r.FlagdConfig.FlagdPort)) - rules[2*i+1] = r.getRule(flagd, host, "ofrep", int32(r.FlagdConfig.OFREPPort)) + rules[2*i] = r.getRule(flagd, host, "/flagd", int32(r.FlagdConfig.FlagdPort)) + rules[2*i+1] = r.getRule(flagd, host, "/ofrep", int32(r.FlagdConfig.OFREPPort)) } return rules } @@ -97,7 +93,7 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host, path string, port int32) n PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ - Name: flagd.GetServiceName(), + Name: flagd.Name, Port: networkingv1.ServiceBackendPort{ Number: port, }, diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index 0523c3075..a96b8e760 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -7,13 +7,15 @@ import ( api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) type ResourceReconciler struct { client.Client - Log logr.Logger + Scheme *runtime.Scheme + Log logr.Logger } func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, newObjFunc func() (client.Object, error), equalFunc func(client.Object, client.Object) bool) (*ctrl.Result, error) { @@ -33,7 +35,7 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob // check if the resource is managed by the operator. // if not, do not continue to not mess with anything user generated - if !common.IsManagedByOFO(existingObj) { + if exists && !common.IsManagedByOFO(existingObj) { r.Log.Info(fmt.Sprintf("Found existing %s '%s/%s' that is not managed by OFO. Will not proceed.", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) return &ctrl.Result{}, nil } @@ -45,11 +47,13 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob } if exists && !equalFunc(existingObj, newObj) { + r.Log.Info(fmt.Sprintf("Updating %v", newObj)) if err := r.Client.Update(ctx, newObj); err != nil { r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) return &ctrl.Result{}, err } } else { + r.Log.Info(fmt.Sprintf("Creating %v", newObj)) if err := r.Client.Create(ctx, newObj); err != nil { r.Log.Error(err, fmt.Sprintf("Failed to create Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) return &ctrl.Result{}, err diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/service.go index 6a251c14f..161884274 100644 --- a/controllers/core/flagd/service.go +++ b/controllers/core/flagd/service.go @@ -61,7 +61,7 @@ func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { OwnerReferences: []metav1.OwnerReference{{ APIVersion: flagd.APIVersion, Kind: flagd.Kind, - Name: flagd.Kind, + Name: flagd.Name, UID: flagd.UID, }}, }, @@ -86,9 +86,9 @@ func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { }, { Name: "metrics", - Port: int32(r.FlagdConfig.MetricsPort), + Port: int32(r.FlagdConfig.ManagementPort), TargetPort: intstr.IntOrString{ - IntVal: int32(r.FlagdConfig.MetricsPort), + IntVal: int32(r.FlagdConfig.ManagementPort), }, }, }, diff --git a/main.go b/main.go index da5fdf7d4..fdcd16531 100644 --- a/main.go +++ b/main.go @@ -22,8 +22,17 @@ import ( "fmt" "log" "os" + "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/kelseyhightower/envconfig" + corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/common/flagdinjector" + "github.com/open-feature/open-feature-operator/common/flagdproxy" + "github.com/open-feature/open-feature-operator/common/types" + "github.com/open-feature/open-feature-operator/controllers/core/featureflagsource" + "github.com/open-feature/open-feature-operator/controllers/core/flagd" + webhooks "github.com/open-feature/open-feature-operator/webhooks" "go.uber.org/zap/zapcore" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -37,16 +46,6 @@ import ( "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/webhook" - - corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" - "github.com/open-feature/open-feature-operator/common" - "github.com/open-feature/open-feature-operator/common/flagdinjector" - "github.com/open-feature/open-feature-operator/common/flagdproxy" - "github.com/open-feature/open-feature-operator/common/types" - "github.com/open-feature/open-feature-operator/controllers/core/featureflagsource" - "github.com/open-feature/open-feature-operator/controllers/core/flagd" - webhooks "github.com/open-feature/open-feature-operator/webhooks" ) const ( @@ -196,6 +195,7 @@ func main() { flagdResourceReconciler := &flagd.ResourceReconciler{ Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), Log: flagdControllerLogger, } flagdConfig := flagd.NewFlagdConfiguration(env) From 3e12f2085001cada0dcf4b53f0381f6801199b0b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 12:46:31 +0200 Subject: [PATCH 06/43] fix creation of resources Signed-off-by: Florian Bacher --- go.mod | 5 ++++- go.sum | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 04c8260c8..3974abc65 100644 --- a/go.mod +++ b/go.mod @@ -78,4 +78,7 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace golang.org/x/net => golang.org/x/net v0.24.0 +replace ( + golang.org/x/net => golang.org/x/net v0.24.0 + github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63 +) diff --git a/go.sum b/go.sum index ff8f1f5c3..d07799e91 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63 h1:z4kYnRzFjltnxDJXSXfFkrup5bmPkEeTbXuupKroTKY= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63/go.mod h1:I/4tLd5D4JpWpaFZxe2o8R2S1isWGNwHDSC/H5h7o3A= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= From e8501923bb1d0fa2ad564e7e1a00bb1efbc31509 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 14:18:47 +0200 Subject: [PATCH 07/43] add unit tests for controller Signed-off-by: Florian Bacher --- Makefile | 3 +- controllers/core/flagd/controller.go | 17 +- controllers/core/flagd/controller_test.go | 220 ++++++++++++++++++++++ controllers/core/flagd/deployment.go | 3 +- controllers/core/flagd/ingress.go | 3 +- controllers/core/flagd/resource.go | 15 +- controllers/core/flagd/service.go | 3 +- 7 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 controllers/core/flagd/controller_test.go diff --git a/Makefile b/Makefile index 3592de420..2f6734632 100644 --- a/Makefile +++ b/Makefile @@ -255,4 +255,5 @@ helm-package: set-helm-overlay generate release-manifests helm install-mockgen: go install github.com/golang/mock/mockgen@v1.6.0 mockgen: install-mockgen - mockgen -source=controllers/common/flagd-injector.go -destination=controllers/common/mock/flagd-injector.go -package=commonmock + mockgen -source=./common/flagdinjector/flagdinjector.go -destination=./common/flagdinjector/mock/flagd-injector.go -package=commonmock + mockgen -source=./controllers/core/flagd/controller.go -destination=controllers/core/flagd/mock/mock.go -package=commonmock diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index 304d499be..e7060f190 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -22,7 +22,6 @@ import ( "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "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" @@ -39,12 +38,10 @@ type FlagdReconciler struct { FlagdDeployment IFlagdResource FlagdService IFlagdResource FlagdIngress IFlagdResource - - operatorOwnerReference *metav1.OwnerReference } type IFlagdResource interface { - Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) + Reconcile(ctx context.Context, flagd *api.Flagd) error } //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete @@ -75,16 +72,16 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - if result, err := r.FlagdDeployment.Reconcile(ctx, flagd); err != nil || result != nil { - return *result, err + if err := r.FlagdDeployment.Reconcile(ctx, flagd); err != nil { + return ctrl.Result{}, err } - if result, err := r.FlagdService.Reconcile(ctx, flagd); err != nil || result != nil { - return *result, err + if err := r.FlagdService.Reconcile(ctx, flagd); err != nil { + return ctrl.Result{}, err } - if result, err := r.FlagdIngress.Reconcile(ctx, flagd); err != nil || result != nil { - return *result, err + if err := r.FlagdIngress.Reconcile(ctx, flagd); err != nil { + return ctrl.Result{}, err } return ctrl.Result{}, nil diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go new file mode 100644 index 000000000..b68e265fa --- /dev/null +++ b/controllers/core/flagd/controller_test.go @@ -0,0 +1,220 @@ +package flagd + +import ( + "context" + "errors" + "fmt" + "github.com/golang/mock/gomock" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + commonmock "github.com/open-feature/open-feature-operator/controllers/core/flagd/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "reflect" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" +) + +type flagdMatcher struct { + flagdObj api.Flagd +} + +func (fm flagdMatcher) Matches(x interface{}) bool { + flagd, ok := x.(*api.Flagd) + if !ok { + return false + } + return reflect.DeepEqual(fm.flagdObj.ObjectMeta, flagd.ObjectMeta) && reflect.DeepEqual(fm.flagdObj.Spec, flagd.Spec) +} + +// String describes what the matcher matches. +func (fm flagdMatcher) String() string { + return fmt.Sprintf("%v", fm.flagdObj) +} + +func TestFlagdReconciler_Reconcile(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{}, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) + serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) + ingressReconciler := commonmock.NewMockIFlagdResource(ctrl) + + // deployment creation succeeds + deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + ingressReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + + r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, ingressReconciler) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + }) + + require.Nil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + +func TestFlagdReconciler_ReconcileResourceNotFound(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects().Build() + + r := setupReconciler(fakeClient, nil, nil, nil) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "my-namespace", + Name: "my-flagd", + }, + }) + + require.Nil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + +func TestFlagdReconciler_ReconcileFailDeployment(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{}, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) + + // deployment creation succeeds + deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentReconciler, nil, nil) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + }) + + require.NotNil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + +func TestFlagdReconciler_ReconcileFailService(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{}, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) + serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) + + deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, nil) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + }) + + require.NotNil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + +func TestFlagdReconciler_ReconcileFailIngress(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{}, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) + serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) + ingressReconciler := commonmock.NewMockIFlagdResource(ctrl) + + deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) + ingressReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, ingressReconciler) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + }) + + require.NotNil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + +func setupReconciler(fakeClient client.WithWatch, deploymentReconciler *commonmock.MockIFlagdResource, serviceReconciler *commonmock.MockIFlagdResource, ingressReconciler *commonmock.MockIFlagdResource) *FlagdReconciler { + return &FlagdReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("flagd controller"), + FlagdConfig: FlagdConfiguration{ + FlagdPort: 8013, + OFREPPort: 8016, + ManagementPort: 8014, + DebugLogging: false, + Image: "flagd", + Tag: "latest", + OperatorNamespace: "ofo-system", + OperatorDeploymentName: "ofo", + }, + FlagdDeployment: deploymentReconciler, + FlagdService: serviceReconciler, + FlagdIngress: ingressReconciler, + } +} diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go index c3e6516d7..e458f8dc4 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/deployment.go @@ -11,7 +11,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "reflect" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -25,7 +24,7 @@ type FlagdDeployment struct { ResourceReconciler *ResourceReconciler } -func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { +func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd) error { return r.ResourceReconciler.Reconcile( ctx, flagd, diff --git a/controllers/core/flagd/ingress.go b/controllers/core/flagd/ingress.go index e05210c99..5fd106303 100644 --- a/controllers/core/flagd/ingress.go +++ b/controllers/core/flagd/ingress.go @@ -8,7 +8,6 @@ import ( networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "reflect" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -21,7 +20,7 @@ type FlagdIngress struct { ResourceReconciler *ResourceReconciler } -func (r FlagdIngress) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { +func (r FlagdIngress) Reconcile(ctx context.Context, flagd *api.Flagd) error { return r.ResourceReconciler.Reconcile( ctx, flagd, diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index a96b8e760..601bbfab8 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -8,7 +8,6 @@ import ( "github.com/open-feature/open-feature-operator/common" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -18,7 +17,7 @@ type ResourceReconciler struct { Log logr.Logger } -func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, newObjFunc func() (client.Object, error), equalFunc func(client.Object, client.Object) bool) (*ctrl.Result, error) { +func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, newObjFunc func() (client.Object, error), equalFunc func(client.Object, client.Object) bool) error { exists := false existingObj := obj err := r.Client.Get(ctx, client.ObjectKey{ @@ -30,34 +29,34 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob exists = true } else if err != nil && !errors.IsNotFound(err) { r.Log.Error(err, fmt.Sprintf("Failed to get flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err + return err } // check if the resource is managed by the operator. // if not, do not continue to not mess with anything user generated if exists && !common.IsManagedByOFO(existingObj) { r.Log.Info(fmt.Sprintf("Found existing %s '%s/%s' that is not managed by OFO. Will not proceed.", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, nil + return fmt.Errorf("resource already exists and is not managed by OFO") } newObj, err := newObjFunc() if err != nil { r.Log.Error(err, fmt.Sprintf("Could not create new flagd %s resource '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err + return err } if exists && !equalFunc(existingObj, newObj) { r.Log.Info(fmt.Sprintf("Updating %v", newObj)) if err := r.Client.Update(ctx, newObj); err != nil { r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err + return err } } else { r.Log.Info(fmt.Sprintf("Creating %v", newObj)) if err := r.Client.Create(ctx, newObj); err != nil { r.Log.Error(err, fmt.Sprintf("Failed to create Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return &ctrl.Result{}, err + return err } } - return nil, nil + return nil } diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/service.go index 161884274..5849e250e 100644 --- a/controllers/core/flagd/service.go +++ b/controllers/core/flagd/service.go @@ -9,7 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "reflect" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -22,7 +21,7 @@ type FlagdService struct { ResourceReconciler *ResourceReconciler } -func (r FlagdService) Reconcile(ctx context.Context, flagd *api.Flagd) (*ctrl.Result, error) { +func (r FlagdService) Reconcile(ctx context.Context, flagd *api.Flagd) error { return r.ResourceReconciler.Reconcile( ctx, flagd, From 841454d27a19d680d130451b24256d983e860cbb Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 14:37:28 +0200 Subject: [PATCH 08/43] add mock Signed-off-by: Florian Bacher --- controllers/core/flagd/mock/mock.go | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 controllers/core/flagd/mock/mock.go diff --git a/controllers/core/flagd/mock/mock.go b/controllers/core/flagd/mock/mock.go new file mode 100644 index 000000000..a321d350f --- /dev/null +++ b/controllers/core/flagd/mock/mock.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./controllers/core/flagd/controller.go + +// Package commonmock is a generated GoMock package. +package commonmock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" +) + +// MockIFlagdResource is a mock of IFlagdResource interface. +type MockIFlagdResource struct { + ctrl *gomock.Controller + recorder *MockIFlagdResourceMockRecorder +} + +// MockIFlagdResourceMockRecorder is the mock recorder for MockIFlagdResource. +type MockIFlagdResourceMockRecorder struct { + mock *MockIFlagdResource +} + +// NewMockIFlagdResource creates a new mock instance. +func NewMockIFlagdResource(ctrl *gomock.Controller) *MockIFlagdResource { + mock := &MockIFlagdResource{ctrl: ctrl} + mock.recorder = &MockIFlagdResourceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIFlagdResource) EXPECT() *MockIFlagdResourceMockRecorder { + return m.recorder +} + +// Reconcile mocks base method. +func (m *MockIFlagdResource) Reconcile(ctx context.Context, flagd *v1beta1.Flagd) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reconcile", ctx, flagd) + ret0, _ := ret[0].(error) + return ret0 +} + +// Reconcile indicates an expected call of Reconcile. +func (mr *MockIFlagdResourceMockRecorder) Reconcile(ctx, flagd interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reconcile", reflect.TypeOf((*MockIFlagdResource)(nil).Reconcile), ctx, flagd) +} From 15922b1a68757afb870c8297535a225b5bfd5062 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 6 May 2024 15:19:56 +0200 Subject: [PATCH 09/43] add unit tests Signed-off-by: Florian Bacher --- common/common.go | 3 +- controllers/core/flagd/resource_test.go | 153 ++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 controllers/core/flagd/resource_test.go diff --git a/common/common.go b/common/common.go index bb08ac6e7..623beb54d 100644 --- a/common/common.go +++ b/common/common.go @@ -30,6 +30,7 @@ const ( ProbeInitialDelay = 5 FeatureFlagSourceAnnotation = "featureflagsource" EnabledAnnotation = "enabled" + ManagedByAnnotationKey = "app.kubernetes.io/managed-by" ManagedByAnnotationValue = "open-feature-operator" OperatorDeploymentName = "open-feature-operator-controller-manager" ) @@ -81,6 +82,6 @@ func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference) } func IsManagedByOFO(obj client.Object) bool { - val, ok := obj.GetLabels()["app.kubernetes.io/managed-by"] + val, ok := obj.GetLabels()[ManagedByAnnotationKey] return ok && val == ManagedByAnnotationValue } diff --git a/controllers/core/flagd/resource_test.go b/controllers/core/flagd/resource_test.go new file mode 100644 index 000000000..3c3b78d9b --- /dev/null +++ b/controllers/core/flagd/resource_test.go @@ -0,0 +1,153 @@ +package flagd + +import ( + "context" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/common" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" +) + +func TestResourceReconciler_Reconcile_CreateResource(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + + r := &ResourceReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("resource-reconciler"), + } + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + } + + err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + Data: map[string]string{}, + }, nil + }, func(o1 client.Object, o2 client.Object) bool { + return false + }) + + require.Nil(t, err) + + result := &corev1.ConfigMap{} + err = fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, result) + + require.Nil(t, err) +} + +func TestResourceReconciler_Reconcile_UpdateManagedResource(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + Labels: map[string]string{ + common.ManagedByAnnotationKey: common.ManagedByAnnotationValue, + }, + }, + Data: map[string]string{}, + }).Build() + + r := &ResourceReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("resource-reconciler"), + } + + err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + Data: map[string]string{ + "foo": "bar", + }, + }, nil + }, func(o1 client.Object, o2 client.Object) bool { + return false + }) + + require.Nil(t, err) + + result := &corev1.ConfigMap{} + err = fakeClient.Get(context.Background(), client.ObjectKey{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, result) + + require.Nil(t, err) + + // verify the resource was updated + require.Equal(t, "bar", result.Data["foo"]) +} + +func TestResourceReconciler_Reconcile_UnmanagedResourceAlreadyExists(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + Data: map[string]string{}, + }).Build() + + r := &ResourceReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("resource-reconciler"), + } + + err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + Data: map[string]string{}, + }, nil + }, func(o1 client.Object, o2 client.Object) bool { + return false + }) + + require.NotNil(t, err) +} From 2d04e04c18f0df2bf03f835dc69009581b0c8988 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 7 May 2024 09:42:31 +0200 Subject: [PATCH 10/43] add unit tests; support flagd grpc port Signed-off-by: Florian Bacher --- common/types/envconfig.go | 1 + controllers/core/flagd/config.go | 2 + controllers/core/flagd/controller_test.go | 28 +-- controllers/core/flagd/deployment.go | 9 + controllers/core/flagd/deployment_test.go | 228 ++++++++++++++++++++++ controllers/core/flagd/ingress.go | 3 +- controllers/core/flagd/service.go | 7 + 7 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 controllers/core/flagd/deployment_test.go diff --git a/common/types/envconfig.go b/common/types/envconfig.go index 29c9e1e6d..6d7c7b546 100644 --- a/common/types/envconfig.go +++ b/common/types/envconfig.go @@ -14,6 +14,7 @@ type EnvConfig struct { FlagdTag string `envconfig:"FLAGD_TAG" default:"v0.5.0"` FlagdPort int `envconfig:"FLAGD_PORT" default:"8013"` FlagdOFREPPort int `envconfig:"FLAGD_OFREP_PORT" default:"8016"` + FlagdSyncPort int `envconfig:"FLAGD_SYNC_PORT" default:"8015"` FlagdManagementPort int `envconfig:"FLAGD_MANAGEMENT_PORT" default:"8014"` FlagdDebugLogging bool `envconfig:"FLAGD_DEBUG_LOGGING" default:"false"` diff --git a/controllers/core/flagd/config.go b/controllers/core/flagd/config.go index 20c7a93b6..ceb2fcd34 100644 --- a/controllers/core/flagd/config.go +++ b/controllers/core/flagd/config.go @@ -8,6 +8,7 @@ import ( type FlagdConfiguration struct { FlagdPort int OFREPPort int + SyncPort int ManagementPort int DebugLogging bool Image string @@ -24,6 +25,7 @@ func NewFlagdConfiguration(env types.EnvConfig) FlagdConfiguration { OperatorDeploymentName: common.OperatorDeploymentName, FlagdPort: env.FlagdPort, OFREPPort: env.FlagdOFREPPort, + SyncPort: env.FlagdSyncPort, ManagementPort: env.FlagdManagementPort, DebugLogging: env.FlagdDebugLogging, } diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go index b68e265fa..cd8832e08 100644 --- a/controllers/core/flagd/controller_test.go +++ b/controllers/core/flagd/controller_test.go @@ -18,6 +18,17 @@ import ( "testing" ) +var testFlagdConfig = FlagdConfiguration{ + FlagdPort: 8013, + OFREPPort: 8016, + ManagementPort: 8014, + DebugLogging: false, + Image: "flagd", + Tag: "latest", + OperatorNamespace: "ofo-system", + OperatorDeploymentName: "ofo", +} + type flagdMatcher struct { flagdObj api.Flagd } @@ -200,19 +211,10 @@ func TestFlagdReconciler_ReconcileFailIngress(t *testing.T) { func setupReconciler(fakeClient client.WithWatch, deploymentReconciler *commonmock.MockIFlagdResource, serviceReconciler *commonmock.MockIFlagdResource, ingressReconciler *commonmock.MockIFlagdResource) *FlagdReconciler { return &FlagdReconciler{ - Client: fakeClient, - Scheme: fakeClient.Scheme(), - Log: controllerruntime.Log.WithName("flagd controller"), - FlagdConfig: FlagdConfiguration{ - FlagdPort: 8013, - OFREPPort: 8016, - ManagementPort: 8014, - DebugLogging: false, - Image: "flagd", - Tag: "latest", - OperatorNamespace: "ofo-system", - OperatorDeploymentName: "ofo", - }, + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("flagd controller"), + FlagdConfig: testFlagdConfig, FlagdDeployment: deploymentReconciler, FlagdService: serviceReconciler, FlagdIngress: ingressReconciler, diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go index e458f8dc4..1c536d988 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/deployment.go @@ -2,6 +2,7 @@ package flagd import ( "context" + "errors" "fmt" "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" @@ -99,6 +100,10 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) } + if len(deployment.Spec.Template.Spec.Containers) == 0 { + return nil, errors.New("no flagd container has been injected into deployment") + } + deployment.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{ { Name: "management", @@ -112,6 +117,10 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla Name: "ofrep", ContainerPort: int32(r.FlagdConfig.OFREPPort), }, + { + Name: "sync", + ContainerPort: int32(r.FlagdConfig.SyncPort), + }, } return deployment, nil diff --git a/controllers/core/flagd/deployment_test.go b/controllers/core/flagd/deployment_test.go new file mode 100644 index 000000000..490d38401 --- /dev/null +++ b/controllers/core/flagd/deployment_test.go @@ -0,0 +1,228 @@ +package flagd + +import ( + "context" + "errors" + "github.com/golang/mock/gomock" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + commonfake "github.com/open-feature/open-feature-operator/common/flagdinjector/fake" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" +) + +func TestFlagdDeployment_getFlagdDeployment(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + FeatureFlagSourceRef: v1.ObjectReference{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + }, + } + + flagSource := &api.FeatureFlagSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagSource, flagdObj).Build() + + ctrl := gomock.NewController(t) + + fakeFlagdInjector := commonfake.NewMockFlagdContainerInjector(ctrl) + fakeFlagdInjector.EXPECT(). + InjectFlagd(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Times(1). + DoAndReturn(func( + ctx context.Context, + objectMeta *metav1.ObjectMeta, + podSpec *v1.PodSpec, + flagSourceConfig *api.FeatureFlagSourceSpec, + ) error { + // simulate the injection of a container into the podspec + podSpec.Containers = []v1.Container{ + { + Name: "flagd", + }, + } + return nil + }) + + r := &FlagdDeployment{ + Client: fakeClient, + Log: controllerruntime.Log.WithName("test"), + FlagdInjector: fakeFlagdInjector, + FlagdConfig: testFlagdConfig, + } + + deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + + require.Nil(t, err) + require.NotNil(t, deploymentResult) + + require.Equal(t, flagdObj.Name, deploymentResult.Name) + require.Equal(t, flagdObj.Namespace, deploymentResult.Namespace) + require.Len(t, deploymentResult.OwnerReferences, 1) + require.Equal(t, []v1.ContainerPort{ + { + Name: "management", + ContainerPort: int32(r.FlagdConfig.ManagementPort), + }, + { + Name: "flagd", + ContainerPort: int32(r.FlagdConfig.FlagdPort), + }, + { + Name: "ofrep", + ContainerPort: int32(r.FlagdConfig.OFREPPort), + }, + { + Name: "sync", + ContainerPort: int32(r.FlagdConfig.SyncPort), + }, + }, deploymentResult.Spec.Template.Spec.Containers[0].Ports) +} + +func TestFlagdDeployment_getFlagdDeployment_ErrorInInjector(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + FeatureFlagSourceRef: v1.ObjectReference{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + }, + } + + flagSource := &api.FeatureFlagSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagSource, flagdObj).Build() + + ctrl := gomock.NewController(t) + + fakeFlagdInjector := commonfake.NewMockFlagdContainerInjector(ctrl) + fakeFlagdInjector.EXPECT(). + InjectFlagd(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Times(1). + Return(errors.New("oops")) + + r := &FlagdDeployment{ + Client: fakeClient, + Log: controllerruntime.Log.WithName("test"), + FlagdInjector: fakeFlagdInjector, + FlagdConfig: testFlagdConfig, + } + + deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + + require.NotNil(t, err) + require.Nil(t, deploymentResult) +} + +func TestFlagdDeployment_getFlagdDeployment_ContainerNotInjected(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + FeatureFlagSourceRef: v1.ObjectReference{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + }, + } + + flagSource := &api.FeatureFlagSource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagSource, flagdObj).Build() + + ctrl := gomock.NewController(t) + + fakeFlagdInjector := commonfake.NewMockFlagdContainerInjector(ctrl) + fakeFlagdInjector.EXPECT(). + InjectFlagd(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + + r := &FlagdDeployment{ + Client: fakeClient, + Log: controllerruntime.Log.WithName("test"), + FlagdInjector: fakeFlagdInjector, + FlagdConfig: testFlagdConfig, + } + + deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + + require.NotNil(t, err) + require.Nil(t, deploymentResult) +} + +func TestFlagdDeployment_getFlagdDeployment_FlagSourceNotFound(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + FeatureFlagSourceRef: v1.ObjectReference{ + Name: "my-flag-source", + Namespace: "my-namespace", + }, + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + fakeFlagdInjector := commonfake.NewMockFlagdContainerInjector(ctrl) + + r := &FlagdDeployment{ + Client: fakeClient, + Log: controllerruntime.Log.WithName("test"), + FlagdInjector: fakeFlagdInjector, + FlagdConfig: testFlagdConfig, + } + + deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + + require.NotNil(t, err) + require.Nil(t, deploymentResult) +} diff --git a/controllers/core/flagd/ingress.go b/controllers/core/flagd/ingress.go index 5fd106303..4440ee4de 100644 --- a/controllers/core/flagd/ingress.go +++ b/controllers/core/flagd/ingress.go @@ -72,10 +72,11 @@ func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { } func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule { - rules := make([]networkingv1.IngressRule, 2*len(flagd.Spec.Ingress.Hosts)) + rules := make([]networkingv1.IngressRule, 3*len(flagd.Spec.Ingress.Hosts)) for i, host := range flagd.Spec.Ingress.Hosts { rules[2*i] = r.getRule(flagd, host, "/flagd", int32(r.FlagdConfig.FlagdPort)) rules[2*i+1] = r.getRule(flagd, host, "/ofrep", int32(r.FlagdConfig.OFREPPort)) + rules[2*i+2] = r.getRule(flagd, host, "/sync", int32(r.FlagdConfig.SyncPort)) } return rules } diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/service.go index 5849e250e..d33bac129 100644 --- a/controllers/core/flagd/service.go +++ b/controllers/core/flagd/service.go @@ -83,6 +83,13 @@ func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { IntVal: int32(r.FlagdConfig.OFREPPort), }, }, + { + Name: "sync", + Port: int32(r.FlagdConfig.SyncPort), + TargetPort: intstr.IntOrString{ + IntVal: int32(r.FlagdConfig.SyncPort), + }, + }, { Name: "metrics", Port: int32(r.FlagdConfig.ManagementPort), From bfa890be7c1e3910a4162255a316b9fe812f5aaf Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 7 May 2024 10:40:49 +0200 Subject: [PATCH 11/43] clean up types, add unit tests Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 4 - .../bases/core.openfeature.dev_flagds.yaml | 5 - config/rbac/role.yaml | 12 -- controllers/core/flagd/deployment.go | 26 +++-- controllers/core/flagd/deployment_test.go | 83 ++++++++++++++ controllers/core/flagd/service.go | 33 +++--- controllers/core/flagd/service_test.go | 105 ++++++++++++++++++ main.go | 2 - 8 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 controllers/core/flagd/service_test.go diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 335b198e2..854066c38 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -38,10 +38,6 @@ type FlagdSpec struct { // +kubebuilder:default=ClusterIP ServiceType v1.ServiceType `json:"serviceType,omitempty"` - // NodePort represents the port at which the NodePort service to allocate - // +optional - NodePort int32 `json:"nodePort,omitempty"` - // ServiceAccountName the service account name for the flagd deployment // +optional ServiceAccountName string `json:"serviceAccountName,omitempty"` diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index a17ddaac9..e72b9605e 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -132,11 +132,6 @@ spec: type: object type: array type: object - nodePort: - description: NodePort represents the port at which the NodePort service - to allocate - format: int32 - type: integer otelCollectorUri: description: OtelCollectorUri defines the OpenTelemetry collector URI to enable OpenTelemetry Tracing in flagd. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c65314e95..a632c9034 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -122,18 +122,6 @@ rules: - get - patch - update -- apiGroups: - - extensions - resources: - - ingresses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - networking.k8s.io resources: diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/deployment.go index 1c536d988..a59a127cb 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/deployment.go @@ -34,17 +34,7 @@ func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd) error return r.getFlagdDeployment(ctx, flagd) }, func(old client.Object, new client.Object) bool { - oldDeployment, ok := old.(*appsv1.Deployment) - if !ok { - return false - } - - newDeployment, ok := new.(*appsv1.Deployment) - if !ok { - return false - } - - return reflect.DeepEqual(oldDeployment.Spec, newDeployment.Spec) + return areDeploymentsEqual(old, new) }, ) } @@ -125,3 +115,17 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla return deployment, nil } + +func areDeploymentsEqual(old client.Object, new client.Object) bool { + oldDeployment, ok := old.(*appsv1.Deployment) + if !ok { + return false + } + + newDeployment, ok := new.(*appsv1.Deployment) + if !ok { + return false + } + + return reflect.DeepEqual(oldDeployment.Spec, newDeployment.Spec) +} diff --git a/controllers/core/flagd/deployment_test.go b/controllers/core/flagd/deployment_test.go index 490d38401..2271cefd0 100644 --- a/controllers/core/flagd/deployment_test.go +++ b/controllers/core/flagd/deployment_test.go @@ -7,10 +7,12 @@ import ( api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" commonfake "github.com/open-feature/open-feature-operator/common/flagdinjector/fake" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "testing" ) @@ -226,3 +228,84 @@ func TestFlagdDeployment_getFlagdDeployment_FlagSourceNotFound(t *testing.T) { require.NotNil(t, err) require.Nil(t, deploymentResult) } + +func Test_areDeploymentsEqual(t *testing.T) { + type args struct { + old client.Object + new client.Object + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "has changed", + args: args{ + old: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(1), + }, + }, + new: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(2), + }, + }, + }, + want: false, + }, + { + name: "has not changed", + args: args{ + old: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(1), + }, + }, + new: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(1), + }, + }, + }, + want: true, + }, + { + name: "old is not a deployment", + args: args{ + old: &v1.ConfigMap{}, + new: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(1), + }, + }, + }, + want: false, + }, + { + name: "new is not a deployment", + args: args{ + old: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: intPtr(1), + }, + }, + new: &v1.ConfigMap{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := areDeploymentsEqual(tt.args.old, tt.args.new) + + require.Equal(t, tt.want, got) + }) + } +} + +func intPtr(i int32) *int32 { + return &i +} diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/service.go index d33bac129..d49924e29 100644 --- a/controllers/core/flagd/service.go +++ b/controllers/core/flagd/service.go @@ -2,7 +2,6 @@ package flagd import ( "context" - "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" v1 "k8s.io/api/core/v1" @@ -13,11 +12,7 @@ import ( ) type FlagdService struct { - client.Client - - Log logr.Logger - FlagdConfig FlagdConfiguration - + FlagdConfig FlagdConfiguration ResourceReconciler *ResourceReconciler } @@ -30,17 +25,7 @@ func (r FlagdService) Reconcile(ctx context.Context, flagd *api.Flagd) error { return r.getService(flagd), nil }, func(old client.Object, new client.Object) bool { - oldService, ok := old.(*v1.Service) - if !ok { - return false - } - - newService, ok := new.(*v1.Service) - if !ok { - return false - } - - return reflect.DeepEqual(oldService.Spec, newService.Spec) + return areServicesEqual(old, new) }, ) } @@ -102,3 +87,17 @@ func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { }, } } + +func areServicesEqual(old client.Object, new client.Object) bool { + oldService, ok := old.(*v1.Service) + if !ok { + return false + } + + newService, ok := new.(*v1.Service) + if !ok { + return false + } + + return reflect.DeepEqual(oldService.Spec, newService.Spec) +} diff --git a/controllers/core/flagd/service_test.go b/controllers/core/flagd/service_test.go new file mode 100644 index 000000000..880166f4c --- /dev/null +++ b/controllers/core/flagd/service_test.go @@ -0,0 +1,105 @@ +package flagd + +import ( + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" +) + +func TestFlagdService_getService(t *testing.T) { + r := FlagdService{ + FlagdConfig: testFlagdConfig, + } + + svc := r.getService(&api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + ServiceType: "ClusterIP", + }, + }) + + require.NotNil(t, svc) +} + +func Test_areServicesEqual(t *testing.T) { + type args struct { + old client.Object + new client.Object + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "has changed", + args: args{ + old: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeNodePort, + }, + }, + new: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + }, + }, + }, + want: false, + }, + { + name: "has not changed", + args: args{ + old: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + }, + }, + new: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + }, + }, + }, + want: true, + }, + { + name: "old is not a service", + args: args{ + old: &v1.ConfigMap{}, + new: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + }, + }, + }, + want: false, + }, + { + name: "new is not a service", + args: args{ + old: &v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + }, + }, + new: &v1.ConfigMap{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := areServicesEqual(tt.args.old, tt.args.new) + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/main.go b/main.go index fdcd16531..bc989a187 100644 --- a/main.go +++ b/main.go @@ -211,8 +211,6 @@ func main() { ResourceReconciler: flagdResourceReconciler, }, FlagdService: &flagd.FlagdService{ - Client: mgr.GetClient(), - Log: flagdControllerLogger, FlagdConfig: flagdConfig, ResourceReconciler: flagdResourceReconciler, }, From f5f580a38e0ce2ce879f6a0baa26daf243907713 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 7 May 2024 15:06:29 +0200 Subject: [PATCH 12/43] clean up types, add unit tests Signed-off-by: Florian Bacher --- controllers/core/flagd/common/common.go | 14 + controllers/core/flagd/config.go | 18 +- controllers/core/flagd/controller.go | 40 ++- controllers/core/flagd/controller_test.go | 147 ++++++++--- controllers/core/flagd/mock/mock.go | 32 +-- controllers/core/flagd/resource.go | 7 +- controllers/core/flagd/resource_test.go | 111 ++++++-- .../core/flagd/{ => resources}/deployment.go | 47 ++-- .../flagd/{ => resources}/deployment_test.go | 29 ++- .../core/flagd/{ => resources}/ingress.go | 83 +++--- .../core/flagd/resources/ingress_test.go | 246 ++++++++++++++++++ controllers/core/flagd/resources/interface.go | 12 + controllers/core/flagd/resources/mock/mock.go | 66 +++++ .../core/flagd/{ => resources}/service.go | 48 ++-- .../flagd/{ => resources}/service_test.go | 10 +- main.go | 29 +-- 16 files changed, 710 insertions(+), 229 deletions(-) create mode 100644 controllers/core/flagd/common/common.go rename controllers/core/flagd/{ => resources}/deployment.go (79%) rename controllers/core/flagd/{ => resources}/deployment_test.go (89%) rename controllers/core/flagd/{ => resources}/ingress.go (54%) create mode 100644 controllers/core/flagd/resources/ingress_test.go create mode 100644 controllers/core/flagd/resources/interface.go create mode 100644 controllers/core/flagd/resources/mock/mock.go rename controllers/core/flagd/{ => resources}/service.go (74%) rename controllers/core/flagd/{ => resources}/service_test.go (88%) diff --git a/controllers/core/flagd/common/common.go b/controllers/core/flagd/common/common.go new file mode 100644 index 000000000..2ca793707 --- /dev/null +++ b/controllers/core/flagd/common/common.go @@ -0,0 +1,14 @@ +package resources + +type FlagdConfiguration struct { + FlagdPort int + OFREPPort int + SyncPort int + ManagementPort int + DebugLogging bool + Image string + Tag string + + OperatorNamespace string + OperatorDeploymentName string +} diff --git a/controllers/core/flagd/config.go b/controllers/core/flagd/config.go index ceb2fcd34..3b0548966 100644 --- a/controllers/core/flagd/config.go +++ b/controllers/core/flagd/config.go @@ -3,23 +3,11 @@ package flagd import ( "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/common/types" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" ) -type FlagdConfiguration struct { - FlagdPort int - OFREPPort int - SyncPort int - ManagementPort int - DebugLogging bool - Image string - Tag string - - OperatorNamespace string - OperatorDeploymentName string -} - -func NewFlagdConfiguration(env types.EnvConfig) FlagdConfiguration { - return FlagdConfiguration{ +func NewFlagdConfiguration(env types.EnvConfig) resources.FlagdConfiguration { + return resources.FlagdConfiguration{ Image: env.FlagdImage, Tag: env.FlagdTag, OperatorDeploymentName: common.OperatorDeploymentName, diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index e7060f190..e1d5f3b26 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -21,6 +21,11 @@ import ( "fmt" "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + resources2 "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -33,15 +38,17 @@ type FlagdReconciler struct { Scheme *runtime.Scheme Log logr.Logger - FlagdConfig FlagdConfiguration + FlagdConfig resources2.FlagdConfiguration - FlagdDeployment IFlagdResource - FlagdService IFlagdResource - FlagdIngress IFlagdResource + ResourceReconciler IFlagdResourceReconciler + + FlagdDeployment resources.IFlagdResource + FlagdService resources.IFlagdResource + FlagdIngress resources.IFlagdResource } -type IFlagdResource interface { - Reconcile(ctx context.Context, flagd *api.Flagd) error +type IFlagdResourceReconciler interface { + Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, resource resources.IFlagdResource) error } //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete @@ -72,15 +79,30 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - if err := r.FlagdDeployment.Reconcile(ctx, flagd); err != nil { + if err := r.ResourceReconciler.Reconcile( + ctx, + flagd, + &appsv1.Deployment{}, + r.FlagdDeployment, + ); err != nil { return ctrl.Result{}, err } - if err := r.FlagdService.Reconcile(ctx, flagd); err != nil { + if err := r.ResourceReconciler.Reconcile( + ctx, + flagd, + &v1.Service{}, + r.FlagdService, + ); err != nil { return ctrl.Result{}, err } - if err := r.FlagdIngress.Reconcile(ctx, flagd); err != nil { + if err := r.ResourceReconciler.Reconcile( + ctx, + flagd, + &networkingv1.Ingress{}, + r.FlagdIngress, + ); err != nil { return ctrl.Result{}, err } diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go index cd8832e08..a02843942 100644 --- a/controllers/core/flagd/controller_test.go +++ b/controllers/core/flagd/controller_test.go @@ -6,8 +6,13 @@ import ( "fmt" "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" commonmock "github.com/open-feature/open-feature-operator/controllers/core/flagd/mock" + resourcemock "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources/mock" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" @@ -18,7 +23,7 @@ import ( "testing" ) -var testFlagdConfig = FlagdConfiguration{ +var testFlagdConfig = resources.FlagdConfiguration{ FlagdPort: 8013, OFREPPort: 8016, ManagementPort: 8014, @@ -62,16 +67,37 @@ func TestFlagdReconciler_Reconcile(t *testing.T) { ctrl := gomock.NewController(t) - deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) - serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) - ingressReconciler := commonmock.NewMockIFlagdResource(ctrl) - - // deployment creation succeeds - deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - ingressReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - - r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, ingressReconciler) + deploymentResource := resourcemock.NewMockIFlagdResource(ctrl) + serviceResource := resourcemock.NewMockIFlagdResource(ctrl) + ingressResource := resourcemock.NewMockIFlagdResource(ctrl) + + resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&appsv1.Deployment{}), + deploymentResource, + ).Times(1).Return(nil) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&v1.Service{}), + serviceResource, + ).Times(1).Return(nil) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&networkingv1.Ingress{}), + ingressResource, + ).Times(1).Return(nil) + + r := setupReconciler(fakeClient, deploymentResource, serviceResource, ingressResource, resourceReconciler) result, err := r.Reconcile(context.Background(), controllerruntime.Request{ NamespacedName: types.NamespacedName{ @@ -90,7 +116,7 @@ func TestFlagdReconciler_ReconcileResourceNotFound(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects().Build() - r := setupReconciler(fakeClient, nil, nil, nil) + r := setupReconciler(fakeClient, nil, nil, nil, nil) result, err := r.Reconcile(context.Background(), controllerruntime.Request{ NamespacedName: types.NamespacedName{ @@ -119,12 +145,19 @@ func TestFlagdReconciler_ReconcileFailDeployment(t *testing.T) { ctrl := gomock.NewController(t) - deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) + deploymentResource := resourcemock.NewMockIFlagdResource(ctrl) - // deployment creation succeeds - deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) + resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl) - r := setupReconciler(fakeClient, deploymentReconciler, nil, nil) + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&appsv1.Deployment{}), + deploymentResource, + ).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentResource, nil, nil, resourceReconciler) result, err := r.Reconcile(context.Background(), controllerruntime.Request{ NamespacedName: types.NamespacedName{ @@ -153,13 +186,28 @@ func TestFlagdReconciler_ReconcileFailService(t *testing.T) { ctrl := gomock.NewController(t) - deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) - serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) + deploymentResource := resourcemock.NewMockIFlagdResource(ctrl) + serviceResource := resourcemock.NewMockIFlagdResource(ctrl) + + resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl) - deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&appsv1.Deployment{}), + deploymentResource, + ).Times(1).Return(nil) - r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, nil) + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&v1.Service{}), + serviceResource, + ).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentResource, serviceResource, nil, resourceReconciler) result, err := r.Reconcile(context.Background(), controllerruntime.Request{ NamespacedName: types.NamespacedName{ @@ -188,15 +236,37 @@ func TestFlagdReconciler_ReconcileFailIngress(t *testing.T) { ctrl := gomock.NewController(t) - deploymentReconciler := commonmock.NewMockIFlagdResource(ctrl) - serviceReconciler := commonmock.NewMockIFlagdResource(ctrl) - ingressReconciler := commonmock.NewMockIFlagdResource(ctrl) - - deploymentReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - serviceReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(nil) - ingressReconciler.EXPECT().Reconcile(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}).Times(1).Return(errors.New("oops")) - - r := setupReconciler(fakeClient, deploymentReconciler, serviceReconciler, ingressReconciler) + deploymentResource := resourcemock.NewMockIFlagdResource(ctrl) + serviceResource := resourcemock.NewMockIFlagdResource(ctrl) + ingressResource := resourcemock.NewMockIFlagdResource(ctrl) + + resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&appsv1.Deployment{}), + deploymentResource, + ).Times(1).Return(nil) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&v1.Service{}), + serviceResource, + ).Times(1).Return(nil) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&networkingv1.Ingress{}), + ingressResource, + ).Times(1).Return(errors.New("oops")) + + r := setupReconciler(fakeClient, deploymentResource, serviceResource, ingressResource, resourceReconciler) result, err := r.Reconcile(context.Background(), controllerruntime.Request{ NamespacedName: types.NamespacedName{ @@ -209,14 +279,15 @@ func TestFlagdReconciler_ReconcileFailIngress(t *testing.T) { require.Equal(t, controllerruntime.Result{}, result) } -func setupReconciler(fakeClient client.WithWatch, deploymentReconciler *commonmock.MockIFlagdResource, serviceReconciler *commonmock.MockIFlagdResource, ingressReconciler *commonmock.MockIFlagdResource) *FlagdReconciler { +func setupReconciler(fakeClient client.WithWatch, deploymentReconciler, serviceReconciler, ingressReconciler *resourcemock.MockIFlagdResource, resourceReconciler *commonmock.MockIFlagdResourceReconciler) *FlagdReconciler { return &FlagdReconciler{ - Client: fakeClient, - Scheme: fakeClient.Scheme(), - Log: controllerruntime.Log.WithName("flagd controller"), - FlagdConfig: testFlagdConfig, - FlagdDeployment: deploymentReconciler, - FlagdService: serviceReconciler, - FlagdIngress: ingressReconciler, + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("flagd controller"), + FlagdConfig: testFlagdConfig, + FlagdDeployment: deploymentReconciler, + FlagdService: serviceReconciler, + FlagdIngress: ingressReconciler, + ResourceReconciler: resourceReconciler, } } diff --git a/controllers/core/flagd/mock/mock.go b/controllers/core/flagd/mock/mock.go index a321d350f..0df09e3d6 100644 --- a/controllers/core/flagd/mock/mock.go +++ b/controllers/core/flagd/mock/mock.go @@ -10,41 +10,43 @@ import ( gomock "github.com/golang/mock/gomock" v1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + resources "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" + client "sigs.k8s.io/controller-runtime/pkg/client" ) -// MockIFlagdResource is a mock of IFlagdResource interface. -type MockIFlagdResource struct { +// MockIFlagdResourceReconciler is a mock of IFlagdResourceReconciler interface. +type MockIFlagdResourceReconciler struct { ctrl *gomock.Controller - recorder *MockIFlagdResourceMockRecorder + recorder *MockIFlagdResourceReconcilerMockRecorder } -// MockIFlagdResourceMockRecorder is the mock recorder for MockIFlagdResource. -type MockIFlagdResourceMockRecorder struct { - mock *MockIFlagdResource +// MockIFlagdResourceReconcilerMockRecorder is the mock recorder for MockIFlagdResourceReconciler. +type MockIFlagdResourceReconcilerMockRecorder struct { + mock *MockIFlagdResourceReconciler } -// NewMockIFlagdResource creates a new mock instance. -func NewMockIFlagdResource(ctrl *gomock.Controller) *MockIFlagdResource { - mock := &MockIFlagdResource{ctrl: ctrl} - mock.recorder = &MockIFlagdResourceMockRecorder{mock} +// NewMockIFlagdResourceReconciler creates a new mock instance. +func NewMockIFlagdResourceReconciler(ctrl *gomock.Controller) *MockIFlagdResourceReconciler { + mock := &MockIFlagdResourceReconciler{ctrl: ctrl} + mock.recorder = &MockIFlagdResourceReconcilerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIFlagdResource) EXPECT() *MockIFlagdResourceMockRecorder { +func (m *MockIFlagdResourceReconciler) EXPECT() *MockIFlagdResourceReconcilerMockRecorder { return m.recorder } // Reconcile mocks base method. -func (m *MockIFlagdResource) Reconcile(ctx context.Context, flagd *v1beta1.Flagd) error { +func (m *MockIFlagdResourceReconciler) Reconcile(ctx context.Context, flagd *v1beta1.Flagd, obj client.Object, resource resources.IFlagdResource) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Reconcile", ctx, flagd) + ret := m.ctrl.Call(m, "Reconcile", ctx, flagd, obj, resource) ret0, _ := ret[0].(error) return ret0 } // Reconcile indicates an expected call of Reconcile. -func (mr *MockIFlagdResourceMockRecorder) Reconcile(ctx, flagd interface{}) *gomock.Call { +func (mr *MockIFlagdResourceReconcilerMockRecorder) Reconcile(ctx, flagd, obj, resource interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reconcile", reflect.TypeOf((*MockIFlagdResource)(nil).Reconcile), ctx, flagd) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reconcile", reflect.TypeOf((*MockIFlagdResourceReconciler)(nil).Reconcile), ctx, flagd, obj, resource) } diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index 601bbfab8..0792f9a0e 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -6,6 +6,7 @@ import ( "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -17,7 +18,7 @@ type ResourceReconciler struct { Log logr.Logger } -func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, newObjFunc func() (client.Object, error), equalFunc func(client.Object, client.Object) bool) error { +func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, obj client.Object, resource resources.IFlagdResource) error { exists := false existingObj := obj err := r.Client.Get(ctx, client.ObjectKey{ @@ -39,13 +40,13 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob return fmt.Errorf("resource already exists and is not managed by OFO") } - newObj, err := newObjFunc() + newObj, err := resource.GetResource(ctx, flagd) if err != nil { r.Log.Error(err, fmt.Sprintf("Could not create new flagd %s resource '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) return err } - if exists && !equalFunc(existingObj, newObj) { + if exists && !resource.AreObjectsEqual(existingObj, newObj) { r.Log.Info(fmt.Sprintf("Updating %v", newObj)) if err := r.Client.Update(ctx, newObj); err != nil { r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) diff --git a/controllers/core/flagd/resource_test.go b/controllers/core/flagd/resource_test.go index 3c3b78d9b..2b1789a22 100644 --- a/controllers/core/flagd/resource_test.go +++ b/controllers/core/flagd/resource_test.go @@ -2,8 +2,11 @@ package flagd import ( "context" + "errors" + "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" + resourcemock "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources/mock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,17 +36,25 @@ func TestResourceReconciler_Reconcile_CreateResource(t *testing.T) { }, } - err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { - return &corev1.ConfigMap{ + ctrl := gomock.NewController(t) + mockRes := resourcemock.NewMockIFlagdResource(ctrl) + mockRes.EXPECT(). + GetResource(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}). + Times(1). + Return(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: flagdObj.Namespace, Name: flagdObj.Name, }, Data: map[string]string{}, - }, nil - }, func(o1 client.Object, o2 client.Object) bool { - return false - }) + }, nil) + + err = r.Reconcile( + context.Background(), + flagdObj, + &corev1.ConfigMap{}, + mockRes, + ) require.Nil(t, err) @@ -84,8 +95,12 @@ func TestResourceReconciler_Reconcile_UpdateManagedResource(t *testing.T) { Log: controllerruntime.Log.WithName("resource-reconciler"), } - err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { - return &corev1.ConfigMap{ + ctrl := gomock.NewController(t) + mockRes := resourcemock.NewMockIFlagdResource(ctrl) + mockRes.EXPECT(). + GetResource(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}). + Times(1). + Return(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: flagdObj.Namespace, Name: flagdObj.Name, @@ -93,10 +108,16 @@ func TestResourceReconciler_Reconcile_UpdateManagedResource(t *testing.T) { Data: map[string]string{ "foo": "bar", }, - }, nil - }, func(o1 client.Object, o2 client.Object) bool { - return false - }) + }, nil) + + mockRes.EXPECT().AreObjectsEqual(gomock.Any(), gomock.Any()).Return(false) + + err = r.Reconcile( + context.Background(), + flagdObj, + &corev1.ConfigMap{}, + mockRes, + ) require.Nil(t, err) @@ -112,7 +133,7 @@ func TestResourceReconciler_Reconcile_UpdateManagedResource(t *testing.T) { require.Equal(t, "bar", result.Data["foo"]) } -func TestResourceReconciler_Reconcile_UnmanagedResourceAlreadyExists(t *testing.T) { +func TestResourceReconciler_Reconcile_CannotCreateResource(t *testing.T) { err := api.AddToScheme(scheme.Scheme) require.Nil(t, err) @@ -127,6 +148,9 @@ func TestResourceReconciler_Reconcile_UnmanagedResourceAlreadyExists(t *testing. ObjectMeta: metav1.ObjectMeta{ Namespace: flagdObj.Namespace, Name: flagdObj.Name, + Labels: map[string]string{ + common.ManagedByAnnotationKey: common.ManagedByAnnotationValue, + }, }, Data: map[string]string{}, }).Build() @@ -137,17 +161,56 @@ func TestResourceReconciler_Reconcile_UnmanagedResourceAlreadyExists(t *testing. Log: controllerruntime.Log.WithName("resource-reconciler"), } - err = r.Reconcile(context.Background(), flagdObj, &corev1.ConfigMap{}, func() (client.Object, error) { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: flagdObj.Namespace, - Name: flagdObj.Name, - }, - Data: map[string]string{}, - }, nil - }, func(o1 client.Object, o2 client.Object) bool { - return false - }) + ctrl := gomock.NewController(t) + mockRes := resourcemock.NewMockIFlagdResource(ctrl) + mockRes.EXPECT(). + GetResource(gomock.Any(), flagdMatcher{flagdObj: *flagdObj}). + Times(1). + Return(nil, errors.New("oops")) + + err = r.Reconcile( + context.Background(), + flagdObj, + &corev1.ConfigMap{}, + mockRes, + ) + + require.NotNil(t, err) +} + +func TestResourceReconciler_Reconcile_UnmanagedResourceAlreadyExists(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + Data: map[string]string{}, + }).Build() + + r := &ResourceReconciler{ + Client: fakeClient, + Scheme: fakeClient.Scheme(), + Log: controllerruntime.Log.WithName("resource-reconciler"), + } + ctrl := gomock.NewController(t) + mockRes := resourcemock.NewMockIFlagdResource(ctrl) + + err = r.Reconcile( + context.Background(), + flagdObj, + &corev1.ConfigMap{}, + mockRes, + ) require.NotNil(t, err) } diff --git a/controllers/core/flagd/deployment.go b/controllers/core/flagd/resources/deployment.go similarity index 79% rename from controllers/core/flagd/deployment.go rename to controllers/core/flagd/resources/deployment.go index a59a127cb..82cef2f90 100644 --- a/controllers/core/flagd/deployment.go +++ b/controllers/core/flagd/resources/deployment.go @@ -1,4 +1,4 @@ -package flagd +package resources import ( "context" @@ -8,6 +8,7 @@ import ( api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/common/flagdinjector" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,26 +21,24 @@ type FlagdDeployment struct { Log logr.Logger FlagdInjector flagdinjector.IFlagdContainerInjector - FlagdConfig FlagdConfiguration - - ResourceReconciler *ResourceReconciler + FlagdConfig resources.FlagdConfiguration } -func (r *FlagdDeployment) Reconcile(ctx context.Context, flagd *api.Flagd) error { - return r.ResourceReconciler.Reconcile( - ctx, - flagd, - &appsv1.Deployment{}, - func() (client.Object, error) { - return r.getFlagdDeployment(ctx, flagd) - }, - func(old client.Object, new client.Object) bool { - return areDeploymentsEqual(old, new) - }, - ) +func (r *FlagdDeployment) AreObjectsEqual(o1 client.Object, o2 client.Object) bool { + oldDeployment, ok := o1.(*appsv1.Deployment) + if !ok { + return false + } + + newDeployment, ok := o2.(*appsv1.Deployment) + if !ok { + return false + } + + return reflect.DeepEqual(oldDeployment.Spec, newDeployment.Spec) } -func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Flagd) (*appsv1.Deployment, error) { +func (r *FlagdDeployment) GetResource(ctx context.Context, flagd *api.Flagd) (client.Object, error) { labels := map[string]string{ "app": flagd.Name, "app.kubernetes.io/name": flagd.Name, @@ -115,17 +114,3 @@ func (r *FlagdDeployment) getFlagdDeployment(ctx context.Context, flagd *api.Fla return deployment, nil } - -func areDeploymentsEqual(old client.Object, new client.Object) bool { - oldDeployment, ok := old.(*appsv1.Deployment) - if !ok { - return false - } - - newDeployment, ok := new.(*appsv1.Deployment) - if !ok { - return false - } - - return reflect.DeepEqual(oldDeployment.Spec, newDeployment.Spec) -} diff --git a/controllers/core/flagd/deployment_test.go b/controllers/core/flagd/resources/deployment_test.go similarity index 89% rename from controllers/core/flagd/deployment_test.go rename to controllers/core/flagd/resources/deployment_test.go index 2271cefd0..f67ad907b 100644 --- a/controllers/core/flagd/deployment_test.go +++ b/controllers/core/flagd/resources/deployment_test.go @@ -1,4 +1,4 @@ -package flagd +package resources import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" commonfake "github.com/open-feature/open-feature-operator/common/flagdinjector/fake" + resources "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -17,6 +18,17 @@ import ( "testing" ) +var testFlagdConfig = resources.FlagdConfiguration{ + FlagdPort: 8013, + OFREPPort: 8016, + ManagementPort: 8014, + DebugLogging: false, + Image: "flagd", + Tag: "latest", + OperatorNamespace: "ofo-system", + OperatorDeploymentName: "ofo", +} + func TestFlagdDeployment_getFlagdDeployment(t *testing.T) { err := api.AddToScheme(scheme.Scheme) require.Nil(t, err) @@ -71,10 +83,12 @@ func TestFlagdDeployment_getFlagdDeployment(t *testing.T) { FlagdConfig: testFlagdConfig, } - deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + res, err := r.GetResource(context.Background(), flagdObj) require.Nil(t, err) - require.NotNil(t, deploymentResult) + require.NotNil(t, res) + + deploymentResult := res.(*appsv1.Deployment) require.Equal(t, flagdObj.Name, deploymentResult.Name) require.Equal(t, flagdObj.Namespace, deploymentResult.Namespace) @@ -140,7 +154,7 @@ func TestFlagdDeployment_getFlagdDeployment_ErrorInInjector(t *testing.T) { FlagdConfig: testFlagdConfig, } - deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + deploymentResult, err := r.GetResource(context.Background(), flagdObj) require.NotNil(t, err) require.Nil(t, deploymentResult) @@ -187,7 +201,7 @@ func TestFlagdDeployment_getFlagdDeployment_ContainerNotInjected(t *testing.T) { FlagdConfig: testFlagdConfig, } - deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + deploymentResult, err := r.GetResource(context.Background(), flagdObj) require.NotNil(t, err) require.Nil(t, deploymentResult) @@ -223,7 +237,7 @@ func TestFlagdDeployment_getFlagdDeployment_FlagSourceNotFound(t *testing.T) { FlagdConfig: testFlagdConfig, } - deploymentResult, err := r.getFlagdDeployment(context.Background(), flagdObj) + deploymentResult, err := r.GetResource(context.Background(), flagdObj) require.NotNil(t, err) require.Nil(t, deploymentResult) @@ -299,7 +313,8 @@ func Test_areDeploymentsEqual(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := areDeploymentsEqual(tt.args.old, tt.args.new) + d := &FlagdDeployment{} + got := d.AreObjectsEqual(tt.args.old, tt.args.new) require.Equal(t, tt.want, got) }) diff --git a/controllers/core/flagd/ingress.go b/controllers/core/flagd/resources/ingress.go similarity index 54% rename from controllers/core/flagd/ingress.go rename to controllers/core/flagd/resources/ingress.go index 4440ee4de..2204dca23 100644 --- a/controllers/core/flagd/ingress.go +++ b/controllers/core/flagd/resources/ingress.go @@ -1,10 +1,10 @@ -package flagd +package resources import ( "context" - "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "reflect" @@ -12,39 +12,24 @@ import ( ) type FlagdIngress struct { - client.Client - - Log logr.Logger - FlagdConfig FlagdConfiguration - - ResourceReconciler *ResourceReconciler + FlagdConfig resources.FlagdConfiguration } -func (r FlagdIngress) Reconcile(ctx context.Context, flagd *api.Flagd) error { - return r.ResourceReconciler.Reconcile( - ctx, - flagd, - &networkingv1.Ingress{}, - func() (client.Object, error) { - return r.getIngress(flagd), nil - }, - func(old client.Object, new client.Object) bool { - oldIngress, ok := old.(*networkingv1.Ingress) - if !ok { - return false - } +func (r FlagdIngress) AreObjectsEqual(o1 client.Object, o2 client.Object) bool { + oldIngress, ok := o1.(*networkingv1.Ingress) + if !ok { + return false + } - newIngress, ok := new.(*networkingv1.Ingress) - if !ok { - return false - } + newIngress, ok := o2.(*networkingv1.Ingress) + if !ok { + return false + } - return reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) - }, - ) + return reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) } -func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { +func (r FlagdIngress) GetResource(_ context.Context, flagd *api.Flagd) (client.Object, error) { return &networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: flagd.Name, @@ -68,20 +53,18 @@ func (r FlagdIngress) getIngress(flagd *api.Flagd) *networkingv1.Ingress { TLS: flagd.Spec.Ingress.TLS, Rules: r.getRules(flagd), }, - } + }, nil } func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule { - rules := make([]networkingv1.IngressRule, 3*len(flagd.Spec.Ingress.Hosts)) + rules := make([]networkingv1.IngressRule, len(flagd.Spec.Ingress.Hosts)) for i, host := range flagd.Spec.Ingress.Hosts { - rules[2*i] = r.getRule(flagd, host, "/flagd", int32(r.FlagdConfig.FlagdPort)) - rules[2*i+1] = r.getRule(flagd, host, "/ofrep", int32(r.FlagdConfig.OFREPPort)) - rules[2*i+2] = r.getRule(flagd, host, "/sync", int32(r.FlagdConfig.SyncPort)) + rules[i] = r.getRule(flagd, host) } return rules } -func (r FlagdIngress) getRule(flagd *api.Flagd, host, path string, port int32) networkingv1.IngressRule { +func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.IngressRule { pathType := networkingv1.PathTypePrefix return networkingv1.IngressRule{ Host: host, @@ -89,13 +72,39 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host, path string, port int32) n HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { - Path: path, + Path: "/flagd", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: flagd.Name, + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.FlagdPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/ofrep", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: flagd.Name, + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.OFREPPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/sync", PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: flagd.Name, Port: networkingv1.ServiceBackendPort{ - Number: port, + Number: int32(r.FlagdConfig.SyncPort), }, }, Resource: nil, diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go new file mode 100644 index 000000000..b8ff61115 --- /dev/null +++ b/controllers/core/flagd/resources/ingress_test.go @@ -0,0 +1,246 @@ +package resources + +import ( + "context" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "testing" +) + +func TestFlagdIngress_getIngress(t *testing.T) { + r := FlagdIngress{ + FlagdConfig: testFlagdConfig, + } + + ingressResult, err := r.GetResource(context.TODO(), &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{ + Ingress: api.IngressSpec{ + Enabled: true, + Annotations: map[string]string{ + "foo": "bar", + }, + Hosts: []string{ + "flagd.test", + "flagd.service", + }, + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{ + "flagd.test", + "flagd.service", + }, + SecretName: "my-secret", + }, + }, + IngressClassName: strPtr("nginx"), + }, + }, + }) + + require.Nil(t, err) + + pathType := networkingv1.PathTypePrefix + + require.NotNil(t, ingressResult) + require.Equal(t, networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + TLS: []networkingv1.IngressTLS{ + { + Hosts: []string{ + "flagd.test", + "flagd.service", + }, + SecretName: "my-secret", + }, + }, + Rules: []networkingv1.IngressRule{ + { + Host: "flagd.test", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/flagd", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(testFlagdConfig.FlagdPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/ofrep", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.OFREPPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/sync", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.SyncPort), + }, + }, + Resource: nil, + }, + }, + }, + }, + }, + }, + { + Host: "flagd.service", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/flagd", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(testFlagdConfig.FlagdPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/ofrep", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.OFREPPort), + }, + }, + Resource: nil, + }, + }, + { + Path: "/sync", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "my-flagd", + Port: networkingv1.ServiceBackendPort{ + Number: int32(r.FlagdConfig.SyncPort), + }, + }, + Resource: nil, + }, + }, + }, + }, + }, + }, + }, + }, ingressResult.(*networkingv1.Ingress).Spec) + +} + +func strPtr(s string) *string { + return &s +} + +func Test_areIngressesEqual(t *testing.T) { + type args struct { + old client.Object + new client.Object + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "has changed", + args: args{ + old: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + }, + }, + new: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("kong"), + }, + }, + }, + want: false, + }, + { + name: "has not changed", + args: args{ + old: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + }, + }, + new: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + }, + }, + }, + want: true, + }, + { + name: "old is not a service", + args: args{ + old: &v1.ConfigMap{}, + new: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + }, + }, + }, + want: false, + }, + { + name: "new is not a service", + args: args{ + old: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + IngressClassName: strPtr("nginx"), + }, + }, + new: &v1.ConfigMap{}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + i := &FlagdIngress{} + got := i.AreObjectsEqual(tt.args.old, tt.args.new) + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/controllers/core/flagd/resources/interface.go b/controllers/core/flagd/resources/interface.go new file mode 100644 index 000000000..98dd714c5 --- /dev/null +++ b/controllers/core/flagd/resources/interface.go @@ -0,0 +1,12 @@ +package resources + +import ( + "context" + "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type IFlagdResource interface { + GetResource(ctx context.Context, flagd *v1beta1.Flagd) (client.Object, error) + AreObjectsEqual(o1 client.Object, o2 client.Object) bool +} diff --git a/controllers/core/flagd/resources/mock/mock.go b/controllers/core/flagd/resources/mock/mock.go new file mode 100644 index 000000000..040941ad6 --- /dev/null +++ b/controllers/core/flagd/resources/mock/mock.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./controllers/core/flagd/resources/interface.go + +// Package commonmock is a generated GoMock package. +package commonmock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" + client "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MockIFlagdResource is a mock of IFlagdResource interface. +type MockIFlagdResource struct { + ctrl *gomock.Controller + recorder *MockIFlagdResourceMockRecorder +} + +// MockIFlagdResourceMockRecorder is the mock recorder for MockIFlagdResource. +type MockIFlagdResourceMockRecorder struct { + mock *MockIFlagdResource +} + +// NewMockIFlagdResource creates a new mock instance. +func NewMockIFlagdResource(ctrl *gomock.Controller) *MockIFlagdResource { + mock := &MockIFlagdResource{ctrl: ctrl} + mock.recorder = &MockIFlagdResourceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIFlagdResource) EXPECT() *MockIFlagdResourceMockRecorder { + return m.recorder +} + +// AreObjectsEqual mocks base method. +func (m *MockIFlagdResource) AreObjectsEqual(o1, o2 client.Object) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AreObjectsEqual", o1, o2) + ret0, _ := ret[0].(bool) + return ret0 +} + +// AreObjectsEqual indicates an expected call of AreObjectsEqual. +func (mr *MockIFlagdResourceMockRecorder) AreObjectsEqual(o1, o2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AreObjectsEqual", reflect.TypeOf((*MockIFlagdResource)(nil).AreObjectsEqual), o1, o2) +} + +// GetResource mocks base method. +func (m *MockIFlagdResource) GetResource(ctx context.Context, flagd *v1beta1.Flagd) (client.Object, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetResource", ctx, flagd) + ret0, _ := ret[0].(client.Object) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResource indicates an expected call of GetResource. +func (mr *MockIFlagdResourceMockRecorder) GetResource(ctx, flagd interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResource", reflect.TypeOf((*MockIFlagdResource)(nil).GetResource), ctx, flagd) +} diff --git a/controllers/core/flagd/service.go b/controllers/core/flagd/resources/service.go similarity index 74% rename from controllers/core/flagd/service.go rename to controllers/core/flagd/resources/service.go index d49924e29..df51647df 100644 --- a/controllers/core/flagd/service.go +++ b/controllers/core/flagd/resources/service.go @@ -1,9 +1,10 @@ -package flagd +package resources import ( "context" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" + "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -12,25 +13,24 @@ import ( ) type FlagdService struct { - FlagdConfig FlagdConfiguration - ResourceReconciler *ResourceReconciler + FlagdConfig resources.FlagdConfiguration } -func (r FlagdService) Reconcile(ctx context.Context, flagd *api.Flagd) error { - return r.ResourceReconciler.Reconcile( - ctx, - flagd, - &v1.Service{}, - func() (client.Object, error) { - return r.getService(flagd), nil - }, - func(old client.Object, new client.Object) bool { - return areServicesEqual(old, new) - }, - ) +func (r FlagdService) AreObjectsEqual(o1 client.Object, o2 client.Object) bool { + oldService, ok := o1.(*v1.Service) + if !ok { + return false + } + + newService, ok := o2.(*v1.Service) + if !ok { + return false + } + + return reflect.DeepEqual(oldService.Spec, newService.Spec) } -func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { +func (r FlagdService) GetResource(_ context.Context, flagd *api.Flagd) (client.Object, error) { return &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: flagd.Name, @@ -85,19 +85,5 @@ func (r FlagdService) getService(flagd *api.Flagd) *v1.Service { }, Type: flagd.Spec.ServiceType, }, - } -} - -func areServicesEqual(old client.Object, new client.Object) bool { - oldService, ok := old.(*v1.Service) - if !ok { - return false - } - - newService, ok := new.(*v1.Service) - if !ok { - return false - } - - return reflect.DeepEqual(oldService.Spec, newService.Spec) + }, nil } diff --git a/controllers/core/flagd/service_test.go b/controllers/core/flagd/resources/service_test.go similarity index 88% rename from controllers/core/flagd/service_test.go rename to controllers/core/flagd/resources/service_test.go index 880166f4c..27dd40a20 100644 --- a/controllers/core/flagd/service_test.go +++ b/controllers/core/flagd/resources/service_test.go @@ -1,6 +1,7 @@ -package flagd +package resources import ( + "context" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" @@ -14,7 +15,7 @@ func TestFlagdService_getService(t *testing.T) { FlagdConfig: testFlagdConfig, } - svc := r.getService(&api.Flagd{ + svc, err := r.GetResource(context.TODO(), &api.Flagd{ ObjectMeta: metav1.ObjectMeta{ Name: "my-flagd", Namespace: "my-namespace", @@ -24,7 +25,9 @@ func TestFlagdService_getService(t *testing.T) { }, }) + require.Nil(t, err) require.NotNil(t, svc) + require.IsType(t, &v1.Service{}, svc) } func Test_areServicesEqual(t *testing.T) { @@ -97,7 +100,8 @@ func Test_areServicesEqual(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := areServicesEqual(tt.args.old, tt.args.new) + s := &FlagdService{} + got := s.AreObjectsEqual(tt.args.old, tt.args.new) require.Equal(t, tt.want, got) }) diff --git a/main.go b/main.go index bc989a187..f8fbd035e 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "context" "flag" "fmt" + resources2 "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" "log" "os" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -201,24 +202,20 @@ func main() { flagdConfig := flagd.NewFlagdConfiguration(env) if err = (&flagd.FlagdReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - FlagdDeployment: &flagd.FlagdDeployment{ - Client: mgr.GetClient(), - Log: flagdControllerLogger, - FlagdInjector: flagdContainerInjector, - FlagdConfig: flagdConfig, - ResourceReconciler: flagdResourceReconciler, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ResourceReconciler: flagdResourceReconciler, + FlagdDeployment: &resources2.FlagdDeployment{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + FlagdInjector: flagdContainerInjector, + FlagdConfig: flagdConfig, }, - FlagdService: &flagd.FlagdService{ - FlagdConfig: flagdConfig, - ResourceReconciler: flagdResourceReconciler, + FlagdService: &resources2.FlagdService{ + FlagdConfig: flagdConfig, }, - FlagdIngress: &flagd.FlagdIngress{ - Client: mgr.GetClient(), - Log: flagdControllerLogger, - FlagdConfig: flagdConfig, - ResourceReconciler: flagdResourceReconciler, + FlagdIngress: &resources2.FlagdIngress{ + FlagdConfig: flagdConfig, }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Flagd") From 4fd0820732300f2b4131059050b458d31559d806 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 7 May 2024 15:37:09 +0200 Subject: [PATCH 13/43] fix linting Signed-off-by: Florian Bacher --- controllers/core/flagd/resource.go | 30 ++++++++++++------- .../core/flagd/resources/deployment.go | 4 +-- .../core/flagd/resources/deployment_test.go | 1 + .../core/flagd/resources/ingress_test.go | 1 + .../core/flagd/resources/service_test.go | 1 + 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index 0792f9a0e..825b6852f 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -47,17 +47,25 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob } if exists && !resource.AreObjectsEqual(existingObj, newObj) { - r.Log.Info(fmt.Sprintf("Updating %v", newObj)) - if err := r.Client.Update(ctx, newObj); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return err - } - } else { - r.Log.Info(fmt.Sprintf("Creating %v", newObj)) - if err := r.Client.Create(ctx, newObj); err != nil { - r.Log.Error(err, fmt.Sprintf("Failed to create Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) - return err - } + return r.updateResource(ctx, flagd, obj, newObj) + } + return r.createResource(ctx, flagd, obj, newObj) +} + +func (r *ResourceReconciler) createResource(ctx context.Context, flagd *api.Flagd, obj client.Object, newObj client.Object) error { + r.Log.Info(fmt.Sprintf("Creating %v", newObj)) + if err := r.Client.Create(ctx, newObj); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to create Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return err + } + return nil +} + +func (r *ResourceReconciler) updateResource(ctx context.Context, flagd *api.Flagd, obj client.Object, newObj client.Object) error { + r.Log.Info(fmt.Sprintf("Updating %v", newObj)) + if err := r.Client.Update(ctx, newObj); err != nil { + r.Log.Error(err, fmt.Sprintf("Failed to update Flagd %s '%s/%s'", obj.GetObjectKind(), flagd.Namespace, flagd.Name)) + return err } return nil } diff --git a/controllers/core/flagd/resources/deployment.go b/controllers/core/flagd/resources/deployment.go index 82cef2f90..28a2b19bb 100644 --- a/controllers/core/flagd/resources/deployment.go +++ b/controllers/core/flagd/resources/deployment.go @@ -81,12 +81,12 @@ func (r *FlagdDeployment) GetResource(ctx context.Context, flagd *api.Flagd) (cl Namespace: flagd.Spec.FeatureFlagSourceRef.Namespace, Name: flagd.Spec.FeatureFlagSourceRef.Name, }, featureFlagSource); err != nil { - return nil, fmt.Errorf("could not look up feature flag source for flagd: %v", err) + return nil, fmt.Errorf("could not look up feature flag source for flagd: %w", err) } err := r.FlagdInjector.InjectFlagd(ctx, &deployment.ObjectMeta, &deployment.Spec.Template.Spec, &featureFlagSource.Spec) if err != nil { - return nil, fmt.Errorf("could not inject flagd container into deployment: %v", err) + return nil, fmt.Errorf("could not inject flagd container into deployment: %w", err) } if len(deployment.Spec.Template.Spec.Containers) == 0 { diff --git a/controllers/core/flagd/resources/deployment_test.go b/controllers/core/flagd/resources/deployment_test.go index f67ad907b..d9a25b1e4 100644 --- a/controllers/core/flagd/resources/deployment_test.go +++ b/controllers/core/flagd/resources/deployment_test.go @@ -1,3 +1,4 @@ +// nolint:dupl package resources import ( diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go index b8ff61115..3cd0e428d 100644 --- a/controllers/core/flagd/resources/ingress_test.go +++ b/controllers/core/flagd/resources/ingress_test.go @@ -1,3 +1,4 @@ +// nolint:dupl package resources import ( diff --git a/controllers/core/flagd/resources/service_test.go b/controllers/core/flagd/resources/service_test.go index 27dd40a20..db5d30b17 100644 --- a/controllers/core/flagd/resources/service_test.go +++ b/controllers/core/flagd/resources/service_test.go @@ -1,3 +1,4 @@ +// nolint:dupl package resources import ( From 8e7e48f72cd342a12b9b5b635a19260cdad07f85 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:23:34 +0200 Subject: [PATCH 14/43] adapt to changes from main branch Signed-off-by: Florian Bacher --- apis/core/v1beta1/zz_generated.deepcopy.go | 136 +++++++++++++++++++++ go.mod | 5 +- main.go | 10 +- 3 files changed, 145 insertions(+), 6 deletions(-) diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index b30239935..7c9ba18c7 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -23,6 +23,7 @@ package v1beta1 import ( "encoding/json" "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -284,6 +285,102 @@ func (in *FlagSpec) DeepCopy() *FlagSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flagd) DeepCopyInto(out *Flagd) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagd. +func (in *Flagd) DeepCopy() *Flagd { + if in == nil { + return nil + } + out := new(Flagd) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Flagd) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdList) DeepCopyInto(out *FlagdList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Flagd, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdList. +func (in *FlagdList) DeepCopy() *FlagdList { + if in == nil { + return nil + } + out := new(FlagdList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlagdList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdSpec) DeepCopyInto(out *FlagdSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + out.FeatureFlagSourceRef = in.FeatureFlagSourceRef + in.Ingress.DeepCopyInto(&out.Ingress) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdSpec. +func (in *FlagdSpec) DeepCopy() *FlagdSpec { + if in == nil { + return nil + } + out := new(FlagdSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlagdStatus) DeepCopyInto(out *FlagdStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlagdStatus. +func (in *FlagdStatus) DeepCopy() *FlagdStatus { + if in == nil { + return nil + } + out := new(FlagdStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Flags) DeepCopyInto(out *Flags) { *out = *in @@ -306,6 +403,45 @@ func (in *Flags) DeepCopy() *Flags { 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.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = make([]networkingv1.IngressTLS, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.IngressClassName != nil { + in, out := &in.IngressClassName, &out.IngressClassName + *out = new(string) + **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 *Source) DeepCopyInto(out *Source) { *out = *in diff --git a/go.mod b/go.mod index 26ea48179..2b3ff6a40 100644 --- a/go.mod +++ b/go.mod @@ -79,4 +79,7 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace golang.org/x/net => golang.org/x/net v0.24.0 +replace ( + golang.org/x/net => golang.org/x/net v0.24.0 + github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63 +) diff --git a/main.go b/main.go index 0b510f95d..0dcb2ef49 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,8 @@ import ( "context" "flag" "fmt" - resources2 "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" "log" "os" - "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/kelseyhightower/envconfig" corev1beta1 "github.com/open-feature/open-feature-operator/apis/core/v1beta1" @@ -33,6 +31,7 @@ import ( "github.com/open-feature/open-feature-operator/common/types" "github.com/open-feature/open-feature-operator/controllers/core/featureflagsource" "github.com/open-feature/open-feature-operator/controllers/core/flagd" + flagdresources "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources" webhooks "github.com/open-feature/open-feature-operator/webhooks" "go.uber.org/zap/zapcore" appsV1 "k8s.io/api/apps/v1" @@ -47,6 +46,7 @@ import ( "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/webhook" ) const ( @@ -205,16 +205,16 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), ResourceReconciler: flagdResourceReconciler, - FlagdDeployment: &resources2.FlagdDeployment{ + FlagdDeployment: &flagdresources.FlagdDeployment{ Client: mgr.GetClient(), Log: flagdControllerLogger, FlagdInjector: flagdContainerInjector, FlagdConfig: flagdConfig, }, - FlagdService: &resources2.FlagdService{ + FlagdService: &flagdresources.FlagdService{ FlagdConfig: flagdConfig, }, - FlagdIngress: &resources2.FlagdIngress{ + FlagdIngress: &flagdresources.FlagdIngress{ FlagdConfig: flagdConfig, }, }).SetupWithManager(mgr); err != nil { From b2b4bbd97b4c015691a006cbe6b0591f93ab86af Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:37:53 +0200 Subject: [PATCH 15/43] update go deps Signed-off-by: Florian Bacher --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2b3ff6a40..82ccff530 100644 --- a/go.mod +++ b/go.mod @@ -81,5 +81,5 @@ require ( replace ( golang.org/x/net => golang.org/x/net v0.24.0 - github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63 + github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3 ) From efedc1997b5b5cdc69da257ddadd7a5a40d732ca Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:41:14 +0200 Subject: [PATCH 16/43] update crd docs Signed-off-by: Florian Bacher --- docs/crds.md | 302 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/docs/crds.md b/docs/crds.md index 12169b6ad..dbf6ac659 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -12,6 +12,8 @@ Resource Types: - [FeatureFlagSource](#featureflagsource) +- [Flagd](#flagd) + @@ -783,4 +785,304 @@ inside a container.
true + + +## Flagd +[↩ Parent](#coreopenfeaturedevv1beta1 ) + + + + + + +Flagd is the Schema for the flagds API + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringcore.openfeature.dev/v1beta1true
kindstringFlagdtrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + FlagdSpec defines the desired state of Flagd
+
false
statusobject + FlagdStatus defines the observed state of Flagd
+
false
+ + +### Flagd.spec +[↩ Parent](#flagd) + + + +FlagdSpec defines the desired state of Flagd + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
featureFlagSourceRefobject + FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves +the feature flag configurations
+
true
ingressobject + Ingress
+
false
otelCollectorUristring + OtelCollectorUri defines the OpenTelemetry collector URI to enable OpenTelemetry Tracing in flagd.
+
false
replicasinteger + Replicas defines the number of replicas to create for the service
+
+ Format: int32
+
false
serviceAccountNamestring + ServiceAccountName the service account name for the flagd deployment
+
false
serviceTypestring + ServiceType represents the type of Service to create. +Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName. +Default: ClusterIP
+
+ Default: ClusterIP
+
false
+ + +### Flagd.spec.featureFlagSourceRef +[↩ Parent](#flagdspec) + + + +FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves +the feature flag configurations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstring + API version of the referent.
+
false
fieldPathstring + If referring to a piece of an object instead of an entire object, this string +should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. +For example, if the object reference is to a container within a pod, this would take on a value like: +"spec.containers{name}" (where "name" refers to the name of the container that triggered +the event) or if no container name is specified "spec.containers[2]" (container with +index 2 in this pod). This syntax is chosen only to have some well-defined way of +referencing a part of an object. +TODO: this design is not final and this field is subject to change in the future.
+
false
kindstring + Kind of the referent. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+
false
namestring + Name of the referent. +More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+
false
namespacestring + Namespace of the referent. +More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
+
false
resourceVersionstring + Specific resourceVersion to which this reference is made, if any. +More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
+
false
uidstring + UID of the referent. +More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
+
false
+ + +### Flagd.spec.ingress +[↩ Parent](#flagdspec) + + + +Ingress + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
annotationsmap[string]string + Annotations the annotations to be added to the ingress
+
false
enabledboolean + Enabled enables/disables the ingress for flagd
+
false
hosts[]string + Hosts list of hosts to be added to the ingress
+
false
ingressClassNamestring + IngressClassName defines the name if the ingress class to be used for flagd
+
false
tls[]object + TLS configuration for the ingress
+
false
+ + +### Flagd.spec.ingress.tls[index] +[↩ Parent](#flagdspecingress) + + + +IngressTLS describes the transport layer security associated with an Ingress. + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
hosts[]string + Hosts are a list of hosts included in the TLS certificate. The values in +this list must match the name/s used in the tlsSecret. Defaults to the +wildcard host setting for the loadbalancer controller fulfilling this +Ingress, if left unspecified.
+
false
secretNamestring + SecretName is the name of the secret used to terminate TLS traffic on +port 443. Field is left optional to allow TLS routing based on SNI +hostname alone. If the SNI host in a listener conflicts with the "Host" +header field used by an IngressRule, the SNI host is used for termination +and value of the Host header is used for routing.
+
false
\ No newline at end of file From b28788b264b4c89471b220296a6b18036ebf9143 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:42:06 +0200 Subject: [PATCH 17/43] go mod tidy Signed-off-by: Florian Bacher --- go.mod | 2 +- go.sum | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 82ccff530..e8fba4c02 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,6 @@ require ( ) replace ( - golang.org/x/net => golang.org/x/net v0.24.0 github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3 + golang.org/x/net => golang.org/x/net v0.24.0 ) diff --git a/go.sum b/go.sum index a8b51b557..7fceb136d 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63 h1:z4kYnRzFjltnxDJXSXfFkrup5bmPkEeTbXuupKroTKY= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240506081856-706b789d5a63/go.mod h1:I/4tLd5D4JpWpaFZxe2o8R2S1isWGNwHDSC/H5h7o3A= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3 h1:B1BTk30fAjlhOWzAKD+3AEULo7+YFwgXVoI20krMWuk= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -173,7 +173,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -228,8 +227,6 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7 h1:oP+BH8RiNEmSWTffKEXz2ciwen7wbvyX0fESx0aoJ80= github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7/go.mod h1:WKtwo1eW9/K6D+4HfgTXWBqCDzpvMhDa5eRxW7R5B2U= -github.com/open-feature/open-feature-operator/apis v0.2.41-0.20240506125212-c4831a3cdc00 h1:EsG43eil7L+4c6BXMGPMRm9qYVz0J1+sjukz5xw+vTM= -github.com/open-feature/open-feature-operator/apis v0.2.41-0.20240506125212-c4831a3cdc00/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From 542ce1d2b5c7f9691e4e05922d26d56d0ffa0b15 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:51:13 +0200 Subject: [PATCH 18/43] fix linting Signed-off-by: Florian Bacher --- common/flagdproxy/flagdproxy.go | 2 +- common/flagdproxy/flagdproxy_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/flagdproxy/flagdproxy.go b/common/flagdproxy/flagdproxy.go index e4ade7a5c..4de163623 100644 --- a/common/flagdproxy/flagdproxy.go +++ b/common/flagdproxy/flagdproxy.go @@ -3,10 +3,10 @@ package flagdproxy import ( "context" "fmt" - "github.com/open-feature/open-feature-operator/common" "reflect" "github.com/go-logr/logr" + "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/common/types" appsV1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" diff --git a/common/flagdproxy/flagdproxy_test.go b/common/flagdproxy/flagdproxy_test.go index dd0232a67..4a2fcc8e4 100644 --- a/common/flagdproxy/flagdproxy_test.go +++ b/common/flagdproxy/flagdproxy_test.go @@ -3,10 +3,10 @@ package flagdproxy import ( "context" "fmt" - "github.com/open-feature/open-feature-operator/common" "testing" "github.com/go-logr/logr/testr" + "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/common/types" "github.com/stretchr/testify/require" appsV1 "k8s.io/api/apps/v1" From d6841ccb6754b64d001b9f50f0abc810576165d1 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 07:58:34 +0200 Subject: [PATCH 19/43] fix linting Signed-off-by: Florian Bacher --- controllers/core/flagd/controller.go | 1 + controllers/core/flagd/controller_test.go | 5 +++-- controllers/core/flagd/resource.go | 1 + controllers/core/flagd/resource_test.go | 3 ++- controllers/core/flagd/resources/deployment.go | 3 ++- controllers/core/flagd/resources/deployment_test.go | 3 ++- controllers/core/flagd/resources/ingress.go | 3 ++- controllers/core/flagd/resources/ingress_test.go | 3 ++- controllers/core/flagd/resources/interface.go | 1 + controllers/core/flagd/resources/service.go | 3 ++- controllers/core/flagd/resources/service_test.go | 3 ++- 11 files changed, 20 insertions(+), 9 deletions(-) diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index e1d5f3b26..3d2346bf4 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -19,6 +19,7 @@ package flagd import ( "context" "fmt" + "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" resources2 "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go index a02843942..93250c67f 100644 --- a/controllers/core/flagd/controller_test.go +++ b/controllers/core/flagd/controller_test.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "reflect" + "testing" + "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" @@ -16,11 +19,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" - "reflect" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" ) var testFlagdConfig = resources.FlagdConfiguration{ diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index 825b6852f..f61e41dfe 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -3,6 +3,7 @@ package flagd import ( "context" "fmt" + "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" diff --git a/controllers/core/flagd/resource_test.go b/controllers/core/flagd/resource_test.go index 2b1789a22..964fe614b 100644 --- a/controllers/core/flagd/resource_test.go +++ b/controllers/core/flagd/resource_test.go @@ -3,6 +3,8 @@ package flagd import ( "context" "errors" + "testing" + "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" @@ -14,7 +16,6 @@ import ( controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" ) func TestResourceReconciler_Reconcile_CreateResource(t *testing.T) { diff --git a/controllers/core/flagd/resources/deployment.go b/controllers/core/flagd/resources/deployment.go index 28a2b19bb..a3c4f0d25 100644 --- a/controllers/core/flagd/resources/deployment.go +++ b/controllers/core/flagd/resources/deployment.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "reflect" + "github.com/go-logr/logr" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" @@ -12,7 +14,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/controllers/core/flagd/resources/deployment_test.go b/controllers/core/flagd/resources/deployment_test.go index d9a25b1e4..bc603c6cd 100644 --- a/controllers/core/flagd/resources/deployment_test.go +++ b/controllers/core/flagd/resources/deployment_test.go @@ -4,6 +4,8 @@ package resources import ( "context" "errors" + "testing" + "github.com/golang/mock/gomock" api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" commonfake "github.com/open-feature/open-feature-operator/common/flagdinjector/fake" @@ -16,7 +18,6 @@ import ( controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" ) var testFlagdConfig = resources.FlagdConfiguration{ diff --git a/controllers/core/flagd/resources/ingress.go b/controllers/core/flagd/resources/ingress.go index 2204dca23..5fae24d8e 100644 --- a/controllers/core/flagd/resources/ingress.go +++ b/controllers/core/flagd/resources/ingress.go @@ -2,12 +2,13 @@ package resources import ( "context" + "reflect" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go index 3cd0e428d..89f4040ec 100644 --- a/controllers/core/flagd/resources/ingress_test.go +++ b/controllers/core/flagd/resources/ingress_test.go @@ -3,13 +3,14 @@ package resources import ( "context" + "testing" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "testing" ) func TestFlagdIngress_getIngress(t *testing.T) { diff --git a/controllers/core/flagd/resources/interface.go b/controllers/core/flagd/resources/interface.go index 98dd714c5..db93e2fab 100644 --- a/controllers/core/flagd/resources/interface.go +++ b/controllers/core/flagd/resources/interface.go @@ -2,6 +2,7 @@ package resources import ( "context" + "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/controllers/core/flagd/resources/service.go b/controllers/core/flagd/resources/service.go index df51647df..5a46b19e9 100644 --- a/controllers/core/flagd/resources/service.go +++ b/controllers/core/flagd/resources/service.go @@ -2,13 +2,14 @@ package resources import ( "context" + "reflect" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/open-feature/open-feature-operator/common" "github.com/open-feature/open-feature-operator/controllers/core/flagd/common" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/controllers/core/flagd/resources/service_test.go b/controllers/core/flagd/resources/service_test.go index db5d30b17..88baf9200 100644 --- a/controllers/core/flagd/resources/service_test.go +++ b/controllers/core/flagd/resources/service_test.go @@ -3,12 +3,13 @@ package resources import ( "context" + "testing" + api "github.com/open-feature/open-feature-operator/apis/core/v1beta1" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "testing" ) func TestFlagdService_getService(t *testing.T) { From 0539105612ac3a1f9faf869173a85af502da31d7 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 09:44:22 +0200 Subject: [PATCH 20/43] create ingress only if enabled Signed-off-by: Florian Bacher --- controllers/core/flagd/controller.go | 17 +++---- controllers/core/flagd/controller_test.go | 57 ++++++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index 3d2346bf4..b19a20590 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -98,15 +98,16 @@ func (r *FlagdReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } - if err := r.ResourceReconciler.Reconcile( - ctx, - flagd, - &networkingv1.Ingress{}, - r.FlagdIngress, - ); err != nil { - return ctrl.Result{}, err + if flagd.Spec.Ingress.Enabled { + if err := r.ResourceReconciler.Reconcile( + ctx, + flagd, + &networkingv1.Ingress{}, + r.FlagdIngress, + ); err != nil { + return ctrl.Result{}, err + } } - return ctrl.Result{}, nil } diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go index 93250c67f..6063fd104 100644 --- a/controllers/core/flagd/controller_test.go +++ b/controllers/core/flagd/controller_test.go @@ -52,7 +52,7 @@ func (fm flagdMatcher) String() string { return fmt.Sprintf("%v", fm.flagdObj) } -func TestFlagdReconciler_Reconcile(t *testing.T) { +func TestFlagdReconciler_ReconcileWithIngress(t *testing.T) { err := api.AddToScheme(scheme.Scheme) require.Nil(t, err) @@ -61,7 +61,9 @@ func TestFlagdReconciler_Reconcile(t *testing.T) { Name: "my-flagd", Namespace: "my-namespace", }, - Spec: api.FlagdSpec{}, + Spec: api.FlagdSpec{ + Ingress: api.IngressSpec{Enabled: true}, + }, } fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() @@ -111,6 +113,57 @@ func TestFlagdReconciler_Reconcile(t *testing.T) { require.Equal(t, controllerruntime.Result{}, result) } +func TestFlagdReconciler_ReconcileWithoutIngress(t *testing.T) { + err := api.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + flagdObj := &api.Flagd{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-flagd", + Namespace: "my-namespace", + }, + Spec: api.FlagdSpec{}, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() + + ctrl := gomock.NewController(t) + + deploymentResource := resourcemock.NewMockIFlagdResource(ctrl) + serviceResource := resourcemock.NewMockIFlagdResource(ctrl) + ingressResource := resourcemock.NewMockIFlagdResource(ctrl) + + resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&appsv1.Deployment{}), + deploymentResource, + ).Times(1).Return(nil) + + resourceReconciler.EXPECT(). + Reconcile( + gomock.Any(), + flagdMatcher{flagdObj: *flagdObj}, + gomock.AssignableToTypeOf(&v1.Service{}), + serviceResource, + ).Times(1).Return(nil) + + r := setupReconciler(fakeClient, deploymentResource, serviceResource, ingressResource, resourceReconciler) + + result, err := r.Reconcile(context.Background(), controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: flagdObj.Namespace, + Name: flagdObj.Name, + }, + }) + + require.Nil(t, err) + require.Equal(t, controllerruntime.Result{}, result) +} + func TestFlagdReconciler_ReconcileResourceNotFound(t *testing.T) { err := api.AddToScheme(scheme.Scheme) require.Nil(t, err) From 2ee275df8e7348fdd1b7bab1a30ec5a1aa13cfb5 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 10:05:33 +0200 Subject: [PATCH 21/43] refer to feature flag source in same namespace Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 4 +- apis/core/v1beta1/zz_generated.deepcopy.go | 1 - .../core_v1beta1_featureflagsource.yaml | 25 ++++-- .../core/flagd/resources/deployment.go | 4 +- docs/crds.md | 88 +------------------ 5 files changed, 24 insertions(+), 98 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 854066c38..a430d9cb6 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -46,9 +46,9 @@ type FlagdSpec struct { // +optional OtelCollectorUri string `json:"otelCollectorUri"` - // FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves + // FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves // the feature flag configurations - FeatureFlagSourceRef v1.ObjectReference `json:"featureFlagSourceRef"` + FeatureFlagSource string `json:"featureFlagSource"` // Ingress // +optional diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index 7c9ba18c7..0f43472f9 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -352,7 +352,6 @@ func (in *FlagdSpec) DeepCopyInto(out *FlagdSpec) { *out = new(int32) **out = **in } - out.FeatureFlagSourceRef = in.FeatureFlagSourceRef in.Ingress.DeepCopyInto(&out.Ingress) } diff --git a/config/samples/core_v1beta1_featureflagsource.yaml b/config/samples/core_v1beta1_featureflagsource.yaml index c1651528a..24c42d04f 100644 --- a/config/samples/core_v1beta1_featureflagsource.yaml +++ b/config/samples/core_v1beta1_featureflagsource.yaml @@ -1,13 +1,22 @@ apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag +metadata: + name: featureflag-sample +spec: + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" +--- +apiVersion: core.openfeature.dev/v1beta1 kind: FeatureFlagSource metadata: - name: featureflagsource-sample + name: end-to-end spec: - managementPort: 8080 - evaluator: json - defaultSyncProvider: file - tag: latest sources: - - source: end-to-end-test - provider: file - probesEnabled: true + - source: featureflag-sample + provider: kubernetes diff --git a/controllers/core/flagd/resources/deployment.go b/controllers/core/flagd/resources/deployment.go index a3c4f0d25..0cedc3bc2 100644 --- a/controllers/core/flagd/resources/deployment.go +++ b/controllers/core/flagd/resources/deployment.go @@ -79,8 +79,8 @@ func (r *FlagdDeployment) GetResource(ctx context.Context, flagd *api.Flagd) (cl featureFlagSource := &api.FeatureFlagSource{} if err := r.Client.Get(ctx, client.ObjectKey{ - Namespace: flagd.Spec.FeatureFlagSourceRef.Namespace, - Name: flagd.Spec.FeatureFlagSourceRef.Name, + Namespace: flagd.Namespace, + Name: flagd.Spec.FeatureFlagSource, }, featureFlagSource); err != nil { return nil, fmt.Errorf("could not look up feature flag source for flagd: %w", err) } diff --git a/docs/crds.md b/docs/crds.md index dbf6ac659..337c1aca4 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -858,10 +858,10 @@ FlagdSpec defines the desired state of Flagd - featureFlagSourceRef - object + featureFlagSource + string - FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves + FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves the feature flag configurations
true @@ -910,88 +910,6 @@ Default: ClusterIP
-### Flagd.spec.featureFlagSourceRef -[↩ Parent](#flagdspec) - - - -FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves -the feature flag configurations - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionRequired
apiVersionstring - API version of the referent.
-
false
fieldPathstring - If referring to a piece of an object instead of an entire object, this string -should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. -For example, if the object reference is to a container within a pod, this would take on a value like: -"spec.containers{name}" (where "name" refers to the name of the container that triggered -the event) or if no container name is specified "spec.containers[2]" (container with -index 2 in this pod). This syntax is chosen only to have some well-defined way of -referencing a part of an object. -TODO: this design is not final and this field is subject to change in the future.
-
false
kindstring - Kind of the referent. -More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
-
false
namestring - Name of the referent. -More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-
false
namespacestring - Namespace of the referent. -More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
-
false
resourceVersionstring - Specific resourceVersion to which this reference is made, if any. -More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
-
false
uidstring - UID of the referent. -More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
-
false
- - ### Flagd.spec.ingress [↩ Parent](#flagdspec) From 08cc258ed722b6e8b865523d48fc0a18c92b64ac Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 10:53:21 +0200 Subject: [PATCH 22/43] refer to feature flag source in same namespace Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 12 ++ .../bases/core.openfeature.dev_flagds.yaml | 69 ++++-------- .../core/flagd/resources/deployment_test.go | 20 +--- controllers/core/flagd/resources/ingress.go | 36 +++++- .../core/flagd/resources/ingress_test.go | 105 ++++++++++++++++++ docs/crds.md | 28 +++++ go.mod | 2 +- go.sum | 4 +- 8 files changed, 209 insertions(+), 67 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index a430d9cb6..90bae0d5c 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -74,6 +74,18 @@ type IngressSpec struct { // IngressClassName defines the name if the ingress class to be used for flagd // +optional IngressClassName *string `json:"ingressClassName,omitempty"` + + // PathType is the path type to be used for the ingress rules + PathType networkingv1.PathType `json:"pathType,omitempty"` + + // FlagdPath is the path to be used for accessing the flagd flag evaluation API + FlagdPath string `json:"flagdPath"` + + // OFREPPath is the path to be used for accessing the OFREP API + OFREPPath string `json:"ofrepPath"` + + // SyncPath is the path to be used for accessing the sync API + SyncPath string `json:"syncPath"` } // FlagdStatus defines the observed state of Flagd diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index e72b9605e..5c4954c6f 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -39,52 +39,11 @@ spec: spec: description: FlagdSpec defines the desired state of Flagd properties: - featureFlagSourceRef: + featureFlagSource: description: |- - FeatureFlagSourceRef references to a FeatureFlagSource from which the created flagd instance retrieves + FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves the feature flag configurations - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - TODO: this design is not final and this field is subject to change in the future. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic + type: string ingress: description: Ingress properties: @@ -96,6 +55,10 @@ spec: enabled: description: Enabled enables/disables the ingress for flagd type: boolean + flagdPath: + description: FlagdPath is the path to be used for accessing the + flagd flag evaluation API + type: string hosts: description: Hosts list of hosts to be added to the ingress items: @@ -105,6 +68,18 @@ spec: description: IngressClassName defines the name if the ingress class to be used for flagd type: string + ofrepPath: + description: OFREPPath is the path to be used for accessing the + OFREP API + type: string + pathType: + description: PathType is the path type to be used for the ingress + rules + type: string + syncPath: + description: SyncPath is the path to be used for accessing the + sync API + type: string tls: description: TLS configuration for the ingress items: @@ -131,6 +106,10 @@ spec: type: string type: object type: array + required: + - flagdPath + - ofrepPath + - syncPath type: object otelCollectorUri: description: OtelCollectorUri defines the OpenTelemetry collector @@ -153,7 +132,7 @@ spec: Default: ClusterIP type: string required: - - featureFlagSourceRef + - featureFlagSource type: object status: description: FlagdStatus defines the observed state of Flagd diff --git a/controllers/core/flagd/resources/deployment_test.go b/controllers/core/flagd/resources/deployment_test.go index bc603c6cd..d886081ab 100644 --- a/controllers/core/flagd/resources/deployment_test.go +++ b/controllers/core/flagd/resources/deployment_test.go @@ -41,10 +41,7 @@ func TestFlagdDeployment_getFlagdDeployment(t *testing.T) { Namespace: "my-namespace", }, Spec: api.FlagdSpec{ - FeatureFlagSourceRef: v1.ObjectReference{ - Name: "my-flag-source", - Namespace: "my-namespace", - }, + FeatureFlagSource: "my-flag-source", }, } @@ -125,10 +122,7 @@ func TestFlagdDeployment_getFlagdDeployment_ErrorInInjector(t *testing.T) { Namespace: "my-namespace", }, Spec: api.FlagdSpec{ - FeatureFlagSourceRef: v1.ObjectReference{ - Name: "my-flag-source", - Namespace: "my-namespace", - }, + FeatureFlagSource: "my-flag-source", }, } @@ -172,10 +166,7 @@ func TestFlagdDeployment_getFlagdDeployment_ContainerNotInjected(t *testing.T) { Namespace: "my-namespace", }, Spec: api.FlagdSpec{ - FeatureFlagSourceRef: v1.ObjectReference{ - Name: "my-flag-source", - Namespace: "my-namespace", - }, + FeatureFlagSource: "my-flag-source", }, } @@ -219,10 +210,7 @@ func TestFlagdDeployment_getFlagdDeployment_FlagSourceNotFound(t *testing.T) { Namespace: "my-namespace", }, Spec: api.FlagdSpec{ - FeatureFlagSourceRef: v1.ObjectReference{ - Name: "my-flag-source", - Namespace: "my-namespace", - }, + FeatureFlagSource: "my-flag-source", }, } diff --git a/controllers/core/flagd/resources/ingress.go b/controllers/core/flagd/resources/ingress.go index 5fae24d8e..737a03d90 100644 --- a/controllers/core/flagd/resources/ingress.go +++ b/controllers/core/flagd/resources/ingress.go @@ -12,6 +12,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + defaultFlagdPath = "/flagd" + defaultOFREPPath = "/ofrep" + defaultSyncPath = "/sync" +) + type FlagdIngress struct { FlagdConfig resources.FlagdConfiguration } @@ -73,7 +79,7 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.Ingres HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { - Path: "/flagd", + Path: getFlagdPath(flagd.Spec.Ingress), PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ @@ -86,7 +92,7 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.Ingres }, }, { - Path: "/ofrep", + Path: getOFREPPath(flagd.Spec.Ingress), PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ @@ -99,7 +105,7 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.Ingres }, }, { - Path: "/sync", + Path: getSyncPath(flagd.Spec.Ingress), PathType: &pathType, Backend: networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ @@ -116,3 +122,27 @@ func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.Ingres }, } } + +func getFlagdPath(i api.IngressSpec) string { + path := defaultFlagdPath + if i.FlagdPath != "" { + path = i.FlagdPath + } + return path +} + +func getOFREPPath(i api.IngressSpec) string { + path := defaultOFREPPath + if i.OFREPPath != "" { + path = i.OFREPPath + } + return path +} + +func getSyncPath(i api.IngressSpec) string { + path := defaultSyncPath + if i.SyncPath != "" { + path = i.SyncPath + } + return path +} diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go index 89f4040ec..f30ea0263 100644 --- a/controllers/core/flagd/resources/ingress_test.go +++ b/controllers/core/flagd/resources/ingress_test.go @@ -246,3 +246,108 @@ func Test_areIngressesEqual(t *testing.T) { }) } } + +func Test_getFlagdPath(t *testing.T) { + type args struct { + i api.IngressSpec + } + tests := []struct { + name string + args args + want string + }{ + { + name: "default path", + args: args{ + i: api.IngressSpec{}, + }, + want: defaultFlagdPath, + }, + { + name: "custom path", + args: args{ + i: api.IngressSpec{ + FlagdPath: "my-path", + }, + }, + want: "my-path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getFlagdPath(tt.args.i); got != tt.want { + t.Errorf("getFlagdPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getOFREPPath(t *testing.T) { + type args struct { + i api.IngressSpec + } + tests := []struct { + name string + args args + want string + }{ + { + name: "default path", + args: args{ + i: api.IngressSpec{}, + }, + want: defaultOFREPPath, + }, + { + name: "custom path", + args: args{ + i: api.IngressSpec{ + OFREPPath: "my-path", + }, + }, + want: "my-path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getOFREPPath(tt.args.i); got != tt.want { + t.Errorf("getOFREPPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSyncPath(t *testing.T) { + type args struct { + i api.IngressSpec + } + tests := []struct { + name string + args args + want string + }{ + { + name: "default path", + args: args{ + i: api.IngressSpec{}, + }, + want: defaultSyncPath, + }, + { + name: "custom path", + args: args{ + i: api.IngressSpec{ + SyncPath: "my-path", + }, + }, + want: "my-path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getSyncPath(tt.args.i); got != tt.want { + t.Errorf("getSyncPath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/docs/crds.md b/docs/crds.md index 337c1aca4..d2ca17b00 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -927,6 +927,27 @@ Ingress + flagdPath + string + + FlagdPath is the path to be used for accessing the flagd flag evaluation API
+ + true + + ofrepPath + string + + OFREPPath is the path to be used for accessing the OFREP API
+ + true + + syncPath + string + + SyncPath is the path to be used for accessing the sync API
+ + true + annotations map[string]string @@ -954,6 +975,13 @@ Ingress IngressClassName defines the name if the ingress class to be used for flagd
false + + pathType + string + + PathType is the path type to be used for the ingress rules
+ + false tls []object diff --git a/go.mod b/go.mod index e8fba4c02..52ea7f34c 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,6 @@ require ( ) replace ( - github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3 + github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73 golang.org/x/net => golang.org/x/net v0.24.0 ) diff --git a/go.sum b/go.sum index 7fceb136d..0e4cdaa68 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3 h1:B1BTk30fAjlhOWzAKD+3AEULo7+YFwgXVoI20krMWuk= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508052334-8e7e48f72cd3/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73 h1:6KTcDLPxkaIgv5bLErruaazbffi44pjsisob3Dz1RJs= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= From 1f9b5e222824f5008973961f3e689a3ca24ebc6e Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 11:05:01 +0200 Subject: [PATCH 23/43] properties for customizing paths and pathtype for ingress rules Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 6 ++- .../bases/core.openfeature.dev_flagds.yaml | 4 -- controllers/core/flagd/resources/ingress.go | 3 ++ .../core/flagd/resources/ingress_test.go | 1 + docs/crds.md | 42 +++++++++---------- go.mod | 2 +- go.sum | 4 +- 7 files changed, 33 insertions(+), 29 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 90bae0d5c..60ed94019 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -76,15 +76,19 @@ type IngressSpec struct { IngressClassName *string `json:"ingressClassName,omitempty"` // PathType is the path type to be used for the ingress rules + // +optional PathType networkingv1.PathType `json:"pathType,omitempty"` // FlagdPath is the path to be used for accessing the flagd flag evaluation API - FlagdPath string `json:"flagdPath"` + // +optional + FlagdPath string `json:"flagdPath,omitempty"` // OFREPPath is the path to be used for accessing the OFREP API + // +optional OFREPPath string `json:"ofrepPath"` // SyncPath is the path to be used for accessing the sync API + // +optional SyncPath string `json:"syncPath"` } diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index 5c4954c6f..abf840cff 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -106,10 +106,6 @@ spec: type: string type: object type: array - required: - - flagdPath - - ofrepPath - - syncPath type: object otelCollectorUri: description: OtelCollectorUri defines the OpenTelemetry collector diff --git a/controllers/core/flagd/resources/ingress.go b/controllers/core/flagd/resources/ingress.go index 737a03d90..06984411f 100644 --- a/controllers/core/flagd/resources/ingress.go +++ b/controllers/core/flagd/resources/ingress.go @@ -73,6 +73,9 @@ func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule { func (r FlagdIngress) getRule(flagd *api.Flagd, host string) networkingv1.IngressRule { pathType := networkingv1.PathTypePrefix + if flagd.Spec.Ingress.PathType != "" { + pathType = flagd.Spec.Ingress.PathType + } return networkingv1.IngressRule{ Host: host, IngressRuleValue: networkingv1.IngressRuleValue{ diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go index f30ea0263..5a04d4f24 100644 --- a/controllers/core/flagd/resources/ingress_test.go +++ b/controllers/core/flagd/resources/ingress_test.go @@ -43,6 +43,7 @@ func TestFlagdIngress_getIngress(t *testing.T) { }, }, IngressClassName: strPtr("nginx"), + PathType: networkingv1.PathTypeImplementationSpecific, }, }, }) diff --git a/docs/crds.md b/docs/crds.md index d2ca17b00..4d0c15a5e 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -927,27 +927,6 @@ Ingress - flagdPath - string - - FlagdPath is the path to be used for accessing the flagd flag evaluation API
- - true - - ofrepPath - string - - OFREPPath is the path to be used for accessing the OFREP API
- - true - - syncPath - string - - SyncPath is the path to be used for accessing the sync API
- - true - annotations map[string]string @@ -961,6 +940,13 @@ Ingress Enabled enables/disables the ingress for flagd
false + + flagdPath + string + + FlagdPath is the path to be used for accessing the flagd flag evaluation API
+ + false hosts []string @@ -975,6 +961,13 @@ Ingress IngressClassName defines the name if the ingress class to be used for flagd
false + + ofrepPath + string + + OFREPPath is the path to be used for accessing the OFREP API
+ + false pathType string @@ -982,6 +975,13 @@ Ingress PathType is the path type to be used for the ingress rules
false + + syncPath + string + + SyncPath is the path to be used for accessing the sync API
+ + false tls []object diff --git a/go.mod b/go.mod index 52ea7f34c..f5330ae65 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,6 @@ require ( ) replace ( - github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73 + github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722 golang.org/x/net => golang.org/x/net v0.24.0 ) diff --git a/go.sum b/go.sum index 0e4cdaa68..e4fbdcb91 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73 h1:6KTcDLPxkaIgv5bLErruaazbffi44pjsisob3Dz1RJs= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508080533-2ee275df8e73/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722 h1:aQQIihlk5394gPicKj0GLCrxyuT6Kpa1sPjnfcuD1+8= +github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= From bc45b9a65805357273e63d8fa89b5d961b9c483f Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 11:27:05 +0200 Subject: [PATCH 24/43] add kuttl tests Signed-off-by: Florian Bacher --- .../00-assert.yaml | 120 ++++++++++++++++++ .../00-install.yaml | 43 +++++++ .../00-assert.yaml | 120 ++++++++++++++++++ .../00-install.yaml | 40 ++++++ 4 files changed, 323 insertions(+) create mode 100644 test/e2e/kuttl/flagd-with-ingress-custom-paths/00-assert.yaml create mode 100644 test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml create mode 100644 test/e2e/kuttl/flagd-with-ingress-default-paths/00-assert.yaml create mode 100644 test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml diff --git a/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-assert.yaml b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-assert.yaml new file mode 100644 index 000000000..867f5ca62 --- /dev/null +++ b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-assert.yaml @@ -0,0 +1,120 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + replicas: 2 + selector: + matchLabels: + app: flagd-sample + template: + metadata: + creationTimestamp: null + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + spec: + containers: + - name: flagd + ports: + - containerPort: 8014 + name: management + protocol: TCP + - containerPort: 8013 + name: flagd + protocol: TCP + - containerPort: 8016 + name: ofrep + protocol: TCP + - containerPort: 8015 + name: sync + protocol: TCP + serviceAccount: default + serviceAccountName: default +status: + readyReplicas: 2 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + ports: + - name: flagd + port: 8013 + protocol: TCP + targetPort: 8013 + - name: ofrep + port: 8016 + protocol: TCP + targetPort: 8016 + - name: sync + port: 8015 + protocol: TCP + targetPort: 8015 + - name: metrics + port: 8014 + protocol: TCP + targetPort: 8014 + selector: + app: flagd-sample + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + ingressClassName: nginx + rules: + - host: flagd-sample + http: + paths: + - backend: + service: + name: flagd-sample + port: + number: 8013 + path: /flagd(/|$)(.*) + pathType: ImplementationSpecific + - backend: + service: + name: flagd-sample + port: + number: 8016 + path: /ofrep(/|$)(.*) + pathType: ImplementationSpecific + - backend: + service: + name: flagd-sample + port: + number: 8015 + path: /sync(/|$)(.*) + pathType: ImplementationSpecific diff --git a/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml new file mode 100644 index 000000000..f5540d479 --- /dev/null +++ b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml @@ -0,0 +1,43 @@ +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag +metadata: + name: featureflag-sample +spec: + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" +--- +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource +metadata: + name: end-to-end +spec: + sources: + - source: featureflag-sample + provider: kubernetes +--- +apiVersion: core.openfeature.dev/v1beta1 +kind: Flagd +metadata: + name: flagd-sample +spec: + replicas: 2 + serviceType: ClusterIP + serviceAccountName: default + featureFlagSource: end-to-end + ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + hosts: + - flagd-sample + ingressClassName: nginx + pathType: ImplementationSpecific + flagdPath: /flagd(/|$)(.*) + ofrepPath: /ofrep(/|$)(.*) + syncPath: /sync(/|$)(.*) \ No newline at end of file diff --git a/test/e2e/kuttl/flagd-with-ingress-default-paths/00-assert.yaml b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-assert.yaml new file mode 100644 index 000000000..c9add36ea --- /dev/null +++ b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-assert.yaml @@ -0,0 +1,120 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + replicas: 2 + selector: + matchLabels: + app: flagd-sample + template: + metadata: + creationTimestamp: null + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + spec: + containers: + - name: flagd + ports: + - containerPort: 8014 + name: management + protocol: TCP + - containerPort: 8013 + name: flagd + protocol: TCP + - containerPort: 8016 + name: ofrep + protocol: TCP + - containerPort: 8015 + name: sync + protocol: TCP + serviceAccount: default + serviceAccountName: default +status: + readyReplicas: 2 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + ports: + - name: flagd + port: 8013 + protocol: TCP + targetPort: 8013 + - name: ofrep + port: 8016 + protocol: TCP + targetPort: 8016 + - name: sync + port: 8015 + protocol: TCP + targetPort: 8015 + - name: metrics + port: 8014 + protocol: TCP + targetPort: 8014 + selector: + app: flagd-sample + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + labels: + app: flagd-sample + app.kubernetes.io/managed-by: open-feature-operator + app.kubernetes.io/name: flagd-sample + name: flagd-sample + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + ownerReferences: + - apiVersion: core.openfeature.dev/v1beta1 + kind: Flagd + name: flagd-sample +spec: + ingressClassName: nginx + rules: + - host: flagd-sample + http: + paths: + - backend: + service: + name: flagd-sample + port: + number: 8013 + path: /flagd + pathType: ImplementationSpecific + - backend: + service: + name: flagd-sample + port: + number: 8016 + path: /ofrep + pathType: ImplementationSpecific + - backend: + service: + name: flagd-sample + port: + number: 8015 + path: /sync + pathType: ImplementationSpecific diff --git a/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml new file mode 100644 index 000000000..9a10d7fd6 --- /dev/null +++ b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml @@ -0,0 +1,40 @@ +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlag +metadata: + name: featureflag-sample +spec: + flagSpec: + flags: + "simple-flag": + state: "ENABLED" + variants: + "on": true + "off": false + defaultVariant: "on" +--- +apiVersion: core.openfeature.dev/v1beta1 +kind: FeatureFlagSource +metadata: + name: end-to-end +spec: + sources: + - source: featureflag-sample + provider: kubernetes +--- +apiVersion: core.openfeature.dev/v1beta1 +kind: Flagd +metadata: + name: flagd-sample +spec: + replicas: 2 + serviceType: ClusterIP + serviceAccountName: default + featureFlagSource: end-to-end + ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + hosts: + - flagd-sample + ingressClassName: nginx + pathType: ImplementationSpecific \ No newline at end of file From 66c30e02c349aebd1fbbee4f17c273d0e8664364 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 12:56:52 +0200 Subject: [PATCH 25/43] fix unit tests Signed-off-by: Florian Bacher --- controllers/core/flagd/controller_test.go | 4 +++- controllers/core/flagd/resources/ingress_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go index 6063fd104..cf81fb8ef 100644 --- a/controllers/core/flagd/controller_test.go +++ b/controllers/core/flagd/controller_test.go @@ -283,7 +283,9 @@ func TestFlagdReconciler_ReconcileFailIngress(t *testing.T) { Name: "my-flagd", Namespace: "my-namespace", }, - Spec: api.FlagdSpec{}, + Spec: api.FlagdSpec{ + Ingress: api.IngressSpec{Enabled: true}, + }, } fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(flagdObj).Build() diff --git a/controllers/core/flagd/resources/ingress_test.go b/controllers/core/flagd/resources/ingress_test.go index 5a04d4f24..c15e56e99 100644 --- a/controllers/core/flagd/resources/ingress_test.go +++ b/controllers/core/flagd/resources/ingress_test.go @@ -50,7 +50,7 @@ func TestFlagdIngress_getIngress(t *testing.T) { require.Nil(t, err) - pathType := networkingv1.PathTypePrefix + pathType := networkingv1.PathTypeImplementationSpecific require.NotNil(t, ingressResult) require.Equal(t, networkingv1.IngressSpec{ From 28651e8a18ddd1612a9d26601d92a1fa87d5f0ea Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 13:10:52 +0200 Subject: [PATCH 26/43] add description for new helm values Signed-off-by: Florian Bacher --- chart/open-feature-operator/values.yaml | 18 ++++++++++++++++++ common/types/envconfig.go | 2 +- config/overlays/helm/manager.yaml | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/chart/open-feature-operator/values.yaml b/chart/open-feature-operator/values.yaml index d2c36863d..cd221f40f 100644 --- a/chart/open-feature-operator/values.yaml +++ b/chart/open-feature-operator/values.yaml @@ -50,6 +50,24 @@ flagdProxyConfiguration: ## @param flagdProxyConfiguration.debugLogging Controls the addition of the `--debug` flag to the container startup arguments. debugLogging: false +## @section Flagd configuration +flagdConfiguration: + ## @param flagdConfiguration.port Sets the port to expose the flagd API on. + port: 8013 + ## @param flagdConfiguration.ofrepPort Sets the port to expose the ofrep API on. + ofrepPort: 8016 + ## @param flagdConfiguration.syncPort Sets the port to expose the sync API on. + syncPort: 8015 + ## @param flagdConfiguration.managementPort Sets the port to expose the management API on. + managementPort: 8014 + image: + ## @param flagdConfiguration.image.repository Sets the image for the flagd deployment. + repository: "ghcr.io/open-feature/flagd" + ## @param flagdConfiguration.image.tag Sets the tag for the flagd deployment. + tag: v0.10.1 + ## @param flagdConfiguration.debugLogging Controls the addition of the `--debug` flag to the container startup arguments. + debugLogging: false + ## @section Operator resource configuration controllerManager: kubeRbacProxy: diff --git a/common/types/envconfig.go b/common/types/envconfig.go index 2537abbb7..e3207fd25 100644 --- a/common/types/envconfig.go +++ b/common/types/envconfig.go @@ -12,7 +12,7 @@ type EnvConfig struct { FlagdImage string `envconfig:"FLAGD_IMAGE" default:"ghcr.io/open-feature/flagd"` // renovate: datasource=github-tags depName=open-feature/flagd/flagd - FlagdTag string `envconfig:"FLAGD_TAG" default:"v0.5.0"` + FlagdTag string `envconfig:"FLAGD_TAG" default:"v0.10.1"` FlagdPort int `envconfig:"FLAGD_PORT" default:"8013"` FlagdOFREPPort int `envconfig:"FLAGD_OFREP_PORT" default:"8016"` FlagdSyncPort int `envconfig:"FLAGD_SYNC_PORT" default:"8015"` diff --git a/config/overlays/helm/manager.yaml b/config/overlays/helm/manager.yaml index 9ff3b2a58..56c224a94 100644 --- a/config/overlays/helm/manager.yaml +++ b/config/overlays/helm/manager.yaml @@ -50,6 +50,20 @@ spec: value: "{{ .Values.flagdProxyConfiguration.managementPort }}" - name: FLAGD_PROXY_DEBUG_LOGGING value: "{{ .Values.flagdProxyConfiguration.debugLogging }}" + - name: FLAGD_IMAGE + value: "{{ .Values.flagdConfiguration.image.repository }}" + - name: FLAGD_TAG + value: "{{ .Values.flagdConfiguration.image.tag }}" + - name: FLAGD_PORT + value: "{{ .Values.flagdConfiguration.port }}" + - name: FLAGD_OFREP_PORT + value: "{{ .Values.flagdConfiguration.ofrepPort }}" + - name: FLAGD_SYNC_PORT + value: "{{ .Values.flagdConfiguration.syncPort }}" + - name: FLAGD_MANAGEMENT_PORT + value: "{{ .Values.flagdConfiguration.managementPort }}" + - name: FLAGD_DEBUG_LOGGING + value: "{{ .Values.flagdConfiguration.debugLogging }}" - name: FLAGS_VALIDATION_ENABLED value: "{{ .Values.managerConfig.flagsValidatonEnabled }}" - name: kube-rbac-proxy From 75512ecd7993b92feda210ea20506bbed584baaf Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 13:33:54 +0200 Subject: [PATCH 27/43] add helm value to enable/disable the flagd resource support Signed-off-by: Florian Bacher --- chart/open-feature-operator/README.md | 51 ++++++++++------- chart/open-feature-operator/values.yaml | 2 + common/types/envconfig.go | 1 + config/overlays/helm/manager.yaml | 2 + main.go | 56 ++++++++++--------- .../00-install.yaml | 2 +- .../00-install.yaml | 2 +- 7 files changed, 68 insertions(+), 48 deletions(-) diff --git a/chart/open-feature-operator/README.md b/chart/open-feature-operator/README.md index 5244d2152..f4e7c7a84 100644 --- a/chart/open-feature-operator/README.md +++ b/chart/open-feature-operator/README.md @@ -126,24 +126,37 @@ The command removes all the Kubernetes components associated with the chart and | `flagdProxyConfiguration.image.tag` | Sets the tag for the flagd-proxy deployment. | `v0.5.0` | | `flagdProxyConfiguration.debugLogging` | Controls the addition of the `--debug` flag to the container startup arguments. | `false` | +### Flagd configuration + +| Name | Description | Value | +| ------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------- | +| `flagdConfiguration.port` | Sets the port to expose the flagd API on. | `8013` | +| `flagdConfiguration.ofrepPort` | Sets the port to expose the ofrep API on. | `8016` | +| `flagdConfiguration.syncPort` | Sets the port to expose the sync API on. | `8015` | +| `flagdConfiguration.managementPort` | Sets the port to expose the management API on. | `8014` | +| `flagdConfiguration.image.repository` | Sets the image for the flagd deployment. | `ghcr.io/open-feature/flagd` | +| `flagdConfiguration.image.tag` | Sets the tag for the flagd deployment. | `v0.10.1` | +| `flagdConfiguration.debugLogging` | Controls the addition of the `--debug` flag to the container startup arguments. | `false` | + ### Operator resource configuration -| Name | Description | Value | -| ------------------------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------- | -| `controllerManager.kubeRbacProxy.image.repository` | Sets the image for the kube-rbac-proxy. | `gcr.io/kubebuilder/kube-rbac-proxy` | -| `controllerManager.kubeRbacProxy.image.tag` | Sets the version tag for the kube-rbac-proxy. | `v0.14.1` | -| `controllerManager.kubeRbacProxy.resources.limits.cpu` | Sets cpu resource limits for kube-rbac-proxy. | `500m` | -| `controllerManager.kubeRbacProxy.resources.limits.memory` | Sets memory resource limits for kube-rbac-proxy. | `128Mi` | -| `controllerManager.kubeRbacProxy.resources.requests.cpu` | Sets cpu resource requests for kube-rbac-proxy. | `5m` | -| `controllerManager.kubeRbacProxy.resources.requests.memory` | Sets memory resource requests for kube-rbac-proxy. | `64Mi` | -| `controllerManager.manager.image.repository` | Sets the image for the operator. | `ghcr.io/open-feature/open-feature-operator` | -| `controllerManager.manager.image.tag` | Sets the version tag for the operator. | `v0.5.4` | -| `controllerManager.manager.resources.limits.cpu` | Sets cpu resource limits for operator. | `500m` | -| `controllerManager.manager.resources.limits.memory` | Sets memory resource limits for operator. | `128Mi` | -| `controllerManager.manager.resources.requests.cpu` | Sets cpu resource requests for operator. | `10m` | -| `controllerManager.manager.resources.requests.memory` | Sets memory resource requests for operator. | `64Mi` | -| `controllerManager.replicas` | Sets number of replicas of the OpenFeature operator pod. | `1` | -| `managerConfig.flagsValidatonEnabled` | Enables the validating webhook for FeatureFlag CR. | `true` | -| `managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress` | Sets the bind address for health probes. | `:8081` | -| `managerConfig.controllerManagerConfigYaml.metrics.bindAddress` | Sets the bind address for metrics. | `127.0.0.1:8080` | -| `managerConfig.controllerManagerConfigYaml.webhook.port` | Sets the bind address for webhook. | `9443` | +| Name | Description | Value | +| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| `controllerManager.kubeRbacProxy.image.repository` | Sets the image for the kube-rbac-proxy. | `gcr.io/kubebuilder/kube-rbac-proxy` | +| `controllerManager.kubeRbacProxy.image.tag` | Sets the version tag for the kube-rbac-proxy. | `v0.14.1` | +| `controllerManager.kubeRbacProxy.resources.limits.cpu` | Sets cpu resource limits for kube-rbac-proxy. | `500m` | +| `controllerManager.kubeRbacProxy.resources.limits.memory` | Sets memory resource limits for kube-rbac-proxy. | `128Mi` | +| `controllerManager.kubeRbacProxy.resources.requests.cpu` | Sets cpu resource requests for kube-rbac-proxy. | `5m` | +| `controllerManager.kubeRbacProxy.resources.requests.memory` | Sets memory resource requests for kube-rbac-proxy. | `64Mi` | +| `controllerManager.manager.image.repository` | Sets the image for the operator. | `ghcr.io/open-feature/open-feature-operator` | +| `controllerManager.manager.image.tag` | Sets the version tag for the operator. | `v0.5.4` | +| `controllerManager.manager.resources.limits.cpu` | Sets cpu resource limits for operator. | `500m` | +| `controllerManager.manager.resources.limits.memory` | Sets memory resource limits for operator. | `128Mi` | +| `controllerManager.manager.resources.requests.cpu` | Sets cpu resource requests for operator. | `10m` | +| `controllerManager.manager.resources.requests.memory` | Sets memory resource requests for operator. | `64Mi` | +| `controllerManager.replicas` | Sets number of replicas of the OpenFeature operator pod. | `1` | +| `managerConfig.flagsValidatonEnabled` | Enables the validating webhook for FeatureFlag CR. | `true` | +| `managerConfig.flagdResourceEnabled` | Enables the controller for the Flagd CR and adds the required permissions to create/update/delete Service and Ingress resources. | `true` | +| `managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress` | Sets the bind address for health probes. | `:8081` | +| `managerConfig.controllerManagerConfigYaml.metrics.bindAddress` | Sets the bind address for metrics. | `127.0.0.1:8080` | +| `managerConfig.controllerManagerConfigYaml.webhook.port` | Sets the bind address for webhook. | `9443` | diff --git a/chart/open-feature-operator/values.yaml b/chart/open-feature-operator/values.yaml index cd221f40f..714fe5b12 100644 --- a/chart/open-feature-operator/values.yaml +++ b/chart/open-feature-operator/values.yaml @@ -110,6 +110,8 @@ controllerManager: managerConfig: ## @param managerConfig.flagsValidatonEnabled Enables the validating webhook for FeatureFlag CR. flagsValidatonEnabled: "true" + ## @param managerConfig.flagdResourceEnabled Enables the controller for the Flagd CR and adds the required permissions to create/update/delete Service and Ingress resources. + flagdResourceEnabled: "true" controllerManagerConfigYaml: health: ## @param managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress Sets the bind address for health probes. diff --git a/common/types/envconfig.go b/common/types/envconfig.go index e3207fd25..8b9c0c3dc 100644 --- a/common/types/envconfig.go +++ b/common/types/envconfig.go @@ -31,4 +31,5 @@ type EnvConfig struct { SidecarSyncProvider string `envconfig:"SIDECAR_SYNC_PROVIDER" default:"kubernetes"` SidecarLogFormat string `envconfig:"SIDECAR_LOG_FORMAT" default:"json"` SidecarProbesEnabled bool `envconfig:"SIDECAR_PROBES_ENABLED" default:"true"` + FlagdResourceEnabled bool `envconfig:"FLAGD_RESOURCE_ENABLED" default:"true"` } diff --git a/config/overlays/helm/manager.yaml b/config/overlays/helm/manager.yaml index 56c224a94..18eec907a 100644 --- a/config/overlays/helm/manager.yaml +++ b/config/overlays/helm/manager.yaml @@ -66,6 +66,8 @@ spec: value: "{{ .Values.flagdConfiguration.debugLogging }}" - name: FLAGS_VALIDATION_ENABLED value: "{{ .Values.managerConfig.flagsValidatonEnabled }}" + - name: FLAGD_RESOURCE_ENABLED + value: "{{ .Values.managerConfig.flagdResourceEnabled }}" - name: kube-rbac-proxy image: "{{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag }}" resources: diff --git a/main.go b/main.go index 0dcb2ef49..48b739689 100644 --- a/main.go +++ b/main.go @@ -192,34 +192,36 @@ func main() { Tag: env.SidecarTag, } - flagdControllerLogger := ctrl.Log.WithName("Flagd Controller") + if env.FlagdResourceEnabled { + flagdControllerLogger := ctrl.Log.WithName("Flagd Controller") - flagdResourceReconciler := &flagd.ResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: flagdControllerLogger, - } - flagdConfig := flagd.NewFlagdConfiguration(env) - - if err = (&flagd.FlagdReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - ResourceReconciler: flagdResourceReconciler, - FlagdDeployment: &flagdresources.FlagdDeployment{ - Client: mgr.GetClient(), - Log: flagdControllerLogger, - FlagdInjector: flagdContainerInjector, - FlagdConfig: flagdConfig, - }, - FlagdService: &flagdresources.FlagdService{ - FlagdConfig: flagdConfig, - }, - FlagdIngress: &flagdresources.FlagdIngress{ - FlagdConfig: flagdConfig, - }, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Flagd") - os.Exit(1) + flagdResourceReconciler := &flagd.ResourceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: flagdControllerLogger, + } + flagdConfig := flagd.NewFlagdConfiguration(env) + + if err = (&flagd.FlagdReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ResourceReconciler: flagdResourceReconciler, + FlagdDeployment: &flagdresources.FlagdDeployment{ + Client: mgr.GetClient(), + Log: flagdControllerLogger, + FlagdInjector: flagdContainerInjector, + FlagdConfig: flagdConfig, + }, + FlagdService: &flagdresources.FlagdService{ + FlagdConfig: flagdConfig, + }, + FlagdIngress: &flagdresources.FlagdIngress{ + FlagdConfig: flagdConfig, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Flagd") + os.Exit(1) + } } if env.FlagsValidationEnabled { diff --git a/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml index f5540d479..9f5c0d5b0 100644 --- a/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml +++ b/test/e2e/kuttl/flagd-with-ingress-custom-paths/00-install.yaml @@ -40,4 +40,4 @@ spec: pathType: ImplementationSpecific flagdPath: /flagd(/|$)(.*) ofrepPath: /ofrep(/|$)(.*) - syncPath: /sync(/|$)(.*) \ No newline at end of file + syncPath: /sync(/|$)(.*) diff --git a/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml index 9a10d7fd6..2ad633a95 100644 --- a/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml +++ b/test/e2e/kuttl/flagd-with-ingress-default-paths/00-install.yaml @@ -37,4 +37,4 @@ spec: hosts: - flagd-sample ingressClassName: nginx - pathType: ImplementationSpecific \ No newline at end of file + pathType: ImplementationSpecific From 5c2bae272b3cb857ce6f35ea13b0b90333406c5e Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 13:40:05 +0200 Subject: [PATCH 28/43] do not grant additional permissions if flagd resource is disabled Signed-off-by: Florian Bacher --- config/overlays/helm/role.yaml | 147 +++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 config/overlays/helm/role.yaml diff --git a/config/overlays/helm/role.yaml b/config/overlays/helm/role.yaml new file mode 100644 index 000000000..acca8b93b --- /dev/null +++ b/config/overlays/helm/role.yaml @@ -0,0 +1,147 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - services + verbs: + - create + - get + - list + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - core.openfeature.dev + resources: + - featureflagsources + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - core.openfeature.dev + resources: + - featureflagsources/finalizers + verbs: + - get + - update + - apiGroups: + - core.openfeature.dev + resources: + - featureflagsources/status + verbs: + - get + - patch + - update + - apiGroups: + - core.openfeature.dev + resources: + - flagds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - core.openfeature.dev + resources: + - flagds/finalizers + verbs: + - update + - apiGroups: + - core.openfeature.dev + resources: + - flagds/status + verbs: + - get + - patch + - update + {{ if eq .Values.managerConfig.flagdResourceEnabled "true" }} + - apiGroups: + - "" + resources: + - services + - services/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + {{ end }} + - apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - open-feature-operator-flagd-kubernetes-sync + resources: + - clusterrolebindings + verbs: + - get + - update From 8af732e3f5489d2fdac0a1f44c2de4cbc9d4c0c4 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 13:56:21 +0200 Subject: [PATCH 29/43] do not grant additional permissions if flagd resource is disabled Signed-off-by: Florian Bacher --- chart/open-feature-operator/.gitignore | 1 + ...le_open-feature-operator-manager-role.yaml | 70 +++++++++---------- config/overlays/helm/exclude-role.yaml | 5 ++ config/overlays/helm/kustomization.yaml | 1 + 4 files changed, 42 insertions(+), 35 deletions(-) rename config/overlays/helm/role.yaml => chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml (74%) create mode 100644 config/overlays/helm/exclude-role.yaml diff --git a/chart/open-feature-operator/.gitignore b/chart/open-feature-operator/.gitignore index 05f3770e9..3b9b2639d 100755 --- a/chart/open-feature-operator/.gitignore +++ b/chart/open-feature-operator/.gitignore @@ -4,3 +4,4 @@ templates/crds/*.yaml # the following files are not generated, they are special cases !templates/namespace.yaml !templates/admissionregistration.k8s.io_v1_validatingwebhookconfiguration_open-feature-operator-validating-webhook-configuration.yaml +!templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml diff --git a/config/overlays/helm/role.yaml b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml similarity index 74% rename from config/overlays/helm/role.yaml rename to chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml index acca8b93b..7668c22ef 100644 --- a/config/overlays/helm/role.yaml +++ b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml @@ -2,7 +2,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: manager-role + name: open-feature-operator-manager-role rules: - apiGroups: - "" @@ -110,38 +110,38 @@ rules: - patch - update {{ if eq .Values.managerConfig.flagdResourceEnabled "true" }} - - apiGroups: - - "" - resources: - - services - - services/finalizers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch +- apiGroups: + - "" + resources: + - services + - services/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch {{ end }} - - apiGroups: - - rbac.authorization.k8s.io - resourceNames: - - open-feature-operator-flagd-kubernetes-sync - resources: - - clusterrolebindings - verbs: - - get - - update +- apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - open-feature-operator-flagd-kubernetes-sync + resources: + - clusterrolebindings + verbs: + - get + - update diff --git a/config/overlays/helm/exclude-role.yaml b/config/overlays/helm/exclude-role.yaml new file mode 100644 index 000000000..03ac94b0a --- /dev/null +++ b/config/overlays/helm/exclude-role.yaml @@ -0,0 +1,5 @@ +$patch: delete +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role diff --git a/config/overlays/helm/kustomization.yaml b/config/overlays/helm/kustomization.yaml index 5b2441662..16f85e647 100644 --- a/config/overlays/helm/kustomization.yaml +++ b/config/overlays/helm/kustomization.yaml @@ -15,6 +15,7 @@ patchesStrategicMerge: - exclude-ns.yaml - manager.yaml - exclude-validatingwebhook.yaml + - exclude-role.yaml configMapGenerator: - name: manager-config From 11cf4d462623e3d8d9ace896eb403e9bdca3834a Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 14:03:21 +0200 Subject: [PATCH 30/43] fix yaml Signed-off-by: Florian Bacher --- ...le_open-feature-operator-manager-role.yaml | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml index 7668c22ef..c510a5c10 100644 --- a/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml +++ b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml @@ -109,39 +109,39 @@ rules: - get - patch - update - {{ if eq .Values.managerConfig.flagdResourceEnabled "true" }} -- apiGroups: +{{ if eq .Values.managerConfig.flagdResourceEnabled "true" }} + - apiGroups: - "" - resources: - - services - - services/finalizers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: + resources: + - services + - services/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: - networking.k8s.io - resources: - - ingresses - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - {{ end }} -- apiGroups: + resources: + - ingresses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +{{ end }} + - apiGroups: - rbac.authorization.k8s.io - resourceNames: - - open-feature-operator-flagd-kubernetes-sync - resources: - - clusterrolebindings - verbs: - - get - - update + resourceNames: + - open-feature-operator-flagd-kubernetes-sync + resources: + - clusterrolebindings + verbs: + - get + - update From aae9d2561015b167ebc37f8af789a33179f70202 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 14:12:49 +0200 Subject: [PATCH 31/43] update sample Signed-off-by: Florian Bacher --- config/samples/core_v1beta1_flagd.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/samples/core_v1beta1_flagd.yaml b/config/samples/core_v1beta1_flagd.yaml index 592ed6fbe..1a849ee68 100644 --- a/config/samples/core_v1beta1_flagd.yaml +++ b/config/samples/core_v1beta1_flagd.yaml @@ -9,15 +9,19 @@ metadata: app.kubernetes.io/created-by: open-feature-operator name: flagd-sample spec: - replicas: 1 + replicas: 2 serviceType: ClusterIP serviceAccountName: default - featureFlagSourceRef: - name: end-to-end - namespace: default + featureFlagSource: end-to-end ingress: enabled: true + annotations: + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" hosts: - flagd-sample ingressClassName: nginx + pathType: ImplementationSpecific + flagdPath: /flagd(/|$)(.*) + ofrepPath: /ofrep(/|$)(.*) + syncPath: /sync(/|$)(.*) From 59f0d1171b40844d52faad5ce8705fa50bef9ebc Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Wed, 8 May 2024 14:13:56 +0200 Subject: [PATCH 32/43] update mockgen commands Signed-off-by: Florian Bacher --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 2f6734632..d72899de5 100644 --- a/Makefile +++ b/Makefile @@ -257,3 +257,4 @@ install-mockgen: mockgen: install-mockgen mockgen -source=./common/flagdinjector/flagdinjector.go -destination=./common/flagdinjector/mock/flagd-injector.go -package=commonmock mockgen -source=./controllers/core/flagd/controller.go -destination=controllers/core/flagd/mock/mock.go -package=commonmock + mockgen -source=./controllers/core/flagd/resources/interface.go -destination=controllers/core/flagd/resources/mock/mock.go -package=commonmock From faeb15653feb1978f41fd1671e49578430ca5e52 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 11:26:58 +0200 Subject: [PATCH 33/43] remove autogenerated placeholder comments Co-authored-by: odubajDT <93584209+odubajDT@users.noreply.github.com> Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 60ed94019..e414c1fde 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -22,9 +22,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// 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. - // FlagdSpec defines the desired state of Flagd type FlagdSpec struct { // Replicas defines the number of replicas to create for the service From b7a6238351f92d8c65c0f76101a826ca8ef89c0e Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 11:29:06 +0200 Subject: [PATCH 34/43] revert out of scope change Signed-off-by: Florian Bacher --- .../core_v1beta1_featureflagsource.yaml | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/config/samples/core_v1beta1_featureflagsource.yaml b/config/samples/core_v1beta1_featureflagsource.yaml index 24c42d04f..c1651528a 100644 --- a/config/samples/core_v1beta1_featureflagsource.yaml +++ b/config/samples/core_v1beta1_featureflagsource.yaml @@ -1,22 +1,13 @@ apiVersion: core.openfeature.dev/v1beta1 -kind: FeatureFlag -metadata: - name: featureflag-sample -spec: - flagSpec: - flags: - "simple-flag": - state: "ENABLED" - variants: - "on": true - "off": false - defaultVariant: "on" ---- -apiVersion: core.openfeature.dev/v1beta1 kind: FeatureFlagSource metadata: - name: end-to-end + name: featureflagsource-sample spec: + managementPort: 8080 + evaluator: json + defaultSyncProvider: file + tag: latest sources: - - source: featureflag-sample - provider: kubernetes + - source: end-to-end-test + provider: file + probesEnabled: true From 9d68103a0ec338cdd73e7e78415c4c3d1d65d880 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 14:15:21 +0200 Subject: [PATCH 35/43] revert out of scope change Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 1 + ...v1_clusterrole_open-feature-operator-manager-role.yaml | 8 -------- config/crd/bases/core.openfeature.dev_flagds.yaml | 5 +++++ config/rbac/role.yaml | 8 -------- config/samples/core_v1beta1_flagd.yaml | 1 - controllers/core/flagd/controller.go | 1 - controllers/core/flagd/resource.go | 6 ++++-- 7 files changed, 10 insertions(+), 20 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index e414c1fde..6b2afb64b 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -33,6 +33,7 @@ type FlagdSpec struct { // Default: ClusterIP // +optional // +kubebuilder:default=ClusterIP + // +kubebuilder:validation:Enum:=ClusterIP;NodePort;LoadBalancer;ExternalName ServiceType v1.ServiceType `json:"serviceType,omitempty"` // ServiceAccountName the service account name for the flagd deployment diff --git a/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml index c510a5c10..b56e223f2 100644 --- a/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml +++ b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml @@ -101,14 +101,6 @@ rules: - flagds/finalizers verbs: - update - - apiGroups: - - core.openfeature.dev - resources: - - flagds/status - verbs: - - get - - patch - - update {{ if eq .Values.managerConfig.flagdResourceEnabled "true" }} - apiGroups: - "" diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index abf840cff..72ed656d7 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -126,6 +126,11 @@ spec: ServiceType represents the type of Service to create. Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName. Default: ClusterIP + enum: + - ClusterIP + - NodePort + - LoadBalancer + - ExternalName type: string required: - featureFlagSource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a632c9034..d577ca5f0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -114,14 +114,6 @@ rules: - flagds/finalizers verbs: - update -- apiGroups: - - core.openfeature.dev - resources: - - flagds/status - verbs: - - get - - patch - - update - apiGroups: - networking.k8s.io resources: diff --git a/config/samples/core_v1beta1_flagd.yaml b/config/samples/core_v1beta1_flagd.yaml index 1a849ee68..92c1b3a02 100644 --- a/config/samples/core_v1beta1_flagd.yaml +++ b/config/samples/core_v1beta1_flagd.yaml @@ -24,4 +24,3 @@ spec: flagdPath: /flagd(/|$)(.*) ofrepPath: /ofrep(/|$)(.*) syncPath: /sync(/|$)(.*) - diff --git a/controllers/core/flagd/controller.go b/controllers/core/flagd/controller.go index b19a20590..bc1a30f56 100644 --- a/controllers/core/flagd/controller.go +++ b/controllers/core/flagd/controller.go @@ -53,7 +53,6 @@ type IFlagdResourceReconciler interface { } //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/status,verbs=get;update;patch //+kubebuilder:rbac:groups=core.openfeature.dev,resources=flagds/finalizers,verbs=update //+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=services;services/finalizers,verbs=get;list;watch;create;update;patch;delete diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index f61e41dfe..72175b4ab 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -47,10 +47,12 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob return err } - if exists && !resource.AreObjectsEqual(existingObj, newObj) { + if !exists { + return r.createResource(ctx, flagd, obj, newObj) + } else if exists && !resource.AreObjectsEqual(existingObj, newObj) { return r.updateResource(ctx, flagd, obj, newObj) } - return r.createResource(ctx, flagd, obj, newObj) + return nil } func (r *ResourceReconciler) createResource(ctx context.Context, flagd *api.Flagd, obj client.Object, newObj client.Object) error { From 6cfdcd44697fdf20a72b7b3fed1132edd8c99637 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 14:27:19 +0200 Subject: [PATCH 36/43] update crd docs Signed-off-by: Florian Bacher --- docs/crds.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/crds.md b/docs/crds.md index 4d0c15a5e..c7eb64fd9 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -897,12 +897,13 @@ the feature flag configurations
false serviceType - string + enum ServiceType represents the type of Service to create. Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName. Default: ClusterIP

+ Enum: ClusterIP, NodePort, LoadBalancer, ExternalName
Default: ClusterIP
false From de745985b6d8d6b2b08f57a6ca58142c4779761f Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 14:31:17 +0200 Subject: [PATCH 37/43] simplify condition Signed-off-by: Florian Bacher --- controllers/core/flagd/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource.go index 72175b4ab..fcb11c938 100644 --- a/controllers/core/flagd/resource.go +++ b/controllers/core/flagd/resource.go @@ -49,7 +49,7 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, flagd *api.Flagd, ob if !exists { return r.createResource(ctx, flagd, obj, newObj) - } else if exists && !resource.AreObjectsEqual(existingObj, newObj) { + } else if !resource.AreObjectsEqual(existingObj, newObj) { return r.updateResource(ctx, flagd, obj, newObj) } return nil From b58261976d03059fe7b214244eb8087519cbbc7c Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Mon, 13 May 2024 15:42:20 +0200 Subject: [PATCH 38/43] Update apis/core/v1beta1/flagd_types.go Co-authored-by: odubajDT <93584209+odubajDT@users.noreply.github.com> Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index 6b2afb64b..be8f57e3c 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -92,8 +92,6 @@ type IngressSpec struct { // FlagdStatus defines the observed state of Flagd type FlagdStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } //+kubebuilder:object:root=true From 8291d51e5dd4ca49a8c01543737a85bf054de19b Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 14 May 2024 07:29:17 +0200 Subject: [PATCH 39/43] adapt to PR review comments Signed-off-by: Florian Bacher --- apis/core/v1beta1/flagd_types.go | 8 +++----- chart/open-feature-operator/values.yaml | 2 +- common/flagdproxy/flagdproxy.go | 4 ++-- config/crd/bases/core.openfeature.dev_flagds.yaml | 10 ++++------ .../core/flagd/{resource.go => resource_reconciler.go} | 0 .../{resource_test.go => resource_reconciler_test.go} | 0 6 files changed, 10 insertions(+), 14 deletions(-) rename controllers/core/flagd/{resource.go => resource_reconciler.go} (100%) rename controllers/core/flagd/{resource_test.go => resource_reconciler_test.go} (100%) diff --git a/apis/core/v1beta1/flagd_types.go b/apis/core/v1beta1/flagd_types.go index be8f57e3c..bd1909fd4 100644 --- a/apis/core/v1beta1/flagd_types.go +++ b/apis/core/v1beta1/flagd_types.go @@ -24,8 +24,10 @@ import ( // FlagdSpec defines the desired state of Flagd type FlagdSpec struct { - // Replicas defines the number of replicas to create for the service + // Replicas defines the number of replicas to create for the service. + // Default: 1 // +optional + // +kubebuilder:default=1 Replicas *int32 `json:"replicas,omitempty"` // ServiceType represents the type of Service to create. @@ -40,10 +42,6 @@ type FlagdSpec struct { // +optional ServiceAccountName string `json:"serviceAccountName,omitempty"` - // OtelCollectorUri defines the OpenTelemetry collector URI to enable OpenTelemetry Tracing in flagd. - // +optional - OtelCollectorUri string `json:"otelCollectorUri"` - // FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves // the feature flag configurations FeatureFlagSource string `json:"featureFlagSource"` diff --git a/chart/open-feature-operator/values.yaml b/chart/open-feature-operator/values.yaml index 714fe5b12..56c46dfcb 100644 --- a/chart/open-feature-operator/values.yaml +++ b/chart/open-feature-operator/values.yaml @@ -110,7 +110,7 @@ controllerManager: managerConfig: ## @param managerConfig.flagsValidatonEnabled Enables the validating webhook for FeatureFlag CR. flagsValidatonEnabled: "true" - ## @param managerConfig.flagdResourceEnabled Enables the controller for the Flagd CR and adds the required permissions to create/update/delete Service and Ingress resources. + ## @param managerConfig.flagdResourceEnabled Enables the controller for the Flagd CR and adds the required permissions to automatically manage the exposure of flagd via Service and Ingress resources. flagdResourceEnabled: "true" controllerManagerConfigYaml: health: diff --git a/common/flagdproxy/flagdproxy.go b/common/flagdproxy/flagdproxy.go index 4de163623..f410b5c0b 100644 --- a/common/flagdproxy/flagdproxy.go +++ b/common/flagdproxy/flagdproxy.go @@ -119,8 +119,8 @@ func (f *FlagdProxyHandler) newFlagdProxyServiceManifest(ownerReference *metav1. }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ - "app.kubernetes.io/name": FlagdProxyDeploymentName, - "app.kubernetes.io/managed-by": common.ManagedByAnnotationValue, + "app.kubernetes.io/name": FlagdProxyDeploymentName, + common.ManagedByAnnotationKey: common.ManagedByAnnotationValue, }, Ports: []corev1.ServicePort{ { diff --git a/config/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml index 72ed656d7..9cb1154aa 100644 --- a/config/crd/bases/core.openfeature.dev_flagds.yaml +++ b/config/crd/bases/core.openfeature.dev_flagds.yaml @@ -107,13 +107,11 @@ spec: type: object type: array type: object - otelCollectorUri: - description: OtelCollectorUri defines the OpenTelemetry collector - URI to enable OpenTelemetry Tracing in flagd. - type: string replicas: - description: Replicas defines the number of replicas to create for - the service + default: 1 + description: |- + Replicas defines the number of replicas to create for the service. + Default: 1 format: int32 type: integer serviceAccountName: diff --git a/controllers/core/flagd/resource.go b/controllers/core/flagd/resource_reconciler.go similarity index 100% rename from controllers/core/flagd/resource.go rename to controllers/core/flagd/resource_reconciler.go diff --git a/controllers/core/flagd/resource_test.go b/controllers/core/flagd/resource_reconciler_test.go similarity index 100% rename from controllers/core/flagd/resource_test.go rename to controllers/core/flagd/resource_reconciler_test.go From 2d2ae2a2149ddb07ac1f20418818282ef3ac6f09 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 14 May 2024 07:32:56 +0200 Subject: [PATCH 40/43] remove replace directive Signed-off-by: Florian Bacher --- go.mod | 5 +---- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index f5330ae65..26ea48179 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,4 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) -replace ( - github.com/open-feature/open-feature-operator/apis => github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722 - golang.org/x/net => golang.org/x/net v0.24.0 -) +replace golang.org/x/net => golang.org/x/net v0.24.0 diff --git a/go.sum b/go.sum index e4fbdcb91..905d32ac7 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722 h1:aQQIihlk5394gPicKj0GLCrxyuT6Kpa1sPjnfcuD1+8= -github.com/bacherfl/open-feature-operator/apis v0.0.0-20240508085321-08cc258ed722/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -227,6 +225,8 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7 h1:oP+BH8RiNEmSWTffKEXz2ciwen7wbvyX0fESx0aoJ80= github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7/go.mod h1:WKtwo1eW9/K6D+4HfgTXWBqCDzpvMhDa5eRxW7R5B2U= +github.com/open-feature/open-feature-operator/apis v0.2.41-0.20240506125212-c4831a3cdc00 h1:EsG43eil7L+4c6BXMGPMRm9qYVz0J1+sjukz5xw+vTM= +github.com/open-feature/open-feature-operator/apis v0.2.41-0.20240506125212-c4831a3cdc00/go.mod h1:BLFkIDsj+coW30XxILSY2X++vVniRTyWozzONuVKh5U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From ba7722a05607c2784dd5d19ef2ce00069d60e9e3 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 14 May 2024 07:34:03 +0200 Subject: [PATCH 41/43] update CRD docs Signed-off-by: Florian Bacher --- docs/crds.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/crds.md b/docs/crds.md index c7eb64fd9..360da228a 100644 --- a/docs/crds.md +++ b/docs/crds.md @@ -872,20 +872,15 @@ the feature flag configurations
Ingress
false - - otelCollectorUri - string - - OtelCollectorUri defines the OpenTelemetry collector URI to enable OpenTelemetry Tracing in flagd.
- - false replicas integer - Replicas defines the number of replicas to create for the service
+ Replicas defines the number of replicas to create for the service. +Default: 1

Format: int32
+ Default: 1
false From 49bc4a075db9c7895790197f1b5f27b05a9ea27a Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 14 May 2024 07:39:01 +0200 Subject: [PATCH 42/43] update helm docs Signed-off-by: Florian Bacher --- chart/open-feature-operator/README.md | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/chart/open-feature-operator/README.md b/chart/open-feature-operator/README.md index f4e7c7a84..b975df0b6 100644 --- a/chart/open-feature-operator/README.md +++ b/chart/open-feature-operator/README.md @@ -140,23 +140,23 @@ The command removes all the Kubernetes components associated with the chart and ### Operator resource configuration -| Name | Description | Value | -| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -| `controllerManager.kubeRbacProxy.image.repository` | Sets the image for the kube-rbac-proxy. | `gcr.io/kubebuilder/kube-rbac-proxy` | -| `controllerManager.kubeRbacProxy.image.tag` | Sets the version tag for the kube-rbac-proxy. | `v0.14.1` | -| `controllerManager.kubeRbacProxy.resources.limits.cpu` | Sets cpu resource limits for kube-rbac-proxy. | `500m` | -| `controllerManager.kubeRbacProxy.resources.limits.memory` | Sets memory resource limits for kube-rbac-proxy. | `128Mi` | -| `controllerManager.kubeRbacProxy.resources.requests.cpu` | Sets cpu resource requests for kube-rbac-proxy. | `5m` | -| `controllerManager.kubeRbacProxy.resources.requests.memory` | Sets memory resource requests for kube-rbac-proxy. | `64Mi` | -| `controllerManager.manager.image.repository` | Sets the image for the operator. | `ghcr.io/open-feature/open-feature-operator` | -| `controllerManager.manager.image.tag` | Sets the version tag for the operator. | `v0.5.4` | -| `controllerManager.manager.resources.limits.cpu` | Sets cpu resource limits for operator. | `500m` | -| `controllerManager.manager.resources.limits.memory` | Sets memory resource limits for operator. | `128Mi` | -| `controllerManager.manager.resources.requests.cpu` | Sets cpu resource requests for operator. | `10m` | -| `controllerManager.manager.resources.requests.memory` | Sets memory resource requests for operator. | `64Mi` | -| `controllerManager.replicas` | Sets number of replicas of the OpenFeature operator pod. | `1` | -| `managerConfig.flagsValidatonEnabled` | Enables the validating webhook for FeatureFlag CR. | `true` | -| `managerConfig.flagdResourceEnabled` | Enables the controller for the Flagd CR and adds the required permissions to create/update/delete Service and Ingress resources. | `true` | -| `managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress` | Sets the bind address for health probes. | `:8081` | -| `managerConfig.controllerManagerConfigYaml.metrics.bindAddress` | Sets the bind address for metrics. | `127.0.0.1:8080` | -| `managerConfig.controllerManagerConfigYaml.webhook.port` | Sets the bind address for webhook. | `9443` | +| Name | Description | Value | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| `controllerManager.kubeRbacProxy.image.repository` | Sets the image for the kube-rbac-proxy. | `gcr.io/kubebuilder/kube-rbac-proxy` | +| `controllerManager.kubeRbacProxy.image.tag` | Sets the version tag for the kube-rbac-proxy. | `v0.14.1` | +| `controllerManager.kubeRbacProxy.resources.limits.cpu` | Sets cpu resource limits for kube-rbac-proxy. | `500m` | +| `controllerManager.kubeRbacProxy.resources.limits.memory` | Sets memory resource limits for kube-rbac-proxy. | `128Mi` | +| `controllerManager.kubeRbacProxy.resources.requests.cpu` | Sets cpu resource requests for kube-rbac-proxy. | `5m` | +| `controllerManager.kubeRbacProxy.resources.requests.memory` | Sets memory resource requests for kube-rbac-proxy. | `64Mi` | +| `controllerManager.manager.image.repository` | Sets the image for the operator. | `ghcr.io/open-feature/open-feature-operator` | +| `controllerManager.manager.image.tag` | Sets the version tag for the operator. | `v0.5.5` | +| `controllerManager.manager.resources.limits.cpu` | Sets cpu resource limits for operator. | `500m` | +| `controllerManager.manager.resources.limits.memory` | Sets memory resource limits for operator. | `128Mi` | +| `controllerManager.manager.resources.requests.cpu` | Sets cpu resource requests for operator. | `10m` | +| `controllerManager.manager.resources.requests.memory` | Sets memory resource requests for operator. | `64Mi` | +| `controllerManager.replicas` | Sets number of replicas of the OpenFeature operator pod. | `1` | +| `managerConfig.flagsValidatonEnabled` | Enables the validating webhook for FeatureFlag CR. | `true` | +| `managerConfig.flagdResourceEnabled` | Enables the controller for the Flagd CR and adds the required permissions to automatically manage the exposure of flagd via Service and Ingress resources. | `true` | +| `managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress` | Sets the bind address for health probes. | `:8081` | +| `managerConfig.controllerManagerConfigYaml.metrics.bindAddress` | Sets the bind address for metrics. | `127.0.0.1:8080` | +| `managerConfig.controllerManagerConfigYaml.webhook.port` | Sets the bind address for webhook. | `9443` | From b1aed490b37d13ababe70746c6715a6b27197499 Mon Sep 17 00:00:00 2001 From: Florian Bacher Date: Tue, 14 May 2024 07:43:26 +0200 Subject: [PATCH 43/43] init workspace before linting Signed-off-by: Florian Bacher --- .github/workflows/golangci-lint.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 88b2a5787..e2522d7dc 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -34,6 +34,9 @@ jobs: go-version: ${{ env.GO_VERSION }} check-latest: true + - name: Workspace Init + run: make workspace-init + - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: