8dd8e806de575a16dcd1cf27c35d16d8df5cd55e
[arvados.git] / lib / controller / federation_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package controller
6
7 import (
8         "encoding/json"
9         "net/http"
10         "net/http/httptest"
11         "net/url"
12         "strings"
13         "time"
14
15         "git.curoverse.com/arvados.git/sdk/go/arvados"
16         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
17         "git.curoverse.com/arvados.git/sdk/go/httpserver"
18         "github.com/Sirupsen/logrus"
19         check "gopkg.in/check.v1"
20 )
21
22 // Gocheck boilerplate
23 var _ = check.Suite(&FederationSuite{})
24
25 type FederationSuite struct {
26         log                *logrus.Logger
27         localServer        *httpserver.Server
28         remoteServer       *httpserver.Server
29         remoteMock         *httpserver.Server
30         remoteMockRequests []http.Request
31         handler            *Handler
32 }
33
34 func (s *FederationSuite) SetUpTest(c *check.C) {
35         s.log = logrus.New()
36         s.log.Formatter = &logrus.JSONFormatter{}
37         s.log.Out = &logWriter{c.Log}
38
39         s.remoteServer = newServerFromIntegrationTestEnv(c)
40         c.Assert(s.remoteServer.Start(), check.IsNil)
41
42         s.remoteMock = newServerFromIntegrationTestEnv(c)
43         s.remoteMock.Server.Handler = http.HandlerFunc(s.remoteMockHandler)
44         c.Assert(s.remoteMock.Start(), check.IsNil)
45
46         nodeProfile := arvados.NodeProfile{
47                 Controller: arvados.SystemServiceInstance{Listen: ":"},
48                 RailsAPI:   arvados.SystemServiceInstance{Listen: ":1"}, // local reqs will error "connection refused"
49         }
50         s.handler = &Handler{Cluster: &arvados.Cluster{
51                 ClusterID: "zhome",
52                 NodeProfiles: map[string]arvados.NodeProfile{
53                         "*": nodeProfile,
54                 },
55         }, NodeProfile: &nodeProfile}
56         s.localServer = newServerFromIntegrationTestEnv(c)
57         s.localServer.Server.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(s.log, s.handler))
58
59         s.handler.Cluster.RemoteClusters = map[string]arvados.RemoteCluster{
60                 "zzzzz": {
61                         Host:   s.remoteServer.Addr,
62                         Proxy:  true,
63                         Scheme: "http",
64                 },
65                 "zmock": {
66                         Host:   s.remoteMock.Addr,
67                         Proxy:  true,
68                         Scheme: "http",
69                 },
70         }
71
72         c.Assert(s.localServer.Start(), check.IsNil)
73 }
74
75 func (s *FederationSuite) remoteMockHandler(w http.ResponseWriter, req *http.Request) {
76         s.remoteMockRequests = append(s.remoteMockRequests, *req)
77 }
78
79 func (s *FederationSuite) TearDownTest(c *check.C) {
80         if s.remoteServer != nil {
81                 s.remoteServer.Close()
82         }
83         if s.localServer != nil {
84                 s.localServer.Close()
85         }
86 }
87
88 func (s *FederationSuite) TestLocalRequest(c *check.C) {
89         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zhome-", 1), nil)
90         resp := httptest.NewRecorder()
91         s.handler.ServeHTTP(resp, req)
92         s.checkHandledLocally(c, resp)
93 }
94
95 func (s *FederationSuite) checkHandledLocally(c *check.C, resp *httptest.ResponseRecorder) {
96         // Our "home" controller can't handle local requests because
97         // it doesn't have its own stub/test Rails API, so we rely on
98         // "connection refused" to indicate the controller tried to
99         // proxy the request to its local Rails API.
100         c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
101         s.checkJSONErrorMatches(c, resp, `.*connection refused`)
102 }
103
104 func (s *FederationSuite) TestNoAuth(c *check.C) {
105         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
106         resp := httptest.NewRecorder()
107         s.handler.ServeHTTP(resp, req)
108         c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
109         s.checkJSONErrorMatches(c, resp, `Not logged in`)
110 }
111
112 func (s *FederationSuite) TestBadAuth(c *check.C) {
113         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
114         req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
115         resp := httptest.NewRecorder()
116         s.handler.ServeHTTP(resp, req)
117         c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
118         s.checkJSONErrorMatches(c, resp, `Not logged in`)
119 }
120
121 func (s *FederationSuite) TestNoAccess(c *check.C) {
122         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
123         req.Header.Set("Authorization", "Bearer "+arvadostest.SpectatorToken)
124         resp := httptest.NewRecorder()
125         s.handler.ServeHTTP(resp, req)
126         c.Check(resp.Code, check.Equals, http.StatusNotFound)
127         s.checkJSONErrorMatches(c, resp, `.*not found`)
128 }
129
130 func (s *FederationSuite) TestGetUnknownRemote(c *check.C) {
131         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zz404-", 1), nil)
132         req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
133         resp := httptest.NewRecorder()
134         s.handler.ServeHTTP(resp, req)
135         c.Check(resp.Code, check.Equals, http.StatusNotFound)
136         s.checkJSONErrorMatches(c, resp, `.*no proxy available for cluster zz404`)
137 }
138
139 func (s *FederationSuite) TestRemoteError(c *check.C) {
140         rc := s.handler.Cluster.RemoteClusters["zzzzz"]
141         rc.Scheme = "https"
142         s.handler.Cluster.RemoteClusters["zzzzz"] = rc
143
144         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
145         req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
146         resp := httptest.NewRecorder()
147         s.handler.ServeHTTP(resp, req)
148         c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
149         s.checkJSONErrorMatches(c, resp, `.*HTTP response to HTTPS client`)
150 }
151
152 func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
153         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
154         req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
155         resp := httptest.NewRecorder()
156         s.handler.ServeHTTP(resp, req)
157         c.Check(resp.Code, check.Equals, http.StatusOK)
158         var wf arvados.Workflow
159         c.Check(json.Unmarshal(resp.Body.Bytes(), &wf), check.IsNil)
160         c.Check(wf.UUID, check.Equals, arvadostest.WorkflowWithDefinitionYAMLUUID)
161         c.Check(wf.OwnerUUID, check.Equals, arvadostest.ActiveUserUUID)
162 }
163
164 func (s *FederationSuite) TestRemoteWithTokenInQuery(c *check.C) {
165         req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zmock-", 1)+"?api_token="+arvadostest.ActiveToken, nil)
166         s.handler.ServeHTTP(httptest.NewRecorder(), req)
167         c.Assert(len(s.remoteMockRequests), check.Equals, 1)
168         c.Check(s.remoteMockRequests[0].URL.String(), check.Not(check.Matches), `.*api_token=.*`)
169 }
170
171 func (s *FederationSuite) TestUpdateRemoteWorkflow(c *check.C) {
172         updateDescription := func(descr string) *httptest.ResponseRecorder {
173                 req := httptest.NewRequest("PATCH", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, strings.NewReader(url.Values{
174                         "workflow": {`{"description":"` + descr + `"}`},
175                 }.Encode()))
176                 req.Header.Set("Content-type", "application/x-www-form-urlencoded")
177                 req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
178                 resp := httptest.NewRecorder()
179                 s.handler.ServeHTTP(resp, req)
180                 s.checkResponseOK(c, resp)
181                 return resp
182         }
183
184         // Update description twice so running this test twice in a
185         // row still causes ModifiedAt to change
186         updateDescription("updated once by TestUpdateRemoteWorkflow")
187         resp := updateDescription("updated twice by TestUpdateRemoteWorkflow")
188
189         var wf arvados.Workflow
190         c.Check(json.Unmarshal(resp.Body.Bytes(), &wf), check.IsNil)
191         c.Check(wf.UUID, check.Equals, arvadostest.WorkflowWithDefinitionYAMLUUID)
192         c.Assert(wf.ModifiedAt, check.NotNil)
193         c.Logf("%s", *wf.ModifiedAt)
194         c.Check(time.Since(*wf.ModifiedAt) < time.Minute, check.Equals, true)
195 }
196
197 func (s *FederationSuite) checkResponseOK(c *check.C, resp *httptest.ResponseRecorder) {
198         c.Check(resp.Code, check.Equals, http.StatusOK)
199         if resp.Code != http.StatusOK {
200                 c.Logf("... response body = %s\n", resp.Body.String())
201         }
202 }
203
204 func (s *FederationSuite) checkJSONErrorMatches(c *check.C, resp *httptest.ResponseRecorder, re string) {
205         var jresp httpserver.ErrorResponse
206         err := json.Unmarshal(resp.Body.Bytes(), &jresp)
207         c.Check(err, check.IsNil)
208         c.Assert(len(jresp.Errors), check.Equals, 1)
209         c.Check(jresp.Errors[0], check.Matches, re)
210 }