@@ -15,6 +15,7 @@ import (
1515 "github.com/flatrun/agent/internal/auth"
1616 "github.com/flatrun/agent/internal/capacity"
1717 "github.com/flatrun/agent/internal/cluster"
18+ "github.com/flatrun/agent/internal/docker"
1819 "github.com/flatrun/agent/internal/orchestrator"
1920 "github.com/flatrun/agent/internal/routing"
2021 "github.com/flatrun/agent/pkg/config"
@@ -102,6 +103,7 @@ func setupClusterTestServer(t *testing.T, serverName string, clusterEnabled bool
102103 configPath : tmpDir + "/config.yml" ,
103104 authManager : authManager ,
104105 clusterManager : clusterManager ,
106+ manager : docker .NewManager (tmpDir ),
105107 }
106108
107109 router := gin .New ()
@@ -116,6 +118,7 @@ func setupClusterTestServer(t *testing.T, serverName string, clusterEnabled bool
116118 protected .Use (authMiddleware .RequireAuth ())
117119 {
118120 protected .GET ("/capacity" , authMiddleware .RequirePermission (auth .PermSystemRead ), server .getCapacityStatus )
121+ protected .GET ("/deployments" , authMiddleware .RequirePermission (auth .PermDeploymentsRead ), server .listDeployments )
119122 protected .GET ("/test/deployments" , authMiddleware .RequirePermission (auth .PermDeploymentsRead ), func (c * gin.Context ) {
120123 c .Status (http .StatusNoContent )
121124 })
@@ -136,7 +139,11 @@ func setupClusterTestServer(t *testing.T, serverName string, clusterEnabled bool
136139 clusterGroup .POST ("/invite" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterInvite )
137140 clusterGroup .POST ("/accept" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterAccept )
138141 clusterGroup .DELETE ("/peers/:name" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterRemovePeer )
139- clusterGroup .Any ("/peers/:name/proxy/*path" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterProxy )
142+ clusterGroup .GET ("/peers/:name/proxy/*path" , server .clusterProxy )
143+ clusterGroup .POST ("/peers/:name/proxy/*path" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterProxy )
144+ clusterGroup .PUT ("/peers/:name/proxy/*path" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterProxy )
145+ clusterGroup .PATCH ("/peers/:name/proxy/*path" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterProxy )
146+ clusterGroup .DELETE ("/peers/:name/proxy/*path" , authMiddleware .RequirePermission (auth .PermClusterWrite ), server .clusterProxy )
140147 clusterGroup .GET ("/deployments" , server .clusterAggregateDeployments )
141148 clusterGroup .GET ("/stats" , server .clusterAggregateStats )
142149 clusterGroup .GET ("/capacity" , server .clusterAggregateCapacity )
@@ -159,6 +166,57 @@ func setupClusterTestServer(t *testing.T, serverName string, clusterEnabled bool
159166 }
160167}
161168
169+ func TestClusterDeploymentsIncludesPeerWhenLocalServerIsEmpty (t * testing.T ) {
170+ local := setupClusterTestServer (t , "local" , true )
171+ defer local .cleanup ()
172+ remote := setupClusterTestServer (t , "remote" , true )
173+ defer remote .cleanup ()
174+
175+ if err := remote .server .manager .CreateDeployment ("remote-app" , `services:
176+ app:
177+ image: nginx:alpine
178+ ` , nil ); err != nil {
179+ t .Fatal (err )
180+ }
181+ const peerKey = "local-to-remote-key"
182+ if err := remote .server .createClusterAPIKey (peerKey , "local" ); err != nil {
183+ t .Fatal (err )
184+ }
185+ remoteHTTP := httptest .NewServer (remote .router )
186+ defer remoteHTTP .Close ()
187+ if err := local .server .clusterManager .AddPeer ("remote" , remoteHTTP .URL , peerKey ); err != nil {
188+ t .Fatal (err )
189+ }
190+
191+ token := clusterLogin (t , local .router )
192+ req := httptest .NewRequest (http .MethodGet , "/api/cluster/deployments" , nil )
193+ req .Header .Set ("Authorization" , "Bearer " + token )
194+ w := httptest .NewRecorder ()
195+ local .router .ServeHTTP (w , req )
196+ if w .Code != http .StatusOK {
197+ t .Fatalf ("status = %d: %s" , w .Code , w .Body .String ())
198+ }
199+ var response struct {
200+ Servers map [string ]struct {
201+ Data struct {
202+ Deployments []struct {
203+ Name string `json:"name"`
204+ } `json:"deployments"`
205+ } `json:"data"`
206+ } `json:"servers"`
207+ }
208+ if err := json .Unmarshal (w .Body .Bytes (), & response ); err != nil {
209+ t .Fatal (err )
210+ }
211+ if len (response .Servers ["local" ].Data .Deployments ) != 0 {
212+ t .Fatalf ("local deployments = %#v" , response .Servers ["local" ].Data .Deployments )
213+ }
214+ remoteDeployments := response .Servers ["remote" ].Data .Deployments
215+ if len (remoteDeployments ) != 1 || remoteDeployments [0 ].Name != "remote-app" {
216+ t .Fatalf ("remote deployments = %#v" , remoteDeployments )
217+ }
218+ }
219+
162220func TestClusterCapacityIncludesLocalOfferPolicy (t * testing.T ) {
163221 env := setupClusterTestServer (t , "server-a" , true )
164222 defer env .cleanup ()
@@ -192,6 +250,52 @@ func TestClusterCapacityIncludesLocalOfferPolicy(t *testing.T) {
192250 }
193251}
194252
253+ func TestClusterRemovePeerDeletesOnlyItsServiceCredential (t * testing.T ) {
254+ env := setupClusterTestServer (t , "server-a" , true )
255+ defer env .cleanup ()
256+
257+ if err := env .server .clusterManager .AddPeer ("server-b" , "https://server-b.example.com" , "peer-key" ); err != nil {
258+ t .Fatal (err )
259+ }
260+ if err := env .server .createClusterAPIKey ("credential-for-server-b" , "server-b" ); err != nil {
261+ t .Fatal (err )
262+ }
263+ admin , err := env .server .authManager .GetUserByUsername ("admin" )
264+ if err != nil {
265+ t .Fatal (err )
266+ }
267+ if _ , _ , err := env .server .authManager .CreateAPIKey (
268+ admin .ID , "cluster-peer-server-b" , "User-managed key" , auth .RoleAdmin , nil , nil , time.Time {},
269+ ); err != nil {
270+ t .Fatal (err )
271+ }
272+
273+ token := clusterLogin (t , env .router )
274+ req := httptest .NewRequest (http .MethodDelete , "/api/cluster/peers/server-b" , nil )
275+ req .Header .Set ("Authorization" , "Bearer " + token )
276+ w := httptest .NewRecorder ()
277+ env .router .ServeHTTP (w , req )
278+ if w .Code != http .StatusOK {
279+ t .Fatalf ("status = %d: %s" , w .Code , w .Body .String ())
280+ }
281+ if _ , err := env .server .clusterManager .GetPeer ("server-b" ); err == nil {
282+ t .Fatal ("peer still exists" )
283+ }
284+ keys , err := env .server .authManager .GetAllAPIKeys ()
285+ if err != nil {
286+ t .Fatal (err )
287+ }
288+ var matching []auth.APIKey
289+ for _ , key := range keys {
290+ if key .Name == "cluster-peer-server-b" {
291+ matching = append (matching , key )
292+ }
293+ }
294+ if len (matching ) != 1 || matching [0 ].UserID != admin .ID {
295+ t .Fatalf ("remaining matching keys = %+v" , matching )
296+ }
297+ }
298+
195299func TestUpdateClusterPeerPolicyThroughHTTP (t * testing.T ) {
196300 env := setupClusterTestServer (t , "server-a" , true )
197301 defer env .cleanup ()
@@ -817,6 +921,47 @@ func TestClusterProxyForwardsToPeer(t *testing.T) {
817921 }
818922}
819923
924+ func TestClusterProxyAllowsReadWithoutWrite (t * testing.T ) {
925+ peerServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
926+ w .Header ().Set ("Content-Type" , "application/json" )
927+ _ , _ = w .Write ([]byte (`{"deployment":{"name":"shop"}}` ))
928+ }))
929+ defer peerServer .Close ()
930+
931+ env := setupClusterTestServer (t , "primary" , true )
932+ defer env .cleanup ()
933+ if err := env .server .clusterManager .AddPeer ("remote" , peerServer .URL , "key" ); err != nil {
934+ t .Fatal (err )
935+ }
936+ user , err := env .server .authManager .CreateUser ("fleet-reader" , "" , "password" , auth .RoleService , nil )
937+ if err != nil {
938+ t .Fatal (err )
939+ }
940+ _ , err = env .server .authManager .CreateAPIKeyFromRaw (
941+ "fleet-reader-key" , user .ID , "fleet-reader" , "Fleet reader" , auth .Role ("" ),
942+ []string {auth .PermClusterRead .String ()}, nil , time.Time {},
943+ )
944+ if err != nil {
945+ t .Fatal (err )
946+ }
947+
948+ req := httptest .NewRequest (http .MethodGet , "/api/cluster/peers/remote/proxy/deployments/shop" , nil )
949+ req .Header .Set ("Authorization" , "Bearer fleet-reader-key" )
950+ w := httptest .NewRecorder ()
951+ env .router .ServeHTTP (w , req )
952+ if w .Code != http .StatusOK {
953+ t .Fatalf ("read status = %d, body = %s" , w .Code , w .Body .String ())
954+ }
955+
956+ req = httptest .NewRequest (http .MethodPost , "/api/cluster/peers/remote/proxy/deployments/shop/restart" , nil )
957+ req .Header .Set ("Authorization" , "Bearer fleet-reader-key" )
958+ w = httptest .NewRecorder ()
959+ env .router .ServeHTTP (w , req )
960+ if w .Code != http .StatusForbidden {
961+ t .Fatalf ("write status = %d, body = %s" , w .Code , w .Body .String ())
962+ }
963+ }
964+
820965func TestClusterProxyUnknownPeer (t * testing.T ) {
821966 env := setupClusterTestServer (t , "primary" , true )
822967 defer env .cleanup ()
0 commit comments