Skip to content

Commit 3422788

Browse files
committed
fix: squadless-provision warning + real AssignToSquad (T1-3)
2 parents a771066 + 6e0396d commit 3422788

4 files changed

Lines changed: 51 additions & 10 deletions

File tree

internal/adapter/remnawave/circuit_breaker.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
TimeoutGetUser = 5 * time.Second
2727
TimeoutUpdateUser = 5 * time.Second
2828
TimeoutGetNodes = 10 * time.Second
29+
TimeoutAssignSquad = 5 * time.Second
2930
)
3031

3132
// Prometheus metric constants for the circuit breaker.
@@ -191,3 +192,13 @@ func (rc *ResilientClient) DisableUser(ctx context.Context, uuid string) error {
191192
return rc.client.DisableUser(ctx, uuid)
192193
})
193194
}
195+
196+
// AddUsersToInternalSquad assigns users to an internal squad through the
197+
// circuit breaker.
198+
func (rc *ResilientClient) AddUsersToInternalSquad(ctx context.Context, squadUUID string, userUUIDs []string) error {
199+
ctx, cancel := withTimeout(ctx, TimeoutAssignSquad)
200+
defer cancel()
201+
return cbExecNoResult(rc.cb, func() error {
202+
return rc.client.AddUsersToInternalSquad(ctx, squadUUID, userUUIDs)
203+
})
204+
}

internal/adapter/remnawave/gateway_adapter.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package remnawave
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67

78
"github.com/BEDOLAGA-DEV/RemnaCore/internal/domain/multisub"
89
"github.com/BEDOLAGA-DEV/RemnaCore/pkg/clock"
@@ -19,13 +20,17 @@ type GatewayAdapter struct {
1920
client *ResilientClient
2021
clock clock.Clock
2122
defaultInternalSquads []string
23+
logger *slog.Logger
2224
}
2325

