diff --git a/apis/cloud.redhat.com/v1alpha1/clowdenvironment_types.go b/apis/cloud.redhat.com/v1alpha1/clowdenvironment_types.go index c6f7ce013..dbc8d30e6 100644 --- a/apis/cloud.redhat.com/v1alpha1/clowdenvironment_types.go +++ b/apis/cloud.redhat.com/v1alpha1/clowdenvironment_types.go @@ -359,7 +359,8 @@ type ObjectStoreConfig struct { } type FeatureFlagsImages struct { - Unleash string `json:"unleash,omitempty"` + Unleash string `json:"unleash,omitempty"` + UnleashEdge string `json:"unleashEdge,omitempty"` } // FeatureFlagsMode details the mode of operation of the Clowder FeatureFlags diff --git a/controllers/cloud.redhat.com/clowderconfig/config.go b/controllers/cloud.redhat.com/clowderconfig/config.go index 6566c6420..dc0de83a5 100644 --- a/controllers/cloud.redhat.com/clowderconfig/config.go +++ b/controllers/cloud.redhat.com/clowderconfig/config.go @@ -8,16 +8,17 @@ import ( type ClowderConfig struct { Images struct { - MBOP string `json:"mbop"` - Caddy string `json:"caddy"` - CaddyGateway string `json:"caddyGateway"` - Keycloak string `json:"Keycloak"` - Mocktitlements string `json:"mocktitlements"` - CaddyReverseProxy string `json:"caddyReverseProxy"` - ObjectStoreMinio string `json:"objectStoreMinio"` - FeatureFlagsUnleash string `json:"featureFlagsUnleash"` - TokenRefresher string `json:"tokenRefresher"` - OtelCollector string `json:"otelCollector"` + MBOP string `json:"mbop"` + Caddy string `json:"caddy"` + CaddyGateway string `json:"caddyGateway"` + Keycloak string `json:"Keycloak"` + Mocktitlements string `json:"mocktitlements"` + CaddyReverseProxy string `json:"caddyReverseProxy"` + ObjectStoreMinio string `json:"objectStoreMinio"` + FeatureFlagsUnleash string `json:"featureFlagsUnleash"` + FeatureFlagsUnleashEdge string `json:"featureFlagsUnleashEdge"` + TokenRefresher string `json:"tokenRefresher"` + OtelCollector string `json:"otelCollector"` } `json:"images"` DebugOptions struct { Logging struct { diff --git a/controllers/cloud.redhat.com/providers/featureflags/localfeatureflags.go b/controllers/cloud.redhat.com/providers/featureflags/localfeatureflags.go index 9f3f872a2..a137185b5 100644 --- a/controllers/cloud.redhat.com/providers/featureflags/localfeatureflags.go +++ b/controllers/cloud.redhat.com/providers/featureflags/localfeatureflags.go @@ -23,12 +23,20 @@ import ( "github.com/RedHatInsights/rhc-osdk-utils/utils" ) +const featureFlagsPort = 4242 + // LocalFFDeployment is the ident referring to the local Feature Flags deployment object. var LocalFFDeployment = rc.NewSingleResourceIdent(ProvName, "ff_deployment", &apps.Deployment{}) // LocalFFService is the ident referring to the local Feature Flags service object. var LocalFFService = rc.NewSingleResourceIdent(ProvName, "ff_service", &core.Service{}) +// LocalFFEdgeDeployment is the ident referring to the local Unleash edge deployment object. +var LocalFFEdgeDeployment = rc.NewSingleResourceIdent(ProvName, "ff_edge_deployment", &apps.Deployment{}) + +// LocalFFEdgeService is the ident referring to the local Unleash edge service object. +var LocalFFEdgeService = rc.NewSingleResourceIdent(ProvName, "ff_service", &core.Service{}) + // LocalFFSecret is the ident referring to the local Feature Flags secret object. var LocalFFSecret = rc.NewSingleResourceIdent(ProvName, "ff_secret", &core.Secret{}) @@ -53,6 +61,8 @@ func NewLocalFeatureFlagsProvider(p *providers.Provider) (providers.ClowderProvi p.Cache.AddPossibleGVKFromIdent( LocalFFDeployment, LocalFFService, + LocalFFEdgeDeployment, + LocalFFEdgeService, LocalFFSecret, LocalFFDBDeployment, LocalFFDBService, @@ -84,6 +94,15 @@ func (ff *localFeatureFlagsProvider) EnvProvide() error { return err } + objList2 := []rc.ResourceIdent{ + LocalFFEdgeDeployment, + LocalFFEdgeService, + } + + if err := providers.CachedMakeComponent(ff.Cache, objList2, ff.Env, "featureflags-edge", makeLocalFeatureFlagsEdge, false, ff.Env.IsNodePort()); err != nil { + return err + } + namespacedNameDb := types.NamespacedName{ Name: "featureflags-db", Namespace: ff.Env.Status.TargetNamespace, @@ -229,7 +248,7 @@ func makeLocalFeatureFlags(o obj.ClowdObject, objMap providers.ObjectMap, _ bool dd.Spec.Template.ObjectMeta.Labels = labels - port := int32(4242) + port := int32(featureFlagsPort) envVars := []core.EnvVar{ { @@ -257,7 +276,7 @@ func makeLocalFeatureFlags(o obj.ClowdObject, objMap providers.ObjectMap, _ bool Path: "/health", Port: intstr.IntOrString{ Type: intstr.Int, - IntVal: 4242, + IntVal: featureFlagsPort, }, }, } @@ -319,3 +338,106 @@ func makeLocalFeatureFlags(o obj.ClowdObject, objMap providers.ObjectMap, _ bool utils.MakeService(svc, nn, labels, servicePorts, o, nodePort) return nil } + +func makeLocalFeatureFlagsEdge(o obj.ClowdObject, objMap providers.ObjectMap, _ bool, nodePort bool) error { + + nnFF := providers.GetNamespacedName(o, "featureflags") + nn := providers.GetNamespacedName(o, "featureflags-edge") + + dd := objMap[LocalFFEdgeDeployment].(*apps.Deployment) + svc := objMap[LocalFFEdgeService].(*core.Service) + + labels := o.GetLabels() + labels["env-app"] = nn.Name + labels["service"] = "featureflags-edge" + labeler := utils.MakeLabeler(nn, labels, o) + + labeler(dd) + + replicas := int32(1) + + dd.Spec.Replicas = &replicas + dd.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} + + dd.Spec.Template.ObjectMeta.Labels = labels + + portEdge := int32(3063) + + envVarsEdge := []core.EnvVar{ + { + // communication with the main featureflags service on localhost + Name: "UPSTREAM_URL", + Value: fmt.Sprintf("http://%s:%d", nnFF.Name, featureFlagsPort), + }, + } + envVarsEdge = provutils.AppendEnvVarsFromSecret(envVarsEdge, nnFF.Name, + provutils.NewSecretEnvVar("TOKENS", "clientAccessToken")) + + portsEdge := []core.ContainerPort{{ + Name: "service", + ContainerPort: portEdge, + Protocol: "TCP", + }} + + readinessProbeEdge := core.Probe{ + ProbeHandler: core.ProbeHandler{ + Exec: &core.ExecAction{ + Command: []string{"/unleash-edge", "ready"}, + }, + }, + InitialDelaySeconds: 1, + TimeoutSeconds: 5, + PeriodSeconds: 30, + SuccessThreshold: 1, + FailureThreshold: 3, + } + + livenessProbeEdge := core.Probe{ + ProbeHandler: core.ProbeHandler{ + Exec: &core.ExecAction{ + Command: []string{"/unleash-edge", "health"}, + }, + }, + InitialDelaySeconds: 30, + TimeoutSeconds: 5, + PeriodSeconds: 30, + SuccessThreshold: 1, + FailureThreshold: 3, + } + + env, ok := o.(*crd.ClowdEnvironment) + if !ok { + return fmt.Errorf("could not get env") + } + + ce := core.Container{ + Name: nn.Name, + Image: GetFeatureFlagsUnleashEdgeImage(env), + Env: envVarsEdge, + Ports: portsEdge, + Args: []string{"edge"}, + ReadinessProbe: &readinessProbeEdge, + LivenessProbe: &livenessProbeEdge, + ImagePullPolicy: core.PullIfNotPresent, + Resources: core.ResourceRequirements{ + Requests: core.ResourceList{ + "memory": resource.MustParse("100Mi"), + "cpu": resource.MustParse("100m"), + }, + }, + } + + dd.Spec.Template.Spec.Containers = []core.Container{ce} + dd.Spec.Template.SetLabels(labels) + + servicePorts := []core.ServicePort{{ + Name: "featureflags-edge", + Port: portEdge, + Protocol: "TCP", + TargetPort: intstr.FromInt(int(portEdge)), + }} + + utils.MakeService(svc, nn, labels, servicePorts, o, nodePort) + + return nil +} diff --git a/controllers/cloud.redhat.com/providers/featureflags/provider.go b/controllers/cloud.redhat.com/providers/featureflags/provider.go index 790b9b3b9..acabe26c3 100644 --- a/controllers/cloud.redhat.com/providers/featureflags/provider.go +++ b/controllers/cloud.redhat.com/providers/featureflags/provider.go @@ -10,6 +10,7 @@ import ( ) var DefaultImageFeatureFlagsUnleash = "unleashorg/unleash-server:5.6.9" +var DefaultImageFeatureFlagsUnleashEdge = "unleashorg/unleash-edge:v19.6.3" func GetFeatureFlagsUnleashImage(env *crd.ClowdEnvironment) string { if env.Spec.Providers.FeatureFlags.Images.Unleash != "" { @@ -21,6 +22,16 @@ func GetFeatureFlagsUnleashImage(env *crd.ClowdEnvironment) string { return DefaultImageFeatureFlagsUnleash } +func GetFeatureFlagsUnleashEdgeImage(env *crd.ClowdEnvironment) string { + if env.Spec.Providers.FeatureFlags.Images.UnleashEdge != "" { + return env.Spec.Providers.FeatureFlags.Images.UnleashEdge + } + if clowderconfig.LoadedConfig.Images.FeatureFlagsUnleashEdge != "" { + return clowderconfig.LoadedConfig.Images.FeatureFlagsUnleashEdge + } + return DefaultImageFeatureFlagsUnleashEdge +} + // ProvName identifies the featureflags provider. var ProvName = "featureflags" diff --git a/tests/kuttl/test-ff-local/test_feature_flags.sh b/tests/kuttl/test-ff-local/test_feature_flags.sh index e7228bf10..bd86f2090 100755 --- a/tests/kuttl/test-ff-local/test_feature_flags.sh +++ b/tests/kuttl/test-ff-local/test_feature_flags.sh @@ -5,6 +5,15 @@ ADMIN_TOKEN=$(kubectl -n test-ff-local get secret test-ff-local-featureflags -o CLIENT_TOKEN=$(kubectl -n test-ff-local get secret test-ff-local-featureflags -o json | jq -r '.data.clientAccessToken | @base64d') FEATURE_TOGGLE_NAME='my-feature-toggle-1' +get_request_edge() { + + local TOKEN="$1" + local ENDPOINT="$2" + + kubectl exec -n test-ff-local "$FEATURE_FLAGS_POD" -- wget -q -O- \ + --header "Authorization: $TOKEN" "test-ff-local-featureflags-edge:3063${ENDPOINT}" +} + get_request() { local TOKEN="$1" @@ -59,3 +68,12 @@ if [ 'true' != "$(get_request "$CLIENT_TOKEN" "/api/client/features/$FEATURE_TOG echo "Feature toggle '$FEATURE_TOGGLE_NAME' should be enabled" exit 1 fi + +# Unleash seems to refresh the cache every 5 seconds, didn't find a way to force it +# see https://github.com/Unleash/unleash-edge/blob/12cf9e3f87099d3c0dce1884bcd305604c1e68ff/server/src/http/refresher/feature_refresher.rs#L443 +echo "Waiting for edge to sync" +sleep 6 + +if ! get_request_edge "$CLIENT_TOKEN" "/api/client/features/$FEATURE_TOGGLE_NAME"; then + echo "Feature toggle '$FEATURE_TOGGLE_NAME' should be available through edge" +fi