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
3 changes: 3 additions & 0 deletions api/v1alpha1/scalityui_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ type ScalityUISpec struct {
Networks *UINetworks `json:"networks,omitempty"`
Auth *AuthConfig `json:"auth,omitempty"`
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`

// Scheduling defines pod scheduling constraints for the UI deployment
Scheduling *PodSchedulingSpec `json:"scheduling,omitempty"`
}

// Themes defines the various themes supported by the UI.
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/scalityuicomponent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type ScalityUIComponentSpec struct {
// ImagePullSecrets is a list of references to secrets in the same namespace to use for pulling any images
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`

// Scheduling defines pod scheduling constraints for the UI component deployment
Scheduling *PodSchedulingSpec `json:"scheduling,omitempty"`
}

// ScalityUIComponentStatus defines the observed state of ScalityUIComponent
Expand Down
40 changes: 40 additions & 0 deletions api/v1alpha1/scheduling_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
corev1 "k8s.io/api/core/v1"
)

// PodSchedulingSpec defines pod scheduling constraints that can be applied to workloads
type PodSchedulingSpec struct {
// Tolerations allows the pods to be scheduled on nodes with matching taints
// +optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`

// NodeSelector constrains the pods to run only on nodes with specific labels
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`

// Affinity defines scheduling constraints using node/pod affinity rules
// +optional
Affinity *corev1.Affinity `json:"affinity,omitempty"`

// PriorityClassName indicates the pod's priority class
// +optional
PriorityClassName string `json:"priorityClassName,omitempty"`
}
44 changes: 44 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.

982 changes: 982 additions & 0 deletions config/crd/bases/ui.scality.com_scalityuicomponents.yaml

Large diffs are not rendered by default.

982 changes: 982 additions & 0 deletions config/crd/bases/ui.scality.com_scalityuis.yaml

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions internal/controller/scalityui/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,57 @@ var _ = Describe("ScalityUI Shell Features", func() {
})
})

Describe("Shell UI Pod Scheduling", func() {
It("should apply tolerations from scheduling spec to deployment", func() {
By("Setting up a ScalityUI with tolerations")
reconciler := NewScalityUIReconcilerForTest(k8sClient, k8sClient.Scheme())

// Get current UI and add scheduling constraints
currentUI := &uiv1alpha1.ScalityUI{}
Expect(k8sClient.Get(ctx, clusterScopedName, currentUI)).To(Succeed())

currentUI.Spec.Scheduling = &uiv1alpha1.PodSchedulingSpec{
Tolerations: []corev1.Toleration{
{
Key: "node-role.kubernetes.io/bootstrap",
Operator: "Exists",
Effect: "NoSchedule",
},
{
Key: "node-role.kubernetes.io/infra",
Operator: "Exists",
Effect: "NoSchedule",
},
},
}

Expect(k8sClient.Update(ctx, currentUI)).To(Succeed())

By("Reconciling the deployment with new scheduling constraints")
_, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: clusterScopedName})
Expect(err).NotTo(HaveOccurred())

By("Verifying tolerations are applied to the deployment")
deployment := &appsv1.Deployment{}
deploymentName := types.NamespacedName{Name: uiAppName + "-deployment", Namespace: getOperatorNamespace()}
Eventually(func() error {
return k8sClient.Get(ctx, deploymentName, deployment)
}, eventuallyTimeout, eventuallyInterval).Should(Succeed())

Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(2))

// Check first toleration
Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("node-role.kubernetes.io/bootstrap"))
Expect(deployment.Spec.Template.Spec.Tolerations[0].Operator).To(Equal(corev1.TolerationOperator("Exists")))
Expect(deployment.Spec.Template.Spec.Tolerations[0].Effect).To(Equal(corev1.TaintEffect("NoSchedule")))

// Check second toleration
Expect(deployment.Spec.Template.Spec.Tolerations[1].Key).To(Equal("node-role.kubernetes.io/infra"))
Expect(deployment.Spec.Template.Spec.Tolerations[1].Operator).To(Equal(corev1.TolerationOperator("Exists")))
Expect(deployment.Spec.Template.Spec.Tolerations[1].Effect).To(Equal(corev1.TaintEffect("NoSchedule")))
})
})

Describe("Shell UI Customization Features", func() {
It("should allow customization of shell branding and navigation", func() {
By("Deploying the Shell UI initially")
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/scalityui/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ func newScalityUIDeploymentReconciler(cr ScalityUI, currentState State) reconcil
},
}
}

// Add tolerations from CR spec
if cr.Spec.Scheduling != nil && len(cr.Spec.Scheduling.Tolerations) > 0 {
spec.Tolerations = cr.Spec.Scheduling.Tolerations
}

return spec
},
Containers: []resources.GenericContainer{
Expand Down
5 changes: 5 additions & 0 deletions internal/controller/scalityuicomponent/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ func (r *ScalityUIComponentReconciler) Reconcile(ctx context.Context, req ctrl.R
}
}

// Apply tolerations from CR spec
if scalityUIComponent.Spec.Scheduling != nil && len(scalityUIComponent.Spec.Scheduling.Tolerations) > 0 {
deployment.Spec.Template.Spec.Tolerations = scalityUIComponent.Spec.Scheduling.Tolerations
}

return nil
})

Expand Down
62 changes: 62 additions & 0 deletions internal/controller/scalityuicomponent/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,68 @@ var _ = Describe("ScalityUIComponent Controller", func() {
Expect(updatedDeployment.Spec.Template.Spec.ImagePullSecrets).To(BeEmpty())
})

It("should apply tolerations from scheduling spec to deployment", func() {
By("Reconciling the created resource initially")
controllerReconciler := NewScalityUIComponentReconciler(k8sClient, k8sClient.Scheme())
mockFetcher := &MockConfigFetcher{
ConfigContent: `{
"kind": "UIModule",
"apiVersion": "v1alpha1",
"metadata": {"kind": "TestKind"},
"spec": {
"remoteEntryPath": "/remoteEntry.js",
"publicPath": "/test-public/",
"version": "1.2.3"
}
}`,
}
controllerReconciler.ConfigFetcher = mockFetcher

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName})
Expect(err).NotTo(HaveOccurred())

By("Adding scheduling constraints to the resource")
fetchedResource := &uiv1alpha1.ScalityUIComponent{}
Expect(k8sClient.Get(ctx, typeNamespacedName, fetchedResource)).To(Succeed())

fetchedResource.Spec.Scheduling = &uiv1alpha1.PodSchedulingSpec{
Tolerations: []corev1.Toleration{
{
Key: "node-role.kubernetes.io/bootstrap",
Operator: "Exists",
Effect: "NoSchedule",
},
{
Key: "node-role.kubernetes.io/infra",
Operator: "Exists",
Effect: "NoSchedule",
},
},
}

Expect(k8sClient.Update(ctx, fetchedResource)).To(Succeed())

By("Reconciling again to apply scheduling constraints")
_, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName})
Expect(err).NotTo(HaveOccurred())

By("Verifying tolerations are applied to the deployment")
deployment := &appsv1.Deployment{}
Expect(k8sClient.Get(ctx, deploymentNamespacedName, deployment)).To(Succeed())

Expect(deployment.Spec.Template.Spec.Tolerations).To(HaveLen(2))

// Check first toleration
Expect(deployment.Spec.Template.Spec.Tolerations[0].Key).To(Equal("node-role.kubernetes.io/bootstrap"))
Expect(deployment.Spec.Template.Spec.Tolerations[0].Operator).To(Equal(corev1.TolerationOperator("Exists")))
Expect(deployment.Spec.Template.Spec.Tolerations[0].Effect).To(Equal(corev1.TaintEffect("NoSchedule")))

// Check second toleration
Expect(deployment.Spec.Template.Spec.Tolerations[1].Key).To(Equal("node-role.kubernetes.io/infra"))
Expect(deployment.Spec.Template.Spec.Tolerations[1].Operator).To(Equal(corev1.TolerationOperator("Exists")))
Expect(deployment.Spec.Template.Spec.Tolerations[1].Effect).To(Equal(corev1.TaintEffect("NoSchedule")))
})

It("should requeue if Deployment is not ready", func() {
By("Reconciling the created resource")
mockFetcher := &MockConfigFetcher{}
Expand Down
Loading