github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/resources_mig_test.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"strings"
    14  	"time"
    15  
    16  	charmresource "github.com/juju/charm/v12/resource"
    17  	jc "github.com/juju/testing/checkers"
    18  	"go.uber.org/mock/gomock"
    19  	gc "gopkg.in/check.v1"
    20  
    21  	"github.com/juju/juju/apiserver"
    22  	"github.com/juju/juju/apiserver/mocks"
    23  	apitesting "github.com/juju/juju/apiserver/testing"
    24  	"github.com/juju/juju/core/resources"
    25  	"github.com/juju/juju/rpc/params"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/testing/factory"
    28  )
    29  
    30  type resourcesUploadSuite struct {
    31  	apiserverBaseSuite
    32  	appName        string
    33  	unit           *state.Unit
    34  	importingState *state.State
    35  	importingModel *state.Model
    36  }
    37  
    38  var _ = gc.Suite(&resourcesUploadSuite{})
    39  
    40  func (s *resourcesUploadSuite) SetUpTest(c *gc.C) {
    41  	s.apiserverBaseSuite.SetUpTest(c)
    42  
    43  	// Create an importing model to work with.
    44  	var err error
    45  	s.importingState = s.Factory.MakeModel(c, nil)
    46  	s.AddCleanup(func(*gc.C) { s.importingState.Close() })
    47  	s.importingModel, err = s.importingState.Model()
    48  	c.Assert(err, jc.ErrorIsNil)
    49  
    50  	newFactory := factory.NewFactory(s.importingState, s.StatePool)
    51  	app := newFactory.MakeApplication(c, nil)
    52  	s.appName = app.Name()
    53  
    54  	s.unit = newFactory.MakeUnit(c, &factory.UnitParams{
    55  		Application: app,
    56  	})
    57  
    58  	err = s.importingModel.SetMigrationMode(state.MigrationModeImporting)
    59  	c.Assert(err, jc.ErrorIsNil)
    60  }
    61  
    62  func (s *resourcesUploadSuite) sendHTTPRequest(c *gc.C, p apitesting.HTTPRequestParams) *http.Response {
    63  	p.ExtraHeaders = map[string]string{
    64  		params.MigrationModelHTTPHeader: s.importingModel.UUID(),
    65  	}
    66  	return s.apiserverBaseSuite.sendHTTPRequest(c, p)
    67  }
    68  
    69  func (s *resourcesUploadSuite) TestServedSecurely(c *gc.C) {
    70  	url := s.resourcesURL("")
    71  	url.Scheme = "http"
    72  	apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    73  		Method:       "GET",
    74  		URL:          url.String(),
    75  		ExpectStatus: http.StatusBadRequest,
    76  	})
    77  }
    78  
    79  func (s *resourcesUploadSuite) TestGETUnsupported(c *gc.C) {
    80  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.resourcesURI("")})
    81  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "GET"`)
    82  }
    83  
    84  func (s *resourcesUploadSuite) TestPUTUnsupported(c *gc.C) {
    85  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "PUT", URL: s.resourcesURI("")})
    86  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
    87  }
    88  
    89  func (s *resourcesUploadSuite) TestPOSTRequiresAuth(c *gc.C) {
    90  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.resourcesURI("")})
    91  	body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8")
    92  	c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n")
    93  }
    94  
    95  func (s *resourcesUploadSuite) TestPOSTRequiresUserAuth(c *gc.C) {
    96  	// Add a machine and try to login.
    97  	machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
    98  		Nonce: "noncy",
    99  	})
   100  	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
   101  		Tag:         machine.Tag().String(),
   102  		Password:    password,
   103  		Method:      "POST",
   104  		URL:         s.resourcesURI(""),
   105  		Nonce:       "noncy",
   106  		ContentType: "foo/bar",
   107  	})
   108  	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
   109  	c.Assert(string(body), gc.Equals, "authorization failed: machine 0 is not a user\n")
   110  
   111  	// Now try a user login.
   112  	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.resourcesURI("")})
   113  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "missing application/unit")
   114  }
   115  
   116  func (s *resourcesUploadSuite) TestRejectsInvalidModel(c *gc.C) {
   117  	params := apitesting.HTTPRequestParams{
   118  		Method: "POST",
   119  		URL:    s.resourcesURI(""),
   120  		ExtraHeaders: map[string]string{
   121  			params.MigrationModelHTTPHeader: "dead-beef-123456",
   122  		},
   123  	}
   124  	resp := s.apiserverBaseSuite.sendHTTPRequest(c, params)
   125  	s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"`)
   126  }
   127  
   128  const content = "stuff"
   129  
   130  func (s *resourcesUploadSuite) makeUploadArgs(c *gc.C) url.Values {
   131  	return s.makeResourceUploadArgs(c, "file")
   132  }
   133  
   134  func (s *resourcesUploadSuite) makeDockerUploadArgs(c *gc.C) url.Values {
   135  	result := s.makeResourceUploadArgs(c, "oci-image")
   136  	result.Del("path")
   137  	return result
   138  }
   139  
   140  func (s *resourcesUploadSuite) makeResourceUploadArgs(c *gc.C, resType string) url.Values {
   141  	fp, err := charmresource.GenerateFingerprint(strings.NewReader(content))
   142  	c.Assert(err, jc.ErrorIsNil)
   143  	q := make(url.Values)
   144  	q.Add("application", s.appName)
   145  	q.Add("user", "napoleon")
   146  	q.Add("name", "bin")
   147  	q.Add("path", "blob.zip")
   148  	q.Add("description", "hmm")
   149  	q.Add("type", resType)
   150  	q.Add("origin", "store")
   151  	q.Add("revision", "3")
   152  	q.Add("size", fmt.Sprint(len(content)))
   153  	q.Add("fingerprint", fp.Hex())
   154  	q.Add("timestamp", fmt.Sprint(time.Now().UnixNano()))
   155  	return q
   156  }
   157  
   158  func (s *resourcesUploadSuite) TestUpload(c *gc.C) {
   159  	outResp := s.uploadAppResource(c, nil)
   160  	c.Check(outResp.ID, gc.Not(gc.Equals), "")
   161  	c.Check(outResp.Timestamp.IsZero(), jc.IsFalse)
   162  
   163  	rSt := s.importingState.Resources()
   164  	res, reader, err := rSt.OpenResource(s.appName, "bin")
   165  	c.Assert(err, jc.ErrorIsNil)
   166  	defer reader.Close()
   167  	readContent, err := io.ReadAll(reader)
   168  	c.Assert(err, jc.ErrorIsNil)
   169  	c.Assert(string(readContent), gc.Equals, content)
   170  	c.Assert(res.ID, gc.Equals, outResp.ID)
   171  }
   172  
   173  func (s *resourcesUploadSuite) TestUnitUpload(c *gc.C) {
   174  	// Upload application resource first. A unit resource can't be
   175  	// uploaded without the application resource being there first.
   176  	s.uploadAppResource(c, nil)
   177  
   178  	q := s.makeUploadArgs(c)
   179  	q.Del("application")
   180  	q.Set("unit", s.unit.Name())
   181  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   182  		Method:      "POST",
   183  		URL:         s.resourcesURI(q.Encode()),
   184  		ContentType: "application/octet-stream",
   185  		Body:        strings.NewReader(content),
   186  	})
   187  	outResp := s.assertResponse(c, resp, http.StatusOK)
   188  	c.Check(outResp.ID, gc.Not(gc.Equals), "")
   189  	c.Check(outResp.Timestamp.IsZero(), jc.IsFalse)
   190  }
   191  
   192  func (s *resourcesUploadSuite) TestPlaceholder(c *gc.C) {
   193  	query := s.makeUploadArgs(c)
   194  	query.Del("timestamp") // No timestamp means placeholder
   195  	outResp := s.uploadAppResource(c, &query)
   196  	c.Check(outResp.ID, gc.Not(gc.Equals), "")
   197  	c.Check(outResp.Timestamp.IsZero(), jc.IsTrue)
   198  
   199  	rSt := s.importingState.Resources()
   200  	res, err := rSt.GetResource(s.appName, "bin")
   201  	c.Assert(err, jc.ErrorIsNil)
   202  	c.Check(res.IsPlaceholder(), jc.IsTrue)
   203  	c.Check(res.ApplicationID, gc.Equals, s.appName)
   204  	c.Check(res.Name, gc.Equals, "bin")
   205  	c.Check(res.Size, gc.Equals, int64(len(content)))
   206  }
   207  
   208  func (s *resourcesUploadSuite) uploadAppResource(c *gc.C, query *url.Values) params.ResourceUploadResult {
   209  	if query == nil {
   210  		q := s.makeUploadArgs(c)
   211  		query = &q
   212  	}
   213  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   214  		Method:      "POST",
   215  		URL:         s.resourcesURI(query.Encode()),
   216  		ContentType: "application/octet-stream",
   217  		Body:        strings.NewReader(content),
   218  	})
   219  	return s.assertResponse(c, resp, http.StatusOK)
   220  }
   221  
   222  func (s *resourcesUploadSuite) TestArgValidation(c *gc.C) {
   223  	checkBadRequest := func(q url.Values, expected string) {
   224  		resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   225  			Method: "POST",
   226  			URL:    s.resourcesURI(q.Encode()),
   227  		})
   228  		s.assertErrorResponse(c, resp, http.StatusBadRequest, expected)
   229  	}
   230  
   231  	q := s.makeUploadArgs(c)
   232  	q.Del("application")
   233  	checkBadRequest(q, "missing application/unit")
   234  
   235  	q = s.makeUploadArgs(c)
   236  	q.Set("unit", "some/0")
   237  	checkBadRequest(q, "application and unit can't be set at the same time")
   238  
   239  	q = s.makeUploadArgs(c)
   240  	q.Del("name")
   241  	checkBadRequest(q, "missing name")
   242  
   243  	q = s.makeUploadArgs(c)
   244  	q.Del("path")
   245  	checkBadRequest(q, "missing path")
   246  
   247  	q = s.makeUploadArgs(c)
   248  	q.Set("type", "fooo")
   249  	checkBadRequest(q, "invalid type")
   250  
   251  	q = s.makeUploadArgs(c)
   252  	q.Set("origin", "fooo")
   253  	checkBadRequest(q, "invalid origin")
   254  
   255  	q = s.makeUploadArgs(c)
   256  	q.Set("revision", "fooo")
   257  	checkBadRequest(q, "invalid revision")
   258  
   259  	q = s.makeUploadArgs(c)
   260  	q.Set("size", "fooo")
   261  	checkBadRequest(q, "invalid size")
   262  
   263  	q = s.makeUploadArgs(c)
   264  	q.Set("fingerprint", "zzz")
   265  	checkBadRequest(q, "invalid fingerprint")
   266  }
   267  
   268  func (s *resourcesUploadSuite) TestArgValidationCAASModel(c *gc.C) {
   269  	content := `{"ImageName": "image-name", "Username": "fred", "Password":"secret"}`
   270  	checkRequest := func(q url.Values) {
   271  		resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   272  			Method: "POST",
   273  			URL:    s.resourcesURI(q.Encode()),
   274  			Body:   bytes.NewReader([]byte(content)),
   275  		})
   276  		s.assertResponse(c, resp, http.StatusOK)
   277  	}
   278  
   279  	q := s.makeDockerUploadArgs(c)
   280  	checkRequest(q)
   281  }
   282  
   283  func (s *resourcesUploadSuite) TestFailsWhenModelNotImporting(c *gc.C) {
   284  	err := s.importingModel.SetMigrationMode(state.MigrationModeNone)
   285  	c.Assert(err, jc.ErrorIsNil)
   286  
   287  	q := s.makeUploadArgs(c)
   288  	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{
   289  		Method:      "POST",
   290  		URL:         s.resourcesURI(q.Encode()),
   291  		ContentType: "application/octet-stream",
   292  		Body:        strings.NewReader(content),
   293  	})
   294  	s.assertResponse(c, resp, http.StatusBadRequest)
   295  }
   296  
   297  func (s *resourcesUploadSuite) resourcesURI(query string) string {
   298  	if query != "" && query[0] == '?' {
   299  		query = query[1:]
   300  	}
   301  	return s.resourcesURL(query).String()
   302  }
   303  
   304  func (s *resourcesUploadSuite) resourcesURL(query string) *url.URL {
   305  	url := s.URL("/migrate/resources", nil)
   306  	url.RawQuery = query
   307  	return url
   308  }
   309  
   310  func (s *resourcesUploadSuite) assertErrorResponse(c *gc.C, resp *http.Response, expStatus int, expError string) {
   311  	outResp := s.assertResponse(c, resp, expStatus)
   312  	err := outResp.Error
   313  	c.Assert(err, gc.NotNil)
   314  	c.Check(err.Message, gc.Matches, expError)
   315  }
   316  
   317  func (s *resourcesUploadSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.ResourceUploadResult {
   318  	body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON)
   319  	var outResp params.ResourceUploadResult
   320  	err := json.Unmarshal(body, &outResp)
   321  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("Body: %s", body))
   322  	return outResp
   323  }
   324  
   325  func (s *resourcesUploadSuite) TestSetResource(c *gc.C) {
   326  	ctrl := gomock.NewController(c)
   327  	defer ctrl.Finish()
   328  
   329  	stResources := mocks.NewMockResources(ctrl)
   330  	gomock.InOrder(
   331  		stResources.EXPECT().SetUnitResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(resources.Resource{}, nil),
   332  		stResources.EXPECT().SetResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), state.DoNotIncrementCharmModifiedVersion).Return(resources.Resource{}, nil),
   333  	)
   334  	apiserver.SetResource(true, "", "", charmresource.Resource{}, nil, stResources)
   335  	apiserver.SetResource(false, "", "", charmresource.Resource{}, nil, stResources)
   336  }