From 43f5a523c3b81518010f7c744ff40f88fc572f06 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Tue, 22 Oct 2024 11:32:42 +0200 Subject: [PATCH 1/6] Add search index to generated configmap --- .gitignore | 2 + api/v1alpha1/frontend_types.go | 6 +- api/v1alpha1/zz_generated.deepcopy.go | 6 +- .../crd/bases/cloud.redhat.com_frontends.yaml | 3 - controllers/frontend_controller_suite_test.go | 222 +++++++++++++++++- controllers/reconcile.go | 40 +++- deploy.yml | 3 - examples/landing.yaml | 12 + kuttl-config.yml | 3 +- .../00-create-namespace.yaml | 8 + .../01-create-resources.yaml | 42 ++++ .../e2e/generate-search-index/02-assert.yaml | 54 +++++ 12 files changed, 378 insertions(+), 23 deletions(-) create mode 100644 tests/e2e/generate-search-index/00-create-namespace.yaml create mode 100644 tests/e2e/generate-search-index/01-create-resources.yaml create mode 100644 tests/e2e/generate-search-index/02-assert.yaml diff --git a/.gitignore b/.gitignore index 62980a49..fd202094 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ artifacts/ # IDE extras .vscode/ build/.build_venv + +kuttl-report.json diff --git a/api/v1alpha1/frontend_types.go b/api/v1alpha1/frontend_types.go index dec29d65..7efbbcf5 100644 --- a/api/v1alpha1/frontend_types.go +++ b/api/v1alpha1/frontend_types.go @@ -44,8 +44,8 @@ type SearchEntry struct { Href string `json:"href" yaml:"href"` Title string `json:"title" yaml:"title"` Description string `json:"description" yaml:"description"` - AltTitle []string `json:"alt_title" yaml:"alt_title"` - IsExternal bool `json:"isExternal" yaml:"isExternal"` + AltTitle []string `json:"alt_title,omitempty" yaml:"alt_title,omitempty"` + IsExternal bool `json:"isExternal,omitempty" yaml:"isExternal,omitempty"` } type ServiceTile struct { @@ -97,7 +97,7 @@ type FrontendSpec struct { EnvName string `json:"envName" yaml:"envName"` Title string `json:"title" yaml:"title"` DeploymentRepo string `json:"deploymentRepo" yaml:"deploymentRepo"` - API APIInfo `json:"API" yaml:"API"` + API *APIInfo `json:"API,omitempty" yaml:"API,omitempty"` Frontend FrontendInfo `json:"frontend" yaml:"frontend"` Image string `json:"image,omitempty" yaml:"image,omitempty"` Service string `json:"service,omitempty" yaml:"service,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 42e1883b..c7a00ee2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -524,7 +524,11 @@ func (in *FrontendList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FrontendSpec) DeepCopyInto(out *FrontendSpec) { *out = *in - in.API.DeepCopyInto(&out.API) + if in.API != nil { + in, out := &in.API, &out.API + *out = new(APIInfo) + (*in).DeepCopyInto(*out) + } in.Frontend.DeepCopyInto(&out.Frontend) out.ServiceMonitor = in.ServiceMonitor if in.Module != nil { diff --git a/config/crd/bases/cloud.redhat.com_frontends.yaml b/config/crd/bases/cloud.redhat.com_frontends.yaml index 0f17e58e..4c944baa 100644 --- a/config/crd/bases/cloud.redhat.com_frontends.yaml +++ b/config/crd/bases/cloud.redhat.com_frontends.yaml @@ -326,11 +326,9 @@ spec: title: type: string required: - - alt_title - description - href - id - - isExternal - title type: object type: array @@ -493,7 +491,6 @@ spec: type: object type: array required: - - API - deploymentRepo - envName - frontend diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index b0835157..a00de6e6 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -58,7 +58,7 @@ var _ = ginkgo.Describe("Frontend controller with image", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -99,7 +99,7 @@ var _ = ginkgo.Describe("Frontend controller with image", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -271,7 +271,7 @@ var _ = ginkgo.Describe("Frontend controller with service", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -410,7 +410,7 @@ var _ = ginkgo.Describe("Frontend controller with chrome", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -450,7 +450,7 @@ var _ = ginkgo.Describe("Frontend controller with chrome", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -490,7 +490,7 @@ var _ = ginkgo.Describe("Frontend controller with chrome", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -633,7 +633,7 @@ var _ = ginkgo.Describe("ServiceMonitor Creation", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -757,7 +757,7 @@ var _ = ginkgo.Describe("Dependencies", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -797,7 +797,7 @@ var _ = ginkgo.Describe("Dependencies", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -837,7 +837,7 @@ var _ = ginkgo.Describe("Dependencies", func() { EnvName: FrontendEnvName, Title: "", DeploymentRepo: "", - API: crd.APIInfo{ + API: &crd.APIInfo{ Versions: []string{"v1"}, }, Frontend: crd.FrontendInfo{ @@ -921,4 +921,206 @@ var _ = ginkgo.Describe("Dependencies", func() { }) }) + +}) +var _ = ginkgo.Describe("Search index", func() { + const ( + FrontendName = "test-search-index" + FrontendName2 = "test-search-index2" + FrontendName3 = "test-search-index3" + FrontendNamespace = "default" + FrontendEnvName = "test-search-index-env" + FrontendEnvName2 = "test-search-index-env2" + + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + + ginkgo.Context("When creating frontend with search entries", func() { + ginkgo.It("Should create the search index", func() { + ginkgo.By("from single Frontend resource", func() { + ctx := context.Background() + + configMapLookupKey := types.NamespacedName{Name: FrontendEnvName, Namespace: FrontendNamespace} + + frontend := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendName, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendSpec{ + EnvName: FrontendEnvName, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{"/things/test"}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "/apps/inventory/fed-mods.json", + Modules: []crd.Module{}, + }, + SearchEntries: []*crd.SearchEntry{{ + ID: "test", + Href: "/test/href", + Title: "Test", + Description: "Test description", + }, { + ID: "test2", + Href: "/test2/href", + Title: "Test2", + Description: "Test2 description", + }}, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) + frontendEnvironment := &crd.FrontendEnvironment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "FrontendEnvironment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendEnvName, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendEnvironmentSpec{ + SSO: "https://something-auth", + Hostname: "something", + Monitoring: &crd.MonitoringConfig{ + Mode: "app-interface", + }, + GenerateNavJSON: false, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) + createdConfigMap := &v1.ConfigMap{} + gomega.Eventually(func() bool { + err := k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + if err != nil { + return err == nil + } + if len(createdConfigMap.Data) != 2 { + return false + } + return true + }, timeout, interval).Should(gomega.BeTrue()) + gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(FrontendEnvName)) + gomega.Expect(createdConfigMap.Data).Should(gomega.Equal(map[string]string{ + "fed-modules.json": "{\"testSearchIndex\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false}}", + "search-index.json": "[{\"id\":\"test-search-index-test-search-index-env-test\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index-test-search-index-env-test2\",\"href\":\"/test2/href\",\"title\":\"Test2\",\"description\":\"Test2 description\"}]", + })) + gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(FrontendEnvName)) + }) + + ginkgo.By("from multiple Frontend resources", func() { + ctx := context.Background() + + configMapLookupKey := types.NamespacedName{Name: FrontendEnvName2, Namespace: FrontendNamespace} + + frontend2 := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendName2, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendSpec{ + EnvName: FrontendEnvName2, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{"/things/test"}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "/apps/inventory/fed-mods.json", + Modules: []crd.Module{}, + }, + SearchEntries: []*crd.SearchEntry{{ + ID: FrontendName2, + Href: "/test/href", + Title: "Test", + Description: "Test description", + }}, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontend2)).Should(gomega.Succeed()) + + frontend3 := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendName3, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendSpec{ + EnvName: FrontendEnvName2, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{"/things/test"}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "/apps/inventory/fed-mods.json", + Modules: []crd.Module{}, + }, + SearchEntries: []*crd.SearchEntry{{ + ID: FrontendName3, + Href: "/test/href", + Title: "Test", + Description: "Test description", + }}, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontend3)).Should(gomega.Succeed()) + + frontendEnvironment := &crd.FrontendEnvironment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "FrontendEnvironment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendEnvName2, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendEnvironmentSpec{ + SSO: "https://something-auth", + Hostname: "something", + Monitoring: &crd.MonitoringConfig{ + Mode: "app-interface", + }, + GenerateNavJSON: false, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) + createdConfigMap := &v1.ConfigMap{} + gomega.Eventually(func() bool { + err := k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + if err != nil { + return err == nil + } + if len(createdConfigMap.Data) != 2 { + return false + } + return true + }, timeout, interval).Should(gomega.BeTrue()) + gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(FrontendEnvName2)) + gomega.Expect(createdConfigMap.Data).Should(gomega.Equal(map[string]string{ + "fed-modules.json": "{\"testSearchIndex2\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false},\"testSearchIndex3\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false}}", + "search-index.json": "[{\"id\":\"test-search-index2-test-search-index-env2-test-search-index2\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index3-test-search-index-env2-test-search-index3\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"}]", + })) + gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(FrontendEnvName2)) + }) + }) + }) }) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 705f6a1e..037c1e03 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -853,6 +853,31 @@ func setupFedModules(feEnv *crd.FrontendEnvironment, frontendList *crd.FrontendL return nil } +func adjustSearchEntry(searchEntry *crd.SearchEntry, frontend crd.Frontend) crd.SearchEntry { + newSearchEntry := crd.SearchEntry{ + // make the id environment and frontend specific to reduce duplicate ids across Frontend resources + ID: fmt.Sprintf("%s-%s-%s", frontend.Name, frontend.Spec.EnvName, searchEntry.ID), + Title: searchEntry.Title, + Description: searchEntry.Description, + Href: searchEntry.Href, + AltTitle: searchEntry.AltTitle, + IsExternal: searchEntry.IsExternal, + } + return newSearchEntry +} + +func setupSearchIndex(feList *crd.FrontendList) []crd.SearchEntry { + searchIndex := []crd.SearchEntry{} + for _, frontend := range feList.Items { + if frontend.Spec.SearchEntries != nil { + for _, searchEntry := range frontend.Spec.SearchEntries { + searchIndex = append(searchIndex, adjustSearchEntry(searchEntry, frontend)) + } + } + } + return searchIndex +} + func (r *FrontendReconciliation) setupBundleData(cfgMap *v1.ConfigMap, cacheMap map[string]crd.Frontend) error { bundleList := &crd.BundleList{} @@ -979,12 +1004,23 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa return fmt.Errorf("error setting up fedModules: %w", err) } - jsonData, err := json.Marshal(fedModules) + searchIndex := setupSearchIndex(feList) + + fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { return err } - cfgMap.Data["fed-modules.json"] = string(jsonData) + searchIndexJSONData, err := json.Marshal(searchIndex) + + if err != nil { + return err + } + + cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) + if len(searchIndex) > 0 { + cfgMap.Data["search-index.json"] = string(searchIndexJSONData) + } return nil } diff --git a/deploy.yml b/deploy.yml index 2a20729d..8b2da6a3 100644 --- a/deploy.yml +++ b/deploy.yml @@ -782,11 +782,9 @@ objects: title: type: string required: - - alt_title - description - href - id - - isExternal - title type: object type: array @@ -949,7 +947,6 @@ objects: type: object type: array required: - - API - deploymentRepo - envName - frontend diff --git a/examples/landing.yaml b/examples/landing.yaml index c3e1be66..aecf7ba9 100644 --- a/examples/landing.yaml +++ b/examples/landing.yaml @@ -22,3 +22,15 @@ spec: routes: - pathname: / exact: true + searchEntries: + - id: "landing" + title: "Landing" + href: / + description: "Landing page description" + alt_title: + - HCC Home page + - Home + - id: "landing-widgets" + title: "Widget fantastic" + href: /widgets + description: "Widget" diff --git a/kuttl-config.yml b/kuttl-config.yml index a2076604..4ad2fc36 100644 --- a/kuttl-config.yml +++ b/kuttl-config.yml @@ -11,4 +11,5 @@ commands: - command: make install - command: make build - command: ./bin/manager - background: true \ No newline at end of file + background: true +reportFormat: JSON diff --git a/tests/e2e/generate-search-index/00-create-namespace.yaml b/tests/e2e/generate-search-index/00-create-namespace.yaml new file mode 100644 index 00000000..1771b261 --- /dev/null +++ b/tests/e2e/generate-search-index/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-search-index +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/generate-search-index/01-create-resources.yaml b/tests/e2e/generate-search-index/01-create-resources.yaml new file mode 100644 index 00000000..83c2c3cb --- /dev/null +++ b/tests/e2e/generate-search-index/01-create-resources.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-search-index-environment +spec: + generateNavJSON: false + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: search + namespace: test-search-index +spec: + envName: test-search-index-environment + title: search + deploymentRepo: https://github.com/RedHatInsights/search-frontend + frontend: + paths: + - /apps/search + image: "quay.io/cloudservices/search-frontend:3244a17" + searchEntries: + - id: "landing" + title: "Landing" + href: / + description: "Landing page description" + alt_title: + - HCC Home page + - Home + - id: "landing-widgets" + title: "Widget fantastic" + href: /widgets + description: "Widget" + module: + manifestLocation: /apps/search/fed-mods.json + modules: [] + moduleID: search + + diff --git a/tests/e2e/generate-search-index/02-assert.yaml b/tests/e2e/generate-search-index/02-assert.yaml new file mode 100644 index 00000000..19513795 --- /dev/null +++ b/tests/e2e/generate-search-index/02-assert.yaml @@ -0,0 +1,54 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: search-frontend + namespace: test-search-index + labels: + frontend: search + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + name: search +spec: + selector: + matchLabels: + frontend: search + template: + spec: + volumes: + - name: config + configMap: + name: test-search-index-environment + defaultMode: 420 + containers: + - name: fe-image + image: quay.io/cloudservices/search-frontend:3244a17 + ports: + - name: web + containerPort: 80 + protocol: TCP + - name: metrics + containerPort: 9000 + protocol: TCP + volumeMounts: + - name: config + mountPath: /opt/app-root/src/build/stable/operator-generated +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: test-search-index-environment + namespace: test-search-index + labels: + frontendenv: test-search-index-environment + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + name: test-search-index-environment +data: + fed-modules.json: >- + {"search":{"manifestLocation":"/apps/search/fed-mods.json","moduleID":"search","fullProfile":false}} + search-index.json: >- + [{"id":"search-test-search-index-environment-landing","href":"/","title":"Landing","description":"Landing page description","alt_title":["HCC Home page","Home"]},{"id":"search-test-search-index-environment-landing-widgets","href":"/widgets","title":"Widget fantastic","description":"Widget"}] + + From 865f1c66965024a4f5277e21c0b1eb12a6bb6715 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Tue, 22 Oct 2024 14:00:40 +0200 Subject: [PATCH 2/6] Add widget registry to generated config map. --- controllers/frontend_controller_suite_test.go | 167 ++++++++++++++++++ controllers/reconcile.go | 22 +++ examples/landing.yaml | 27 +++ .../00-create-namespace.yaml | 8 + .../01-create-resources.yaml | 57 ++++++ .../generate-widget-registry/02-assert.yaml | 54 ++++++ 6 files changed, 335 insertions(+) create mode 100644 tests/e2e/generate-widget-registry/00-create-namespace.yaml create mode 100644 tests/e2e/generate-widget-registry/01-create-resources.yaml create mode 100644 tests/e2e/generate-widget-registry/02-assert.yaml diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index a00de6e6..8118a152 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -2,6 +2,7 @@ package controllers import ( "context" + "encoding/json" "fmt" "time" @@ -1124,3 +1125,169 @@ var _ = ginkgo.Describe("Search index", func() { }) }) }) + +type WidgetFrontendTestEntry struct { + Widgets []*crd.WidgetEntry + FrontendName string +} + +type WidgetCase struct { + WidgetsFrontend []WidgetFrontendTestEntry + Namespace string + Environment string + ExpectedConfigMapEntry string +} + +func frontendFromWidget(wc WidgetCase, wf WidgetFrontendTestEntry) *crd.Frontend { + frontend := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: wf.FrontendName, + Namespace: wc.Namespace, + }, + Spec: crd.FrontendSpec{ + EnvName: wc.Environment, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{""}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "", + Modules: []crd.Module{}, + }, + WidgetRegistry: wf.Widgets, + }, + } + return frontend +} + +func mockFrontendEnv(env string, namespace string) *crd.FrontendEnvironment { + return &crd.FrontendEnvironment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "FrontendEnvironment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: env, + Namespace: namespace, + }, + Spec: crd.FrontendEnvironmentSpec{ + SSO: "https://something-auth", + Hostname: "something", + Monitoring: &crd.MonitoringConfig{ + Mode: "app-interface", + }, + GenerateNavJSON: false, + }, + } + +} + +var _ = ginkgo.Describe("Widget registry", func() { + const ( + FrontendName = "test-widget-registry" + FrontendName2 = "test-widget-registry2" + FrontendNamespace = "default" + FrontendEnvName = "test-widget-registry-env" + + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + + var ( + DefaultWidgetVariant = crd.WidgetDefaultVariant{ + Width: 1, + Height: 1, + MaxHeight: 2, + MinHeight: 1, + } + WidgetDefaults = crd.WidgetDefaults{ + Small: DefaultWidgetVariant, + Medium: DefaultWidgetVariant, + Large: DefaultWidgetVariant, + XLarge: DefaultWidgetVariant, + } + Widget1 = &crd.WidgetEntry{ + Scope: "test", + Module: "./foo", + Config: crd.WidgetConfig{ + Icon: "icon", + Title: "title", + }, + Defaults: WidgetDefaults, + } + Widget2 = &crd.WidgetEntry{ + Scope: "test", + Module: "./bar", + Config: crd.WidgetConfig{ + Icon: "icon-bar", + Title: "Bar", + }, + Defaults: WidgetDefaults, + } + Widget3 = &crd.WidgetEntry{ + Scope: "baz", + Module: "./default", + Config: crd.WidgetConfig{ + Icon: "baz", + Title: "Baz", + }, + Defaults: WidgetDefaults, + } + ) + + ginkgo.It("Should create widget registry", func() { + ginkgo.By("collection entries from Frontend resources", func() { + expectedResult, err := json.Marshal([]crd.WidgetEntry{*Widget1, *Widget2, *Widget3}) + gomega.Expect(err).Should(gomega.BeNil()) + widgetCases := []WidgetCase{{ + WidgetsFrontend: []WidgetFrontendTestEntry{{ + Widgets: []*crd.WidgetEntry{Widget1, Widget2}, + FrontendName: FrontendName, + }, { + Widgets: []*crd.WidgetEntry{Widget3}, + FrontendName: FrontendName2, + }, + }, + Namespace: FrontendNamespace, + Environment: FrontendEnvName, + ExpectedConfigMapEntry: string(expectedResult), + }} + + for _, widgetCase := range widgetCases { + ctx := context.Background() + configMapLookupKey := types.NamespacedName{Name: widgetCase.Environment, Namespace: widgetCase.Namespace} + for _, wf := range widgetCase.WidgetsFrontend { + frontend := frontendFromWidget(widgetCase, wf) + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) + } + + frontendEnvironment := mockFrontendEnv(widgetCase.Environment, widgetCase.Namespace) + gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) + createdConfigMap := &v1.ConfigMap{} + gomega.Eventually(func() bool { + err := k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + if err != nil { + return err == nil + } + if len(createdConfigMap.Data) != 2 { + return false + } + return true + }, timeout, interval).Should(gomega.BeTrue()) + + widgetRegistryMap := createdConfigMap.Data["widget-registry.json"] + + gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(widgetCase.Environment)) + gomega.Expect(widgetRegistryMap).Should(gomega.Equal(widgetCase.ExpectedConfigMapEntry)) + gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(widgetCase.Environment)) + } + }) + }) +}) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 037c1e03..6b927458 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -878,6 +878,17 @@ func setupSearchIndex(feList *crd.FrontendList) []crd.SearchEntry { return searchIndex } +func setupWidgetRegistry(felist *crd.FrontendList) []crd.WidgetEntry { + widgetRegistry := []crd.WidgetEntry{} + for _, frontend := range felist.Items { + for _, widget := range frontend.Spec.WidgetRegistry { + widgetRegistry = append(widgetRegistry, *widget) + } + } + + return widgetRegistry +} + func (r *FrontendReconciliation) setupBundleData(cfgMap *v1.ConfigMap, cacheMap map[string]crd.Frontend) error { bundleList := &crd.BundleList{} @@ -1006,6 +1017,8 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa searchIndex := setupSearchIndex(feList) + widgetRegistry := setupWidgetRegistry(feList) + fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { return err @@ -1017,10 +1030,19 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa return err } + widgetRegistryJSONData, err := json.Marshal(widgetRegistry) + if err != nil { + return err + } + cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) if len(searchIndex) > 0 { cfgMap.Data["search-index.json"] = string(searchIndexJSONData) } + + if len(widgetRegistry) > 0 { + cfgMap.Data["widget-registry.json"] = string(widgetRegistryJSONData) + } return nil } diff --git a/examples/landing.yaml b/examples/landing.yaml index aecf7ba9..a4179259 100644 --- a/examples/landing.yaml +++ b/examples/landing.yaml @@ -34,3 +34,30 @@ spec: title: "Widget fantastic" href: /widgets description: "Widget" + widgetRegistry: + - scope: "landing" + module: "./RandomWidget" + config: + icon: "CogIcon" + title: "Random Widget" + defaults: + sm: + w: 1 + h: 1 + maxH: 1 + minH: 1 + md: + w: 1 + h: 1 + maxH: 1 + minH: 1 + lg: + w: 1 + h: 1 + maxH: 1 + minH: 1 + xl: + w: 1 + h: 1 + maxH: 1 + minH: 1 diff --git a/tests/e2e/generate-widget-registry/00-create-namespace.yaml b/tests/e2e/generate-widget-registry/00-create-namespace.yaml new file mode 100644 index 00000000..e99d7cfa --- /dev/null +++ b/tests/e2e/generate-widget-registry/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-widget-registry +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/generate-widget-registry/01-create-resources.yaml b/tests/e2e/generate-widget-registry/01-create-resources.yaml new file mode 100644 index 00000000..95672dff --- /dev/null +++ b/tests/e2e/generate-widget-registry/01-create-resources.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-widget-registry-environment +spec: + generateNavJSON: false + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: widgets + namespace: test-widget-registry +spec: + envName: test-widget-registry-environment + title: widgets + deploymentRepo: https://github.com/RedHatInsights/widgets-frontend + frontend: + paths: + - /apps/widgets + image: "quay.io/cloudservices/widgets-frontend:3244a17" + widgetRegistry: + - scope: "widgets" + module: "./RandomWidget" + config: + icon: "CogIcon" + title: "Random Widget" + defaults: + sm: + w: 1 + h: 1 + maxH: 1 + minH: 1 + md: + w: 1 + h: 1 + maxH: 1 + minH: 1 + lg: + w: 1 + h: 1 + maxH: 1 + minH: 1 + xl: + w: 1 + h: 1 + maxH: 1 + minH: 1 + module: + manifestLocation: /apps/widgets/fed-mods.json + modules: [] + moduleID: widgets + + diff --git a/tests/e2e/generate-widget-registry/02-assert.yaml b/tests/e2e/generate-widget-registry/02-assert.yaml new file mode 100644 index 00000000..bad23d0e --- /dev/null +++ b/tests/e2e/generate-widget-registry/02-assert.yaml @@ -0,0 +1,54 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: widgets-frontend + namespace: test-widget-registry + labels: + frontend: widgets + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + name: widgets +spec: + selector: + matchLabels: + frontend: widgets + template: + spec: + volumes: + - name: config + configMap: + name: test-widget-registry-environment + defaultMode: 420 + containers: + - name: fe-image + image: quay.io/cloudservices/widgets-frontend:3244a17 + ports: + - name: web + containerPort: 80 + protocol: TCP + - name: metrics + containerPort: 9000 + protocol: TCP + volumeMounts: + - name: config + mountPath: /opt/app-root/src/build/stable/operator-generated +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: test-widget-registry-environment + namespace: test-widget-registry + labels: + frontendenv: test-widget-registry-environment + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + name: test-widget-registry-environment +data: + fed-modules.json: >- + {"widgets":{"manifestLocation":"/apps/widgets/fed-mods.json","moduleID":"widgets","fullProfile":false}} + widget-registry.json: >- + [{"scope":"widgets","module":"./RandomWidget","config":{"icon":"CogIcon","title":"Random Widget","headerLink":{"title":"","href":""}},"defaults":{"sm":{"w":1,"h":1,"maxH":1,"minH":1},"md":{"w":1,"h":1,"maxH":1,"minH":1},"lg":{"w":1,"h":1,"maxH":1,"minH":1},"xl":{"w":1,"h":1,"maxH":1,"minH":1}}}] + + From 683f9034ea8235390ba7d84e574a3e45d04b1e73 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Tue, 22 Oct 2024 16:58:25 +0200 Subject: [PATCH 3/6] Add service tiles entry to generated config map. --- api/v1alpha1/frontend_types.go | 15 +- api/v1alpha1/frontendenvironment_types.go | 29 ++++ api/v1alpha1/zz_generated.deepcopy.go | 92 ++++++++++ ...cloud.redhat.com_frontendenvironments.yaml | 29 ++++ .../crd/bases/cloud.redhat.com_frontends.yaml | 3 + controllers/frontend_controller_suite_test.go | 162 ++++++++++++++++++ controllers/reconcile.go | 65 +++++++ deploy.yml | 33 ++++ examples/feenvironment.yaml | 13 ++ examples/landing.yaml | 16 ++ kuttl-config.yml | 2 +- .../00-create-namespace.yaml | 8 + .../01-create-resources.yaml | 57 ++++++ .../e2e/generate-service-tiles/02-assert.yaml | 52 ++++++ 14 files changed, 568 insertions(+), 8 deletions(-) create mode 100644 tests/e2e/generate-service-tiles/00-create-namespace.yaml create mode 100644 tests/e2e/generate-service-tiles/01-create-resources.yaml create mode 100644 tests/e2e/generate-service-tiles/02-assert.yaml diff --git a/api/v1alpha1/frontend_types.go b/api/v1alpha1/frontend_types.go index 7efbbcf5..04a2ff0a 100644 --- a/api/v1alpha1/frontend_types.go +++ b/api/v1alpha1/frontend_types.go @@ -49,13 +49,14 @@ type SearchEntry struct { } type ServiceTile struct { - Section string `json:"section" yaml:"section"` - Group string `json:"group" yaml:"group"` - ID string `json:"id" yaml:"id"` - Href string `json:"href" yaml:"href"` - Title string `json:"title" yaml:"title"` - Icon string `json:"icon" yaml:"icon"` - IsExternal bool `json:"isExternal,omitempty" yaml:"isExternal,omitempty"` + Section string `json:"section" yaml:"section"` + Group string `json:"group" yaml:"group"` + ID string `json:"id" yaml:"id"` + Href string `json:"href" yaml:"href"` + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Icon string `json:"icon" yaml:"icon"` + IsExternal bool `json:"isExternal,omitempty" yaml:"isExternal,omitempty"` } type WidgetHeaderLink struct { diff --git a/api/v1alpha1/frontendenvironment_types.go b/api/v1alpha1/frontendenvironment_types.go index 3632abee..ed1e79e6 100644 --- a/api/v1alpha1/frontendenvironment_types.go +++ b/api/v1alpha1/frontendenvironment_types.go @@ -24,6 +24,33 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +type FrontendServiceCategoryGroup struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` +} + +// FrontendServiceCategory defines the category to which service can inject ServiceTiles +// Chroming UI will use this to render the service dropdown component +type FrontendServiceCategory struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + // +kubebuilder:validation:items:MinItems:=1 + Groups []FrontendServiceCategoryGroup `json:"groups" yaml:"groups"` +} + +type FrontendServiceCategoryGroupGenerated struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + Tiles *[]ServiceTile `json:"tiles" yaml:"tiles"` +} + +// The categories but with the groups filled with service tiles +type FrontendServiceCategoryGenerated struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + Groups []FrontendServiceCategoryGroupGenerated `json:"groups" yaml:"groups"` +} + // FrontendEnvironmentSpec defines the desired state of FrontendEnvironment type FrontendEnvironmentSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -65,6 +92,8 @@ type FrontendEnvironmentSpec struct { // List of namespaces that should receive a copy of the frontend configuration as a config map // By configurations we mean the fed-modules.json, navigation files, etc. TargetNamespaces []string `json:"targetNamespaces,omitempty" yaml:"targetNamespaces,omitempty"` + // For the ChromeUI to render additional global components + ServiceCategories *[]FrontendServiceCategory `json:"serviceCategories,omitempty" yaml:"serviceCategories,omitempty"` } type MonitoringConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index c7a00ee2..a9ef641e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -442,6 +442,17 @@ func (in *FrontendEnvironmentSpec) DeepCopyInto(out *FrontendEnvironmentSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ServiceCategories != nil { + in, out := &in.ServiceCategories, &out.ServiceCategories + *out = new([]FrontendServiceCategory) + if **in != nil { + in, out := *in, *out + *out = make([]FrontendServiceCategory, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendEnvironmentSpec. @@ -521,6 +532,87 @@ func (in *FrontendList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendServiceCategory) DeepCopyInto(out *FrontendServiceCategory) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]FrontendServiceCategoryGroup, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendServiceCategory. +func (in *FrontendServiceCategory) DeepCopy() *FrontendServiceCategory { + if in == nil { + return nil + } + out := new(FrontendServiceCategory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendServiceCategoryGenerated) DeepCopyInto(out *FrontendServiceCategoryGenerated) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]FrontendServiceCategoryGroupGenerated, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendServiceCategoryGenerated. +func (in *FrontendServiceCategoryGenerated) DeepCopy() *FrontendServiceCategoryGenerated { + if in == nil { + return nil + } + out := new(FrontendServiceCategoryGenerated) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendServiceCategoryGroup) DeepCopyInto(out *FrontendServiceCategoryGroup) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendServiceCategoryGroup. +func (in *FrontendServiceCategoryGroup) DeepCopy() *FrontendServiceCategoryGroup { + if in == nil { + return nil + } + out := new(FrontendServiceCategoryGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendServiceCategoryGroupGenerated) DeepCopyInto(out *FrontendServiceCategoryGroupGenerated) { + *out = *in + if in.Tiles != nil { + in, out := &in.Tiles, &out.Tiles + *out = new([]ServiceTile) + if **in != nil { + in, out := *in, *out + *out = make([]ServiceTile, len(*in)) + copy(*out, *in) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendServiceCategoryGroupGenerated. +func (in *FrontendServiceCategoryGroupGenerated) DeepCopy() *FrontendServiceCategoryGroupGenerated { + if in == nil { + return nil + } + out := new(FrontendServiceCategoryGroupGenerated) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FrontendSpec) DeepCopyInto(out *FrontendSpec) { *out = *in diff --git a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml index d17949cd..821c57a4 100644 --- a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml +++ b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml @@ -92,6 +92,35 @@ spec: - disabled - mode type: object + serviceCategories: + description: For the ChromeUI to render additional global components + items: + description: |- + FrontendServiceCategory defines the category to which service can inject ServiceTiles + Chroming UI will use this to render the service dropdown component + properties: + groups: + items: + properties: + id: + type: string + title: + type: string + required: + - id + - title + type: object + type: array + id: + type: string + title: + type: string + required: + - groups + - id + - title + type: object + type: array ssl: description: |- SSL mode requests SSL from the services in openshift and k8s and then applies them to the diff --git a/config/crd/bases/cloud.redhat.com_frontends.yaml b/config/crd/bases/cloud.redhat.com_frontends.yaml index 4c944baa..20ba72df 100644 --- a/config/crd/bases/cloud.redhat.com_frontends.yaml +++ b/config/crd/bases/cloud.redhat.com_frontends.yaml @@ -343,6 +343,8 @@ spec: description: Data for the all services dropdown items: properties: + description: + type: string group: type: string href: @@ -358,6 +360,7 @@ spec: title: type: string required: + - description - group - href - icon diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index 8118a152..00f5562d 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -1291,3 +1291,165 @@ var _ = ginkgo.Describe("Widget registry", func() { }) }) }) + +type ServiceTileTestEntry struct { + ServiceTiles []*crd.ServiceTile + FrontendName string +} + +type ServiceTileCase struct { + ServiceTiles []*ServiceTileTestEntry + Namespace string + Environment string + ExpectedConfigMapEntry string +} + +func frontendFromServiceTile(sct ServiceTileCase, ste ServiceTileTestEntry) *crd.Frontend { + frontend := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ste.FrontendName, + Namespace: sct.Namespace, + }, + Spec: crd.FrontendSpec{ + EnvName: sct.Environment, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{""}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "", + Modules: []crd.Module{}, + }, + ServiceTiles: ste.ServiceTiles, + }, + } + return frontend +} + +var _ = ginkgo.Describe("Service tiles", func() { + const ( + FrontendName = "test-service-tile" + FrontendName2 = "test-service-tile2" + FrontendNamespace = "default" + FrontendEnvName = "test-service-tile-env" + ServiceSectionID = "test-service-section" + ServiceSectionGroupID1 = "test-service-section-group1" + ServiceSectionGroupID2 = "test-service-section-group2" + + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + + var ( + ServiceTile1 = &crd.ServiceTile{ + Section: ServiceSectionID, + Group: ServiceSectionGroupID1, + ID: "test-service-tile1", + Href: "/foo", + Title: "bar", + Description: "", + Icon: "", + } + ServiceTile2 = &crd.ServiceTile{ + Section: ServiceSectionID, + Group: ServiceSectionGroupID1, + ID: "test-service-tile2", + Href: "/bar", + Title: "bar", + Description: "", + Icon: "", + } + ServiceTile3 = &crd.ServiceTile{ + Section: ServiceSectionID, + Group: ServiceSectionGroupID2, + ID: "test-service-tile3", + Href: "/baz", + Title: "baz", + Description: "", + Icon: "", + } + ExpectedServiceTiles1 = []crd.FrontendServiceCategoryGenerated{ + { + ID: ServiceSectionID, + Title: "Service Section", + Groups: []crd.FrontendServiceCategoryGroupGenerated{{ + ID: ServiceSectionGroupID1, + Title: "Service Section Group 1", + Tiles: &[]crd.ServiceTile{*ServiceTile1, *ServiceTile2}, + }, { + ID: ServiceSectionGroupID2, + Title: "Service Section Group 2", + Tiles: &[]crd.ServiceTile{*ServiceTile3}, + }}, + }, + } + ) + + ginkgo.It("Should create service tiles", func() { + ginkgo.By("collection entries from Frontend resources", func() { + expectedResult, err := json.Marshal(ExpectedServiceTiles1) + gomega.Expect(err).Should(gomega.BeNil()) + serviceTileCases := []ServiceTileCase{{ + Namespace: FrontendNamespace, + Environment: FrontendEnvName, + ExpectedConfigMapEntry: string(expectedResult), + ServiceTiles: []*ServiceTileTestEntry{{ + ServiceTiles: []*crd.ServiceTile{ServiceTile1, ServiceTile2, ServiceTile3}, + FrontendName: FrontendName, + }}, + }} + + for _, serviceCase := range serviceTileCases { + ctx := context.Background() + configMapLookupKey := types.NamespacedName{Name: serviceCase.Environment, Namespace: serviceCase.Namespace} + for _, sc := range serviceCase.ServiceTiles { + frontend := frontendFromServiceTile(serviceCase, *sc) + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) + } + + frontendEnvironment := mockFrontendEnv(serviceCase.Environment, serviceCase.Namespace) + frontendEnvironment.Spec.ServiceCategories = &[]crd.FrontendServiceCategory{ + { + ID: ServiceSectionID, + Title: "Service Section", + Groups: []crd.FrontendServiceCategoryGroup{ + { + ID: ServiceSectionGroupID1, + Title: "Service Section Group 1", + }, + { + ID: ServiceSectionGroupID2, + Title: "Service Section Group 2", + }, + }, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) + createdConfigMap := &v1.ConfigMap{} + gomega.Eventually(func() bool { + err := k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + if err != nil { + return err == nil + } + if len(createdConfigMap.Data) != 2 { + return false + } + return true + }, timeout, interval).Should(gomega.BeTrue()) + + serviceTileRegistryMap := createdConfigMap.Data["service-tiles.json"] + + gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(serviceCase.Environment)) + gomega.Expect(serviceTileRegistryMap).Should(gomega.Equal(serviceCase.ExpectedConfigMapEntry)) + gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(serviceCase.Environment)) + } + }) + }) +}) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 6b927458..57c96589 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -889,6 +889,58 @@ func setupWidgetRegistry(felist *crd.FrontendList) []crd.WidgetEntry { return widgetRegistry } +func getServiceTilePath(section string, group string) string { + return fmt.Sprintf("%s-%s", section, group) +} + +func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendEnvironment) []crd.FrontendServiceCategoryGenerated { + categories := []crd.FrontendServiceCategoryGenerated{} + if feEnvironment.Spec.ServiceCategories == nil { + // skip if we do not have service categories + return categories + } + + // just a quick cache to make it easier and faster to assign tiles to their destination + tileGroupAccessMap := make(map[string]*[]crd.ServiceTile) + + for _, category := range *feEnvironment.Spec.ServiceCategories { + groups := []crd.FrontendServiceCategoryGroupGenerated{} + for _, gr := range category.Groups { + tiles := []crd.ServiceTile{} + group := crd.FrontendServiceCategoryGroupGenerated{ + ID: gr.ID, + Title: gr.Title, + Tiles: &tiles, + } + groups = append(groups, group) + groupKey := getServiceTilePath(category.ID, gr.ID) + tileGroupAccessMap[groupKey] = &tiles + } + newCategory := crd.FrontendServiceCategoryGenerated{ + ID: category.ID, + Title: category.Title, + Groups: groups, + } + + categories = append(categories, newCategory) + } + + for _, frontend := range felist.Items { + if frontend.Spec.ServiceTiles != nil { + for _, tile := range frontend.Spec.ServiceTiles { + groupKey := getServiceTilePath(tile.Section, tile.Group) + // ignore the tile if destination does not exist + if groupTiles, ok := tileGroupAccessMap[groupKey]; ok { + // assign the tile to the service category and group + *groupTiles = append(*groupTiles, *tile) + } + } + } + } + + return categories +} + func (r *FrontendReconciliation) setupBundleData(cfgMap *v1.ConfigMap, cacheMap map[string]crd.Frontend) error { bundleList := &crd.BundleList{} @@ -1019,6 +1071,8 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa widgetRegistry := setupWidgetRegistry(feList) + serviceCategories := setupServiceTilesData(feList, *r.FrontendEnvironment) + fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { return err @@ -1035,6 +1089,12 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa return err } + serviceCategoriesJSONData, err := json.Marshal(serviceCategories) + + if err != nil { + return err + } + cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) if len(searchIndex) > 0 { cfgMap.Data["search-index.json"] = string(searchIndexJSONData) @@ -1043,6 +1103,11 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa if len(widgetRegistry) > 0 { cfgMap.Data["widget-registry.json"] = string(widgetRegistryJSONData) } + + if len(serviceCategories) > 0 { + cfgMap.Data["service-tiles.json"] = string(serviceCategoriesJSONData) + } + return nil } diff --git a/deploy.yml b/deploy.yml index 8b2da6a3..5393e0fd 100644 --- a/deploy.yml +++ b/deploy.yml @@ -410,6 +410,36 @@ objects: - disabled - mode type: object + serviceCategories: + description: For the ChromeUI to render additional global components + items: + description: 'FrontendServiceCategory defines the category to + which service can inject ServiceTiles + + Chroming UI will use this to render the service dropdown component' + properties: + groups: + items: + properties: + id: + type: string + title: + type: string + required: + - id + - title + type: object + type: array + id: + type: string + title: + type: string + required: + - groups + - id + - title + type: object + type: array ssl: description: 'SSL mode requests SSL from the services in openshift and k8s and then applies them to the @@ -799,6 +829,8 @@ objects: description: Data for the all services dropdown items: properties: + description: + type: string group: type: string href: @@ -814,6 +846,7 @@ objects: title: type: string required: + - description - group - href - icon diff --git a/examples/feenvironment.yaml b/examples/feenvironment.yaml index 059da5db..d1dcb5ce 100644 --- a/examples/feenvironment.yaml +++ b/examples/feenvironment.yaml @@ -9,3 +9,16 @@ spec: monitoring: mode: "local" disabled: false + serviceCategories: + - id: automation + title: Automation + groups: + - id: ansible + title: Ansible + - id: rhel + title: Red Hat Enterprise Linux + - id: iam + title: Identity and Access Management + groups: + - id: iam + title: IAM diff --git a/examples/landing.yaml b/examples/landing.yaml index a4179259..380e2790 100644 --- a/examples/landing.yaml +++ b/examples/landing.yaml @@ -61,3 +61,19 @@ spec: h: 1 maxH: 1 minH: 1 + serviceTiles: + - section: automation + group: ansible + id: ansible-link + title: Ansible FOO + href: /ansible/foo + description: Ansible FOO description thing + icon: AnsibleIcon + - section: iam + group: iam + id: iam-link + title: IAM FOO + href: /iam + description: Some Iam thing + icon: IAMIcon + isExternal: false diff --git a/kuttl-config.yml b/kuttl-config.yml index 4ad2fc36..e18b057d 100644 --- a/kuttl-config.yml +++ b/kuttl-config.yml @@ -5,7 +5,7 @@ startControlPlane: true crdDir: config/crd/test-resources/ testDirs: - tests/e2e/ -timeout: 320 +timeout: 30 parallel: 1 commands: - command: make install diff --git a/tests/e2e/generate-service-tiles/00-create-namespace.yaml b/tests/e2e/generate-service-tiles/00-create-namespace.yaml new file mode 100644 index 00000000..f4964545 --- /dev/null +++ b/tests/e2e/generate-service-tiles/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-service-tiles +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/generate-service-tiles/01-create-resources.yaml b/tests/e2e/generate-service-tiles/01-create-resources.yaml new file mode 100644 index 00000000..e5ca068b --- /dev/null +++ b/tests/e2e/generate-service-tiles/01-create-resources.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-service-tiles-environment +spec: + generateNavJSON: false + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com + serviceCategories: + - id: automation + title: Automation + groups: + - id: ansible + title: Ansible + - id: rhel + title: Red Hat Enterprise Linux + - id: iam + title: Identity and Access Management + groups: + - id: iam + title: IAM +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: service-tiles + namespace: test-service-tiles +spec: + envName: test-service-tiles-environment + title: service-tiles + deploymentRepo: https://github.com/RedHatInsights/service-tiles-frontend + frontend: + paths: + - /apps/service-tiles + image: "quay.io/cloudservices/service-tiles-frontend:3244a17" + module: + manifestLocation: /apps/service-tiles/fed-mods.json + modules: [] + moduleID: service-tiles + serviceTiles: + - section: automation + group: ansible + id: ansible-link + title: Ansible FOO + href: /ansible/foo + description: Ansible FOO description thing + icon: AnsibleIcon + - section: iam + group: iam + id: iam-link + title: IAM FOO + href: /iam + description: Some Iam thing + icon: IAMIcon + isExternal: true diff --git a/tests/e2e/generate-service-tiles/02-assert.yaml b/tests/e2e/generate-service-tiles/02-assert.yaml new file mode 100644 index 00000000..50511d79 --- /dev/null +++ b/tests/e2e/generate-service-tiles/02-assert.yaml @@ -0,0 +1,52 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: service-tiles-frontend + namespace: test-service-tiles + labels: + frontend: service-tiles + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + name: service-tiles +spec: + selector: + matchLabels: + frontend: service-tiles + template: + spec: + volumes: + - name: config + configMap: + name: test-service-tiles-environment + defaultMode: 420 + containers: + - name: fe-image + image: quay.io/cloudservices/service-tiles-frontend:3244a17 + ports: + - name: web + containerPort: 80 + protocol: TCP + - name: metrics + containerPort: 9000 + protocol: TCP + volumeMounts: + - name: config + mountPath: /opt/app-root/src/build/stable/operator-generated +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: test-service-tiles-environment + namespace: test-service-tiles + labels: + frontendenv: test-service-tiles-environment + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + name: test-service-tiles-environment +data: + fed-modules.json: >- + {"service-tiles":{"manifestLocation":"/apps/service-tiles/fed-mods.json","moduleID":"service-tiles","fullProfile":false}} + service-tiles.json: >- + [{"id":"automation","title":"Automation","groups":[{"id":"ansible","title":"Ansible","tiles":[{"section":"automation","group":"ansible","id":"ansible-link","href":"/ansible/foo","title":"Ansible FOO","description":"Ansible FOO description thing","icon":"AnsibleIcon"}]},{"id":"rhel","title":"Red Hat Enterprise Linux","tiles":[]}]},{"id":"iam","title":"Identity and Access Management","groups":[{"id":"iam","title":"IAM","tiles":[{"section":"iam","group":"iam","id":"iam-link","href":"/iam","title":"IAM FOO","description":"Some Iam thing","icon":"IAMIcon","isExternal":true}]}]}] From 3636b394f5fbf68f8bf575cc5741c8e2c4e0c929 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Tue, 22 Oct 2024 17:51:17 +0200 Subject: [PATCH 4/6] Make sure order does not matter in configmap tests. --- controllers/frontend_controller_suite_test.go | 32 +++++++++++++------ controllers/reconcile.go | 17 +++++++--- kuttl-config.yml | 2 +- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index 00f5562d..0adef3bf 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "time" crd "github.com/RedHatInsights/frontend-operator/api/v1alpha1" @@ -1115,11 +1116,19 @@ var _ = ginkgo.Describe("Search index", func() { } return true }, timeout, interval).Should(gomega.BeTrue()) + searchIndexMap, ok := createdConfigMap.Data["search-index.json"] + gomega.Expect(ok).Should(gomega.BeTrue()) + var sortedSearchIndex []crd.SearchEntry + err := json.Unmarshal([]byte(searchIndexMap), &sortedSearchIndex) + gomega.Expect(err).Should(gomega.BeNil()) + sort.Slice(sortedSearchIndex, func(i, j int) bool { + return sortedSearchIndex[i].ID < sortedSearchIndex[j].ID + }) + var expectedIndex []crd.SearchEntry + err = json.Unmarshal([]byte("[{\"id\":\"test-search-index2-test-search-index-env2-test-search-index2\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index3-test-search-index-env2-test-search-index3\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"}]"), &expectedIndex) + gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(FrontendEnvName2)) - gomega.Expect(createdConfigMap.Data).Should(gomega.Equal(map[string]string{ - "fed-modules.json": "{\"testSearchIndex2\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false},\"testSearchIndex3\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false}}", - "search-index.json": "[{\"id\":\"test-search-index2-test-search-index-env2-test-search-index2\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index3-test-search-index-env2-test-search-index3\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"}]", - })) + gomega.Expect(sortedSearchIndex).Should(gomega.ConsistOf(expectedIndex[0], expectedIndex[1])) gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(FrontendEnvName2)) }) }) @@ -1135,7 +1144,7 @@ type WidgetCase struct { WidgetsFrontend []WidgetFrontendTestEntry Namespace string Environment string - ExpectedConfigMapEntry string + ExpectedConfigMapEntry []crd.WidgetEntry } func frontendFromWidget(wc WidgetCase, wf WidgetFrontendTestEntry) *crd.Frontend { @@ -1185,7 +1194,6 @@ func mockFrontendEnv(env string, namespace string) *crd.FrontendEnvironment { GenerateNavJSON: false, }, } - } var _ = ginkgo.Describe("Widget registry", func() { @@ -1244,8 +1252,7 @@ var _ = ginkgo.Describe("Widget registry", func() { ginkgo.It("Should create widget registry", func() { ginkgo.By("collection entries from Frontend resources", func() { - expectedResult, err := json.Marshal([]crd.WidgetEntry{*Widget1, *Widget2, *Widget3}) - gomega.Expect(err).Should(gomega.BeNil()) + expectedResult := []crd.WidgetEntry{*Widget1, *Widget2, *Widget3} widgetCases := []WidgetCase{{ WidgetsFrontend: []WidgetFrontendTestEntry{{ Widgets: []*crd.WidgetEntry{Widget1, Widget2}, @@ -1257,7 +1264,7 @@ var _ = ginkgo.Describe("Widget registry", func() { }, Namespace: FrontendNamespace, Environment: FrontendEnvName, - ExpectedConfigMapEntry: string(expectedResult), + ExpectedConfigMapEntry: expectedResult, }} for _, widgetCase := range widgetCases { @@ -1283,9 +1290,14 @@ var _ = ginkgo.Describe("Widget registry", func() { }, timeout, interval).Should(gomega.BeTrue()) widgetRegistryMap := createdConfigMap.Data["widget-registry.json"] + var widgetRegistry []crd.WidgetEntry + err := json.Unmarshal([]byte(widgetRegistryMap), &widgetRegistry) + gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(widgetCase.Environment)) - gomega.Expect(widgetRegistryMap).Should(gomega.Equal(widgetCase.ExpectedConfigMapEntry)) + for _, w := range expectedResult { + gomega.Expect(widgetRegistry).Should(gomega.ContainElement(w)) + } gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(widgetCase.Environment)) } }) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 57c96589..a90c3144 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -893,11 +893,11 @@ func getServiceTilePath(section string, group string) string { return fmt.Sprintf("%s-%s", section, group) } -func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendEnvironment) []crd.FrontendServiceCategoryGenerated { +func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendServiceCategoryGenerated, []string) { categories := []crd.FrontendServiceCategoryGenerated{} if feEnvironment.Spec.ServiceCategories == nil { // skip if we do not have service categories - return categories + return categories, []string{} } // just a quick cache to make it easier and faster to assign tiles to their destination @@ -925,20 +925,23 @@ func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendE categories = append(categories, newCategory) } + skippedTiles := []string{} for _, frontend := range felist.Items { if frontend.Spec.ServiceTiles != nil { for _, tile := range frontend.Spec.ServiceTiles { groupKey := getServiceTilePath(tile.Section, tile.Group) - // ignore the tile if destination does not exist if groupTiles, ok := tileGroupAccessMap[groupKey]; ok { // assign the tile to the service category and group *groupTiles = append(*groupTiles, *tile) + } else { + // ignore the tile if destination does not exist + skippedTiles = append(skippedTiles, tile.ID) } } } } - return categories + return categories, skippedTiles } func (r *FrontendReconciliation) setupBundleData(cfgMap *v1.ConfigMap, cacheMap map[string]crd.Frontend) error { @@ -1071,7 +1074,7 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa widgetRegistry := setupWidgetRegistry(feList) - serviceCategories := setupServiceTilesData(feList, *r.FrontendEnvironment) + serviceCategories, skippedTiles := setupServiceTilesData(feList, *r.FrontendEnvironment) fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { @@ -1095,6 +1098,10 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa return err } + if len(skippedTiles) > 0 { + r.Log.Info("Unable to find service categories for tiles:", strings.Join(skippedTiles, ",")) + } + cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) if len(searchIndex) > 0 { cfgMap.Data["search-index.json"] = string(searchIndexJSONData) diff --git a/kuttl-config.yml b/kuttl-config.yml index e18b057d..4ad2fc36 100644 --- a/kuttl-config.yml +++ b/kuttl-config.yml @@ -5,7 +5,7 @@ startControlPlane: true crdDir: config/crd/test-resources/ testDirs: - tests/e2e/ -timeout: 30 +timeout: 320 parallel: 1 commands: - command: make install From 2d23975e5232cd749176bdd4a06ee4343db5776f Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Wed, 23 Oct 2024 12:00:52 +0200 Subject: [PATCH 5/6] Reduce search index copy paste in tests. --- controllers/frontend_controller_suite_test.go | 240 ++++++++---------- 1 file changed, 102 insertions(+), 138 deletions(-) diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index 0adef3bf..5d4ce538 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -925,6 +925,69 @@ var _ = ginkgo.Describe("Dependencies", func() { }) }) + +type SearchFrontendEntry struct { + Name string + SearchEntries []*crd.SearchEntry +} + +type SearchIndexCase struct { + SearchFrontendEntries []SearchFrontendEntry + Env string + ExpectedResult string + Namespace string +} + +func frontendFromSearchEntry(tc SearchIndexCase, entry SearchFrontendEntry) *crd.Frontend { + frontend := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: entry.Name, + Namespace: tc.Namespace, + }, + Spec: crd.FrontendSpec{ + EnvName: tc.Env, + Title: "", + DeploymentRepo: "", + Frontend: crd.FrontendInfo{ + Paths: []string{"/"}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "", + Modules: []crd.Module{}, + }, + SearchEntries: entry.SearchEntries, + }, + } + + return frontend +} + +func mockFrontendEnv(env string, namespace string) *crd.FrontendEnvironment { + return &crd.FrontendEnvironment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "FrontendEnvironment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: env, + Namespace: namespace, + }, + Spec: crd.FrontendEnvironmentSpec{ + SSO: "https://something-auth", + Hostname: "something", + Monitoring: &crd.MonitoringConfig{ + Mode: "app-interface", + }, + GenerateNavJSON: false, + }, + } +} + var _ = ginkgo.Describe("Search index", func() { const ( FrontendName = "test-search-index" @@ -944,29 +1007,12 @@ var _ = ginkgo.Describe("Search index", func() { ginkgo.By("from single Frontend resource", func() { ctx := context.Background() - configMapLookupKey := types.NamespacedName{Name: FrontendEnvName, Namespace: FrontendNamespace} - - frontend := &crd.Frontend{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "Frontend", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: FrontendName, - Namespace: FrontendNamespace, - }, - Spec: crd.FrontendSpec{ - EnvName: FrontendEnvName, - Title: "", - DeploymentRepo: "", - Frontend: crd.FrontendInfo{ - Paths: []string{"/things/test"}, - }, - Image: "my-image:version", - Module: &crd.FedModule{ - ManifestLocation: "/apps/inventory/fed-mods.json", - Modules: []crd.Module{}, - }, + testCase := SearchIndexCase{ + Env: FrontendEnvName, + Namespace: FrontendNamespace, + ExpectedResult: "[{\"id\":\"test-search-index-test-search-index-env-test\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index-test-search-index-env-test2\",\"href\":\"/test2/href\",\"title\":\"Test2\",\"description\":\"Test2 description\"}]", + SearchFrontendEntries: []SearchFrontendEntry{{ + Name: FrontendName, SearchEntries: []*crd.SearchEntry{{ ID: "test", Href: "/test/href", @@ -978,27 +1024,14 @@ var _ = ginkgo.Describe("Search index", func() { Title: "Test2", Description: "Test2 description", }}, - }, + }}, } - gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) - frontendEnvironment := &crd.FrontendEnvironment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "FrontendEnvironment", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: FrontendEnvName, - Namespace: FrontendNamespace, - }, - Spec: crd.FrontendEnvironmentSpec{ - SSO: "https://something-auth", - Hostname: "something", - Monitoring: &crd.MonitoringConfig{ - Mode: "app-interface", - }, - GenerateNavJSON: false, - }, + configMapLookupKey := types.NamespacedName{Name: testCase.Env, Namespace: testCase.Namespace} + for _, tc := range testCase.SearchFrontendEntries { + frontend := frontendFromSearchEntry(testCase, tc) + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) } + frontendEnvironment := mockFrontendEnv(testCase.Env, testCase.Namespace) gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) createdConfigMap := &v1.ConfigMap{} gomega.Eventually(func() bool { @@ -1012,98 +1045,46 @@ var _ = ginkgo.Describe("Search index", func() { return true }, timeout, interval).Should(gomega.BeTrue()) gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(FrontendEnvName)) - gomega.Expect(createdConfigMap.Data).Should(gomega.Equal(map[string]string{ - "fed-modules.json": "{\"testSearchIndex\":{\"manifestLocation\":\"/apps/inventory/fed-mods.json\",\"fullProfile\":false}}", - "search-index.json": "[{\"id\":\"test-search-index-test-search-index-env-test\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index-test-search-index-env-test2\",\"href\":\"/test2/href\",\"title\":\"Test2\",\"description\":\"Test2 description\"}]", - })) + + searchIndexMap, ok := createdConfigMap.Data["search-index.json"] + gomega.Expect(ok).Should(gomega.BeTrue()) + gomega.Expect(searchIndexMap).Should(gomega.Equal(testCase.ExpectedResult)) gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(FrontendEnvName)) }) ginkgo.By("from multiple Frontend resources", func() { ctx := context.Background() - configMapLookupKey := types.NamespacedName{Name: FrontendEnvName2, Namespace: FrontendNamespace} - - frontend2 := &crd.Frontend{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "Frontend", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: FrontendName2, - Namespace: FrontendNamespace, - }, - Spec: crd.FrontendSpec{ - EnvName: FrontendEnvName2, - Title: "", - DeploymentRepo: "", - Frontend: crd.FrontendInfo{ - Paths: []string{"/things/test"}, - }, - Image: "my-image:version", - Module: &crd.FedModule{ - ManifestLocation: "/apps/inventory/fed-mods.json", - Modules: []crd.Module{}, - }, + testCase := SearchIndexCase{ + Env: FrontendEnvName2, + Namespace: FrontendNamespace, + ExpectedResult: "[{\"id\":\"test-search-index2-test-search-index-env2-test-search-index2\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index3-test-search-index-env2-test-search-index3\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"}]", + SearchFrontendEntries: []SearchFrontendEntry{{ + Name: FrontendName2, SearchEntries: []*crd.SearchEntry{{ ID: FrontendName2, Href: "/test/href", Title: "Test", Description: "Test description", }}, - }, - } - gomega.Expect(k8sClient.Create(ctx, frontend2)).Should(gomega.Succeed()) - - frontend3 := &crd.Frontend{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "Frontend", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: FrontendName3, - Namespace: FrontendNamespace, - }, - Spec: crd.FrontendSpec{ - EnvName: FrontendEnvName2, - Title: "", - DeploymentRepo: "", - Frontend: crd.FrontendInfo{ - Paths: []string{"/things/test"}, - }, - Image: "my-image:version", - Module: &crd.FedModule{ - ManifestLocation: "/apps/inventory/fed-mods.json", - Modules: []crd.Module{}, - }, + }, { + Name: FrontendName3, SearchEntries: []*crd.SearchEntry{{ ID: FrontendName3, Href: "/test/href", Title: "Test", Description: "Test description", }}, - }, + }}, } - gomega.Expect(k8sClient.Create(ctx, frontend3)).Should(gomega.Succeed()) - frontendEnvironment := &crd.FrontendEnvironment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "FrontendEnvironment", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: FrontendEnvName2, - Namespace: FrontendNamespace, - }, - Spec: crd.FrontendEnvironmentSpec{ - SSO: "https://something-auth", - Hostname: "something", - Monitoring: &crd.MonitoringConfig{ - Mode: "app-interface", - }, - GenerateNavJSON: false, - }, + configMapLookupKey := types.NamespacedName{Name: testCase.Env, Namespace: testCase.Namespace} + for _, tc := range testCase.SearchFrontendEntries { + frontend := frontendFromSearchEntry(testCase, tc) + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) } + + frontendEnvironment := mockFrontendEnv(testCase.Env, testCase.Namespace) gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) createdConfigMap := &v1.ConfigMap{} gomega.Eventually(func() bool { @@ -1118,6 +1099,7 @@ var _ = ginkgo.Describe("Search index", func() { }, timeout, interval).Should(gomega.BeTrue()) searchIndexMap, ok := createdConfigMap.Data["search-index.json"] gomega.Expect(ok).Should(gomega.BeTrue()) + // Make sure the order does not break the tests var sortedSearchIndex []crd.SearchEntry err := json.Unmarshal([]byte(searchIndexMap), &sortedSearchIndex) gomega.Expect(err).Should(gomega.BeNil()) @@ -1125,11 +1107,14 @@ var _ = ginkgo.Describe("Search index", func() { return sortedSearchIndex[i].ID < sortedSearchIndex[j].ID }) var expectedIndex []crd.SearchEntry - err = json.Unmarshal([]byte("[{\"id\":\"test-search-index2-test-search-index-env2-test-search-index2\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"},{\"id\":\"test-search-index3-test-search-index-env2-test-search-index3\",\"href\":\"/test/href\",\"title\":\"Test\",\"description\":\"Test description\"}]"), &expectedIndex) + err = json.Unmarshal([]byte(testCase.ExpectedResult), &expectedIndex) gomega.Expect(err).Should(gomega.BeNil()) - gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(FrontendEnvName2)) - gomega.Expect(sortedSearchIndex).Should(gomega.ConsistOf(expectedIndex[0], expectedIndex[1])) - gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(FrontendEnvName2)) + gomega.Expect(createdConfigMap.Name).Should(gomega.Equal(testCase.Env)) + + for _, expectedCase := range expectedIndex { + gomega.Expect(sortedSearchIndex).Should(gomega.ContainElement(expectedCase)) + } + gomega.Expect(createdConfigMap.ObjectMeta.OwnerReferences[0].Name).Should(gomega.Equal(testCase.Env)) }) }) }) @@ -1175,27 +1160,6 @@ func frontendFromWidget(wc WidgetCase, wf WidgetFrontendTestEntry) *crd.Frontend return frontend } -func mockFrontendEnv(env string, namespace string) *crd.FrontendEnvironment { - return &crd.FrontendEnvironment{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "cloud.redhat.com/v1", - Kind: "FrontendEnvironment", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: env, - Namespace: namespace, - }, - Spec: crd.FrontendEnvironmentSpec{ - SSO: "https://something-auth", - Hostname: "something", - Monitoring: &crd.MonitoringConfig{ - Mode: "app-interface", - }, - GenerateNavJSON: false, - }, - } -} - var _ = ginkgo.Describe("Widget registry", func() { const ( FrontendName = "test-widget-registry" From ba894273f0b09988c5bd7dc64a16c63cc59f2654 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Fri, 25 Oct 2024 12:06:38 +0200 Subject: [PATCH 6/6] Pr feedback --- controllers/reconcile.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index a90c3144..5da07293 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -854,13 +854,15 @@ func setupFedModules(feEnv *crd.FrontendEnvironment, frontendList *crd.FrontendL } func adjustSearchEntry(searchEntry *crd.SearchEntry, frontend crd.Frontend) crd.SearchEntry { + altTitleCopy := make([]string, len(searchEntry.AltTitle)) + copy(altTitleCopy, searchEntry.AltTitle) newSearchEntry := crd.SearchEntry{ // make the id environment and frontend specific to reduce duplicate ids across Frontend resources ID: fmt.Sprintf("%s-%s-%s", frontend.Name, frontend.Spec.EnvName, searchEntry.ID), Title: searchEntry.Title, Description: searchEntry.Description, Href: searchEntry.Href, - AltTitle: searchEntry.AltTitle, + AltTitle: altTitleCopy, IsExternal: searchEntry.IsExternal, } return newSearchEntry @@ -871,16 +873,18 @@ func setupSearchIndex(feList *crd.FrontendList) []crd.SearchEntry { for _, frontend := range feList.Items { if frontend.Spec.SearchEntries != nil { for _, searchEntry := range frontend.Spec.SearchEntries { - searchIndex = append(searchIndex, adjustSearchEntry(searchEntry, frontend)) + if searchEntry != nil { + searchIndex = append(searchIndex, adjustSearchEntry(searchEntry, frontend)) + } } } } return searchIndex } -func setupWidgetRegistry(felist *crd.FrontendList) []crd.WidgetEntry { +func setupWidgetRegistry(feList *crd.FrontendList) []crd.WidgetEntry { widgetRegistry := []crd.WidgetEntry{} - for _, frontend := range felist.Items { + for _, frontend := range feList.Items { for _, widget := range frontend.Spec.WidgetRegistry { widgetRegistry = append(widgetRegistry, *widget) } @@ -893,7 +897,7 @@ func getServiceTilePath(section string, group string) string { return fmt.Sprintf("%s-%s", section, group) } -func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendServiceCategoryGenerated, []string) { +func setupServiceTilesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendServiceCategoryGenerated, []string) { categories := []crd.FrontendServiceCategoryGenerated{} if feEnvironment.Spec.ServiceCategories == nil { // skip if we do not have service categories @@ -926,7 +930,7 @@ func setupServiceTilesData(felist *crd.FrontendList, feEnvironment crd.FrontendE } skippedTiles := []string{} - for _, frontend := range felist.Items { + for _, frontend := range feList.Items { if frontend.Spec.ServiceTiles != nil { for _, tile := range frontend.Spec.ServiceTiles { groupKey := getServiceTilePath(tile.Section, tile.Group)