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

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state_test
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/names/v5"
    10  	jc "github.com/juju/testing/checkers"
    11  	"github.com/juju/utils/v3"
    12  	gc "gopkg.in/check.v1"
    13  
    14  	"github.com/juju/juju/cloud"
    15  	"github.com/juju/juju/core/status"
    16  	"github.com/juju/juju/state"
    17  	statetesting "github.com/juju/juju/state/testing"
    18  	"github.com/juju/juju/storage"
    19  	"github.com/juju/juju/testing"
    20  	"github.com/juju/juju/testing/factory"
    21  )
    22  
    23  type ModelCredentialSuite struct {
    24  	ConnSuite
    25  
    26  	credentialTag names.CloudCredentialTag
    27  }
    28  
    29  var _ = gc.Suite(&ModelCredentialSuite{})
    30  
    31  func (s *ModelCredentialSuite) SetUpTest(c *gc.C) {
    32  	s.ConnSuite.SetUpTest(c)
    33  
    34  	s.credentialTag = s.createCloudCredential(c, "foobar")
    35  }
    36  
    37  func (s *ModelCredentialSuite) TestInvalidateModelCredentialNone(c *gc.C) {
    38  	// The model created in ConnSuite does not have a credential.
    39  	m, err := s.State.Model()
    40  	c.Assert(err, jc.ErrorIsNil)
    41  	_, exists := m.CloudCredentialTag()
    42  	c.Assert(exists, jc.IsFalse)
    43  	_, exists, err = m.CloudCredential()
    44  	c.Assert(err, jc.ErrorIsNil)
    45  	c.Assert(exists, jc.IsFalse)
    46  
    47  	reason := "special invalidation"
    48  	err = s.State.InvalidateModelCredential(reason)
    49  	c.Assert(err, jc.ErrorIsNil)
    50  }
    51  
    52  func (s *ModelCredentialSuite) TestInvalidateModelCredential(c *gc.C) {
    53  	st := s.addModel(c, "abcmodel", s.credentialTag)
    54  	defer st.Close()
    55  	credential, err := s.State.CloudCredential(s.credentialTag)
    56  	c.Assert(err, jc.ErrorIsNil)
    57  	c.Assert(credential.IsValid(), jc.IsTrue)
    58  
    59  	reason := "special invalidation"
    60  	err = st.InvalidateModelCredential(reason)
    61  	c.Assert(err, jc.ErrorIsNil)
    62  
    63  	invalidated, err := s.State.CloudCredential(s.credentialTag)
    64  	c.Assert(err, jc.ErrorIsNil)
    65  	c.Assert(invalidated.IsValid(), jc.IsFalse)
    66  	c.Assert(invalidated.InvalidReason, gc.DeepEquals, reason)
    67  
    68  	m, err := st.Model()
    69  	c.Assert(err, jc.ErrorIsNil)
    70  	info, err := m.Status()
    71  	c.Assert(err, jc.ErrorIsNil)
    72  	c.Assert(info, jc.DeepEquals, status.StatusInfo{
    73  		Status:  "suspended",
    74  		Message: "suspended since cloud credential is not valid",
    75  		Data:    map[string]interface{}{"reason": "special invalidation"},
    76  	})
    77  }
    78  
    79  func (s *ModelCredentialSuite) TestValidateCloudCredentialWrongCloud(c *gc.C) {
    80  	m, err := s.State.Model()
    81  	c.Assert(err, jc.ErrorIsNil)
    82  	tag := names.NewCloudCredentialTag("stratus/bob/foobar")
    83  	cred := cloud.NewCredential(cloud.UserPassAuthType, nil)
    84  	err = m.ValidateCloudCredential(tag, cred)
    85  	c.Assert(err, gc.ErrorMatches, `validating credential "stratus/bob/foobar" for cloud "dummy": cloud "stratus" not valid`)
    86  }
    87  
    88  func (s *ModelCredentialSuite) TestValidateCloudCredentialWrongAuthType(c *gc.C) {
    89  	m, err := s.State.Model()
    90  	c.Assert(err, jc.ErrorIsNil)
    91  	tag := names.NewCloudCredentialTag("dummy/bob/foobar")
    92  	cred := cloud.NewCredential(cloud.AccessKeyAuthType, nil)
    93  	err = m.ValidateCloudCredential(tag, cred)
    94  	c.Assert(err, gc.ErrorMatches, `validating credential "dummy/bob/foobar" for cloud "dummy": supported auth-types \["empty"\], "access-key" not supported`)
    95  }
    96  
    97  func (s *ModelCredentialSuite) TestValidateCloudCredentialModel(c *gc.C) {
    98  	m, err := s.State.Model()
    99  	c.Assert(err, jc.ErrorIsNil)
   100  	tag := names.NewCloudCredentialTag("dummy/bob/foobar")
   101  	cred := cloud.NewCredential(cloud.EmptyAuthType, nil)
   102  	err = m.ValidateCloudCredential(tag, cred)
   103  	c.Assert(err, jc.ErrorIsNil)
   104  }
   105  
   106  func (s *ModelCredentialSuite) TestSetCloudCredential(c *gc.C) {
   107  	s.assertSetCloudCredential(c,
   108  		names.NewCloudCredentialTag("dummy/bob/foobar"),
   109  		cloud.NewCredential(cloud.EmptyAuthType, nil),
   110  	)
   111  }
   112  
   113  func (s *ModelCredentialSuite) TestSetCloudCredentialNoUpdate(c *gc.C) {
   114  	tag := names.NewCloudCredentialTag("dummy/bob/foobar")
   115  	m := s.assertSetCloudCredential(c,
   116  		tag,
   117  		cloud.NewCredential(cloud.EmptyAuthType, nil),
   118  	)
   119  
   120  	set, err := m.SetCloudCredential(tag)
   121  	c.Assert(err, jc.ErrorIsNil)
   122  	// This should be false as cloud credential change was an no-op.
   123  	c.Assert(set, jc.IsFalse)
   124  
   125  	// Check credential is still set.
   126  	credentialTag, credentialSet := m.CloudCredentialTag()
   127  	c.Assert(credentialTag, gc.DeepEquals, tag)
   128  	c.Assert(credentialSet, jc.IsTrue)
   129  	cred, credentialSet, err := m.CloudCredential()
   130  	c.Assert(err, jc.ErrorIsNil)
   131  	c.Assert(credentialSet, jc.IsTrue)
   132  	stateCred, err := s.State.CloudCredential(credentialTag)
   133  	c.Assert(err, jc.ErrorIsNil)
   134  	c.Assert(cred, jc.DeepEquals, stateCred)
   135  }
   136  
   137  func (s *ModelCredentialSuite) TestSetCloudCredentialInvalidCredentialContent(c *gc.C) {
   138  	tag := names.NewCloudCredentialTag("dummy/bob/foobar")
   139  	credential := cloud.NewCredential(cloud.EmptyAuthType, nil)
   140  	err := s.State.UpdateCloudCredential(tag, credential)
   141  	c.Assert(err, jc.ErrorIsNil)
   142  	err = s.State.InvalidateCloudCredential(tag, "test")
   143  	c.Assert(err, jc.ErrorIsNil)
   144  
   145  	m, err := s.State.Model()
   146  	c.Assert(err, jc.ErrorIsNil)
   147  	set, err := m.SetCloudCredential(tag)
   148  	c.Assert(err, gc.ErrorMatches, `credential "dummy/bob/foobar" not valid`)
   149  	c.Assert(set, jc.IsFalse)
   150  
   151  	credentialTag, credentialSet := m.CloudCredentialTag()
   152  	// Make sure no credential is set.
   153  	c.Assert(credentialTag, gc.DeepEquals, names.CloudCredentialTag{})
   154  	c.Assert(credentialSet, jc.IsFalse)
   155  	_, credentialSet, err = m.CloudCredential()
   156  	c.Assert(err, jc.ErrorIsNil)
   157  	c.Assert(credentialSet, jc.IsFalse)
   158  }
   159  
   160  func (s *ModelCredentialSuite) TestSetCloudCredentialInvalidCredentialForModel(c *gc.C) {
   161  	err := s.State.AddCloud(lowCloud, s.Owner.Name())
   162  	c.Assert(err, jc.ErrorIsNil)
   163  	tag := names.NewCloudCredentialTag("stratus/bob/foobar")
   164  	credential := cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{
   165  		"access-key": "someverysecretaccesskey",
   166  		"secret-key": "someverysercretplainkey",
   167  	})
   168  	err = s.State.UpdateCloudCredential(tag, credential)
   169  	c.Assert(err, jc.ErrorIsNil)
   170  
   171  	m, err := s.State.Model()
   172  	c.Assert(err, jc.ErrorIsNil)
   173  	set, err := m.SetCloudCredential(tag)
   174  	c.Assert(err, gc.ErrorMatches, `cloud "stratus" not valid`)
   175  	c.Assert(set, jc.IsFalse)
   176  
   177  	credentialTag, credentialSet := m.CloudCredentialTag()
   178  	// Make sure no credential is set.
   179  	c.Assert(credentialTag, gc.DeepEquals, names.CloudCredentialTag{})
   180  	c.Assert(credentialSet, jc.IsFalse)
   181  	_, credentialSet, err = m.CloudCredential()
   182  	c.Assert(err, jc.ErrorIsNil)
   183  	c.Assert(credentialSet, jc.IsFalse)
   184  }
   185  
   186  func (s *ModelCredentialSuite) TestWatchModelCredential(c *gc.C) {
   187  	// Credential to use in this test.
   188  	tag := names.NewCloudCredentialTag("dummy/bob/foobar")
   189  	credential := cloud.NewCredential(cloud.EmptyAuthType, nil)
   190  	err := s.State.UpdateCloudCredential(tag, credential)
   191  	c.Assert(err, jc.ErrorIsNil)
   192  
   193  	// Model with credential watcher for this test.
   194  	m, err := s.State.Model()
   195  	c.Assert(err, jc.ErrorIsNil)
   196  	w := m.WatchModelCredential()
   197  	defer statetesting.AssertStop(c, w)
   198  	wc := statetesting.NewNotifyWatcherC(c, w)
   199  
   200  	// Initial event.
   201  	wc.AssertOneChange()
   202  
   203  	// Check the watcher reacts to credential reference changes.
   204  	set, err := m.SetCloudCredential(tag)
   205  	c.Assert(err, jc.ErrorIsNil)
   206  	c.Assert(set, jc.IsTrue)
   207  	wc.AssertOneChange()
   208  
   209  	// Check the watcher does not react to other changes on this model.
   210  	err = m.SetDead()
   211  	c.Assert(err, jc.ErrorIsNil)
   212  	wc.AssertNoChange()
   213  
   214  	// Check that changes on another model do not affect this watcher.
   215  	st := s.addModel(c, "abcmodel", s.credentialTag)
   216  	defer st.Close()
   217  	anotherM, err := st.Model()
   218  	c.Assert(err, jc.ErrorIsNil)
   219  	set, err = anotherM.SetCloudCredential(tag)
   220  	c.Assert(err, jc.ErrorIsNil)
   221  	c.Assert(set, jc.IsTrue)
   222  	wc.AssertNoChange()
   223  }
   224  
   225  func (s *ModelCredentialSuite) assertSetCloudCredential(c *gc.C, tag names.CloudCredentialTag, credential cloud.Credential) *state.Model {
   226  	m, err := s.State.Model()
   227  	c.Assert(err, jc.ErrorIsNil)
   228  	credentialTag, credentialSet := m.CloudCredentialTag()
   229  	// Make sure no credential is set.
   230  	c.Assert(credentialTag, gc.DeepEquals, names.CloudCredentialTag{})
   231  	c.Assert(credentialSet, jc.IsFalse)
   232  	_, credentialSet, err = m.CloudCredential()
   233  	c.Assert(err, jc.ErrorIsNil)
   234  	c.Assert(credentialSet, jc.IsFalse)
   235  
   236  	err = s.State.UpdateCloudCredential(tag, credential)
   237  	c.Assert(err, jc.ErrorIsNil)
   238  
   239  	set, err := m.SetCloudCredential(tag)
   240  	c.Assert(err, jc.ErrorIsNil)
   241  	c.Assert(set, jc.IsTrue)
   242  
   243  	// Check credential is set.
   244  	credentialTag, credentialSet = m.CloudCredentialTag()
   245  	c.Assert(credentialTag, gc.DeepEquals, tag)
   246  	c.Assert(credentialSet, jc.IsTrue)
   247  	cred, credentialSet, err := m.CloudCredential()
   248  	c.Assert(err, jc.ErrorIsNil)
   249  	c.Assert(credentialSet, jc.IsTrue)
   250  	stateCred, err := s.State.CloudCredential(credentialTag)
   251  	c.Assert(err, jc.ErrorIsNil)
   252  	c.Assert(cred, jc.DeepEquals, stateCred)
   253  	return m
   254  }
   255  
   256  func (s *ModelCredentialSuite) createCloudCredential(c *gc.C, credentialName string) names.CloudCredentialTag {
   257  	// Cloud name is always "dummy" as deep within the testing infrastructure,
   258  	// we create a testing controller on a cloud "dummy".
   259  	// Test cloud "dummy" only allows credentials with an empty auth type.
   260  	tag := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s", "dummy", s.Owner.Id(), credentialName))
   261  	err := s.State.UpdateCloudCredential(tag, cloud.NewEmptyCredential())
   262  	c.Assert(err, jc.ErrorIsNil)
   263  	return tag
   264  }
   265  
   266  func (s *ModelCredentialSuite) addModel(c *gc.C, modelName string, tag names.CloudCredentialTag) *state.State {
   267  	uuid, err := utils.NewUUID()
   268  	c.Assert(err, jc.ErrorIsNil)
   269  	cfg := testing.CustomModelConfig(c, testing.Attrs{
   270  		"name": modelName,
   271  		"uuid": uuid.String(),
   272  	})
   273  	_, st, err := s.Controller.NewModel(state.ModelArgs{
   274  		Type:                    state.ModelTypeIAAS,
   275  		CloudName:               "dummy",
   276  		CloudRegion:             "dummy-region",
   277  		Config:                  cfg,
   278  		Owner:                   tag.Owner(),
   279  		CloudCredential:         tag,
   280  		StorageProviderRegistry: storage.StaticProviderRegistry{},
   281  	})
   282  	c.Assert(err, jc.ErrorIsNil)
   283  	return st
   284  }
   285  
   286  func (s *ModelCredentialSuite) TestInvalidateModelCredentialTouchesAllCredentialModels(c *gc.C) {
   287  	// This test checks that all models are affected when one of them invalidates a credential they all use...
   288  
   289  	// 1. create a credential
   290  	cloudName, credentialOwner, credentialTag := assertCredentialCreated(c, s.ConnSuite)
   291  
   292  	// 2. create some models to use it
   293  	modelUUIDs := make([]string, 5)
   294  	for i := 0; i < 5; i++ {
   295  		modelUUIDs[i] = assertModelCreated(c, s.ConnSuite, cloudName, credentialTag, credentialOwner.Tag(), fmt.Sprintf("model-for-cloud%v", i))
   296  	}
   297  
   298  	// 3. invalidate credential
   299  	oneModelState, helper, err := s.StatePool.GetModel(modelUUIDs[0])
   300  	c.Assert(err, jc.ErrorIsNil)
   301  	defer helper.Release()
   302  	c.Assert(oneModelState.State().InvalidateModelCredential("testing invalidate for all credential models"), jc.ErrorIsNil)
   303  
   304  	// 4. check all models are suspended
   305  	for _, uuid := range modelUUIDs {
   306  		assertModelStatus(c, s.StatePool, uuid, status.Suspended)
   307  		assertModelHistories(c, s.StatePool, uuid, status.Suspended, status.Available)
   308  	}
   309  }
   310  
   311  func (s *ModelCredentialSuite) TestSetCredentialRevertsModelStatus(c *gc.C) {
   312  	// 1. create a credential
   313  	cloudName, credentialOwner, credentialTag := assertCredentialCreated(c, s.ConnSuite)
   314  
   315  	// 2. create some models to use it
   316  	validModelStatuses := []status.Status{
   317  		status.Available,
   318  		status.Busy,
   319  		status.Destroying,
   320  		status.Error,
   321  	}
   322  	desiredNumber := len(validModelStatuses)
   323  
   324  	modelUUIDs := make([]string, desiredNumber)
   325  	for i := 0; i < desiredNumber; i++ {
   326  		modelUUIDs[i] = assertModelCreated(c, s.ConnSuite, cloudName, credentialTag, credentialOwner.Tag(), fmt.Sprintf("model-for-cloud%v", i))
   327  		oneModelState, helper, err := s.StatePool.GetModel(modelUUIDs[i])
   328  		c.Assert(err, jc.ErrorIsNil)
   329  		defer helper.Release()
   330  		if validModelStatuses[i] != status.Available {
   331  			// any model would be in 'available' status on setup.
   332  			err = oneModelState.SetStatus(status.StatusInfo{Status: validModelStatuses[i]})
   333  			c.Assert(err, jc.ErrorIsNil)
   334  			c.Assert(oneModelState.Refresh(), jc.ErrorIsNil)
   335  		}
   336  		if i == desiredNumber-1 {
   337  			// 3. invalidate credential on last model
   338  			c.Assert(oneModelState.State().InvalidateModelCredential("testing"), jc.ErrorIsNil)
   339  		}
   340  	}
   341  
   342  	// 4. check model is suspended
   343  	for i := 0; i < desiredNumber; i++ {
   344  		assertModelStatus(c, s.StatePool, modelUUIDs[i], status.Suspended)
   345  		if validModelStatuses[i] == status.Available {
   346  			assertModelHistories(c, s.StatePool, modelUUIDs[i], status.Suspended, status.Available)
   347  		} else {
   348  			assertModelHistories(c, s.StatePool, modelUUIDs[i], status.Suspended, validModelStatuses[i], status.Available)
   349  		}
   350  	}
   351  
   352  	// 5. create another credential on the same cloud
   353  	owner := s.Factory.MakeUser(c, &factory.UserParams{
   354  		Password: "secret",
   355  		Name:     "uncle",
   356  	})
   357  	anotherCredentialTag := createCredential(c, s.ConnSuite, cloudName, owner.Name(), "barfoo")
   358  
   359  	for i := 0; i < desiredNumber; i++ {
   360  		oneModelState, helper, err := s.StatePool.GetModel(modelUUIDs[i])
   361  		c.Assert(err, jc.ErrorIsNil)
   362  		defer helper.Release()
   363  
   364  		isSet, err := oneModelState.SetCloudCredential(anotherCredentialTag)
   365  		c.Assert(err, jc.ErrorIsNil)
   366  		c.Assert(isSet, jc.IsTrue)
   367  
   368  		// 5. Check model status is reverted
   369  		if validModelStatuses[i] == status.Available {
   370  			assertModelStatus(c, s.StatePool, modelUUIDs[i], status.Available)
   371  			assertModelHistories(c, s.StatePool, modelUUIDs[i], status.Available, status.Suspended, status.Available)
   372  		} else {
   373  			assertModelStatus(c, s.StatePool, modelUUIDs[i], validModelStatuses[i])
   374  			assertModelHistories(c, s.StatePool, modelUUIDs[i], validModelStatuses[i], status.Suspended, validModelStatuses[i], status.Available)
   375  		}
   376  	}
   377  }
   378  
   379  func assertModelHistories(c *gc.C, pool *state.StatePool, testModelUUID string, expected ...status.Status) []status.StatusInfo {
   380  	aModel, helper, err := pool.GetModel(testModelUUID)
   381  	c.Assert(err, jc.ErrorIsNil)
   382  	defer helper.Release()
   383  	statusHistories, err := aModel.StatusHistory(status.StatusHistoryFilter{Size: 100})
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	c.Assert(statusHistories, gc.HasLen, len(expected))
   386  	for i, one := range expected {
   387  		c.Assert(statusHistories[i].Status, gc.Equals, one)
   388  	}
   389  	return statusHistories
   390  }