diff --git a/api/v1alpha1/frontend_types.go b/api/v1alpha1/frontend_types.go index 361e2aee..124c3716 100644 --- a/api/v1alpha1/frontend_types.go +++ b/api/v1alpha1/frontend_types.go @@ -93,7 +93,7 @@ type WidgetEntry struct { } type NavigationSegment struct { - SectionID string `json:"sectionId" yaml:"sectionId"` + SegmentID string `json:"segmentId" yaml:"segmentId"` // Id of the bundle to which the segment should be injected BundleID string `json:"bundleId" yaml:"bundleId"` // A position of the segment within the bundle diff --git a/api/v1alpha1/frontendenvironment_types.go b/api/v1alpha1/frontendenvironment_types.go index dfc034dc..7ceff5bd 100644 --- a/api/v1alpha1/frontendenvironment_types.go +++ b/api/v1alpha1/frontendenvironment_types.go @@ -24,6 +24,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +// FrontendBundles defines the bundles specific to an environment that will be used to +// construct navigation +type FrontendBundles struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` +} + +// The frontend bundles but with the nav items filled with chrome nav items +type FrontendBundlesGenerated struct { + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + NavItems *[]ChromeNavItem `json:"navItems" yaml:"navItems"` +} + type FrontendServiceCategoryGroup struct { ID string `json:"id" yaml:"id"` Title string `json:"title" yaml:"title"` @@ -105,6 +119,8 @@ type FrontendEnvironmentSpec struct { HTTPHeaders map[string]string `json:"httpHeaders,omitempty"` DefaultReplicas *int32 `json:"defaultReplicas,omitempty" yaml:"defaultReplicas,omitempty"` + // For the ChromeUI to render navigation bundles + Bundles *[]FrontendBundles `json:"bundles,omitempty" yaml:"bundles,omitempty"` } type MonitoringConfig struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index cf9e3ee5..ebb66322 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -386,6 +386,47 @@ func (in *Frontend) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendBundles) DeepCopyInto(out *FrontendBundles) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendBundles. +func (in *FrontendBundles) DeepCopy() *FrontendBundles { + if in == nil { + return nil + } + out := new(FrontendBundles) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FrontendBundlesGenerated) DeepCopyInto(out *FrontendBundlesGenerated) { + *out = *in + if in.NavItems != nil { + in, out := &in.NavItems, &out.NavItems + *out = new([]ChromeNavItem) + if **in != nil { + in, out := *in, *out + *out = make([]ChromeNavItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendBundlesGenerated. +func (in *FrontendBundlesGenerated) DeepCopy() *FrontendBundlesGenerated { + if in == nil { + return nil + } + out := new(FrontendBundlesGenerated) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FrontendDeployments) DeepCopyInto(out *FrontendDeployments) { *out = *in @@ -513,6 +554,15 @@ func (in *FrontendEnvironmentSpec) DeepCopyInto(out *FrontendEnvironmentSpec) { *out = new(int32) **out = **in } + if in.Bundles != nil { + in, out := &in.Bundles, &out.Bundles + *out = new([]FrontendBundles) + if **in != nil { + in, out := *in, *out + *out = make([]FrontendBundles, len(*in)) + copy(*out, *in) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrontendEnvironmentSpec. diff --git a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml index 193019a8..500068be 100644 --- a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml +++ b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml @@ -67,6 +67,22 @@ spec: description: The name of the secret we will use to get the akamai credentials type: string + bundles: + description: For the ChromeUI to render navigation bundles + items: + description: |- + FrontendBundles defines the bundles specific to an environment that will be used to + construct navigation + properties: + id: + type: string + title: + type: string + required: + - id + - title + type: object + type: array defaultReplicas: format: int32 type: integer diff --git a/config/crd/bases/cloud.redhat.com_frontends.yaml b/config/crd/bases/cloud.redhat.com_frontends.yaml index cd025490..5dc591c4 100644 --- a/config/crd/bases/cloud.redhat.com_frontends.yaml +++ b/config/crd/bases/cloud.redhat.com_frontends.yaml @@ -376,13 +376,13 @@ spec: 0 is the first position The position "steps" should be at least 100 to make sure there is enough space in case some segments should be injected between existing ones type: integer - sectionId: + segmentId: type: string required: - bundleId - navItems - position - - sectionId + - segmentId type: object type: array replicas: diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 53be5957..565ca579 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -988,6 +988,56 @@ func setupServiceTilesData(feList *crd.FrontendList, feEnvironment crd.FrontendE return categories, skippedTiles } +func getNavItemPath(feName string, bundleID string, segmentID string) string { + return fmt.Sprintf("%s-%s-%s", feName, bundleID, segmentID) +} + +func setupBundlesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendBundlesGenerated, []string) { + bundles := []crd.FrontendBundlesGenerated{} + if feEnvironment.Spec.Bundles == nil { + // skip if we do not have bundles in fe environment + return bundles, []string{} + } + + skippedNavItemsMap := make(map[string][]string) + bundleNavSegmentMap := make(map[string][]crd.NavigationSegment) + for _, frontend := range feList.Items { + if frontend.Spec.FeoConfigEnabled && frontend.Spec.NavigationSegments != nil { + for _, navSegment := range frontend.Spec.NavigationSegments { + bundleNavSegmentMap[navSegment.BundleID] = append(bundleNavSegmentMap[navSegment.BundleID], *navSegment) + skippedNavItemsMap[navSegment.BundleID] = append(skippedNavItemsMap[navSegment.BundleID], getNavItemPath(frontend.Name, navSegment.BundleID, navSegment.SegmentID)) + } + } + } + + for _, bundle := range *feEnvironment.Spec.Bundles { + delete(skippedNavItemsMap, bundle.ID) + sort.Slice(bundleNavSegmentMap[bundle.ID], func(i, j int) bool { + if (bundleNavSegmentMap[bundle.ID])[i].Position == (bundleNavSegmentMap[bundle.ID])[j].Position { + return (bundleNavSegmentMap[bundle.ID])[i].SegmentID[0] < (bundleNavSegmentMap[bundle.ID])[j].SegmentID[0] + } + return (bundleNavSegmentMap[bundle.ID])[i].Position < (bundleNavSegmentMap[bundle.ID])[j].Position + }) + navItems := []crd.ChromeNavItem{} + for _, navSegment := range bundleNavSegmentMap[bundle.ID] { + navItems = append(navItems, *navSegment.NavItems...) + } + newBundle := crd.FrontendBundlesGenerated{ + ID: bundle.ID, + Title: bundle.Title, + NavItems: &navItems, + } + bundles = append(bundles, newBundle) + } + + skippedNavItems := []string{} + for _, skipped := range skippedNavItemsMap { + skippedNavItems = append(skippedNavItems, skipped...) + } + + return bundles, skippedNavItems +} + func (r *FrontendReconciliation) setupBundleData(_ *v1.ConfigMap, _ map[string]crd.Frontend) error { bundleList := &crd.BundleList{} @@ -1127,6 +1177,8 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa serviceCategories, skippedTiles := setupServiceTilesData(feList, *r.FrontendEnvironment) + bundles, skippedBundles := setupBundlesData(feList, *r.FrontendEnvironment) + fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { return err @@ -1149,10 +1201,19 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa return err } + bundlesJSONData, err := json.Marshal(bundles) + if err != nil { + return err + } + if len(skippedTiles) > 0 { r.Log.Info("Unable to find service categories for tiles:", strings.Join(skippedTiles, ",")) } + if len(skippedBundles) > 0 { + r.Log.Info("Unable to find bundle for nav items:", "skippedBundles", strings.Join(skippedBundles, ",")) + } + cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) if len(searchIndex) > 0 { cfgMap.Data["search-index.json"] = string(searchIndexJSONData) @@ -1166,6 +1227,10 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa cfgMap.Data["service-tiles.json"] = string(serviceCategoriesJSONData) } + if len(bundles) > 0 { + cfgMap.Data["bundles.json"] = string(bundlesJSONData) + } + return nil } diff --git a/deploy.yml b/deploy.yml index f0a25363..92b678ad 100644 --- a/deploy.yml +++ b/deploy.yml @@ -270,6 +270,23 @@ objects: description: The name of the secret we will use to get the akamai credentials type: string + bundles: + description: For the ChromeUI to render navigation bundles + items: + description: 'FrontendBundles defines the bundles specific to + an environment that will be used to + + construct navigation' + properties: + id: + type: string + title: + type: string + required: + - id + - title + type: object + type: array defaultReplicas: format: int32 type: integer @@ -780,13 +797,13 @@ objects: there is enough space in case some segments should be injected between existing ones' type: integer - sectionId: + segmentId: type: string required: - bundleId - navItems - position - - sectionId + - segmentId type: object type: array replicas: diff --git a/examples/feenvironment.yaml b/examples/feenvironment.yaml index d1dcb5ce..63760c9c 100644 --- a/examples/feenvironment.yaml +++ b/examples/feenvironment.yaml @@ -22,3 +22,10 @@ spec: groups: - id: iam title: IAM + bundles: + - id: rhel + title: Red Hat Enterprise Linux + - id: ansible + title: Ansible + - id: settings + title: Settings diff --git a/examples/landing.yaml b/examples/landing.yaml index 380e2790..89826760 100644 --- a/examples/landing.yaml +++ b/examples/landing.yaml @@ -77,3 +77,21 @@ spec: description: Some Iam thing icon: IAMIcon isExternal: false + navigationSegments: + - segmentId: inventory-partial + bundleId: insights + position: 100 + navItems: + - id: landing + title: Landing section + href: /apps/landing + - id: bar + title: Some new link + expandable: true + routes: + - id: foo + title: Foo + href: /nested/bar + - id: baz + title: Some new link + href: /baz diff --git a/tests/e2e/generate-bundles/00-create-namespace.yaml b/tests/e2e/generate-bundles/00-create-namespace.yaml new file mode 100644 index 00000000..ead7ebc5 --- /dev/null +++ b/tests/e2e/generate-bundles/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-generate-bundles +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/generate-bundles/01-create-resources.yaml b/tests/e2e/generate-bundles/01-create-resources.yaml new file mode 100644 index 00000000..9cddc798 --- /dev/null +++ b/tests/e2e/generate-bundles/01-create-resources.yaml @@ -0,0 +1,109 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-generate-bundles-environment +spec: + generateNavJSON: false + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com + bundles: + - id: rhel + title: Red Hat Enterprise Linux + - id: ansible + title: Ansible + - id: settings + title: Settings +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: landing-page + namespace: test-generate-bundles +spec: + envName: test-generate-bundles-environment + title: landing-page + deploymentRepo: https://github.com/RedHatInsights/landing-page-frontend + frontend: + paths: + - /apps/landing-page + image: "quay.io/cloudservices/landing-page-frontend:3244a17" + module: + manifestLocation: /apps/landing-page/fed-mods.json + modules: [] + moduleID: landing-page + navigationSegments: + - segmentId: inventory-last-segment + bundleId: rhel + position: 200 # should be last based on position values + navItems: + - id: landing2 + title: Landing section last + href: /apps/landing + - id: bar2 + title: Some new link + expandable: true + routes: + - id: foo + title: Foo + href: /nested/bar + - id: baz2 + title: Some new link + href: /baz + - segmentId: c-inventory-segment + bundleId: rhel + position: 100 + navItems: + - id: landing + title: Landing section c + href: /apps/landing + - id: bar + title: Some new link + expandable: true + routes: + - id: foo + title: Foo + href: /nested/bar + - id: baz + title: Some new link + href: /baz + - segmentId: a-inventory-segment + bundleId: rhel + position: 100 # collision with above segment - should sort by segmentId alphabetically + navItems: + - id: landing + title: Landing section A + href: /apps/landing + - segmentId: b-inventory-segment + bundleId: rhel + position: 100 # collision with above segment - should sort by segmentId alphabetically + navItems: + - id: landing + title: Landing section B + href: /apps/landing + - segmentId: d-inventory-segment + bundleId: rhel + position: 100 # collision with above segment - should sort by segmentId alphabetically + navItems: + - id: landing + title: Landing section D + href: /apps/landing + - segmentId: skipped-partial + bundleId: skipped + position: 100 + navItems: + - id: landing + title: Landing section + href: /apps/landing + - id: bar + title: Some new link + expandable: true + routes: + - id: foo + title: Foo + href: /nested/bar + - id: baz + title: Some new link + href: /baz + feoConfigEnabled: true diff --git a/tests/e2e/generate-bundles/02-assert.yaml b/tests/e2e/generate-bundles/02-assert.yaml new file mode 100644 index 00000000..498b9d2a --- /dev/null +++ b/tests/e2e/generate-bundles/02-assert.yaml @@ -0,0 +1,57 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: landing-page-frontend + namespace: test-generate-bundles + labels: + frontend: landing-page + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + name: landing-page +spec: + selector: + matchLabels: + frontend: landing-page + template: + spec: + volumes: + - name: config + configMap: + name: test-generate-bundles-environment + defaultMode: 420 + containers: + - name: fe-image + image: quay.io/cloudservices/landing-page-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-generate-bundles-environment + namespace: test-generate-bundles + labels: + frontendenv: test-generate-bundles-environment + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + name: test-generate-bundles-environment +data: + fed-modules.json: >- + {"landing-page":{"manifestLocation":"/apps/landing-page/fed-mods.json","moduleID":"landing-page","fullProfile":false}} + bundles.json: >- + [{"id":"rhel","title":"Red Hat Enterprise Linux","navItems":[{"href":"/apps/landing","title":"Landing + section A","id":"landing"},{"href":"/apps/landing","title":"Landing section B","id":"landing"},{"href":"/apps/landing","title":"Landing + section c","id":"landing"},{"expandable":true,"title":"Some new link","id":"bar","routes":[{"href":"/nested/bar","title":"Foo","id":"foo"}]},{"href":"/baz","title":"Some + new link","id":"baz"},{"href":"/apps/landing","title":"Landing section D","id":"landing"},{"href":"/apps/landing","title":"Landing + section last","id":"landing2"},{"expandable":true,"title":"Some new link","id":"bar2","routes":[{"href":"/nested/bar","title":"Foo","id":"foo"}]},{"href":"/baz","title":"Some + new link","id":"baz2"}]},{"id":"ansible","title":"Ansible","navItems":[]},{"id":"settings","title":"Settings","navItems":[]}]