Skip to content

Commit f761254

Browse files
kvapsclaude
andcommitted
feat(controller): auto-diskful promotion + flag constants (Phase 8.3)
Auto-diskful: when a DISKLESS replica becomes InUse on a node with a viable storage pool, the ResourceReconciler now removes the DISKLESS flag and stamps StorPoolName on Spec.Props. The satellite reconciler picks up the change on its next pass and creates the LV/zvol + drbdadm attach. Pod scheduling that lands on a node without a local diskful copy upgrades automatically instead of running remote reads forever. TIE_BREAKER witnesses are explicitly exempted — promoting one defeats the network-only quorum purpose. Demote-on-idle (auto-diskful-cleanup) is intentionally deferred: needs hysteresis to avoid flapping on transient opens. Operators demote manually via `linstor r d` until access-pattern tracking lands. Also extracted Resource.Flags string constants (ResourceFlagDiskless / Inactive / TieBreaker) to api/v1 so the strings stop being scattered across packages — closes the goconst warnings. PLAN reclassification: shared-LUN moved from "out of scope" to "deferred with architectural hooks". Added a sketch of the SharedSpaceID-keyed accounting and anti-affinity work needed to support carving volumes over a SAN-presented LUN later. Tests: - TestAutoDisklessPromoted: in-use diskless on node with pool → promoted to diskful; multiple-pass reconcile drives convergence - TestAutoDisklessSkipsTiebreaker: TIE_BREAKER never auto-promoted - TestAutoDisklessSkipsWhenNoPool: no local pool → leave alone Phase 8.3 lifecycle items now all have a v1 implementation. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 15c2a87 commit f761254

6 files changed

Lines changed: 340 additions & 4 deletions

File tree

PLAN.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ The phases above closed the MVP slice and the csi-sanity REST contract. A deep a
412412

