diff --git a/api/v1/ytsaurus_types.go b/api/v1/ytsaurus_types.go
index 702e4777..b566582f 100644
--- a/api/v1/ytsaurus_types.go
+++ b/api/v1/ytsaurus_types.go
@@ -614,7 +614,7 @@ type YtsaurusSpec struct {
//+optional
EnableFullUpdate bool `json:"enableFullUpdate"`
//+optional
- //+kubebuilder:validation:Enum={"","Nothing","StatelessOnly","MasterOnly","TabletNodesOnly","ExecNodesOnly","Everything"}
+ //+kubebuilder:validation:Enum={"","Nothing","MasterOnly","DataNodesOnly","TabletNodesOnly","ExecNodesOnly","StatelessOnly","Everything"}
// UpdateSelector is an experimental field. Behaviour may change.
// If UpdateSelector is not empty EnableFullUpdate is ignored.
UpdateSelector UpdateSelector `json:"updateSelector"`
@@ -699,15 +699,17 @@ const (
UpdateSelectorUnspecified UpdateSelector = ""
// UpdateSelectorNothing means that no component could be updated.
UpdateSelectorNothing UpdateSelector = "Nothing"
- // UpdateSelectorStatelessOnly means that only stateless components (everything but master and tablet nodes)
- // could be updated.
- UpdateSelectorStatelessOnly UpdateSelector = "StatelessOnly"
// UpdateSelectorMasterOnly means that only master could be updated.
UpdateSelectorMasterOnly UpdateSelector = "MasterOnly"
+ // UpdateSelectorTabletNodesOnly means that only data nodes could be updated
+ UpdateSelectorDataNodesOnly UpdateSelector = "DataNodesOnly"
// UpdateSelectorTabletNodesOnly means that only tablet nodes could be updated
UpdateSelectorTabletNodesOnly UpdateSelector = "TabletNodesOnly"
// UpdateSelectorExecNodesOnly means that only tablet nodes could be updated
UpdateSelectorExecNodesOnly UpdateSelector = "ExecNodesOnly"
+ // UpdateSelectorStatelessOnly means that only stateless components (everything but master, data nodes, and tablet nodes)
+ // could be updated.
+ UpdateSelectorStatelessOnly UpdateSelector = "StatelessOnly"
// UpdateSelectorEverything means that all components could be updated.
// With this setting and if master or tablet nodes need update all the components would be updated.
UpdateSelectorEverything UpdateSelector = "Everything"
diff --git a/config/crd/bases/cluster.ytsaurus.tech_ytsaurus.yaml b/config/crd/bases/cluster.ytsaurus.tech_ytsaurus.yaml
index c8d18a16..16eb28f8 100644
--- a/config/crd/bases/cluster.ytsaurus.tech_ytsaurus.yaml
+++ b/config/crd/bases/cluster.ytsaurus.tech_ytsaurus.yaml
@@ -34637,10 +34637,11 @@ spec:
enum:
- ""
- Nothing
- - StatelessOnly
- MasterOnly
+ - DataNodesOnly
- TabletNodesOnly
- ExecNodesOnly
+ - StatelessOnly
- Everything
type: string
useIpv4:
diff --git a/controllers/sync.go b/controllers/sync.go
index 766185da..5350bb91 100644
--- a/controllers/sync.go
+++ b/controllers/sync.go
@@ -3,6 +3,7 @@ package controllers
import (
"context"
"fmt"
+ "strings"
"time"
"github.com/ytsaurus/ytsaurus-k8s-operator/pkg/components"
@@ -386,77 +387,80 @@ func (r *YtsaurusReconciler) handleTabletNodesOnly(
return nil, nil
}
-func getComponentNames(components []components.Component) []string {
- if components == nil {
- return nil
- }
- names := make([]string, 0)
- for _, cmp := range components {
- names = append(names, cmp.GetName())
- }
- return names
-}
-
type updateMeta struct {
flow ytv1.UpdateFlow
// componentNames is a list of component names that will be updated. It is built according to the update selector.
componentNames []string
}
+func canUpdateComponent(selector ytv1.UpdateSelector, component consts.ComponentType) bool {
+ switch selector {
+ case ytv1.UpdateSelectorNothing:
+ return false
+ case ytv1.UpdateSelectorMasterOnly:
+ return component == consts.MasterType
+ case ytv1.UpdateSelectorDataNodesOnly:
+ return component == consts.DataNodeType
+ case ytv1.UpdateSelectorTabletNodesOnly:
+ return component == consts.TabletNodeType
+ case ytv1.UpdateSelectorExecNodesOnly:
+ return component == consts.ExecNodeType
+ case ytv1.UpdateSelectorStatelessOnly:
+ switch component {
+ case consts.MasterType:
+ return false
+ case consts.DataNodeType:
+ return false
+ case consts.TabletNodeType:
+ return false
+ }
+ return true
+ case ytv1.UpdateSelectorEverything:
+ return true
+ default:
+ return false
+ }
+}
+
// chooseUpdateFlow considers spec and decides if operator should proceed with update or block.
// Block case is indicated with non-empty blockMsg.
// If update is not blocked, updateMeta containing a chosen flow and the component names to update returned.
func chooseUpdateFlow(spec ytv1.YtsaurusSpec, needUpdate []components.Component) (meta updateMeta, blockMsg string) {
- isFullUpdateEnabled := spec.EnableFullUpdate
configuredSelector := spec.UpdateSelector
+ if configuredSelector == ytv1.UpdateSelectorUnspecified {
+ if spec.EnableFullUpdate {
+ configuredSelector = ytv1.UpdateSelectorEverything
+ } else {
+ configuredSelector = ytv1.UpdateSelectorStatelessOnly
+ }
+ }
+
+ var canUpdate []string
+ var cannotUpdate []string
+ needFullUpdate := false
- masterNeedsUpdate := false
- tabletNodesNeedUpdate := false
- execNodesNeedUpdate := false
- statelessNeedUpdate := false
- var masterNames []string
- var tabletNodeNames []string
- var execNodeNames []string
- var statelessNames []string
for _, comp := range needUpdate {
- if comp.GetType() == consts.MasterType {
- masterNeedsUpdate = true
- masterNames = append(masterNames, comp.GetName())
- continue
- }
- if comp.GetType() == consts.TabletNodeType {
- tabletNodesNeedUpdate = true
- tabletNodeNames = append(tabletNodeNames, comp.GetName())
- continue
- }
- if comp.GetType() == consts.ExecNodeType {
- execNodesNeedUpdate = true
- execNodeNames = append(execNodeNames, comp.GetName())
- }
- statelessNames = append(statelessNames, comp.GetName())
- statelessNeedUpdate = true
+ component := comp.GetType()
+ if canUpdateComponent(configuredSelector, component) {
+ canUpdate = append(canUpdate, string(component))
+ } else {
+ cannotUpdate = append(cannotUpdate, string(component))
+ }
+ if !canUpdateComponent(ytv1.UpdateSelectorStatelessOnly, component) && component != consts.DataNodeType {
+ needFullUpdate = true
+ }
}
- statefulNeedUpdate := masterNeedsUpdate || tabletNodesNeedUpdate
-
- allNamesNeedingUpdate := getComponentNames(needUpdate)
- // Fallback to EnableFullUpdate field.
- if configuredSelector == ytv1.UpdateSelectorUnspecified {
- if statefulNeedUpdate {
- if isFullUpdateEnabled {
- return updateMeta{flow: ytv1.UpdateFlowFull, componentNames: nil}, ""
- } else {
- return updateMeta{flow: "", componentNames: nil}, "Full update is not allowed by enableFullUpdate field, ignoring it"
- }
+ if len(canUpdate) == 0 {
+ if len(cannotUpdate) != 0 {
+ return updateMeta{}, fmt.Sprintf("All components allowed by updateSelector are uptodate, update of {%s} is not allowed", strings.Join(cannotUpdate, ", "))
}
- return updateMeta{flow: ytv1.UpdateFlowStateless, componentNames: allNamesNeedingUpdate}, ""
+ return updateMeta{}, "All components are uptodate"
}
switch configuredSelector {
- case ytv1.UpdateSelectorNothing:
- return updateMeta{}, "All updates are blocked by updateSelector field."
case ytv1.UpdateSelectorEverything:
- if statefulNeedUpdate {
+ if needFullUpdate {
return updateMeta{
flow: ytv1.UpdateFlowFull,
componentNames: nil,
@@ -464,44 +468,29 @@ func chooseUpdateFlow(spec ytv1.YtsaurusSpec, needUpdate []components.Component)
} else {
return updateMeta{
flow: ytv1.UpdateFlowStateless,
- componentNames: allNamesNeedingUpdate,
+ componentNames: canUpdate,
}, ""
}
case ytv1.UpdateSelectorMasterOnly:
- if !masterNeedsUpdate {
- return updateMeta{}, "Only Master update is allowed by updateSelector, but it doesn't need update"
- }
return updateMeta{
flow: ytv1.UpdateFlowMaster,
- componentNames: masterNames,
+ componentNames: canUpdate,
}, ""
case ytv1.UpdateSelectorTabletNodesOnly:
- if !tabletNodesNeedUpdate {
- return updateMeta{}, "Only Tablet nodes update is allowed by updateSelector, but they don't need update"
- }
return updateMeta{
flow: ytv1.UpdateFlowTabletNodes,
- componentNames: tabletNodeNames,
+ componentNames: canUpdate,
}, ""
+ case ytv1.UpdateSelectorDataNodesOnly:
case ytv1.UpdateSelectorExecNodesOnly:
- if !execNodesNeedUpdate {
- return updateMeta{}, "Only Exec nodes update is allowed by updateSelector, but they don't need update"
- }
- return updateMeta{
- flow: ytv1.UpdateFlowStateless,
- componentNames: execNodeNames,
- }, ""
case ytv1.UpdateSelectorStatelessOnly:
- if !statelessNeedUpdate {
- return updateMeta{}, "Only stateless components update is allowed by updateSelector, but they don't need update"
- }
return updateMeta{
flow: ytv1.UpdateFlowStateless,
- componentNames: statelessNames,
+ componentNames: canUpdate,
}, ""
- default:
- return updateMeta{}, fmt.Sprintf("Unexpected update selector %s", configuredSelector)
}
+
+ return updateMeta{}, fmt.Sprintf("Unexpected update selector %s", configuredSelector)
}
func (r *YtsaurusReconciler) Sync(ctx context.Context, resource *ytv1.Ytsaurus) (ctrl.Result, error) {
diff --git a/docs/api.md b/docs/api.md
index 4e7a9fc0..a39ec988 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1836,7 +1836,7 @@ _Appears in:_
| `oauthService` _[OauthServiceSpec](#oauthservicespec)_ | | | |
| `isManaged` _boolean_ | | true | |
| `enableFullUpdate` _boolean_ | | true | |
-| `updateSelector` _[UpdateSelector](#updateselector)_ | UpdateSelector is an experimental field. Behaviour may change.
If UpdateSelector is not empty EnableFullUpdate is ignored. | | Enum: [ Nothing StatelessOnly MasterOnly TabletNodesOnly ExecNodesOnly Everything]
|
+| `updateSelector` _[UpdateSelector](#updateselector)_ | UpdateSelector is an experimental field. Behaviour may change.
If UpdateSelector is not empty EnableFullUpdate is ignored. | | Enum: [ Nothing MasterOnly DataNodesOnly TabletNodesOnly ExecNodesOnly StatelessOnly Everything]
|
| `nodeSelector` _object (keys:string, values:string)_ | | | |
| `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#toleration-v1-core) array_ | | | |
| `bootstrap` _[BootstrapSpec](#bootstrapspec)_ | | | |
diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go
index fa6e16e0..ab2ad1f9 100644
--- a/test/e2e/suite_test.go
+++ b/test/e2e/suite_test.go
@@ -164,6 +164,12 @@ func NewYtsaurusStatusTracker() func(*ytv1.Ytsaurus) bool {
}
}
+func ExpectYtsaurus(ctx context.Context, name types.NamespacedName) Assertion {
+ var ytsaurus ytv1.Ytsaurus
+ Expect(k8sClient.Get(ctx, name, &ytsaurus)).Should(Succeed())
+ return Expect(ytsaurus)
+}
+
func EventuallyYtsaurus(ctx context.Context, name types.NamespacedName, timeout time.Duration) AsyncAssertion {
var ytsaurus ytv1.Ytsaurus
trackStatus := NewYtsaurusStatusTracker()
@@ -192,16 +198,24 @@ func HaveClusterState(state ytv1.ClusterState) otypes.GomegaMatcher {
return HaveField("Status.State", state)
}
+func HaveClusterStateRunning() otypes.GomegaMatcher {
+ return HaveClusterState(ytv1.ClusterStateRunning)
+}
+
+func HaveClusterStateUpdating() otypes.GomegaMatcher {
+ return HaveClusterState(ytv1.ClusterStateUpdating)
+}
+
func HaveClusterUpdateState(updateState ytv1.UpdateState) otypes.GomegaMatcher {
return And(
- HaveClusterState(ytv1.ClusterStateUpdating),
+ HaveClusterStateUpdating(),
HaveField("Status.UpdateStatus.State", updateState),
)
}
func HaveClusterUpdatingComponents(components ...string) otypes.GomegaMatcher {
return And(
- HaveClusterState(ytv1.ClusterStateUpdating),
+ HaveClusterStateRunning(),
HaveField("Status.UpdateStatus.Components", components),
)
}
diff --git a/test/e2e/ytsaurus_controller_test.go b/test/e2e/ytsaurus_controller_test.go
index 6f9e0d3a..6b7f47c4 100644
--- a/test/e2e/ytsaurus_controller_test.go
+++ b/test/e2e/ytsaurus_controller_test.go
@@ -173,7 +173,7 @@ func runYtsaurus(ytsaurus *ytv1.Ytsaurus) {
}
By("Checking that ytsaurus state is equal to `Running`")
- EventuallyYtsaurus(ctx, ytsaurusLookupKey, bootstrapTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, ytsaurusLookupKey, bootstrapTimeout).Should(HaveClusterStateRunning())
By("Check pods are running")
for _, podName := range pods {
@@ -272,7 +272,7 @@ func runImpossibleUpdateAndRollback(ytsaurus *ytv1.Ytsaurus, ytClient yt.Client)
}, reactionTimeout, pollInterval).Should(Succeed())
By("Wait for running")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
By("Check that cluster alive after update")
res := make([]string, 0)
@@ -322,7 +322,7 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
By("Ensure cluster doesn't start updating for 5 seconds")
- ConsistentlyYtsaurus(ctx, name, 5*time.Second).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ ConsistentlyYtsaurus(ctx, name, 5*time.Second).Should(HaveClusterStateRunning())
podsAfterBlockedUpdate := getComponentPods(ctx, namespace)
Expect(podsBeforeUpdate).To(
Equal(podsAfterBlockedUpdate),
@@ -334,10 +334,11 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
ytsaurus.Spec.UpdateSelector = ytv1.UpdateSelectorEverything
ytsaurus.Spec.Discovery.InstanceCount += 1
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterUpdatingComponents())
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
+ ExpectYtsaurus(ctx, name).Should(HaveClusterUpdatingComponents())
By("Wait cluster update with full update complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
podsAfterFullUpdate := getComponentPods(ctx, namespace)
podDiff := diffPodsCreation(podsBeforeUpdate, podsAfterFullUpdate)
@@ -363,10 +364,11 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
ytsaurus.Spec.UpdateSelector = ytv1.UpdateSelectorExecNodesOnly
ytsaurus.Spec.Discovery.InstanceCount += 1
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterUpdatingComponents("ExecNode"))
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
+ ExpectYtsaurus(ctx, name).Should(HaveClusterUpdatingComponents("ExecNode"))
By("Wait cluster update with selector:ExecNodesOnly complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
ytClient := createYtsaurusClient(ytsaurus, namespace)
checkClusterBaseViability(ytClient)
@@ -382,10 +384,11 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
ytsaurus.Spec.UpdateSelector = ytv1.UpdateSelectorTabletNodesOnly
ytsaurus.Spec.Discovery.InstanceCount += 1
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterUpdatingComponents("TabletNode"))
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
+ ExpectYtsaurus(ctx, name).Should(HaveClusterUpdatingComponents("TabletNode"))
By("Wait cluster update with selector:TabletNodesOnly complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
checkClusterBaseViability(ytClient)
podsAfterTndUpdate := getComponentPods(ctx, namespace)
@@ -413,10 +416,11 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
ytsaurus.Spec.UpdateSelector = ytv1.UpdateSelectorMasterOnly
ytsaurus.Spec.Discovery.InstanceCount += 1
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterUpdatingComponents("Master"))
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
+ ExpectYtsaurus(ctx, name).Should(HaveClusterUpdatingComponents("Master"))
By("Wait cluster update with selector:MasterOnly complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
ytClient := createYtsaurusClient(ytsaurus, namespace)
checkClusterBaseViability(ytClient)
podsAfterMasterUpdate := getComponentPods(ctx, namespace)
@@ -431,11 +435,16 @@ var _ = Describe("Basic test for Ytsaurus controller", func() {
ytsaurus.Spec.UpdateSelector = ytv1.UpdateSelectorStatelessOnly
ytsaurus.Spec.Discovery.InstanceCount += 1
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(
- HaveClusterUpdatingComponents("Discovery", "DataNode", "HttpProxy", "ExecNode", "Scheduler", "ControllerAgent"),
- )
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
+ ExpectYtsaurus(ctx, name).Should(HaveClusterUpdatingComponents(
+ "Discovery",
+ "HttpProxy",
+ "ExecNode",
+ "Scheduler",
+ "ControllerAgent",
+ ))
By("Wait cluster update with selector:StatelessOnly complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
checkClusterBaseViability(ytClient)
podsAfterStatelessUpdate := getComponentPods(ctx, namespace)
podDiff = diffPodsCreation(podsAfterMasterUpdate, podsAfterStatelessUpdate)
@@ -1107,10 +1116,10 @@ func getSimpleUpdateScenario(namespace, newImage string) func(ctx context.Contex
ytsaurus.Spec.CoreImage = newImage
Expect(k8sClient.Update(ctx, ytsaurus)).Should(Succeed())
- EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterState(ytv1.ClusterStateUpdating))
+ EventuallyYtsaurus(ctx, name, reactionTimeout).Should(HaveClusterStateUpdating())
By("Wait cluster update complete")
- EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterState(ytv1.ClusterStateRunning))
+ EventuallyYtsaurus(ctx, name, upgradeTimeout).Should(HaveClusterStateRunning())
g := ytconfig.NewGenerator(ytsaurus, "local")
ytClient := getYtClient(g, namespace)
checkClusterBaseViability(ytClient)
diff --git a/ytop-chart/templates/crds/ytsaurus.cluster.ytsaurus.tech.yaml b/ytop-chart/templates/crds/ytsaurus.cluster.ytsaurus.tech.yaml
index a2c37e16..d537e354 100644
--- a/ytop-chart/templates/crds/ytsaurus.cluster.ytsaurus.tech.yaml
+++ b/ytop-chart/templates/crds/ytsaurus.cluster.ytsaurus.tech.yaml
@@ -34648,10 +34648,11 @@ spec:
enum:
- ""
- Nothing
- - StatelessOnly
- MasterOnly
+ - DataNodesOnly
- TabletNodesOnly
- ExecNodesOnly
+ - StatelessOnly
- Everything
type: string
useIpv4: