Skip to content

Commit 940efe2

Browse files
kvapsclaude
andcommitted
fix(rest): csi-sanity unblockers — int64 ret_code, /v1/remotes/* stubs
Two regressions surfaced by csi-sanity: 1. APICallRc.RetCode was uint64. golinstor decodes it as int64; our 0xC000_0000_0000_0000 (= 13835058055282163712 unsigned) didn't fit. Switch to int64 with the negative two's-complement wire shape that upstream Java actually emits. 2. golinstor's snapshot-list path probes /v1/remotes/s3 first; a 404 surfaced as 'failed to list available s3 remotes'. Add /v1/remotes(/s3|/linstor|/ebs) returning [] — cozystack doesn't do cross-cluster shipping, but we owe the empty-list shape so golinstor stops choking. server.Start refactored into Start + buildMux to stay under funlen. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 9d5b45f commit 940efe2

4 files changed

Lines changed: 86 additions & 30 deletions

File tree

pkg/api/v1/storage_pool.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ type StoragePool struct {
3636
// APICallRc is the upstream `ApiCallRc` envelope. We define a minimal subset
3737
// here so types compile. Phase 2 will populate it from `golinstor` apiconsts.
3838
type APICallRc struct {
39-
RetCode uint64 `json:"ret_code"`
39+
// RetCode is the LINSTOR-style status mask. golinstor's
40+
// `client.ApiCallError` decodes this as int64; the upstream
41+
// `MASK_ERROR` bit makes the value negative on the wire (the
42+
// high bit of a signed int64), so we use int64 to match.
43+
RetCode int64 `json:"ret_code"`
4044
Message string `json:"message,omitempty"`
4145
Cause string `json:"cause,omitempty"`
4246
Details string `json:"details,omitempty"`

pkg/rest/nodes.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ func writeError(w http.ResponseWriter, status int, msg string) {
175175
}})
176176
}
177177

178-
// apiCallRcError is upstream LINSTOR's ERROR mask (high bit set on a
179-
// 64-bit mask). golinstor checks for this bit to decide pass/fail.
180-
const apiCallRcError uint64 = 0xC000_0000_0000_0000
178+
// apiCallRcError carries upstream LINSTOR's MASK_ERROR + WARN bits.
179+
// Upstream uses 0xC000_0000_0000_0000 as a `long` literal — that's
180+
// a negative int64 once you set both top bits. We literal-cast to
181+
// match the wire shape golinstor expects.
182+
const apiCallRcError int64 = -0x4000_0000_0000_0000

pkg/rest/remotes.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
// registerRemotes wires the LINSTOR `remotes` endpoints. Cozystack
24+
// doesn't use cross-cluster snapshot shipping, so the typed-by-kind
25+
// stubs return empty arrays — golinstor's snapshot list path calls
26+
// `/v1/remotes/s3` before enumerating snapshots, and a 404 there
27+
// surfaces as `failed to list available s3 remotes` from linstor-csi.
28+
//
29+
// Empty `[]` keeps the wire format LINSTOR-shaped without exposing
30+
// any remote-shipping surface area we don't implement.
31+
func (s *Server) registerRemotes(mux *http.ServeMux) {
32+
mux.HandleFunc("GET /v1/remotes", handleEmptyArray)
33+
mux.HandleFunc("GET /v1/remotes/s3", handleEmptyArray)
34+
mux.HandleFunc("GET /v1/remotes/linstor", handleEmptyArray)
35+
mux.HandleFunc("GET /v1/remotes/ebs", handleEmptyArray)
36+
}
37+
38+
func handleEmptyArray(w http.ResponseWriter, _ *http.Request) {
39+
writeJSON(w, http.StatusOK, []map[string]string{})
40+
}

pkg/rest/server.go

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,7 @@ func (s *Server) NeedLeaderElection() bool { return false }
5454
func (s *Server) Start(ctx context.Context) error {
5555
logger := log.FromContext(ctx).WithName("rest")
5656

57-
mux := http.NewServeMux()
58-
mux.HandleFunc("GET /v1/controller/version", handleVersion)
59-
mux.HandleFunc("GET /v1/healthz", handleHealth)
60-
s.registerNodes(mux)
61-
s.registerStoragePools(mux)
62-
s.registerResourceGroups(mux)
63-
s.registerResourceDefinitions(mux)
64-
s.registerVolumeDefinitions(mux)
65-
s.registerResources(mux)
66-
s.registerKeyValueStore(mux)
67-
s.registerSnapshots(mux)
68-
s.registerSpawn(mux)
69-
s.registerAutoplace(mux)
70-
s.registerControllerProperties(mux)
71-
s.registerAdjust(mux)
72-
s.registerStats(mux)
73-
s.registerErrorReports(mux)
74-
s.registerPropertiesInfo(mux)
75-
s.registerSnapshotRestore(mux)
76-
s.registerEncryption(mux)
77-
s.registerNodeLifecycle(mux)
78-
s.registerDRBDProxy(mux)
79-
s.registerExternalFiles(mux)
80-
s.registerDRBDPassphrase(mux)
81-
s.registerRDClone(mux)
82-
s.registerSOSReport(mux)
57+
mux := s.buildMux()
8358

8459
srv := &http.Server{
8560
Addr: s.Addr,
@@ -114,6 +89,41 @@ func (s *Server) Start(ctx context.Context) error {
11489
}
11590
}
11691

92+
// buildMux registers every endpoint on a fresh ServeMux. Pulled out
93+
// of Start to keep the latter under the funlen budget — Start now
94+
// only handles lifecycle (listener + shutdown), this owns routing.
95+
func (s *Server) buildMux() *http.ServeMux {
96+
mux := http.NewServeMux()
97+
mux.HandleFunc("GET /v1/controller/version", handleVersion)
98+
mux.HandleFunc("GET /v1/healthz", handleHealth)
99+
s.registerNodes(mux)
100+
s.registerStoragePools(mux)
101+
s.registerResourceGroups(mux)
102+
s.registerResourceDefinitions(mux)
103+
s.registerVolumeDefinitions(mux)
104+
s.registerResources(mux)
105+
s.registerKeyValueStore(mux)
106+
s.registerSnapshots(mux)
107+
s.registerSpawn(mux)
108+
s.registerAutoplace(mux)
109+
s.registerControllerProperties(mux)
110+
s.registerAdjust(mux)
111+
s.registerStats(mux)
112+
s.registerErrorReports(mux)
113+
s.registerPropertiesInfo(mux)
114+
s.registerSnapshotRestore(mux)
115+
s.registerEncryption(mux)
116+
s.registerNodeLifecycle(mux)
117+
s.registerDRBDProxy(mux)
118+
s.registerExternalFiles(mux)
119+
s.registerDRBDPassphrase(mux)
120+
s.registerRDClone(mux)
121+
s.registerSOSReport(mux)
122+
s.registerRemotes(mux)
123+
124+
return mux
125+
}
126+
117127
// Compile-time check that we satisfy the runnable contract.
118128
var _ manager.Runnable = (*Server)(nil)
119129

0 commit comments

Comments
 (0)