2426
// NewGatewayAdapter creates a GatewayAdapter backed by the given resilient
2527
// client. defaultInternalSquads is applied to every CreateUser call that does
2628
// not supply its own ActiveInternalSquads override.
27-
func NewGatewayAdapter(client *ResilientClient, clk clock.Clock, defaultInternalSquads []string) *GatewayAdapter {
28-
return &GatewayAdapter{client: client, clock: clk, defaultInternalSquads: defaultInternalSquads}
29+
func NewGatewayAdapter(client *ResilientClient, clk clock.Clock, defaultInternalSquads []string, logger *slog.Logger) *GatewayAdapter {
30+
if logger == nil {
31+
logger = slog.Default()
32+
}
33+
return &GatewayAdapter{client: client, clock: clk, defaultInternalSquads: defaultInternalSquads, logger: logger}
2934
}
3035

3136
// CreateUser provisions a VPN user in Remnawave, translating the domain
@@ -39,6 +44,13 @@ func (a *GatewayAdapter) CreateUser(ctx context.Context, req multisub.CreateRemn
3944
if len(rwReq.ActiveInternalSquads) == 0 {
4045
rwReq.ActiveInternalSquads = a.defaultInternalSquads
4146
}
47+
if len(rwReq.ActiveInternalSquads) == 0 {
48+
// A user with no internal squads gets an EMPTY, non-working subscription
49+
// (no inbounds). This is almost always a misconfiguration — warn loudly
50+
// per provision, not just once at boot, so it surfaces in operations.
51+
a.logger.WarnContext(ctx, "remnawave: provisioning VPN user with NO internal squads — subscription will not work; set REMNAWAVE_DEFAULT_INTERNAL_SQUADS or supply squads per request",
52+
slog.String("username", req.Username))
53+
}
4254
if req.ExpireAt != nil {
4355
rwReq.ExpireAt = *req.ExpireAt
4456
} else {
@@ -96,11 +108,12 @@ func (a *GatewayAdapter) DisableUser(ctx context.Context, remnawaveUUID string)
96108
return nil
97109
}
98110

99-
// AssignToSquad assigns a Remnawave user to a squad (server group). This maps
100-
// to a future Remnawave API endpoint; currently a no-op placeholder that
101-
// returns nil to indicate success.
102-
func (a *GatewayAdapter) AssignToSquad(_ context.Context, _, _ string) error {
103-
// TODO: implement when Remnawave adds squad assignment API endpoint.
111+
// AssignToSquad assigns a Remnawave user to an internal squad (server group)
112+
// via the bulk-add-users endpoint.
113+
func (a *GatewayAdapter) AssignToSquad(ctx context.Context, remnawaveUUID, squadUUID string) error {
114+
if err := a.client.AddUsersToInternalSquad(ctx, squadUUID, []string{remnawaveUUID}); err != nil {
115+
return fmt.Errorf("remnawave assign user %s to squad %s: %w", remnawaveUUID, squadUUID, err)
116+
}
104117
return nil
105118
}
106119

internal/adapter/remnawave/gateway_adapter_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestGatewayAdapter_CreateUser_AppliesSquads(t *testing.T) {
2424
}))
2525
defer srv.Close()
2626
mk := func(def []string) *GatewayAdapter {
27-
return NewGatewayAdapter(NewResilientClient(NewClient(srv.URL, "t"), circuitbreaker.DefaultConfig(), slog.Default()), clock.NewReal(), def)
27+
return NewGatewayAdapter(NewResilientClient(NewClient(srv.URL, "t"), circuitbreaker.DefaultConfig(), slog.Default()), clock.NewReal(), def, slog.Default())
2828
}
2929
_, err := mk([]string{"default-sq"}).CreateUser(context.Background(), multisub.CreateRemnawaveUserRequest{Username: "u"})
3030
require.NoError(t, err)
@@ -51,7 +51,7 @@ func TestGatewayAdapter_GetUser_StatusMapping(t *testing.T) {
5151
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5252
fmt.Fprintf(w, `{"response":{"uuid":"u1","status":%q,"userTraffic":{"usedTrafficBytes":1024}}}`, tc.status)
5353
}))
54-
a := NewGatewayAdapter(NewResilientClient(NewClient(srv.URL, "t"), circuitbreaker.DefaultConfig(), slog.Default()), clock.NewReal(), nil)
54+
a := NewGatewayAdapter(NewResilientClient(NewClient(srv.URL, "t"), circuitbreaker.DefaultConfig(), slog.Default()), clock.NewReal(), nil, slog.Default())
5555
got, err := a.GetUser(context.Background(), "u1")
5656
require.NoError(t, err)
5757
require.Equal(t, tc.wantEnabled, got.Enabled, "status %s enabled", tc.status)
@@ -60,3 +60,20 @@ func TestGatewayAdapter_GetUser_StatusMapping(t *testing.T) {
6060
srv.Close()
6161
}
6262
}
63+
64+
func TestGatewayAdapter_AssignToSquad_HitsBulkAddEndpoint(t *testing.T) {
65+
var gotPath string
66+
var gotBody []byte
67+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68+
gotPath = r.URL.Path
69+
gotBody, _ = io.ReadAll(r.Body)
70+
_, _ = w.Write([]byte(`{"response":{}}`))
71+
}))
72+
defer srv.Close()
73+
74+
a := NewGatewayAdapter(NewResilientClient(NewClient(srv.URL, "t"), circuitbreaker.DefaultConfig(), slog.Default()), clock.NewReal(), nil, slog.Default())
75+
err := a.AssignToSquad(context.Background(), "user-uuid-1", "squad-uuid-9")
76+
require.NoError(t, err)
77+
require.Equal(t, "/api/internal-squads/squad-uuid-9/bulk-actions/add-users", gotPath)
78+
require.Contains(t, string(gotBody), "user-uuid-1")
79+
}

internal/app/wiring_multisub.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ var multisubWiring = fx.Options(
5656
logger.Warn("remnawave: no default internal squads configured; new VPN users will be provisioned without squads (empty/non-working subscriptions) unless a per-request override is supplied",
5757
slog.String("config_key", "remnawave.default_internal_squads"))
5858
}
59-
return remnawave.NewGatewayAdapter(client, clk, cfg.Remnawave.DefaultInternalSquads)
59+
return remnawave.NewGatewayAdapter(client, clk, cfg.Remnawave.DefaultInternalSquads, logger)
6060
}),
6161
fx.Provide(func(adapter *remnawave.GatewayAdapter) multisub.RemnawaveGateway { return adapter }),
6262

0 commit comments

Comments
 (0)