Skip to content

Commit 4794ae2

Browse files
kvapsclaude
andcommitted
feat(satellite/controllers): scaffold pkg/satellite/controllers/ (Phase 10.1)
New package with four reconcilers + a NewManager builder. The satellite-as-controller-runtime migration's first concrete step. Reconcilers: * ResourceReconciler — Resources where Spec.NodeName == self. Stubbed body (Get + log); follow-up wires it through Config.Apply (the pre-existing pkg/satellite.Reconciler that owns storage + DRBD + LUKS). * ResourceDefinitionReconciler — RDs. Cache-warming-only with the `dropAllEventsPredicate()`; the informer fills, but the reconciler body never runs. Resource reconciler does client.Get(rd) when computing desired state. * SnapshotReconciler — Snapshots where Spec.Nodes ∋ self. Stubbed; follow-up delegates to Config.Apply.CreateSnapshot / DeleteSnapshot. * StoragePoolReconciler — StoragePools where Spec.NodeName == self. Stubbed; follow-up calls satellite.NewProviderFromKind + Reconciler.RegisterProvider (Phase 10.5 plumbing already in place). NewManager builds the controller-runtime manager with the shared scheme (apiv1alpha1 + client-go core types for Secrets), leader election disabled (DaemonSet enforces one-per-node), and registers all four. Reconciler bodies stay minimal so this commit is a pure scaffold — compiles, lints clean, doesn't yet supplant the gRPC path. Subsequent commits fill in the apply logic and wire NewManager into agent.Run (which then retires the gRPC server in Phase 10.6). Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 639ca5a commit 4794ae2

7 files changed

Lines changed: 632 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 controllers
18+
19+
import (
20+
"github.com/cozystack/blockstor/pkg/satellite"
21+
)
22+
23+
// Config carries the bits the four reconcilers need beyond what
24+
// the controller-runtime manager wires for them automatically.
25+
// Mirrors the existing `pkg/satellite.ReconcilerConfig` shape:
26+
// the apply-chain on a Resource event ultimately invokes the
27+
// pre-existing `pkg/satellite.Reconciler.applyOne` body via the
28+
// `Apply` shim, so we hold one and reuse it.
29+
type Config struct {
30+
// NodeName is the satellite's own name. The Resource +
31+
// StoragePool reconcilers filter `Spec.NodeName == NodeName`
32+
// at the predicate layer; the Snapshot reconciler checks
33+
// `Spec.Nodes ∋ NodeName` inside its reconcile body.
34+
NodeName string
35+
36+
// Apply is the existing satellite reconciler — the one that
37+
// today serves the gRPC `ApplyResources` handler and owns
38+
// the storage + DRBD + LUKS chain. Phase 10.1 reuses it so
39+
// the controller-runtime path doesn't duplicate logic.
40+
Apply *satellite.Reconciler
41+
}