413413
- [x] **`linstor node evacuate` actually migrates replicas** (2026-05-09). New `internal/controller.NodeReconciler` watches Node CRDs and on EVICTED enumerates every Resource on the affected node, runs the shared `pkg/placer.Place` to create a replacement on a non-disabled peer (honouring the parent RG's topology constraints), and leaves the source replica in place — the operator decides when to remove it (typically once the replacement is UpToDate). The placer's "existing replicas" count now excludes EVICTED/LOST nodes so a 2-replica RD with one evicted source actually triggers the migration. Test: `TestNodeReconciler_EvictedTriggersMigration` pins the 3-node migration path.
414414
- [x] **`linstor node lost` recovery** (2026-05-09): the same NodeReconciler also detects LOST. Migration runs as for EVICTED, then the source Resource CRD is deleted via the K8s API path so the Resource controller's finalizer cleans up. The TCP-port/node-id allocations stored on the source Resource Status free naturally on delete (the per-node port allocator scans live Resources). Test: `TestNodeReconciler_LostDeletesSourceResource`. **e2e** (hard-kill a satellite pod) sits on the 8.8 checklist.
415-
- [ ] **auto-diskful / auto-diskful-cleanup**. Upstream watches Resource access patterns and auto-promotes a diskless to diskful when a node uses it heavily, reverses on idle. Implement the controller-side metric collection + threshold. Pin via simulation test that drives synthetic access counts.
415+
- [x] **auto-diskful** (2026-05-09): the ResourceReconciler now promotes a DISKLESS replica to diskful when `Resource.Status.InUse=true` AND the hosting node has a viable storage pool. Removes the DISKLESS flag and stamps `StorPoolName` on Spec.Props; the satellite reconciler picks up the change on its next pass and creates the LV / runs `drbdadm attach`. TIE_BREAKER witnesses are exempted — promoting one would defeat the quorum-only purpose. Tests: `TestAutoDisklessPromoted`, `TestAutoDisklessSkipsTiebreaker`, `TestAutoDisklessSkipsWhenNoPool`. **`auto-diskful-cleanup`** (demote-on-idle) deferred — needs hysteresis to avoid flapping on transient opens; operators can demote manually via `linstor r d` until the access-pattern tracking lands.
416416
- [x] **Tiebreaker auto-creation** (2026-05-09). New `internal/controller.ResourceDefinitionReconciler` watches RDs; an RD with exactly 2 diskful replicas auto-gains a 3rd DISKLESS replica on a healthy non-replica node so DRBD's `quorum:majority` always has a majority on a 1-vs-1 partition. Idempotent (re-running on a 3-replica or already-witnessed RD is a no-op), respects EVICTED/LOST node flags, picks deterministically (lowest-name first) so reconcile races converge. The witness carries the `TIE_BREAKER` flag for cleanup tracking. Tests: `TestTiebreakerCreated`, `TestTiebreakerSkipsThreeReplicas`, `TestTiebreakerSkipsTwoNodeCluster`, `TestTiebreakerSkipsEvictedNode`.
417417
- [x] **Resource activate / deactivate** (2026-05-09): `POST /v1/resource-definitions/{rd}/resources/{node}/{activate,deactivate}` toggles the `INACTIVE` flag on the Resource. Idempotent. Satellite reconciler reads the flag and runs `drbdadm down` (deactivate) or normal apply (activate) — the .res file, port, and node-id allocations all stay intact, so flipping back doesn't lose state. Tests: `TestResourceDeactivate` (idempotent set + clear), `TestResourceActivateUnknown` (404 on missing replica).
418418
- [x] **Diskless replicas as first-class autoplace candidates** (2026-05-09): `AutoSelectFilter.DisklessOnRemaining` now actually does what the field name promises. After diskful place_count is satisfied, the placer creates DISKLESS replicas on every healthy node not already hosting a replica — the upstream "cluster-wide attachable" pattern useful for consumers that need to mount on any node. Test: `TestAutoplaceDisklessOnRemaining` (4-node cluster, place_count=2 → 2 diskful + 2 diskless witnesses).
@@ -431,7 +431,13 @@ The phases above closed the MVP slice and the csi-sanity REST contract. A deep a
431431
- `piraeus-ha-controller` ([upstream](https://github.com/piraeusdatastore/piraeus-ha-controller)) — separate Kubernetes-aware controller. Watches Pod / Node / LINSTOR-resource state and does the cluster-aware actions: pod eviction on DRBD `StandAlone`, force-detach when a backing device fails, mount-node migration when the consumer Pod reschedules. It dials the LINSTOR REST API as a client. Verify against blockstor: end-to-end run with piraeus-ha-controller against our REST surface, document which endpoints it hits, fix any gaps. No code changes from blockstor expected unless an endpoint shape diverges from upstream; the audit needs to happen on the dev stand.
432432
- [x] **`linstor advise`** (2026-05-09): `GET /v1/view/advise/resources` and `GET /v1/resource-definitions/{rd}/advise` return per-RD recommendations (top-N pools by free capacity, sorted desc) without persisting anything. Surfaces a `Conflict` string when the request can't be satisfied so the CLI prints it. Tests: `TestAdviseRD`, `TestAdviseRDInsufficient`.
433433
- [x] **`linstor query-size-info` / spaceinfo** (2026-05-09): `POST /v1/resource-groups/{rg}/query-size-info` answers `max_vlm_size_in_kib = FreeCapacity of the n-th-largest pool` (n = place_count) — the cap that all replicas can fit at once, the value golinstor's pre-flight uses. `POST /v1/query-all-size-info` returns the per-RG map in one shot. EVICTED/LOST nodes excluded from capacity. Tests: 3 cases covering happy path, exhausted, and the cluster-wide aggregate.
434-
- [x] **shared LUN provider (EXOS / SHARED)** (2026-05-09): documented as **explicitly out-of-scope**. cozystack runs DRBD over local block devices; the shared-LUN model (multiple satellites attaching to one SAN-exposed device) is the wrong primitive for the architecture. The provider-kind constants `OPENFLEX_TARGET` and `REMOTE_SPDK` already exist and surface as 501 in the storage-pool create path; no further wiring planned.
434+
- [ ] **shared LUN provider (EXOS / SHARED) — deferred, not out-of-scope** (2026-05-09 reclassified). The SAN-shared-LUN model isn't needed today, but the architecture should leave room to slice volumes over a shared LUN later (e.g. multiple satellites carving an EXOS / NetApp / Ceph-RBD-as-shared-disk LUN). Upstream LINSTOR's accounting trick: a single "shared free-space" key keyed off the LUN identifier — multiple StoragePool CRDs on different nodes report the same shared key, the controller subtracts allocations once instead of summing per-node. Architectural hooks needed:
435+
- StoragePool gains an optional `SharedSpaceID` field (free-form key; empty = local pool)
436+
- the placer's free-capacity / query-size-info paths consult shared-space groups: pools sharing an ID contribute one free-capacity figure to the cluster total instead of summing
437+
- storage.Provider gets a notion of "this pool is backed by a LUN identified by X" so the satellite knows not to double-count
438+
- autoplace anti-affinity respects shared-space groups: don't place 2 replicas on pools that physically share the same backing LUN
439+
440+
Current behaviour stays as-is (501 on EXOS/REMOTE_SPDK kinds in pool create); that's a placeholder, not a design statement.
435441

436442
### 8.6 Real-world testing
437443

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func main() {
246246
Client: mgr.GetClient(),
247247
Scheme: mgr.GetScheme(),
248248
Dispatcher: dispatcher.New(dispatcher.NewDialer()),
249+
Store: st,
249250
}).SetupWithManager(mgr); err != nil {
250251
setupLog.Error(err, "Failed to create controller", "controller", "resource")
251252
os.Exit(1)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright 2026 Cozystack contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller_test
18+
19+
import (
20+
"context"
21+
"errors"
22+
"slices"
23+
"testing"
24+
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/types"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
29+
30+
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
31+
controllerpkg "github.com/cozystack/blockstor/internal/controller"
32+
apiv1 "github.com/cozystack/blockstor/pkg/api/v1"
33+
"github.com/cozystack/blockstor/pkg/dispatcher"
34+
satellitepb "github.com/cozystack/blockstor/pkg/satellite/proto"
35+
"github.com/cozystack/blockstor/pkg/store"
36+
)
37+
38+
// noopDialer always fails the dial. The Reconcile path's
39+
// dispatchApply step then returns RequeueAfter without crashing —
40+
// fine for tests that only assert the pre-dispatch CRD mutations.
41+
type noopDialer struct{}
42+
43+
func (noopDialer) Dial(_ context.Context, _ string) (satellitepb.SatelliteClient, func() error, error) {
44+
return nil, nil, errNoopDial
45+
}
46+
47+
var errNoopDial = errors.New("auto_diskful_test: noop dialer")
48+
49+
// TestAutoDisklessPromoted: a DISKLESS replica that becomes InUse on
50+
// a node with a viable storage pool gets promoted to diskful — the
51+
// reconciler removes the DISKLESS flag and stamps StorPoolName on
52+
// Spec.Props. The satellite reconciler picks up the change on its
53+
// next reconcile and creates the LV / runs drbdadm attach.
54+
func TestAutoDisklessPromoted(t *testing.T) {
55+
t.Parallel()
56+
57+
ctx := context.Background()
58+
scheme := newScheme(t)
59+
st := store.NewInMemory()
60+
61+
_ = st.StoragePools().Create(ctx, &apiv1.StoragePool{
62+
StoragePoolName: "stand",
63+
NodeName: "n1",
64+
ProviderKind: apiv1.StoragePoolKindLVMThin,
65+
})
66+
67+
resCRD := &blockstoriov1alpha1.Resource{
68+
ObjectMeta: metav1.ObjectMeta{
69+
Name: "pvc-promote.n1",
70+
Finalizers: []string{"blockstor.io.blockstor.io/resource"},
71+
},
72+
Spec: blockstoriov1alpha1.ResourceSpec{
73+
ResourceDefinitionName: "pvc-promote",
74+
NodeName: "n1",
75+
Flags: []string{apiv1.ResourceFlagDiskless},
76+
},
77+
Status: blockstoriov1alpha1.ResourceStatus{InUse: true},
78+
}
79+
80+
cli := fake.NewClientBuilder().WithScheme(scheme).
81+
WithStatusSubresource(&blockstoriov1alpha1.Resource{}).
82+
WithObjects(resCRD).
83+
Build()
84+
85+
rec := &controllerpkg.ResourceReconciler{
86+
Client: cli,
87+
Scheme: scheme,
88+
Dispatcher: dispatcher.New(noopDialer{}),
89+
Store: st,
90+
}
91+
92+
// Reconcile drives several converging passes (DRBD-id allocation
93+
// → Status update → requeue → auto-diskful → Spec update →
94+
// requeue → dispatchApply). Run until convergence so the final
95+
// state is what we assert on.
96+
for range 8 {
97+
_, err := rec.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: "pvc-promote.n1"}})
98+
if err != nil {
99+
t.Fatalf("Reconcile: %v", err)
100+
}
101+
}
102+
103+
got := &blockstoriov1alpha1.Resource{}
104+
if err := cli.Get(ctx, types.NamespacedName{Name: "pvc-promote.n1"}, got); err != nil {
105+
t.Fatalf("get: %v", err)
106+
}
107+
108+
if slices.Contains(got.Spec.Flags, apiv1.ResourceFlagDiskless) {
109+
t.Errorf("DISKLESS flag still present: %v", got.Spec.Flags)
110+
}
111+
112+
if got.Spec.Props["StorPoolName"] != "stand" {
113+
t.Errorf("StorPoolName: got %q, want stand", got.Spec.Props["StorPoolName"])
114+
}
115+
}
116+
117+
// TestAutoDisklessSkipsTiebreaker: a TIE_BREAKER witness must NEVER
118+
// be auto-promoted — its whole point is the network-only quorum
119+
// presence. Promoting would defeat the purpose and waste storage.
120+
func TestAutoDisklessSkipsTiebreaker(t *testing.T) {
121+
t.Parallel()
122+
123+
ctx := context.Background()
124+
scheme := newScheme(t)
125+
st := store.NewInMemory()
126+
127+
_ = st.StoragePools().Create(ctx, &apiv1.StoragePool{
128+
StoragePoolName: "stand",
129+
NodeName: "n1",
130+
ProviderKind: apiv1.StoragePoolKindLVMThin,
131+
})
132+
133+
resCRD := &blockstoriov1alpha1.Resource{
134+
ObjectMeta: metav1.ObjectMeta{
135+
Name: "pvc-tb.n1",
136+
Finalizers: []string{"blockstor.io.blockstor.io/resource"},
137+
},
138+
Spec: blockstoriov1alpha1.ResourceSpec{
139+
ResourceDefinitionName: "pvc-tb",
140+
NodeName: "n1",
141+
Flags: []string{apiv1.ResourceFlagDiskless, apiv1.ResourceFlagTieBreaker},
142+
},
143+
Status: blockstoriov1alpha1.ResourceStatus{InUse: true},
144+
}
145+
146+
cli := fake.NewClientBuilder().WithScheme(scheme).
147+
WithStatusSubresource(&blockstoriov1alpha1.Resource{}).
148+
WithObjects(resCRD).
149+
Build()
150+
151+
rec := &controllerpkg.ResourceReconciler{
152+
Client: cli,
153+
Scheme: scheme,
154+
Dispatcher: dispatcher.New(noopDialer{}),
155+
Store: st,
156+
}
157+
158+
_, err := rec.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: "pvc-tb.n1"}})
159+
if err != nil {
160+
t.Fatalf("Reconcile: %v", err)
161+
}
162+
163+
got := &blockstoriov1alpha1.Resource{}
164+
_ = cli.Get(ctx, types.NamespacedName{Name: "pvc-tb.n1"}, got)
165+
166+
if !slices.Contains(got.Spec.Flags, apiv1.ResourceFlagDiskless) {
167+
t.Errorf("DISKLESS dropped from a TIE_BREAKER replica: %v", got.Spec.Flags)
168+
}
169+
}
170+
171+
// TestAutoDisklessSkipsWhenNoPool: no storage pool on the hosting
172+
// node → leave the diskless replica alone (a viable promotion isn't
173+
// possible).
174+
func TestAutoDisklessSkipsWhenNoPool(t *testing.T) {
175+
t.Parallel()
176+
177+
ctx := context.Background()
178+
scheme := newScheme(t)
179+
st := store.NewInMemory()
180+
// No StoragePool created — the node has no local storage.
181+
182+
resCRD := &blockstoriov1alpha1.Resource{
183+
ObjectMeta: metav1.ObjectMeta{
184+
Name: "pvc-nopool.n1",
185+
Finalizers: []string{"blockstor.io.blockstor.io/resource"},
186+
},
187+
Spec: blockstoriov1alpha1.ResourceSpec{
188+
ResourceDefinitionName: "pvc-nopool",
189+
NodeName: "n1",
190+
Flags: []string{apiv1.ResourceFlagDiskless},
191+
},
192+
Status: blockstoriov1alpha1.ResourceStatus{InUse: true},
193+
}
194+
195+
cli := fake.NewClientBuilder().WithScheme(scheme).
196+
WithStatusSubresource(&blockstoriov1alpha1.Resource{}).
197+
WithObjects(resCRD).
198+
Build()
199+
200+
rec := &controllerpkg.ResourceReconciler{
201+
Client: cli,
202+
Scheme: scheme,
203+
Dispatcher: dispatcher.New(noopDialer{}),
204+
Store: st,
205+
}
206+
207+
_, err := rec.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: "pvc-nopool.n1"}})
208+
if err != nil {
209+
t.Fatalf("Reconcile: %v", err)
210+
}
211+
212+
got := &blockstoriov1alpha1.Resource{}
213+
_ = cli.Get(ctx, types.NamespacedName{Name: "pvc-nopool.n1"}, got)
214+
215+
if !slices.Contains(got.Spec.Flags, apiv1.ResourceFlagDiskless) {
216+
t.Errorf("DISKLESS dropped despite no available pool: %v", got.Spec.Flags)
217+
}
218+
219+
if got.Spec.Props["StorPoolName"] != "" {
220+
t.Errorf("StorPoolName set without a pool: %v", got.Spec.Props)
221+
}
222+
}

