Skip to content

Commit 2f34e3c

Browse files
kvapsclaude
andcommitted
feat(store): CRD-backed Store implementation; default it in cmd/main.go
Phase 2 backbone. The same pkg/store.Store interface is now satisfied by both pkg/store (in-process map) and pkg/store/k8s (controller-runtime client against Node and StoragePool CRDs). - api/v1alpha1: StoragePool CRD scaffolded; Spec carries (nodeName, poolName, providerKind, props), Status carries observed capacity/traits/conditions. Node spec.type validated as enum, so empty-Type Creates now fail uniformly on both stores (the in-memory store didn't catch this; CRDs do). - pkg/store/k8s: Nodes / StoragePools mapping CRD <-> wire shape. The StoragePool composite (node, pool) key is encoded as `<node>.<pool>` in metadata.name and indexed via blockstor.io/{node,pool}-name labels so ListByNode is server-side filtered. - pkg/store/storetest: shared test suite — every branch every Store must honour. pkg/store and pkg/store/k8s both call into it; behavioural drift between the two implementations would fail the same subtest immediately. - pkg/store/k8s/k8s_test.go: envtest harness (TestMain spins up etcd + kube-apiserver, loads our CRDs, wipes between subtests). Skips cleanly when KUBEBUILDER_ASSETS is missing so plain `go test ./...` stays green without `make setup-envtest`. With assets installed, all 18 NodeStore + StoragePoolStore subtests pass against real CRD validation. - cmd/main.go: --store={k8s|memory} flag; default k8s (uses mgr.GetClient()). - .golangci.yml: extra ignore-names + per-path exclusions for storetest. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 239e952 commit 2f34e3c

28 files changed

Lines changed: 1798 additions & 445 deletions

.golangci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ linters:
7272
- c
7373
- m
7474
- n
75+
- in
7576
- sp
77+
- st
7678
- rd
7779
- vd
7880
- rg
@@ -116,6 +118,23 @@ linters:
116118
- goconst
117119
- noinlineerr
118120
path: _test\.go
121+
# storetest is a shared test suite imported by both InMemory and k8s
122+
# store tests. It contains test code, just not in _test.go files, so
123+
# the same exclusions apply.
124+
- linters:
125+
- funlen
126+
- dupl
127+
- gocognit
128+
- gocyclo
129+
- cyclop
130+
- gocritic
131+
- nlreturn
132+
- wsl_v5
133+
- varnamelen
134+
- goconst
135+
- noinlineerr
136+
- thelper
137+
path: pkg/store/storetest/
119138
- linters:
120139
- forbidigo
121140
- gocyclo

PROJECT

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ resources:
1717
kind: Node
1818
path: github.com/cozystack/blockstor/api/v1alpha1
1919
version: v1alpha1
20+
- api:
21+
crdVersion: v1
22+
controller: true
23+
domain: blockstor.io
24+
group: blockstor.io
25+
kind: StoragePool
26+
path: github.com/cozystack/blockstor/api/v1alpha1
27+
version: v1alpha1
2028
version: "3"

api/v1alpha1/storagepool_types.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 v1alpha1
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
// StoragePoolSpec is the desired state of a LINSTOR storage pool.
24+
//
25+
// LINSTOR storage pools are keyed by (node_name, pool_name); a single CRD
26+
// instance represents one pool on one satellite. The metadata.name is the
27+
// LINSTOR pool name, and spec.nodeName names the satellite hosting it.
28+
type StoragePoolSpec struct {
29+
// nodeName is the name of the satellite hosting this pool. Same as the
30+
// LINSTOR Node CRD's metadata.name.
31+
// +required
32+
NodeName string `json:"nodeName"`
33+
34+
// poolName is the LINSTOR pool name (LVM VG, ZFS dataset, etc.). When
35+
// empty, defaults to metadata.name.
36+
// +optional
37+
PoolName string `json:"poolName,omitempty"`
38+
39+
// providerKind is the storage backend.
40+
// +kubebuilder:validation:Enum=LVM;LVM_THIN;ZFS;ZFS_THIN;FILE;FILE_THIN;DISKLESS
41+
ProviderKind string `json:"providerKind"`
42+
43+
// props is the LINSTOR property map for this pool.
44+
// +optional
45+
Props map[string]string `json:"props,omitempty"`
46+
}
47+
48+
// StoragePoolStatus is the observed state of a storage pool, populated by
49+
// the satellite once it brings up the backing volume group.
50+
type StoragePoolStatus struct {
51+
// freeCapacity is the free capacity in bytes reported by the satellite.
52+
// +optional
53+
FreeCapacity int64 `json:"freeCapacity,omitempty"`
54+
55+
// totalCapacity is the total capacity in bytes reported by the satellite.
56+
// +optional
57+
TotalCapacity int64 `json:"totalCapacity,omitempty"`
58+
59+
// supportsSnapshots is whether the backing provider can create snapshots.
60+
// +optional
61+
SupportsSnapshots bool `json:"supportsSnapshots,omitempty"`
62+
63+
// staticTraits exposes provider-static attributes (kind, etc.).
64+
// +optional
65+
StaticTraits map[string]string `json:"staticTraits,omitempty"`
66+
67+
// conditions represent the current state of the StoragePool resource.
68+
// +listType=map
69+
// +listMapKey=type
70+
// +optional
71+
Conditions []metav1.Condition `json:"conditions,omitempty"`
72+
}
73+
74+
// +kubebuilder:object:root=true
75+
// +kubebuilder:subresource:status
76+
// +kubebuilder:resource:scope=Cluster
77+
78+
// StoragePool is the Schema for the storagepools API
79+
type StoragePool struct {
80+
metav1.TypeMeta `json:",inline"`
81+
82+
// metadata is a standard object metadata
83+
// +optional
84+
metav1.ObjectMeta `json:"metadata,omitzero"`
85+
86+
// spec defines the desired state of StoragePool
87+
// +required
88+
Spec StoragePoolSpec `json:"spec"`
89+
90+
// status defines the observed state of StoragePool
91+
// +optional
92+
Status StoragePoolStatus `json:"status,omitzero"`
93+
}
94+
95+
// +kubebuilder:object:root=true
96+
97+
// StoragePoolList contains a list of StoragePool
98+
type StoragePoolList struct {
99+
metav1.TypeMeta `json:",inline"`
100+
metav1.ListMeta `json:"metadata,omitzero"`
101+
Items []StoragePool `json:"items"`
102+
}
103+
104+
func init() {
105+
SchemeBuilder.Register(&StoragePool{}, &StoragePoolList{})
106+
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/k8s/1.35.0-darwin-arm64/etcd

24.7 MB
Binary file not shown.
78.5 MB
Binary file not shown.
54.1 MB
Binary file not shown.

bin/setup-envtest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/kvaps/git/blockstor/bin/setup-envtest-release-0.23

bin/setup-envtest-release-0.23

10 MB
Binary file not shown.

cmd/main.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"github.com/cozystack/blockstor/internal/controller"
4040
"github.com/cozystack/blockstor/pkg/rest"
4141
"github.com/cozystack/blockstor/pkg/store"
42+
storek8s "github.com/cozystack/blockstor/pkg/store/k8s"
4243
// +kubebuilder:scaffold:imports
4344
)
4445

@@ -63,9 +64,12 @@ func main() {
6364
var secureMetrics bool
6465
var enableHTTP2 bool
6566
var restAddr string
67+
var storeKind string
6668
var tlsOpts []func(*tls.Config)
6769
flag.StringVar(&restAddr, "rest-bind-address", ":3370",
6870
"The address the LINSTOR-compatible REST API binds to (upstream LINSTOR plain-text port is 3370).")
71+
flag.StringVar(&storeKind, "store", "k8s",
72+
"Persistence backend for the REST API: 'k8s' (CRD-backed, default) or 'memory' (in-process, lost on restart, useful for tests).")
6973
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
7074
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
7175
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
@@ -189,6 +193,13 @@ func main() {
189193
setupLog.Error(err, "Failed to create controller", "controller", "node")
190194
os.Exit(1)
191195
}
196+
if err := (&controller.StoragePoolReconciler{
197+
Client: mgr.GetClient(),
198+
Scheme: mgr.GetScheme(),
199+
}).SetupWithManager(mgr); err != nil {
200+
setupLog.Error(err, "Failed to create controller", "controller", "storagepool")
201+
os.Exit(1)
202+
}
192203
// +kubebuilder:scaffold:builder
193204

194205
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
@@ -200,9 +211,22 @@ func main() {
200211
os.Exit(1)
201212
}
202213

203-
// Phase 1 store: in-memory. Phase 2 swaps in a CRD-backed implementation
204-
// behind the same interface.
205-
st := store.NewInMemory()
214+
// REST persistence: Kubernetes CRDs (default) or in-process map (tests).
215+
// Both implementations satisfy pkg/store.Store; the same test suite (in
216+
// pkg/store/storetest) exercises both, so behaviour cannot diverge.
217+
var st store.Store
218+
219+
switch storeKind {
220+
case "k8s":
221+
st = storek8s.New(mgr.GetClient())
222+
case "memory":
223+
st = store.NewInMemory()
224+
225+
setupLog.Info("Using in-memory store; data will be lost on restart")
226+
default:
227+
setupLog.Error(nil, "Unknown --store value", "store", storeKind)
228+
os.Exit(1)
229+
}
206230

207231
if err := mgr.Add(&rest.Server{Addr: restAddr, Store: st}); err != nil {
208232
setupLog.Error(err, "Failed to register REST API server")

0 commit comments

Comments
 (0)