Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/v1alpha1/scalityui_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ import (
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// DeployedUIApp represents a deployed UI application entry
type DeployedUIApp struct {
// AppHistoryBasePath is the base path for the app's history
// +kubebuilder:validation:Required
AppHistoryBasePath string `json:"appHistoryBasePath"`
// Kind specifies the type of UI app (e.g., "micro-app", "solution")
// +kubebuilder:validation:Required
Kind string `json:"kind"`
// Name is the unique identifier for the app
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// URL is the path where the app is served
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
URL string `json:"url"`
// Version is the app version
// +kubebuilder:validation:Required
Version string `json:"version"`
}

// ScalityUISpec defines the desired state of ScalityUI
type ScalityUISpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Expand All @@ -39,6 +60,7 @@ type ScalityUISpec struct {
Auth *AuthConfig `json:"auth,omitempty"`
UIConfig *UIConfig `json:"uiConfig,omitempty"`
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
ExtraUIApps []DeployedUIApp `json:"extraUIApps,omitempty"`

// Scheduling defines pod scheduling constraints for the UI deployment
Scheduling *PodSchedulingSpec `json:"scheduling,omitempty"`
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions config/crd/bases/ui.scality.com_scalityuis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ spec:
description: Scopes specifies the OIDC scopes
type: string
type: object
extraUIApps:
items:
description: DeployedUIApp represents a deployed UI application
entry
properties:
appHistoryBasePath:
description: AppHistoryBasePath is the base path for the app's
history
type: string
kind:
description: Kind specifies the type of UI app (e.g., "micro-app",
"solution")
type: string
name:
description: Name is the unique identifier for the app
minLength: 1
type: string
url:
description: URL is the path where the app is served
minLength: 1
type: string
version:
description: Version is the app version
type: string
required:
- appHistoryBasePath
- kind
- name
- url
- version
type: object
type: array
image:
description: |-
INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Expand Down
9 changes: 0 additions & 9 deletions internal/controller/scalityui/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,6 @@ import (
"github.com/scality/ui-operator/internal/services"
)

// DeployedUIApp represents a deployed UI application entry
type DeployedUIApp struct {
AppHistoryBasePath string `json:"appHistoryBasePath"`
Kind string `json:"kind"`
Name string `json:"name"`
URL string `json:"url"`
Version string `json:"version"`
}

// getOperatorNamespace returns the namespace where the operator is deployed.
// It reads from the POD_NAMESPACE environment variable, which should be set
// using the downward API in the operator's deployment.
Expand Down
137 changes: 137 additions & 0 deletions internal/controller/scalityui/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,143 @@ var _ = Describe("ScalityUI Shell Features", func() {
Expect(deployedAppsConfigMap.Annotations).To(HaveKey("scality.com/deployed-apps-hash"))
})

It("should include ExtraUIApps in deployed-ui-apps ConfigMap", func() {
By("Creating ScalityUI with ExtraUIApps")
extraUIAppName := "extra-ui-apps-test"
extraUI := &uiv1alpha1.ScalityUI{
ObjectMeta: metav1.ObjectMeta{Name: extraUIAppName},
Spec: uiv1alpha1.ScalityUISpec{
Image: "shell-ui:1.0.0",
ProductName: "Extra UI Apps Test",
ExtraUIApps: []uiv1alpha1.DeployedUIApp{
{
Name: "external-app",
Kind: "solution",
URL: "/external-app",
Version: "2.0.0",
AppHistoryBasePath: "/external-app",
},
{
Name: "another-app",
Kind: "micro-app",
URL: "/another-app",
Version: "1.5.0",
AppHistoryBasePath: "/another",
},
},
},
}
Expect(k8sClient.Create(ctx, extraUI)).To(Succeed())
defer k8sClient.Delete(ctx, extraUI)

By("Reconciling to create deployed-ui-apps ConfigMap")
reconciler := NewScalityUIReconcilerForTest(k8sClient, k8sClient.Scheme())
_, err := reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: extraUIAppName},
})
Expect(err).NotTo(HaveOccurred())

By("Verifying deployed-ui-apps ConfigMap contains ExtraUIApps")
verifyDeployedUIAppsContent(ctx, extraUIAppName, []map[string]interface{}{
{
"appHistoryBasePath": "/external-app",
"kind": "solution",
"name": "external-app",
"url": "/external-app",
"version": "2.0.0",
},
{
"appHistoryBasePath": "/another",
"kind": "micro-app",
"name": "another-app",
"url": "/another-app",
"version": "1.5.0",
},
})
})

