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:
diff --git a/Makefile b/Makefile
index 5d0a09b00..6c7d9570f 100644
--- a/Makefile
+++ b/Makefile
@@ -257,7 +257,9 @@ 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
+ mockgen -source=./controllers/core/flagd/resources/interface.go -destination=controllers/core/flagd/resources/mock/mock.go -package=commonmock
workspace-init: workspace-clean
go work init
diff --git a/PROJECT b/PROJECT
index 337783678..f7e96df06 100644
--- a/PROJECT
+++ b/PROJECT
@@ -67,4 +67,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..bd1909fd4
--- /dev/null
+++ b/apis/core/v1beta1/flagd_types.go
@@ -0,0 +1,118 @@
+/*
+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"
+)
+
+// FlagdSpec defines the desired state of Flagd
+type FlagdSpec struct {
+ // 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.
+ // Must be one of: ClusterIP, NodePort, LoadBalancer, and ExternalName.
+ // 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
+ // +optional
+ ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
+ // FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves
+ // the feature flag configurations
+ FeatureFlagSource string `json:"featureFlagSource"`
+
+ // 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"`
+
+ // 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
+ // +optional
+ PathType networkingv1.PathType `json:"pathType,omitempty"`
+
+ // FlagdPath is the path to be used for accessing the flagd flag evaluation API
+ // +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"`
+}
+
+// FlagdStatus defines the observed state of Flagd
+type FlagdStatus struct {
+}
+
+//+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 b30239935..0f43472f9 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,101 @@ 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
+ }
+ 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 +402,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/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/chart/open-feature-operator/README.md b/chart/open-feature-operator/README.md
index 5244d2152..b975df0b6 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.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` |
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
new file mode 100644
index 000000000..b56e223f2
--- /dev/null
+++ b/chart/open-feature-operator/templates/rbac.authorization.k8s.io_v1_clusterrole_open-feature-operator-manager-role.yaml
@@ -0,0 +1,139 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: open-feature-operator-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
+{{ 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
diff --git a/chart/open-feature-operator/values.yaml b/chart/open-feature-operator/values.yaml
index 6f0659db9..2d1d8be9b 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:
@@ -92,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 automatically manage the exposure of flagd via Service and Ingress resources.
+ flagdResourceEnabled: "true"
controllerManagerConfigYaml:
health:
## @param managerConfig.controllerManagerConfigYaml.health.healthProbeBindAddress Sets the bind address for health probes.
diff --git a/common/common.go b/common/common.go
index 42ec9d462..623beb54d 100644
--- a/common/common.go
+++ b/common/common.go
@@ -30,6 +30,9 @@ const (
ProbeInitialDelay = 5
FeatureFlagSourceAnnotation = "featureflagsource"
EnabledAnnotation = "enabled"
+ ManagedByAnnotationKey = "app.kubernetes.io/managed-by"
+ ManagedByAnnotationValue = "open-feature-operator"
+ OperatorDeploymentName = "open-feature-operator-controller-manager"
)
var ErrFlagdProxyNotReady = errors.New("flagd-proxy is not ready, deferring pod admission")
@@ -77,3 +80,8 @@ func SharedOwnership(ownerReferences1, ownerReferences2 []metav1.OwnerReference)
}
return false
}
+
+func IsManagedByOFO(obj client.Object) bool {
+ val, ok := obj.GetLabels()[ManagedByAnnotationKey]
+ return ok && val == ManagedByAnnotationValue
+}
diff --git a/common/flagdproxy/flagdproxy.go b/common/flagdproxy/flagdproxy.go
index 056c61d65..f410b5c0b 100644
--- a/common/flagdproxy/flagdproxy.go
+++ b/common/flagdproxy/flagdproxy.go
@@ -6,6 +6,7 @@ import (
"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"
@@ -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,
@@ -120,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": ManagedByAnnotationValue,
+ "app.kubernetes.io/name": FlagdProxyDeploymentName,
+ common.ManagedByAnnotationKey: 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..4a2fcc8e4 100644
--- a/common/flagdproxy/flagdproxy_test.go
+++ b/common/flagdproxy/flagdproxy_test.go
@@ -6,6 +6,7 @@ import (
"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"
@@ -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/common/types/envconfig.go b/common/types/envconfig.go
index a4925e721..8b9c0c3dc 100644
--- a/common/types/envconfig.go
+++ b/common/types/envconfig.go
@@ -10,6 +10,15 @@ 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.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"`
+ 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"`
@@ -22,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/crd/bases/core.openfeature.dev_flagds.yaml b/config/crd/bases/core.openfeature.dev_flagds.yaml
new file mode 100644
index 000000000..9cb1154aa
--- /dev/null
+++ b/config/crd/bases/core.openfeature.dev_flagds.yaml
@@ -0,0 +1,143 @@
+---
+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:
+ featureFlagSource:
+ description: |-
+ FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves
+ the feature flag configurations
+ type: string
+ 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
+ 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:
+ type: string
+ type: array
+ ingressClassName:
+ 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:
+ 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
+ replicas:
+ default: 1
+ description: |-
+ Replicas defines the number of replicas to create for the service.
+ Default: 1
+ 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
+ enum:
+ - ClusterIP
+ - NodePort
+ - LoadBalancer
+ - ExternalName
+ type: string
+ required:
+ - featureFlagSource
+ 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/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
diff --git a/config/overlays/helm/manager.yaml b/config/overlays/helm/manager.yaml
index 9ff3b2a58..18eec907a 100644
--- a/config/overlays/helm/manager.yaml
+++ b/config/overlays/helm/manager.yaml
@@ -50,8 +50,24 @@ 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: 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/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..d577ca5f0 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,36 @@ 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:
+ - 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
new file mode 100644
index 000000000..92c1b3a02
--- /dev/null
+++ b/config/samples/core_v1beta1_flagd.yaml
@@ -0,0 +1,26 @@
+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:
+ 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(/|$)(.*)
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
new file mode 100644
index 000000000..3b0548966
--- /dev/null
+++ b/controllers/core/flagd/config.go
@@ -0,0 +1,20 @@
+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"
+)
+
+func NewFlagdConfiguration(env types.EnvConfig) resources.FlagdConfiguration {
+ return resources.FlagdConfiguration{
+ Image: env.FlagdImage,
+ Tag: env.FlagdTag,
+ 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.go b/controllers/core/flagd/controller.go
new file mode 100644
index 000000000..bc1a30f56
--- /dev/null
+++ b/controllers/core/flagd/controller.go
@@ -0,0 +1,118 @@
+/*
+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"
+ 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"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// FlagdReconciler reconciles a Flagd object
+type FlagdReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Log logr.Logger
+
+ FlagdConfig resources2.FlagdConfiguration
+
+ ResourceReconciler IFlagdResourceReconciler
+
+ FlagdDeployment resources.IFlagdResource
+ FlagdService resources.IFlagdResource
+ FlagdIngress resources.IFlagdResource
+}
+
+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
+//+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
+//+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
+ }
+
+ if err := r.ResourceReconciler.Reconcile(
+ ctx,
+ flagd,
+ &appsv1.Deployment{},
+ r.FlagdDeployment,
+ ); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ if err := r.ResourceReconciler.Reconcile(
+ ctx,
+ flagd,
+ &v1.Service{},
+ r.FlagdService,
+ ); 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
+}
+
+// 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)
+}
diff --git a/controllers/core/flagd/controller_test.go b/controllers/core/flagd/controller_test.go
new file mode 100644
index 000000000..cf81fb8ef
--- /dev/null
+++ b/controllers/core/flagd/controller_test.go
@@ -0,0 +1,349 @@
+package flagd
+
+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"
+ 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"
+ controllerruntime "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+var testFlagdConfig = resources.FlagdConfiguration{
+ FlagdPort: 8013,
+ OFREPPort: 8016,
+ ManagementPort: 8014,
+ DebugLogging: false,
+ Image: "flagd",
+ Tag: "latest",
+ OperatorNamespace: "ofo-system",
+ OperatorDeploymentName: "ofo",
+}
+
+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_ReconcileWithIngress(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{
+ Ingress: api.IngressSpec{Enabled: true},
+ },
+ }
+
+ 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)
+
+ 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{
+ Namespace: flagdObj.Namespace,
+ Name: flagdObj.Name,
+ },
+ })
+
+ require.Nil(t, err)
+ 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)
+
+ fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects().Build()
+
+ r := setupReconciler(fakeClient, nil, 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)
+
+ deploymentResource := resourcemock.NewMockIFlagdResource(ctrl)
+
+ resourceReconciler := commonmock.NewMockIFlagdResourceReconciler(ctrl)
+
+ 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{
+ 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)
+
+ deploymentResource := resourcemock.NewMockIFlagdResource(ctrl)
+ serviceResource := 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(errors.New("oops"))
+
+ r := setupReconciler(fakeClient, deploymentResource, serviceResource, nil, resourceReconciler)
+
+ 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{
+ Ingress: api.IngressSpec{Enabled: true},
+ },
+ }
+
+ 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)
+
+ 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{
+ Namespace: flagdObj.Namespace,
+ Name: flagdObj.Name,
+ },
+ })
+
+ require.NotNil(t, err)
+ require.Equal(t, controllerruntime.Result{}, result)
+}
+
+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,
+ ResourceReconciler: resourceReconciler,
+ }
+}
diff --git a/controllers/core/flagd/mock/mock.go b/controllers/core/flagd/mock/mock.go
new file mode 100644
index 000000000..0df09e3d6
--- /dev/null
+++ b/controllers/core/flagd/mock/mock.go
@@ -0,0 +1,52 @@
+// 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"
+ resources "github.com/open-feature/open-feature-operator/controllers/core/flagd/resources"
+ client "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// MockIFlagdResourceReconciler is a mock of IFlagdResourceReconciler interface.
+type MockIFlagdResourceReconciler struct {
+ ctrl *gomock.Controller
+ recorder *MockIFlagdResourceReconcilerMockRecorder
+}
+
+// MockIFlagdResourceReconcilerMockRecorder is the mock recorder for MockIFlagdResourceReconciler.
+type MockIFlagdResourceReconcilerMockRecorder struct {
+ mock *MockIFlagdResourceReconciler
+}
+
+// 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 *MockIFlagdResourceReconciler) EXPECT() *MockIFlagdResourceReconcilerMockRecorder {
+ return m.recorder
+}
+
+// Reconcile mocks base method.
+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, obj, resource)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Reconcile indicates an expected call of Reconcile.
+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((*MockIFlagdResourceReconciler)(nil).Reconcile), ctx, flagd, obj, resource)
+}
diff --git a/controllers/core/flagd/resource_reconciler.go b/controllers/core/flagd/resource_reconciler.go
new file mode 100644
index 000000000..fcb11c938
--- /dev/null
+++ b/controllers/core/flagd/resource_reconciler.go
@@ -0,0 +1,74 @@
+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/controllers/core/flagd/resources"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type ResourceReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Log logr.Logger
+}
+
+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{
+ 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 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 fmt.Errorf("resource already exists and is not managed by OFO")
+ }
+
+ 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 {
+ return r.createResource(ctx, flagd, obj, newObj)
+ } else if !resource.AreObjectsEqual(existingObj, newObj) {
+ return r.updateResource(ctx, flagd, obj, newObj)
+ }
+ return nil
+}
+
+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/resource_reconciler_test.go b/controllers/core/flagd/resource_reconciler_test.go
new file mode 100644
index 000000000..964fe614b
--- /dev/null
+++ b/controllers/core/flagd/resource_reconciler_test.go
@@ -0,0 +1,217 @@
+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"
+ 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"
+ "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"
+)
+
+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",
+ },
+ }
+
+ 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)
+
+ err = r.Reconcile(
+ context.Background(),
+ flagdObj,
+ &corev1.ConfigMap{},
+ mockRes,
+ )
+
+ 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"),
+ }
+
+ 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{
+ "foo": "bar",
+ },
+ }, nil)
+
+ mockRes.EXPECT().AreObjectsEqual(gomock.Any(), gomock.Any()).Return(false)
+
+ err = r.Reconcile(
+ context.Background(),
+ flagdObj,
+ &corev1.ConfigMap{},
+ mockRes,
+ )
+
+ 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_CannotCreateResource(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"),
+ }
+
+ 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/resources/deployment.go b/controllers/core/flagd/resources/deployment.go
new file mode 100644
index 000000000..0cedc3bc2
--- /dev/null
+++ b/controllers/core/flagd/resources/deployment.go
@@ -0,0 +1,117 @@
+package resources
+
+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"
+ "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"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type FlagdDeployment struct {
+ client.Client
+ Log logr.Logger
+
+ FlagdInjector flagdinjector.IFlagdContainerInjector
+ FlagdConfig resources.FlagdConfiguration
+}
+
+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) GetResource(ctx context.Context, flagd *api.Flagd) (client.Object, 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{{
+ APIVersion: flagd.APIVersion,
+ Kind: flagd.Kind,
+ Name: flagd.Name,
+ UID: flagd.UID,
+ }},
+ },
+ 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.Namespace,
+ Name: flagd.Spec.FeatureFlagSource,
+ }, featureFlagSource); err != nil {
+ 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: %w", 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",
+ 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),
+ },
+ }
+
+ return deployment, nil
+}
diff --git a/controllers/core/flagd/resources/deployment_test.go b/controllers/core/flagd/resources/deployment_test.go
new file mode 100644
index 000000000..d886081ab
--- /dev/null
+++ b/controllers/core/flagd/resources/deployment_test.go
@@ -0,0 +1,316 @@
+// nolint:dupl
+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"
+ 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"
+ 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"
+)
+
+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)
+
+ flagdObj := &api.Flagd{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-flagd",
+ Namespace: "my-namespace",
+ },
+ Spec: api.FlagdSpec{
+ FeatureFlagSource: "my-flag-source",
+ },
+ }
+
+ 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,
+ }
+
+ res, err := r.GetResource(context.Background(), flagdObj)
+
+ require.Nil(t, err)
+ require.NotNil(t, res)
+
+ deploymentResult := res.(*appsv1.Deployment)
+
+ 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{
+ FeatureFlagSource: "my-flag-source",
+ },
+ }
+
+ 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.GetResource(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{
+ FeatureFlagSource: "my-flag-source",
+ },
+ }
+
+ 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.GetResource(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{
+ FeatureFlagSource: "my-flag-source",
+ },
+ }
+
+ 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.GetResource(context.Background(), flagdObj)
+
+ 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) {
+
+ d := &FlagdDeployment{}
+ got := d.AreObjectsEqual(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/resources/ingress.go b/controllers/core/flagd/resources/ingress.go
new file mode 100644
index 000000000..06984411f
--- /dev/null
+++ b/controllers/core/flagd/resources/ingress.go
@@ -0,0 +1,151 @@
+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"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ defaultFlagdPath = "/flagd"
+ defaultOFREPPath = "/ofrep"
+ defaultSyncPath = "/sync"
+)
+
+type FlagdIngress struct {
+ FlagdConfig resources.FlagdConfiguration
+}
+
+func (r FlagdIngress) AreObjectsEqual(o1 client.Object, o2 client.Object) bool {
+ oldIngress, ok := o1.(*networkingv1.Ingress)
+ if !ok {
+ return false
+ }
+
+ newIngress, ok := o2.(*networkingv1.Ingress)
+ if !ok {
+ return false
+ }
+
+ return reflect.DeepEqual(oldIngress.Spec, newIngress.Spec)
+}
+
+func (r FlagdIngress) GetResource(_ context.Context, flagd *api.Flagd) (client.Object, error) {
+ 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.Name,
+ UID: flagd.UID,
+ }},
+ },
+ Spec: networkingv1.IngressSpec{
+ IngressClassName: flagd.Spec.Ingress.IngressClassName,
+ TLS: flagd.Spec.Ingress.TLS,
+ Rules: r.getRules(flagd),
+ },
+ }, nil
+}
+
+func (r FlagdIngress) getRules(flagd *api.Flagd) []networkingv1.IngressRule {
+ rules := make([]networkingv1.IngressRule, len(flagd.Spec.Ingress.Hosts))
+ for i, host := range flagd.Spec.Ingress.Hosts {
+ rules[i] = r.getRule(flagd, host)
+ }
+ return rules
+}
+
+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{
+ HTTP: &networkingv1.HTTPIngressRuleValue{
+ Paths: []networkingv1.HTTPIngressPath{
+ {
+ Path: getFlagdPath(flagd.Spec.Ingress),
+ PathType: &pathType,
+ Backend: networkingv1.IngressBackend{
+ Service: &networkingv1.IngressServiceBackend{
+ Name: flagd.Name,
+ Port: networkingv1.ServiceBackendPort{
+ Number: int32(r.FlagdConfig.FlagdPort),
+ },
+ },
+ Resource: nil,
+ },
+ },
+ {
+ Path: getOFREPPath(flagd.Spec.Ingress),
+ PathType: &pathType,
+ Backend: networkingv1.IngressBackend{
+ Service: &networkingv1.IngressServiceBackend{
+ Name: flagd.Name,
+ Port: networkingv1.ServiceBackendPort{
+ Number: int32(r.FlagdConfig.OFREPPort),
+ },
+ },
+ Resource: nil,
+ },
+ },
+ {
+ Path: getSyncPath(flagd.Spec.Ingress),
+ PathType: &pathType,
+ Backend: networkingv1.IngressBackend{
+ Service: &networkingv1.IngressServiceBackend{
+ Name: flagd.Name,
+ Port: networkingv1.ServiceBackendPort{
+ Number: int32(r.FlagdConfig.SyncPort),
+ },
+ },
+ Resource: nil,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+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
new file mode 100644
index 000000000..c15e56e99
--- /dev/null
+++ b/controllers/core/flagd/resources/ingress_test.go
@@ -0,0 +1,354 @@
+// nolint:dupl
+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"
+)
+
+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"),
+ PathType: networkingv1.PathTypeImplementationSpecific,
+ },
+ },
+ })
+
+ require.Nil(t, err)
+
+ pathType := networkingv1.PathTypeImplementationSpecific
+
+ 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)
+ })
+ }
+}
+
+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/controllers/core/flagd/resources/interface.go b/controllers/core/flagd/resources/interface.go
new file mode 100644
index 000000000..db93e2fab
--- /dev/null
+++ b/controllers/core/flagd/resources/interface.go
@@ -0,0 +1,13 @@
+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/resources/service.go b/controllers/core/flagd/resources/service.go
new file mode 100644
index 000000000..5a46b19e9
--- /dev/null
+++ b/controllers/core/flagd/resources/service.go
@@ -0,0 +1,90 @@
+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"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type FlagdService struct {
+ FlagdConfig resources.FlagdConfiguration
+}
+
+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) GetResource(_ context.Context, flagd *api.Flagd) (client.Object, error) {
+ 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.Name,
+ 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: "sync",
+ Port: int32(r.FlagdConfig.SyncPort),
+ TargetPort: intstr.IntOrString{
+ IntVal: int32(r.FlagdConfig.SyncPort),
+ },
+ },
+ {
+ Name: "metrics",
+ Port: int32(r.FlagdConfig.ManagementPort),
+ TargetPort: intstr.IntOrString{
+ IntVal: int32(r.FlagdConfig.ManagementPort),
+ },
+ },
+ },
+ Type: flagd.Spec.ServiceType,
+ },
+ }, nil
+}
diff --git a/controllers/core/flagd/resources/service_test.go b/controllers/core/flagd/resources/service_test.go
new file mode 100644
index 000000000..88baf9200
--- /dev/null
+++ b/controllers/core/flagd/resources/service_test.go
@@ -0,0 +1,111 @@
+// nolint:dupl
+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"
+)
+
+func TestFlagdService_getService(t *testing.T) {
+ r := FlagdService{
+ FlagdConfig: testFlagdConfig,
+ }
+
+ svc, err := r.GetResource(context.TODO(), &api.Flagd{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-flagd",
+ Namespace: "my-namespace",
+ },
+ Spec: api.FlagdSpec{
+ ServiceType: "ClusterIP",
+ },
+ })
+
+ require.Nil(t, err)
+ require.NotNil(t, svc)
+ require.IsType(t, &v1.Service{}, 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) {
+
+ s := &FlagdService{}
+ got := s.AreObjectsEqual(tt.args.old, tt.args.new)
+
+ require.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/docs/crds.md b/docs/crds.md
index 12169b6ad..360da228a 100644
--- a/docs/crds.md
+++ b/docs/crds.md
@@ -12,6 +12,8 @@ Resource Types:
- [FeatureFlagSource](#featureflagsource)
+- [Flagd](#flagd)
+
@@ -783,4 +785,246 @@ inside a container.
Name | +Type | +Description | +Required | +
---|---|---|---|
apiVersion | +string | +core.openfeature.dev/v1beta1 | +true | +
kind | +string | +Flagd | +true | +
metadata | +object | +Refer to the Kubernetes API documentation for the fields of the `metadata` field. | +true | +
spec | +object | +
+ FlagdSpec defines the desired state of Flagd + |
+ false | +
status | +object | +
+ FlagdStatus defines the observed state of Flagd + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
featureFlagSource | +string | +
+ FeatureFlagSource references to a FeatureFlagSource from which the created flagd instance retrieves
+the feature flag configurations + |
+ true | +
ingress | +object | +
+ Ingress + |
+ false | +
replicas | +integer | +
+ Replicas defines the number of replicas to create for the service.
+Default: 1 + + Format: int32 + Default: 1 + |
+ false | +
serviceAccountName | +string | +
+ ServiceAccountName the service account name for the flagd deployment + |
+ false | +
serviceType | +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 | +
Name | +Type | +Description | +Required | +
---|---|---|---|
annotations | +map[string]string | +
+ Annotations the annotations to be added to the ingress + |
+ false | +
enabled | +boolean | +
+ 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 | +
+ Hosts list of hosts to be added to the ingress + |
+ false | +
ingressClassName | +string | +
+ 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 | +
+ 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 | +
+ TLS configuration for the ingress + |
+ false | +
Name | +Type | +Description | +Required | +
---|---|---|---|
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 | +
secretName | +string | +
+ 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 | +