@@ -26,13 +26,16 @@ package satellite
2626import (
2727 "context"
2828 "log/slog"
29+ "net"
2930 "time"
3031
3132 "github.com/cockroachdb/errors"
3233 "google.golang.org/grpc"
3334 "google.golang.org/grpc/credentials/insecure"
3435
36+ "github.com/cozystack/blockstor/pkg/drbd"
3537 satellitepb "github.com/cozystack/blockstor/pkg/satellite/proto"
38+ "github.com/cozystack/blockstor/pkg/storage"
3639 "github.com/cozystack/blockstor/pkg/version"
3740)
3841
@@ -45,10 +48,21 @@ type Config struct {
4548 // ControllerAddr is the gRPC dial address of the blockstor-controller.
4649 ControllerAddr string
4750
51+ // ListenAddr is the bind address for the satellite's own gRPC server
52+ // (the side that the controller dials for ApplyResources, snapshot
53+ // RPCs, ship). Empty disables the server (useful for unit tests).
54+ ListenAddr string
55+
4856 // StateDir is the on-disk directory the satellite uses for DRBD .res
4957 // files and per-resource state. Required.
5058 StateDir string
5159
60+ // Providers maps storage-pool name → provider implementation. The
61+ // satellite reconciler uses this to resolve which backend a
62+ // DesiredVolume's StoragePool refers to. Seeded at startup from CLI
63+ // flags / env (LVM-thin, ZFS, file).
64+ Providers map [string ]storage.Provider
65+
5266 // DialTimeout caps gRPC connection establishment per attempt.
5367 DialTimeout time.Duration
5468
@@ -65,6 +79,8 @@ type Agent struct {
6579}
6680
6781// NewAgent constructs an Agent without yet dialling the controller.
82+ //
83+ //nolint:gocritic // value receiver is the ergonomic public API; Config is the binary's flag bundle.
6884func NewAgent (cfg Config ) * Agent {
6985 logger := cfg .Logger
7086 if logger == nil {
@@ -75,11 +91,9 @@ func NewAgent(cfg Config) *Agent {
7591}
7692
7793// Run is the agent's main loop. It dials the controller, performs the
78- // hello handshake to register the node, then keeps long-running apply
79- // and observe loops supervised. When ctx is cancelled the loops drain
80- // and Run returns ctx.Err().
81- //
82- // Phase 3.1: hello is the only real RPC; reconcile loops still no-op.
94+ // hello handshake to register the node, starts the satellite-side
95+ // gRPC server (so the controller can push desired state), then waits
96+ // for ctx to cancel.
8397func (a * Agent ) Run (ctx context.Context ) error {
8498 if a .cfg .NodeName == "" {
8599 return errors .New ("NodeName is required" )
@@ -88,28 +102,84 @@ func (a *Agent) Run(ctx context.Context) error {
88102 a .logger .Info ("agent starting" ,
89103 "node" , a .cfg .NodeName ,
90104 "blockstor_version" , version .Version ,
91- "controller" , a .cfg .ControllerAddr )
105+ "controller" , a .cfg .ControllerAddr ,
106+ "listen" , a .cfg .ListenAddr )
92107
93108 conn , err := a .dial (ctx )
94109 if err != nil {
95110 return errors .Wrap (err , "dial controller" )
96111 }
97112 defer func () { _ = conn .Close () }()
98113
99- client := satellitepb .NewSatelliteClient (conn )
114+ client := satellitepb .NewControllerClient (conn )
100115
101116 err = a .hello (ctx , client )
102117 if err != nil {
103118 return errors .Wrap (err , "hello" )
104119 }
105120
121+ // Bring up the satellite-side gRPC server so the controller can push
122+ // ApplyResources / snapshot RPCs at us. The Reconciler is wired with
123+ // the configured providers + drbdadm wrapper + state dir.
124+ srv , stop , err := a .startGRPCServer (ctx )
125+ if err != nil {
126+ return errors .Wrap (err , "start gRPC server" )
127+ }
128+ defer stop ()
129+
130+ a .logger .Info ("satellite gRPC ready" , "addr" , srv )
131+
106132 <- ctx .Done ()
107133
108134 a .logger .Info ("agent stopping" , "node" , a .cfg .NodeName )
109135
110136 return ctx .Err () //nolint:wrapcheck // bubbling ctx.Err() unwrapped is the convention
111137}
112138
139+ // startGRPCServer binds the satellite's `service Satellite` listener.
140+ // Empty cfg.ListenAddr disables the server (returns a no-op stop) so
141+ // unit tests that only exercise Hello don't need a free port.
142+ func (a * Agent ) startGRPCServer (ctx context.Context ) (string , func (), error ) {
143+ if a .cfg .ListenAddr == "" {
144+ return "<disabled>" , func () {}, nil
145+ }
146+
147+ rec := NewReconciler (ReconcilerConfig {
148+ Providers : a .cfg .Providers ,
149+ Adm : drbd .NewAdm (storage.RealExec {}),
150+ StateDir : a .cfg .StateDir ,
151+ NodeName : a .cfg .NodeName ,
152+ })
153+
154+ listenCfg := & net.ListenConfig {}
155+
156+ listener , err := listenCfg .Listen (ctx , "tcp" , a .cfg .ListenAddr )
157+ if err != nil {
158+ return "" , nil , errors .Wrapf (err , "listen %s" , a .cfg .ListenAddr )
159+ }
160+
161+ gs := grpc .NewServer ()
162+ satellitepb .RegisterSatelliteServer (gs , NewGRPCServer (rec ))
163+
164+ done := make (chan struct {})
165+
166+ go func () {
167+ defer close (done )
168+
169+ err := gs .Serve (listener )
170+ if err != nil {
171+ a .logger .Error ("gRPC Serve returned" , "err" , err )
172+ }
173+ }()
174+
175+ stop := func () {
176+ gs .GracefulStop ()
177+ <- done
178+ }
179+
180+ return listener .Addr ().String (), stop , nil
181+ }
182+
113183// dial opens an insecure gRPC connection to the controller. TLS comes in
114184// Phase 6 alongside the rest of the encryption work; cluster traffic is
115185// expected to ride a private k8s network until then.
@@ -134,7 +204,7 @@ func (a *Agent) dial(ctx context.Context) (*grpc.ClientConn, error) {
134204// hello is the registration handshake. The satellite tells the controller
135205// who it is and what layers / providers it can drive; the controller
136206// upserts the corresponding Node CRD and replies with the cluster id.
137- func (a * Agent ) hello (ctx context.Context , client satellitepb.SatelliteClient ) error {
207+ func (a * Agent ) hello (ctx context.Context , client satellitepb.ControllerClient ) error {
138208 rpcCtx , cancel := context .WithTimeout (ctx , a .cfg .DialTimeout )
139209 defer cancel ()
140210
0 commit comments