It("should merge ExtraUIApps with exposer-based apps", func() {
By("Creating ScalityUI with ExtraUIApps")
mergeUIAppName := "merge-ui-apps-test"
mergeUI := &uiv1alpha1.ScalityUI{
ObjectMeta: metav1.ObjectMeta{Name: mergeUIAppName},
Spec: uiv1alpha1.ScalityUISpec{
Image: "shell-ui:1.0.0",
ProductName: "Merge UI Apps Test",
ExtraUIApps: []uiv1alpha1.DeployedUIApp{
{
Name: "extra-app",
Kind: "solution",
URL: "/extra-app",
Version: "3.0.0",
AppHistoryBasePath: "/extra",
},
},
},
}
Expect(k8sClient.Create(ctx, mergeUI)).To(Succeed())
defer k8sClient.Delete(ctx, mergeUI)

By("Creating component and exposer")
testNamespace := "test-merge-apps"
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
defer k8sClient.Delete(ctx, ns)

component := &uiv1alpha1.ScalityUIComponent{
ObjectMeta: metav1.ObjectMeta{Name: "merge-component", Namespace: testNamespace},
Spec: uiv1alpha1.ScalityUIComponentSpec{Image: "component:1.0.0"},
}
Expect(k8sClient.Create(ctx, component)).To(Succeed())
component.Status = uiv1alpha1.ScalityUIComponentStatus{
Kind: "micro-app", PublicPath: "/apps/merge-component", Version: "1.0.0",
}
component.Status.Conditions = []metav1.Condition{
{
Type: "ConfigurationRetrieved",
Status: metav1.ConditionTrue,
Reason: "FetchSucceeded",
LastTransitionTime: metav1.Now(),
},
}
Expect(k8sClient.Status().Update(ctx, component)).To(Succeed())
defer k8sClient.Delete(ctx, component)

exposer := &uiv1alpha1.ScalityUIComponentExposer{
ObjectMeta: metav1.ObjectMeta{Name: "merge-exposer", Namespace: testNamespace},
Spec: uiv1alpha1.ScalityUIComponentExposerSpec{
ScalityUI: mergeUIAppName, ScalityUIComponent: "merge-component", AppHistoryBasePath: "/merge-app",
},
}
Expect(k8sClient.Create(ctx, exposer)).To(Succeed())
defer k8sClient.Delete(ctx, exposer)

By("Reconciling to merge apps")
reconciler := NewScalityUIReconcilerForTest(k8sClient, k8sClient.Scheme())
_, err := reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: mergeUIAppName},
})
Expect(err).NotTo(HaveOccurred())

By("Verifying deployed-ui-apps ConfigMap contains both exposer-based and ExtraUIApps")
verifyDeployedUIAppsContent(ctx, mergeUIAppName, []map[string]interface{}{
{
"appHistoryBasePath": "/merge-app",
"kind": "micro-app",
"name": "merge-component",
"url": "/apps/merge-component",
"version": "1.0.0",
},
{
"appHistoryBasePath": "/extra",
"kind": "solution",
"name": "extra-app",
"url": "/extra-app",
"version": "3.0.0",
},
})
})

It("should trigger rolling update when exposer is removed", func() {
By("Deploying the Shell UI with an exposer")
reconciler := NewScalityUIReconcilerForTest(k8sClient, k8sClient.Scheme())
Expand Down
13 changes: 10 additions & 3 deletions internal/controller/scalityui/deployed_apps_configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/go-logr/logr"
uiv1alpha1 "github.com/scality/ui-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -26,7 +27,7 @@ func newDeployedAppsConfigMapReducer(r *ScalityUIReconciler, cr ScalityUI, curre
}

// Build the deployed apps list with component deduplication
deployedApps := make([]DeployedUIApp, 0)
deployedApps := make([]uiv1alpha1.DeployedUIApp, 0)
processedComponents := make(map[string]bool) // Track which components we've already processed

for _, exposer := range exposers {
Expand All @@ -48,7 +49,7 @@ func newDeployedAppsConfigMapReducer(r *ScalityUIReconciler, cr ScalityUI, curre
}

if meta.IsStatusConditionTrue(component.Status.Conditions, "ConfigurationRetrieved") {
deployedApp := DeployedUIApp{
deployedApp := uiv1alpha1.DeployedUIApp{
AppHistoryBasePath: exposer.Spec.AppHistoryBasePath,
Kind: component.Status.Kind,
Name: component.Name,
Expand All @@ -60,6 +61,9 @@ func newDeployedAppsConfigMapReducer(r *ScalityUIReconciler, cr ScalityUI, curre
}
}

// Append ExtraUIApps directly from ScalityUI spec
deployedApps = append(deployedApps, cr.Spec.ExtraUIApps...)
Comment on lines +64 to +65
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: No deduplication logic for app names between ExtraUIApps and exposer-based apps. If an app in ExtraUIApps has the same name as an exposer-based app, both will be included in the deployed apps list, which could cause conflicts or unexpected behavior in the UI. Consider adding logic to detect and handle duplicate app names, either by warning the user, preventing duplicates, or documenting the expected behavior when duplicates occur.

Copilot uses AI. Check for mistakes.

// Create or update the deployed-ui-apps ConfigMap
configMapName := cr.Name + "-deployed-ui-apps"
configMap := &corev1.ConfigMap{
Expand Down Expand Up @@ -102,7 +106,10 @@ func newDeployedAppsConfigMapReducer(r *ScalityUIReconciler, cr ScalityUI, curre

logOperationResult(log, result, "ConfigMap deployed-ui-apps", configMapName)
log.Info("Successfully reconciled deployed UI apps",
"appsCount", len(deployedApps), "configMap", configMapName)
"totalAppsCount", len(deployedApps),
"exposerAppsCount", len(deployedApps)-len(cr.Spec.ExtraUIApps),
"extraUIAppsCount", len(cr.Spec.ExtraUIApps),
"configMap", configMapName)

// Store hash in memory for deployment to use (avoids cache sync issues)
currentState.SetSubresourceHash(deployedAppsConfigMapHashKey, deployedAppsHash)
Expand Down
Loading