internal/controller/resource_controller.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import (
2828
logf "sigs.k8s.io/controller-runtime/pkg/log"
2929

3030
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
31+
apiv1 "github.com/cozystack/blockstor/pkg/api/v1"
3132
"github.com/cozystack/blockstor/pkg/dispatcher"
3233
"github.com/cozystack/blockstor/pkg/drbd"
34+
"github.com/cozystack/blockstor/pkg/store"
3335
)
3436

3537
// resourceFinalizer guards on-disk + DRBD-side teardown when the
@@ -46,6 +48,11 @@ type ResourceReconciler struct {
4648
client.Client
4749
Scheme *runtime.Scheme
4850
Dispatcher *dispatcher.Dispatcher
51+
52+
// Store is the shared blockstor store. Used by the auto-diskful
53+
// promotion path to look up storage pools per node without
54+
// requiring a separate StoragePool client cache.
55+
Store store.Store
4956
}
5057

5158
// +kubebuilder:rbac:groups=blockstor.io.blockstor.io,resources=resources,verbs=get;list;watch;create;update;patch;delete
@@ -144,9 +151,98 @@ func (r *ResourceReconciler) runApply(ctx context.Context, target *blockstoriov1
144151
return ctrl.Result{Requeue: true}, nil
145152
}
146153

