github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/controller/migrationtarget/client_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package migrationtarget_test 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "net/textproto" 14 "net/url" 15 "strings" 16 "time" 17 18 "github.com/juju/errors" 19 "github.com/juju/names/v5" 20 jujutesting "github.com/juju/testing" 21 jc "github.com/juju/testing/checkers" 22 "github.com/juju/version/v2" 23 gc "gopkg.in/check.v1" 24 "gopkg.in/httprequest.v1" 25 26 "github.com/juju/juju/api/base" 27 apitesting "github.com/juju/juju/api/base/testing" 28 "github.com/juju/juju/api/controller/migrationtarget" 29 coremigration "github.com/juju/juju/core/migration" 30 resourcetesting "github.com/juju/juju/core/resources/testing" 31 "github.com/juju/juju/rpc/params" 32 coretesting "github.com/juju/juju/testing" 33 "github.com/juju/juju/tools" 34 jujuversion "github.com/juju/juju/version" 35 ) 36 37 type ClientSuite struct { 38 jujutesting.IsolationSuite 39 } 40 41 var _ = gc.Suite(&ClientSuite{}) 42 43 func (s *ClientSuite) getClientAndStub(c *gc.C) (*migrationtarget.Client, *jujutesting.Stub) { 44 var stub jujutesting.Stub 45 apiCaller := apitesting.BestVersionCaller{APICallerFunc: apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { 46 stub.AddCall(objType+"."+request, id, arg) 47 return errors.New("boom") 48 }), BestVersion: 2} 49 client := migrationtarget.NewClient(apiCaller) 50 return client, &stub 51 } 52 53 func (s *ClientSuite) TestPrechecks(c *gc.C) { 54 client, stub := s.getClientAndStub(c) 55 56 ownerTag := names.NewUserTag("owner") 57 vers := version.MustParse("1.2.3") 58 controllerVers := version.MustParse("1.2.5") 59 60 err := client.Prechecks(coremigration.ModelInfo{ 61 UUID: "uuid", 62 Owner: ownerTag, 63 Name: "name", 64 AgentVersion: vers, 65 ControllerAgentVersion: controllerVers, 66 }) 67 c.Assert(err, gc.ErrorMatches, "boom") 68 69 expectedArg := params.MigrationModelInfo{ 70 UUID: "uuid", 71 Name: "name", 72 OwnerTag: ownerTag.String(), 73 AgentVersion: vers, 74 ControllerAgentVersion: controllerVers, 75 } 76 stub.CheckCallNames(c, "MigrationTarget.Prechecks") 77 78 arg := stub.Calls()[0].Args[1].(params.MigrationModelInfo) 79 80 mc := jc.NewMultiChecker() 81 mc.AddExpr("_.FacadeVersions", gc.Not(gc.HasLen), 0) 82 83 c.Assert(arg, mc, expectedArg) 84 } 85 86 func (s *ClientSuite) TestImport(c *gc.C) { 87 client, stub := s.getClientAndStub(c) 88 89 err := client.Import([]byte("foo")) 90 91 expectedArg := params.SerializedModel{Bytes: []byte("foo")} 92 stub.CheckCalls(c, []jujutesting.StubCall{ 93 {FuncName: "MigrationTarget.Import", Args: []interface{}{"", expectedArg}}, 94 }) 95 c.Assert(err, gc.ErrorMatches, "boom") 96 } 97 98 func (s *ClientSuite) TestAbort(c *gc.C) { 99 client, stub := s.getClientAndStub(c) 100 101 uuid := "fake" 102 err := client.Abort(uuid) 103 s.AssertModelCall(c, stub, names.NewModelTag(uuid), "Abort", err, true) 104 } 105 106 func (s *ClientSuite) TestActivate(c *gc.C) { 107 client, stub := s.getClientAndStub(c) 108 109 uuid := "fake" 110 sourceInfo := coremigration.SourceControllerInfo{ 111 ControllerTag: coretesting.ControllerTag, 112 ControllerAlias: "mycontroller", 113 Addrs: []string{"source-addr"}, 114 CACert: "cacert", 115 } 116 relatedModels := []string{"related-model-uuid"} 117 err := client.Activate(uuid, sourceInfo, relatedModels) 118 expectedArg := params.ActivateModelArgs{ 119 ModelTag: names.NewModelTag(uuid).String(), 120 ControllerTag: coretesting.ControllerTag.String(), 121 ControllerAlias: "mycontroller", 122 SourceAPIAddrs: []string{"source-addr"}, 123 SourceCACert: "cacert", 124 CrossModelUUIDs: relatedModels, 125 } 126 stub.CheckCalls(c, []jujutesting.StubCall{ 127 {FuncName: "MigrationTarget.Activate", Args: []interface{}{"", expectedArg}}, 128 }) 129 c.Assert(err, gc.ErrorMatches, "boom") 130 } 131 132 func (s *ClientSuite) TestOpenLogTransferStream(c *gc.C) { 133 caller := fakeConnector{Stub: &jujutesting.Stub{}} 134 client := migrationtarget.NewClient(caller) 135 stream, err := client.OpenLogTransferStream("bad-dad") 136 c.Assert(stream, gc.IsNil) 137 c.Assert(err, gc.ErrorMatches, "sound hound") 138 139 caller.Stub.CheckCall(c, 0, "ConnectControllerStream", "/migrate/logtransfer", 140 url.Values{}, 141 http.Header{textproto.CanonicalMIMEHeaderKey(params.MigrationModelHTTPHeader): {"bad-dad"}}, 142 ) 143 } 144 145 func (s *ClientSuite) TestLatestLogTime(c *gc.C) { 146 var stub jujutesting.Stub 147 t1 := time.Date(2016, 12, 1, 10, 31, 0, 0, time.UTC) 148 149 apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { 150 target, ok := result.(*time.Time) 151 c.Assert(ok, jc.IsTrue) 152 *target = t1 153 stub.AddCall(objType+"."+request, id, arg) 154 return nil 155 }) 156 client := migrationtarget.NewClient(apiCaller) 157 result, err := client.LatestLogTime("fake") 158 159 c.Assert(result, gc.Equals, t1) 160 s.AssertModelCall(c, &stub, names.NewModelTag("fake"), "LatestLogTime", err, false) 161 } 162 163 func (s *ClientSuite) TestLatestLogTimeError(c *gc.C) { 164 client, stub := s.getClientAndStub(c) 165 result, err := client.LatestLogTime("fake") 166 167 c.Assert(result, gc.Equals, time.Time{}) 168 s.AssertModelCall(c, stub, names.NewModelTag("fake"), "LatestLogTime", err, true) 169 } 170 171 func (s *ClientSuite) TestAdoptResources(c *gc.C) { 172 client, stub := s.getClientAndStub(c) 173 err := client.AdoptResources("the-model") 174 c.Assert(err, gc.ErrorMatches, "boom") 175 stub.CheckCall(c, 0, "MigrationTarget.AdoptResources", "", params.AdoptResourcesArgs{ 176 ModelTag: "model-the-model", 177 SourceControllerVersion: jujuversion.Current, 178 }) 179 } 180 181 func (s *ClientSuite) TestCheckMachines(c *gc.C) { 182 var stub jujutesting.Stub 183 apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { 184 target, ok := result.(*params.ErrorResults) 185 c.Assert(ok, jc.IsTrue) 186 *target = params.ErrorResults{Results: []params.ErrorResult{ 187 {Error: ¶ms.Error{Message: "oops"}}, 188 {Error: ¶ms.Error{Message: "oh no"}}, 189 }} 190 stub.AddCall(objType+"."+request, id, arg) 191 return nil 192 }) 193 client := migrationtarget.NewClient(apiCaller) 194 results, err := client.CheckMachines("django") 195 c.Assert(results, gc.HasLen, 2) 196 c.Assert(results[0], gc.ErrorMatches, "oops") 197 c.Assert(results[1], gc.ErrorMatches, "oh no") 198 s.AssertModelCall(c, &stub, names.NewModelTag("django"), "CheckMachines", err, false) 199 } 200 201 func (s *ClientSuite) TestUploadCharm(c *gc.C) { 202 const charmBody = "charming" 203 curl := "ch:foo-2" 204 charmRef := "foo-abcdef0" 205 doer := newFakeDoer(c, "", map[string]string{"Juju-Curl": curl}) 206 caller := &fakeHTTPCaller{ 207 httpClient: &httprequest.Client{Doer: doer}, 208 } 209 client := migrationtarget.NewClient(caller) 210 outCurl, err := client.UploadCharm("uuid", curl, charmRef, strings.NewReader(charmBody)) 211 c.Assert(err, jc.ErrorIsNil) 212 c.Assert(outCurl, gc.DeepEquals, curl) 213 c.Assert(doer.method, gc.Equals, "PUT") 214 c.Assert(doer.url, gc.Equals, "/migrate/charms/foo-abcdef0") 215 c.Assert(doer.headers.Get("Juju-Curl"), gc.Equals, curl) 216 c.Assert(doer.body, gc.Equals, charmBody) 217 } 218 219 func (s *ClientSuite) TestUploadCharmHubCharm(c *gc.C) { 220 const charmBody = "charming" 221 curl := "ch:s390x/bionic/juju-qa-test-15" 222 charmRef := "juju-qa-test-abcdef0" 223 doer := newFakeDoer(c, "", map[string]string{"Juju-Curl": curl}) 224 caller := &fakeHTTPCaller{ 225 httpClient: &httprequest.Client{Doer: doer}, 226 } 227 client := migrationtarget.NewClient(caller) 228 outCurl, err := client.UploadCharm("uuid", curl, charmRef, strings.NewReader(charmBody)) 229 c.Assert(err, jc.ErrorIsNil) 230 c.Assert(outCurl, gc.DeepEquals, curl) 231 c.Assert(doer.method, gc.Equals, "PUT") 232 c.Assert(doer.url, gc.Equals, "/migrate/charms/juju-qa-test-abcdef0") 233 c.Assert(doer.headers.Get("Juju-Curl"), gc.Equals, curl) 234 c.Assert(doer.body, gc.Equals, charmBody) 235 } 236 237 func (s *ClientSuite) TestUploadTools(c *gc.C) { 238 const toolsBody = "toolie" 239 vers := version.MustParseBinary("2.0.0-ubuntu-amd64") 240 someTools := &tools.Tools{Version: vers} 241 doer := newFakeDoer(c, params.ToolsResult{ 242 ToolsList: []*tools.Tools{someTools}, 243 }, nil) 244 caller := &fakeHTTPCaller{ 245 httpClient: &httprequest.Client{Doer: doer}, 246 } 247 client := migrationtarget.NewClient(caller) 248 toolsList, err := client.UploadTools( 249 "uuid", 250 strings.NewReader(toolsBody), 251 vers, 252 ) 253 c.Assert(err, jc.ErrorIsNil) 254 c.Assert(toolsList, gc.HasLen, 1) 255 c.Assert(toolsList[0], gc.DeepEquals, someTools) 256 c.Assert(doer.method, gc.Equals, "POST") 257 c.Assert(doer.url, gc.Equals, "/migrate/tools?binaryVersion=2.0.0-ubuntu-amd64") 258 c.Assert(doer.body, gc.Equals, toolsBody) 259 } 260 261 func (s *ClientSuite) TestUploadResource(c *gc.C) { 262 const resourceBody = "resourceful" 263 doer := newFakeDoer(c, "", nil) 264 caller := &fakeHTTPCaller{ 265 httpClient: &httprequest.Client{Doer: doer}, 266 } 267 client := migrationtarget.NewClient(caller) 268 269 res := resourcetesting.NewResource(c, nil, "blob", "app", resourceBody).Resource 270 res.Revision = 1 271 272 err := client.UploadResource("uuid", res, strings.NewReader(resourceBody)) 273 c.Assert(err, jc.ErrorIsNil) 274 c.Assert(doer.method, gc.Equals, "POST") 275 expectedURL := fmt.Sprintf("/migrate/resources?application=app&description=blob+description&fingerprint=%s&name=blob&origin=upload&path=blob.tgz&revision=1&size=11×tamp=%d&type=file&user=a-user", res.Fingerprint.Hex(), res.Timestamp.UnixNano()) 276 c.Assert(doer.url, gc.Equals, expectedURL) 277 c.Assert(doer.body, gc.Equals, resourceBody) 278 } 279 280 func (s *ClientSuite) TestSetUnitResource(c *gc.C) { 281 const resourceBody = "resourceful" 282 doer := newFakeDoer(c, "", nil) 283 caller := &fakeHTTPCaller{ 284 httpClient: &httprequest.Client{Doer: doer}, 285 } 286 client := migrationtarget.NewClient(caller) 287 288 res := resourcetesting.NewResource(c, nil, "blob", "app", resourceBody).Resource 289 res.Revision = 2 290 291 err := client.SetUnitResource("uuid", "app/0", res) 292 c.Assert(err, jc.ErrorIsNil) 293 c.Assert(doer.method, gc.Equals, "POST") 294 expectedURL := fmt.Sprintf("/migrate/resources?description=blob+description&fingerprint=%s&name=blob&origin=upload&path=blob.tgz&revision=2&size=11×tamp=%d&type=file&unit=app%%2F0&user=a-user", res.Fingerprint.Hex(), res.Timestamp.UnixNano()) 295 c.Assert(doer.url, gc.Equals, expectedURL) 296 c.Assert(doer.body, gc.Equals, "") 297 } 298 299 func (s *ClientSuite) TestPlaceholderResource(c *gc.C) { 300 doer := newFakeDoer(c, "", nil) 301 caller := &fakeHTTPCaller{ 302 httpClient: &httprequest.Client{Doer: doer}, 303 } 304 client := migrationtarget.NewClient(caller) 305 306 res := resourcetesting.NewPlaceholderResource(c, "blob", "app") 307 res.Revision = 3 308 res.Size = 123 309 310 err := client.SetPlaceholderResource("uuid", res) 311 c.Assert(err, jc.ErrorIsNil) 312 c.Assert(doer.method, gc.Equals, "POST") 313 expectedURL := fmt.Sprintf("/migrate/resources?application=app&description=blob+description&fingerprint=%s&name=blob&origin=upload&path=blob.tgz&revision=3&size=123&type=file", res.Fingerprint.Hex()) 314 c.Assert(doer.url, gc.Equals, expectedURL) 315 c.Assert(doer.body, gc.Equals, "") 316 } 317 318 func (s *ClientSuite) TestCACert(c *gc.C) { 319 call := func(objType string, version int, id, request string, args, response interface{}) error { 320 c.Check(objType, gc.Equals, "MigrationTarget") 321 c.Check(request, gc.Equals, "CACert") 322 c.Check(args, gc.Equals, nil) 323 c.Check(response, gc.FitsTypeOf, (*params.BytesResult)(nil)) 324 response.(*params.BytesResult).Result = []byte("foo cert") 325 return nil 326 } 327 client := migrationtarget.NewClient(apitesting.APICallerFunc(call)) 328 r, err := client.CACert() 329 c.Assert(err, jc.ErrorIsNil) 330 c.Assert(r, gc.Equals, "foo cert") 331 } 332 333 func (s *ClientSuite) AssertModelCall(c *gc.C, stub *jujutesting.Stub, tag names.ModelTag, call string, err error, expectError bool) { 334 expectedArg := params.ModelArgs{ModelTag: tag.String()} 335 stub.CheckCalls(c, []jujutesting.StubCall{ 336 {FuncName: "MigrationTarget." + call, Args: []interface{}{"", expectedArg}}, 337 }) 338 if expectError { 339 c.Assert(err, gc.ErrorMatches, "boom") 340 } else { 341 c.Assert(err, jc.ErrorIsNil) 342 } 343 } 344 345 type fakeConnector struct { 346 base.APICaller 347 348 *jujutesting.Stub 349 } 350 351 func (fakeConnector) BestFacadeVersion(string) int { 352 return 0 353 } 354 355 func (c fakeConnector) ConnectControllerStream(path string, attrs url.Values, headers http.Header) (base.Stream, error) { 356 c.Stub.AddCall("ConnectControllerStream", path, attrs, headers) 357 return nil, errors.New("sound hound") 358 } 359 360 type fakeHTTPCaller struct { 361 base.APICaller 362 httpClient *httprequest.Client 363 err error 364 } 365 366 func (fakeHTTPCaller) BestFacadeVersion(string) int { 367 return 0 368 } 369 370 func (c fakeHTTPCaller) RootHTTPClient() (*httprequest.Client, error) { 371 return c.httpClient, c.err 372 } 373 374 func (r *fakeHTTPCaller) Context() context.Context { 375 return context.Background() 376 } 377 378 func newFakeDoer(c *gc.C, respBody interface{}, respHeaders map[string]string) *fakeDoer { 379 body, err := json.Marshal(respBody) 380 c.Assert(err, jc.ErrorIsNil) 381 resp := &http.Response{ 382 StatusCode: 200, 383 Body: io.NopCloser(bytes.NewReader(body)), 384 } 385 resp.Header = make(http.Header) 386 resp.Header.Add("Content-Type", "application/json") 387 for k, v := range respHeaders { 388 resp.Header.Set(k, v) 389 } 390 return &fakeDoer{ 391 response: resp, 392 } 393 } 394 395 type fakeDoer struct { 396 response *http.Response 397 398 method string 399 url string 400 body string 401 headers http.Header 402 } 403 404 func (d *fakeDoer) Do(req *http.Request) (*http.Response, error) { 405 d.method = req.Method 406 d.url = req.URL.String() 407 d.headers = req.Header 408 409 // If the body is nil, don't do anything about reading the req.Body 410 // The underlying net http go library deals with nil bodies for requests, 411 // so our fake stub should also mirror this. 412 // https://golang.org/src/net/http/client.go?s=17323:17375#L587 413 if req.Body == nil { 414 return d.response, nil 415 } 416 417 // ReadAll the body if it's found. 418 body, err := io.ReadAll(req.Body) 419 if err != nil { 420 panic(err) 421 } 422 d.body = string(body) 423 return d.response, nil 424 }