Skip to content

Commit 991b994

Browse files
kvapsclaude
andcommitted
feat(rest): resource adjust / adjust-all nudge endpoints
Verify the target exists and return 200; the per-replica drbdadm work runs out-of-band on the satellite's next reconcile pass. We don't synchronously shell out — that would block the REST handler on kernel I/O and surface flaky timeouts. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 30e346e commit 991b994

4 files changed

Lines changed: 170 additions & 1 deletion

File tree

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ Full scope list lives in `docs/csi-api-surface.md` (to be created in Phase 1).
271271
- [ ] All `/v1/view/*` aggregates
272272
- [x] Controller properties endpoints (`/v1/controller/properties` GET/POST) — backed by KV-store instance "ControllerProps". Covers `linstor controller list-properties` / `set-property`. 3 contract tests.
273273
- [ ] Property-info endpoints (`*/properties/info`)
274-
- [ ] Resource adjust / adjust-all
274+
- [x] Resource adjust / adjust-all (`POST /v1/resource-definitions/{rd}/adjust` and `.../resources/{node}/adjust`): existence check + 200; per-replica `drbdadm adjust` runs out-of-band via the satellite reconciler. 4 contract tests.
275275

276276
### Phase 8 — Java decommission
277277

pkg/rest/resource_adjust.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 rest
18+
19+
import (
20+
"net/http"
21+
)
22+
23+
// registerAdjust wires the adjust nudges. Both endpoints are
24+
// fire-and-forget: they verify the target exists and return 200; the
25+
// per-replica work (`drbdadm adjust`) happens out-of-band via the
26+
// satellite reconcile loop, which already runs adjust on every Apply.
27+
//
28+
// Upstream LINSTOR runs `drbdadm adjust` synchronously here. We
29+
// intentionally don't — synchronous adjust blocks the REST handler on
30+
// kernel I/O and surfaces flaky timeouts. The reconciler's next pass
31+
// picks up the change idempotently.
32+
func (s *Server) registerAdjust(mux *http.ServeMux) {
33+
mux.HandleFunc("POST /v1/resource-definitions/{rd}/adjust",
34+
s.requireStore(s.handleAdjustAll))
35+
mux.HandleFunc("POST /v1/resource-definitions/{rd}/resources/{node}/adjust",
36+
s.requireStore(s.handleAdjustOne))
37+
}
38+
39+
// handleAdjustAll verifies the RD exists, then returns 200. A real
40+
// implementation would bump a generation token the satellite watches,
41+
// but until WatchResources lands the reconciler polls.
42+
func (s *Server) handleAdjustAll(w http.ResponseWriter, r *http.Request) {
43+
rdName := r.PathValue("rd")
44+
45+
_, err := s.Store.ResourceDefinitions().Get(r.Context(), rdName)
46+
if err != nil {
47+
writeStoreError(w, err)
48+
49+
return
50+
}
51+
52+
w.WriteHeader(http.StatusOK)
53+
}
54+
55+
// handleAdjustOne is the per-replica counterpart.
56+
func (s *Server) handleAdjustOne(w http.ResponseWriter, r *http.Request) {
57+
rdName := r.PathValue("rd")
58+
node := r.PathValue("node")
59+
60+
_, err := s.Store.Resources().Get(r.Context(), rdName, node)
61+
if err != nil {
62+
writeStoreError(w, err)
63+
64+
return
65+
}
66+
67+
w.WriteHeader(http.StatusOK)
68+
}

pkg/rest/resource_adjust_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 rest
18+
19+
import (
20+
"net/http"
21+
"testing"
22+
23+
apiv1 "github.com/cozystack/blockstor/pkg/api/v1"
24+
"github.com/cozystack/blockstor/pkg/store"
25+
)
26+
27+
// TestAdjustAllOnExistingRD: posting to /v1/resource-definitions/{rd}/adjust
28+
// returns 200 — the controller's job is to mark the RD for resync; the
29+
// per-replica work happens out-of-band via the satellite reconciler.
30+
func TestAdjustAllOnExistingRD(t *testing.T) {
31+
st := store.NewInMemory()
32+
if err := st.ResourceDefinitions().Create(t.Context(), &apiv1.ResourceDefinition{Name: "pvc-1"}); err != nil {
33+
t.Fatalf("seed: %v", err)
34+
}
35+
36+
base, stop := startServerWithStore(t, st)
37+
defer stop()
38+
39+
resp := httpPost(t, base+"/v1/resource-definitions/pvc-1/adjust", nil)
40+
_ = resp.Body.Close()
41+
42+
if resp.StatusCode != http.StatusOK {
43+
t.Errorf("status: got %d, want 200", resp.StatusCode)
44+
}
45+
}
46+
47+
// TestAdjustAllUnknownRD: 404 on missing RD.
48+
func TestAdjustAllUnknownRD(t *testing.T) {
49+
base, stop := startServerWithStore(t, store.NewInMemory())
50+
defer stop()
51+
52+
resp := httpPost(t, base+"/v1/resource-definitions/ghost/adjust", nil)
53+
_ = resp.Body.Close()
54+
55+
if resp.StatusCode != http.StatusNotFound {
56+
t.Errorf("status: got %d, want 404", resp.StatusCode)
57+
}
58+
}
59+
60+
// TestAdjustResource: POST /resources/{node}/adjust nudges one replica.
61+
func TestAdjustResource(t *testing.T) {
62+
st := store.NewInMemory()
63+
ctx := t.Context()
64+
65+
if err := st.ResourceDefinitions().Create(ctx, &apiv1.ResourceDefinition{Name: "pvc-1"}); err != nil {
66+
t.Fatalf("seed RD: %v", err)
67+
}
68+
69+
if err := st.Resources().Create(ctx, &apiv1.Resource{Name: "pvc-1", NodeName: "n1"}); err != nil {
70+
t.Fatalf("seed Resource: %v", err)
71+
}
72+
73+
base, stop := startServerWithStore(t, st)
74+
defer stop()
75+
76+
resp := httpPost(t, base+"/v1/resource-definitions/pvc-1/resources/n1/adjust", nil)
77+
_ = resp.Body.Close()
78+
79+
if resp.StatusCode != http.StatusOK {
80+
t.Errorf("status: got %d, want 200", resp.StatusCode)
81+
}
82+
}
83+
84+
// TestAdjustResourceUnknown: 404 if the per-replica Resource is missing.
85+
func TestAdjustResourceUnknown(t *testing.T) {
86+
st := store.NewInMemory()
87+
if err := st.ResourceDefinitions().Create(t.Context(), &apiv1.ResourceDefinition{Name: "pvc-1"}); err != nil {
88+
t.Fatalf("seed: %v", err)
89+
}
90+
91+
base, stop := startServerWithStore(t, st)
92+
defer stop()
93+
94+
resp := httpPost(t, base+"/v1/resource-definitions/pvc-1/resources/n1/adjust", nil)
95+
_ = resp.Body.Close()
96+
97+
if resp.StatusCode != http.StatusNotFound {
98+
t.Errorf("status: got %d, want 404", resp.StatusCode)
99+
}
100+
}

pkg/rest/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (s *Server) Start(ctx context.Context) error {
6868
s.registerSpawn(mux)
6969
s.registerAutoplace(mux)
7070
s.registerControllerProperties(mux)
71+
s.registerAdjust(mux)
7172

7273
srv := &http.Server{
7374
Addr: s.Addr,

0 commit comments

Comments
 (0)