154+
// Auto-diskful: when a DISKLESS replica is actively used by a
155+
// consumer (InUse=true on this node) AND the hosting node has
156+
// a viable storage pool, promote it to diskful so reads stay
157+
// local. Cleanup (demote on idle) is intentionally not
158+
// automated yet — needs hysteresis to avoid flapping on
159+
// transient opens; operators demote via `linstor r d` until
160+
// then.
161+
promoted, err := r.maybeAutoDiskful(ctx, target)
162+
if err != nil {
163+
return ctrl.Result{}, err
164+
}
165+
166+
if promoted {
167+
return ctrl.Result{Requeue: true}, nil
168+
}
169+
147170
return r.dispatchApply(ctx, target, peers, nodeList.Items, rdPtr)
148171
}
149172

173+
// maybeAutoDiskful flips a DISKLESS-but-actively-used replica to
174+
// diskful when the hosting node has a usable storage pool. The
175+
// satellite reconciler picks up the spec change on the next pass
176+
// and creates the LV/zvol + attach. Returns true when Spec was
177+
// mutated so the caller can requeue.
178+
func (r *ResourceReconciler) maybeAutoDiskful(ctx context.Context, target *blockstoriov1alpha1.Resource) (bool, error) {
179+
if !slices.Contains(target.Spec.Flags, apiv1.ResourceFlagDiskless) {
180+
return false, nil
181+
}
182+
183+
if !target.Status.InUse {
184+
return false, nil
185+
}
186+
187+
if slices.Contains(target.Spec.Flags, apiv1.ResourceFlagTieBreaker) {
188+
// Tiebreaker witnesses must stay diskless — they're chosen
189+
// for the network presence, not local storage. Promoting a
190+
// tiebreaker would defeat the quorum semantic.
191+
return false, nil
192+
}
193+
194+
pool, err := r.firstAvailablePool(ctx, target.Spec.NodeName)
195+
if err != nil {
196+
return false, err
197+
}
198+
199+
if pool == "" {
200+
// No pool on this node → can't promote. Stay diskless.
201+
return false, nil
202+
}
203+
204+
target.Spec.Flags = slices.DeleteFunc(target.Spec.Flags,
205+
func(s string) bool { return s == apiv1.ResourceFlagDiskless })
206+
207+
if target.Spec.Props == nil {
208+
target.Spec.Props = map[string]string{}
209+
}
210+
211+
target.Spec.Props["StorPoolName"] = pool
212+
213+
err = r.Update(ctx, target)
214+
if err != nil {
215+
return false, err
216+
}
217+
218+
return true, nil
219+
}
220+
221+
// firstAvailablePool returns any non-diskless storage pool present on
222+
// the named node. Used by the auto-diskful promotion to pick a
223+
// destination for the freshly-attached LV. We don't try to be
224+
// clever: production clusters typically have one pool per node.
225+
func (r *ResourceReconciler) firstAvailablePool(ctx context.Context, nodeName string) (string, error) {
226+
pools, err := r.Store.StoragePools().List(ctx)
227+
if err != nil {
228+
return "", err
229+
}
230+
231+
for i := range pools {
232+
if pools[i].NodeName != nodeName {
233+
continue
234+
}
235+
236+
if pools[i].ProviderKind == apiv1.StoragePoolKindDiskless {
237+
continue
238+
}
239+
240+
return pools[i].StoragePoolName, nil
241+
}
242+
243+
return "", nil
244+
}
245+
150246
// dispatchApply resolves DRBD options and pushes the desired state to
151247
// the satellite. Pulled out of runApply so the latter stays under the
152248
// funlen budget — the resolver step grew non-trivial with the option

internal/controller/resourcedefinition_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func (r *ResourceDefinitionReconciler) ensureTiebreaker(ctx context.Context, rd
130130
witness := apiv1.Resource{
131131
Name: rd.Name,
132132
NodeName: tiebreakerNode,
133-
Flags: []string{"DISKLESS", "TIE_BREAKER"},
133+
Flags: []string{apiv1.ResourceFlagDiskless, apiv1.ResourceFlagTieBreaker},
134134
}
135135

136136
err = r.Store.Resources().Create(ctx, &witness)
@@ -148,7 +148,7 @@ func splitByDiskless(replicas []apiv1.Resource) ([]apiv1.Resource, []apiv1.Resou
148148
var diskful, diskless []apiv1.Resource
149149

150150
for i := range replicas {
151-
if slices.Contains(replicas[i].Flags, "DISKLESS") {
151+
if slices.Contains(replicas[i].Flags, apiv1.ResourceFlagDiskless) {
152152
diskless = append(diskless, replicas[i])
153153
} else {
154154
diskful = append(diskful, replicas[i])

0 commit comments

Comments
 (0)