pkg/satellite/controllers/doc.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 controllers is the satellite-side controller-runtime
18+
// migration target. Phase 10.1 of the architectural overhaul:
19+
// the satellite stops being a gRPC server that consumes
20+
// `ApplyResources` from the controller and instead watches the
21+
// apiserver directly for the CRDs it acts on.
22+
//
23+
// Four reconcilers live here, one per CRD type the satellite
24+
// cares about:
25+
//
26+
// - `ResourceReconciler` — Resources where
27+
// `Spec.NodeName == cfg.NodeName`. Replaces the gRPC
28+
// `ApplyResources` consumer.
29+
// - `ResourceDefinitionReconciler` — RDs. No own write path;
30+
// just feeds the cache (the Resource reconciler does
31+
// `client.Get(rd)` when computing the desired state).
32+
// - `SnapshotReconciler` — Snapshots whose `Spec.Nodes`
33+
// contains the satellite's name. Replaces the gRPC
34+
// `CreateSnapshot` / `DeleteSnapshot` consumer.
35+
// - `StoragePoolReconciler` — StoragePools where
36+
// `Spec.NodeName == cfg.NodeName`. Replaces the gRPC
37+
// `ApplyStoragePools` consumer. Phase 10.5's dynamic
38+
// `Reconciler.RegisterProvider` plugs in here.
39+
//
40+
// `NewManager` builds the controller-runtime manager and
41+
// registers all four reconcilers. Once Phase 10.1 lands, the
42+
// satellite's `agent.Run` becomes "build manager, register
43+
// reconcilers, start manager" — mirroring `cmd/main.go` on
44+
// the controller side. Until then the gRPC server keeps
45+
// running; this package's reconcilers are scaffolded but not
46+
// yet wired into the agent.
47+
//
48+
// Reconciler bodies are intentionally minimal in this initial
49+
// skeleton — they fetch the CRD, log what they see, and exit.
50+
// Subsequent commits fill in the actual apply logic by
51+
// delegating to the existing `pkg/satellite.Reconciler`
52+
// methods, which already cover the storage + DRBD + LUKS
53+
// chain.
54+
package controllers
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 controllers
18+
19+
import (
20+
"github.com/cockroachdb/errors"
21+
"k8s.io/apimachinery/pkg/runtime"
22+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
23+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
24+
"k8s.io/client-go/rest"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/manager"
27+
28+
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
29+
)
30+
31+
// scheme carries the runtime types this manager understands —
32+
// blockstor CRDs + the core Kubernetes types (Secrets for
33+
// passphrases / shared-secret references the satellite reads
34+
// directly). Package-level state matches controller-runtime's
35+
// own convention (`cmd/main.go` does the same on the
36+
// controller side).
37+
//
38+
//nolint:gochecknoglobals // package-init scheme registry, controller-runtime convention
39+
var scheme = runtime.NewScheme()
40+
41+
func init() {
42+
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
43+
utilruntime.Must(blockstoriov1alpha1.AddToScheme(scheme))
44+
}
45+
46+
// NewManager constructs a controller-runtime manager wired
47+
// with all four satellite-side reconcilers. The caller is
48+
// expected to call `mgr.Start(ctx)` to begin reconciling.
49+
//
50+
// `restCfg` is typically the in-cluster config the satellite's
51+
// DaemonSet pod gets from its ServiceAccount; for local dev /
52+
// envtest the caller passes the test environment's REST config.
53+
//
54+
// `cfg.Apply` is the existing `pkg/satellite.Reconciler` (the
55+
// one the gRPC `ApplyResources` consumer drives today). The
56+
// satellite-side reconcilers translate the CRD events into
57+
// `Apply.Apply([...DesiredResource])` calls so the storage +
58+
// DRBD + LUKS chain stays a single code path during the
59+
// migration.
60+
//
61+
// Phase 10.1. The reconcilers in this package are scaffolded
62+
// but not yet wired into `agent.Run`; this `NewManager` exists
63+
// so tests can validate the registration + filter-predicate
64+
// plumbing independently of the agent's gRPC-server-still-
65+
// running mainline.
66+
func NewManager(restCfg *rest.Config, cfg Config) (manager.Manager, error) {
67+
if cfg.NodeName == "" {
68+
return nil, errors.New("controllers: NodeName is required")
69+
}
70+
71+
if cfg.Apply == nil {
72+
return nil, errors.New("controllers: Apply is required")
73+
}
74+
75+
mgr, err := ctrl.NewManager(restCfg, ctrl.Options{
76+
Scheme: scheme,
77+
// Satellite is per-node; leader election would require
78+
// a cluster-wide Lease the satellite has no business
79+
// holding. Skip it; the DaemonSet enforces
80+
// one-pod-per-node already.
81+
LeaderElection: false,
82+
})
83+
if err != nil {
84+
return nil, errors.Wrap(err, "create manager")
85+
}
86+
87+
err = (&ResourceReconciler{Config: cfg, Client: mgr.GetClient()}).SetupWithManager(mgr)
88+
if err != nil {
89+
return nil, errors.Wrap(err, "setup ResourceReconciler")
90+
}
91+
92+
err = (&ResourceDefinitionReconciler{Config: cfg, Client: mgr.GetClient()}).SetupWithManager(mgr)
93+
if err != nil {
94+
return nil, errors.Wrap(err, "setup ResourceDefinitionReconciler")
95+
}
96+
97+
err = (&SnapshotReconciler{Config: cfg, Client: mgr.GetClient()}).SetupWithManager(mgr)
98+
if err != nil {
99+
return nil, errors.Wrap(err, "setup SnapshotReconciler")
100+
}
101+
102+
err = (&StoragePoolReconciler{Config: cfg, Client: mgr.GetClient()}).SetupWithManager(mgr)
103+
if err != nil {
104+
return nil, errors.Wrap(err, "setup StoragePoolReconciler")
105+
}
106+
107+
return mgr, nil
108+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 controllers
18+
19+
import (
20+
"context"
21+
22+
"github.com/cockroachdb/errors"
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/builder"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/event"
28+
"sigs.k8s.io/controller-runtime/pkg/log"
29+
"sigs.k8s.io/controller-runtime/pkg/predicate"
30+
31+
blockstoriov1alpha1 "github.com/cozystack/blockstor/api/v1alpha1"
32+
)
33+
34+
// ResourceReconciler watches Resource CRDs filtered to those
35+
// placed on this satellite's node and translates them into the
36+
// existing apply chain. Phase 10.1: replaces the gRPC
37+
// `ApplyResources` consumer.
38+
//
39+
// The reconciler is intentionally minimal in this initial
40+
// commit — it logs what it sees and exits. Subsequent commits
41+
// fill in the desired-state-builder + apply path by delegating
42+
// to `Config.Apply` (the pre-existing satellite reconciler that
43+
// owns storage + DRBD + LUKS).
44+
type ResourceReconciler struct {
45+
client.Client
46+
47+
Config Config
48+
}
49+
50+
// Reconcile reads a Resource and drives the satellite-side
51+
// apply chain when the Resource is placed on this node.
52+
// Deletion is handled via finalizer in a follow-up commit.
53+
func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
54+
logger := log.FromContext(ctx).WithValues("resource", req.Name)
55+
56+
var res blockstoriov1alpha1.Resource
57+
58+
err := r.Get(ctx, req.NamespacedName, &res)
59+
if err != nil {
60+
if apierrors.IsNotFound(err) {
61+
return ctrl.Result{}, nil
62+
}
63+
64+
return ctrl.Result{}, errors.Wrap(err, "get Resource")
65+
}
66+
67+
if res.Spec.NodeName != r.Config.NodeName {
68+
// Predicate should have filtered this out — defensive
69+
// check in case the watch cache is mid-resync.
70+
return ctrl.Result{}, nil
71+
}
72+
73+
logger.V(1).Info("observed Resource",
74+
"rd", res.Spec.ResourceDefinitionName,
75+
"flags", res.Spec.Flags,
76+
"deletionTimestamp", res.DeletionTimestamp)
77+
78+
// Phase 10.1 follow-up: translate to DesiredResource +
79+
// call r.Config.Apply.Apply(...). Until then the gRPC
80+
// `ApplyResources` consumer continues to drive applies.
81+
return ctrl.Result{}, nil
82+
}
83+
84+
// SetupWithManager wires this reconciler with a node-name
85+
// predicate so only Resources placed on this satellite trigger
86+
// reconciles. The predicate also fires on Delete events (the
87+
// CRD finalizer dance still needs us to clean up local state).
88+
func (r *ResourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
89+
err := ctrl.NewControllerManagedBy(mgr).
90+
For(&blockstoriov1alpha1.Resource{},
91+
builder.WithPredicates(nodeNamePredicate(r.Config.NodeName))).
92+
Named("satellite-resource").
93+
Complete(r)
94+
if err != nil {
95+
return errors.Wrap(err, "register ResourceReconciler")
96+
}
97+
98+
return nil
99+
}
100+
101+
// nodeNamePredicate filters events down to objects whose
102+
// `Spec.NodeName` matches the given node. Centralised so the
103+
// StoragePool reconciler can reuse the same shape.
104+
func nodeNamePredicate(nodeName string) predicate.Predicate {
105+
matches := func(obj client.Object) bool {
106+
if r, ok := obj.(*blockstoriov1alpha1.Resource); ok {
107+
return r.Spec.NodeName == nodeName
108+
}
109+
110+
if sp, ok := obj.(*blockstoriov1alpha1.StoragePool); ok {
111+
return sp.Spec.NodeName == nodeName
112+
}
113+
114+
return false
115+
}
116+
117+
return predicate.Funcs{
118+
CreateFunc: func(e event.CreateEvent) bool { return matches(e.Object) },
119+
UpdateFunc: func(e event.UpdateEvent) bool {
120+
return matches(e.ObjectNew) || matches(e.ObjectOld)
121+
},
122+
DeleteFunc: func(e event.DeleteEvent) bool { return matches(e.Object) },
123+
GenericFunc: func(e event.GenericEvent) bool { return matches(e.Object) },
124+
}
125+
}

0 commit comments

Comments
 (0)