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: &params.Error{Message: "oops"}},
   188  			{Error: &params.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&timestamp=%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&timestamp=%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  }