github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/modelconfig/modelconfig_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package modelconfig_test 5 6 import ( 7 "github.com/juju/errors" 8 "github.com/juju/names/v5" 9 "github.com/juju/testing" 10 jc "github.com/juju/testing/checkers" 11 "go.uber.org/mock/gomock" 12 gc "gopkg.in/check.v1" 13 14 commonsecrets "github.com/juju/juju/apiserver/common/secrets" 15 "github.com/juju/juju/apiserver/facades/client/modelconfig" 16 "github.com/juju/juju/apiserver/facades/client/modelconfig/mocks" 17 apiservertesting "github.com/juju/juju/apiserver/testing" 18 "github.com/juju/juju/core/constraints" 19 coresecrets "github.com/juju/juju/core/secrets" 20 "github.com/juju/juju/environs/config" 21 "github.com/juju/juju/feature" 22 "github.com/juju/juju/provider/dummy" 23 "github.com/juju/juju/rpc/params" 24 secretsprovider "github.com/juju/juju/secrets/provider" 25 "github.com/juju/juju/state" 26 coretesting "github.com/juju/juju/testing" 27 ) 28 29 type modelconfigSuite struct { 30 testing.IsolationSuite 31 coretesting.JujuOSEnvSuite 32 backend *mockBackend 33 authorizer apiservertesting.FakeAuthorizer 34 api *modelconfig.ModelConfigAPIV3 35 } 36 37 var _ = gc.Suite(&modelconfigSuite{}) 38 39 func (s *modelconfigSuite) SetUpTest(c *gc.C) { 40 s.SetInitialFeatureFlags(feature.DeveloperMode) 41 s.IsolationSuite.SetUpTest(c) 42 s.JujuOSEnvSuite.SetUpTest(c) 43 s.authorizer = apiservertesting.FakeAuthorizer{ 44 Tag: names.NewUserTag("bruce@local"), 45 AdminTag: names.NewUserTag("bruce@local"), 46 } 47 s.backend = &mockBackend{ 48 cfg: config.ConfigValues{ 49 "type": {Value: "dummy", Source: "model"}, 50 "agent-version": {Value: "1.2.3.4", Source: "model"}, 51 "ftp-proxy": {Value: "http://proxy", Source: "model"}, 52 "authorized-keys": {Value: coretesting.FakeAuthKeys, Source: "model"}, 53 "charmhub-url": {Value: "http://meshuggah.rocks", Source: "model"}, 54 }, 55 secretBackend: &coresecrets.SecretBackend{ 56 ID: "backend-1", 57 Name: "backend-1", 58 BackendType: "vault", 59 Config: map[string]interface{}{ 60 "endpoint": "http://0.0.0.0:8200", 61 }, 62 }, 63 } 64 var err error 65 s.api, err = modelconfig.NewModelConfigAPI(s.backend, &s.authorizer) 66 c.Assert(err, jc.ErrorIsNil) 67 } 68 69 func (s *modelconfigSuite) TestAdminModelGet(c *gc.C) { 70 result, err := s.api.ModelGet() 71 c.Assert(err, jc.ErrorIsNil) 72 c.Assert(result.Config, jc.DeepEquals, map[string]params.ConfigValue{ 73 "type": {Value: "dummy", Source: "model"}, 74 "ftp-proxy": {Value: "http://proxy", Source: "model"}, 75 "agent-version": {Value: "1.2.3.4", Source: "model"}, 76 "charmhub-url": {Value: "http://meshuggah.rocks", Source: "model"}, 77 "default-series": {Value: "", Source: "default"}, 78 }) 79 } 80 81 func (s *modelconfigSuite) TestUserModelGet(c *gc.C) { 82 s.authorizer = apiservertesting.FakeAuthorizer{ 83 Tag: names.NewUserTag("bruce@local"), 84 HasWriteTag: names.NewUserTag("bruce@local"), 85 AdminTag: names.NewUserTag("mary@local"), 86 } 87 result, err := s.api.ModelGet() 88 c.Assert(err, jc.ErrorIsNil) 89 c.Assert(result.Config, jc.DeepEquals, map[string]params.ConfigValue{ 90 "type": {Value: "dummy", Source: "model"}, 91 "ftp-proxy": {Value: "http://proxy", Source: "model"}, 92 "agent-version": {Value: "1.2.3.4", Source: "model"}, 93 "charmhub-url": {Value: "http://meshuggah.rocks", Source: "model"}, 94 "default-series": {Value: "", Source: "default"}, 95 }) 96 } 97 98 func (s *modelconfigSuite) assertConfigValue(c *gc.C, key string, expected interface{}) { 99 value, found := s.backend.cfg[key] 100 c.Assert(found, jc.IsTrue) 101 c.Assert(value.Value, gc.Equals, expected) 102 } 103 104 func (s *modelconfigSuite) assertConfigValueMissing(c *gc.C, key string) { 105 _, found := s.backend.cfg[key] 106 c.Assert(found, jc.IsFalse) 107 } 108 109 func (s *modelconfigSuite) TestAdminModelSet(c *gc.C) { 110 params := params.ModelSet{ 111 Config: map[string]interface{}{ 112 "some-key": "value", 113 "other-key": "other value", 114 }, 115 } 116 err := s.api.ModelSet(params) 117 c.Assert(err, jc.ErrorIsNil) 118 s.assertConfigValue(c, "some-key", "value") 119 s.assertConfigValue(c, "other-key", "other value") 120 } 121 122 func (s *modelconfigSuite) blockAllChanges(c *gc.C, msg string) { 123 s.backend.msg = msg 124 s.backend.b = state.ChangeBlock 125 } 126 127 func (s *modelconfigSuite) assertBlocked(c *gc.C, err error, msg string) { 128 c.Assert(params.IsCodeOperationBlocked(err), jc.IsTrue, gc.Commentf("error: %#v", err)) 129 c.Assert(errors.Cause(err), jc.DeepEquals, ¶ms.Error{ 130 Message: msg, 131 Code: "operation is blocked", 132 }) 133 } 134 135 func (s *modelconfigSuite) assertModelSetBlocked(c *gc.C, args map[string]interface{}, msg string) { 136 err := s.api.ModelSet(params.ModelSet{Config: args}) 137 s.assertBlocked(c, err, msg) 138 } 139 140 func (s *modelconfigSuite) TestBlockChangesModelSet(c *gc.C) { 141 s.blockAllChanges(c, "TestBlockChangesModelSet") 142 args := map[string]interface{}{"some-key": "value"} 143 s.assertModelSetBlocked(c, args, "TestBlockChangesModelSet") 144 } 145 146 func (s *modelconfigSuite) TestModelSetCannotChangeAgentVersion(c *gc.C) { 147 old, err := config.New(config.UseDefaults, dummy.SampleConfig().Merge(coretesting.Attrs{ 148 "agent-version": "1.2.3.4", 149 })) 150 c.Assert(err, jc.ErrorIsNil) 151 s.backend.old = old 152 args := params.ModelSet{ 153 Config: map[string]interface{}{"agent-version": "9.9.9"}, 154 } 155 err = s.api.ModelSet(args) 156 c.Assert(err, gc.ErrorMatches, "agent-version cannot be changed") 157 158 // It's okay to pass config back with the same agent-version. 159 result, err := s.api.ModelGet() 160 c.Assert(err, jc.ErrorIsNil) 161 c.Assert(result.Config["agent-version"], gc.NotNil) 162 args.Config["agent-version"] = result.Config["agent-version"].Value 163 err = s.api.ModelSet(args) 164 c.Assert(err, jc.ErrorIsNil) 165 } 166 167 func (s *modelconfigSuite) TestModelSetCannotChangeCharmHubURL(c *gc.C) { 168 old, err := config.New(config.UseDefaults, dummy.SampleConfig().Merge(coretesting.Attrs{ 169 "charmhub-url": "http://meshuggah.rocks", 170 })) 171 c.Assert(err, jc.ErrorIsNil) 172 s.backend.old = old 173 args := params.ModelSet{ 174 Config: map[string]interface{}{"charmhub-url": "http://another-url.com"}, 175 } 176 err = s.api.ModelSet(args) 177 c.Assert(err, gc.ErrorMatches, "charmhub-url cannot be changed") 178 179 // It's okay to pass config back with the same charmhub-url. 180 result, err := s.api.ModelGet() 181 c.Assert(err, jc.ErrorIsNil) 182 c.Assert(result.Config["charmhub-url"], gc.NotNil) 183 args.Config["charmhub-url"] = result.Config["charmhub-url"].Value 184 err = s.api.ModelSet(args) 185 c.Assert(err, jc.ErrorIsNil) 186 } 187 188 func (s *modelconfigSuite) TestModelSetCannotChangeBothDefaultSeriesAndDefaultBaseWithSeries(c *gc.C) { 189 old, err := config.New(config.UseDefaults, dummy.SampleConfig().Merge(coretesting.Attrs{ 190 "default-series": "jammy", 191 })) 192 c.Assert(err, jc.ErrorIsNil) 193 194 s.backend.old = old 195 args := params.ModelSet{ 196 Config: map[string]interface{}{ 197 "default-series": "jammy", 198 "default-base": "ubuntu@22.04", 199 }, 200 } 201 err = s.api.ModelSet(args) 202 c.Assert(err, gc.ErrorMatches, "cannot set both default-series and default-base") 203 204 err = s.api.ModelSet(params.ModelSet{ 205 Config: map[string]interface{}{ 206 "default-series": "jammy", 207 }, 208 }) 209 c.Assert(err, jc.ErrorIsNil) 210 211 result, err := s.api.ModelGet() 212 c.Assert(err, jc.ErrorIsNil) 213 c.Assert(result.Config["default-series"], gc.NotNil) 214 c.Assert(result.Config["default-series"].Value, gc.Equals, "jammy") 215 c.Assert(result.Config["default-base"].Value, gc.Equals, "ubuntu@22.04/stable") 216 } 217 218 func (s *modelconfigSuite) TestModelSetCannotChangeBothDefaultSeriesAndDefaultBaseWithBase(c *gc.C) { 219 old, err := config.New(config.UseDefaults, dummy.SampleConfig().Merge(coretesting.Attrs{ 220 "default-base": "ubuntu@22.04", 221 })) 222 c.Assert(err, jc.ErrorIsNil) 223 224 s.backend.old = old 225 args := params.ModelSet{ 226 Config: map[string]interface{}{ 227 "default-series": "jammy", 228 "default-base": "ubuntu@22.04", 229 }, 230 } 231 err = s.api.ModelSet(args) 232 c.Assert(err, gc.ErrorMatches, "cannot set both default-series and default-base") 233 234 err = s.api.ModelSet(params.ModelSet{ 235 Config: map[string]interface{}{ 236 "default-series": "jammy", 237 }, 238 }) 239 c.Assert(err, jc.ErrorIsNil) 240 241 result, err := s.api.ModelGet() 242 c.Assert(err, jc.ErrorIsNil) 243 c.Assert(result.Config["default-series"], gc.NotNil) 244 c.Assert(result.Config["default-series"].Value, gc.Equals, "jammy") 245 c.Assert(result.Config["default-base"].Value, gc.Equals, "ubuntu@22.04/stable") 246 } 247 248 func (s *modelconfigSuite) TestModelSetCannotSetAuthorizedKeys(c *gc.C) { 249 // Try to set the authorized-keys model config. 250 args := params.ModelSet{ 251 Config: map[string]interface{}{"authorized-keys": "ssh-rsa new Juju:juju-client-key"}, 252 } 253 err := s.api.ModelSet(args) 254 c.Assert(err, gc.ErrorMatches, "authorized-keys cannot be set") 255 // Make sure the authorized-keys still contains its original value. 256 s.assertConfigValue(c, "authorized-keys", coretesting.FakeAuthKeys) 257 } 258 259 func (s *modelconfigSuite) TestAdminCanSetLogTrace(c *gc.C) { 260 args := params.ModelSet{ 261 Config: map[string]interface{}{"logging-config": "<root>=DEBUG;somepackage=TRACE"}, 262 } 263 err := s.api.ModelSet(args) 264 c.Assert(err, jc.ErrorIsNil) 265 266 result, err := s.api.ModelGet() 267 c.Assert(err, jc.ErrorIsNil) 268 c.Assert(result.Config["logging-config"].Value, gc.Equals, "<root>=DEBUG;somepackage=TRACE") 269 } 270 271 func (s *modelconfigSuite) TestUserCanSetLogNoTrace(c *gc.C) { 272 args := params.ModelSet{ 273 Config: map[string]interface{}{"logging-config": "<root>=DEBUG;somepackage=ERROR"}, 274 } 275 apiUser := names.NewUserTag("fred") 276 s.authorizer.Tag = apiUser 277 s.authorizer.HasWriteTag = apiUser 278 err := s.api.ModelSet(args) 279 c.Assert(err, jc.ErrorIsNil) 280 281 result, err := s.api.ModelGet() 282 c.Assert(err, jc.ErrorIsNil) 283 c.Assert(result.Config["logging-config"].Value, gc.Equals, "<root>=DEBUG;somepackage=ERROR") 284 } 285 286 func (s *modelconfigSuite) TestUserReadAccess(c *gc.C) { 287 apiUser := names.NewUserTag("read") 288 s.authorizer.Tag = apiUser 289 290 _, err := s.api.ModelGet() 291 c.Assert(err, jc.ErrorIsNil) 292 293 err = s.api.ModelSet(params.ModelSet{}) 294 c.Assert(errors.Cause(err), gc.ErrorMatches, "permission denied") 295 } 296 297 func (s *modelconfigSuite) TestUserCannotSetLogTrace(c *gc.C) { 298 args := params.ModelSet{ 299 Config: map[string]interface{}{"logging-config": "<root>=DEBUG;somepackage=TRACE"}, 300 } 301 apiUser := names.NewUserTag("fred") 302 s.authorizer.Tag = apiUser 303 s.authorizer.HasWriteTag = apiUser 304 err := s.api.ModelSet(args) 305 c.Assert(err, gc.ErrorMatches, `only controller admins can set a model's logging level to TRACE`) 306 } 307 308 func (s *modelconfigSuite) TestSetSecretBackend(c *gc.C) { 309 args := params.ModelSet{ 310 Config: map[string]interface{}{"secret-backend": 1}, 311 } 312 err := s.api.ModelSet(args) 313 c.Assert(err, gc.ErrorMatches, `"secret-backend" config value is not a string`) 314 315 args.Config = map[string]interface{}{"secret-backend": ""} 316 err = s.api.ModelSet(args) 317 c.Assert(err, gc.ErrorMatches, `empty "secret-backend" config value not valid`) 318 319 args.Config = map[string]interface{}{"secret-backend": "auto"} 320 err = s.api.ModelSet(args) 321 c.Assert(err, jc.ErrorIsNil) 322 result, err := s.api.ModelGet() 323 c.Assert(err, jc.ErrorIsNil) 324 c.Assert(result.Config["secret-backend"].Value, gc.Equals, "auto") 325 } 326 327 func (s *modelconfigSuite) TestSetSecretBackendExternal(c *gc.C) { 328 ctrl := gomock.NewController(c) 329 defer ctrl.Finish() 330 331 vaultProvider := mocks.NewMockSecretBackendProvider(ctrl) 332 s.PatchValue(&commonsecrets.GetProvider, func(string) (secretsprovider.SecretBackendProvider, error) { return vaultProvider, nil }) 333 vaultBackend := mocks.NewMockSecretsBackend(ctrl) 334 335 gomock.InOrder( 336 vaultProvider.EXPECT().Type().Return("vault"), 337 vaultProvider.EXPECT().NewBackend(&secretsprovider.ModelBackendConfig{ 338 BackendConfig: secretsprovider.BackendConfig{ 339 BackendType: "vault", 340 Config: s.backend.secretBackend.Config, 341 }, 342 }).Return(vaultBackend, nil), 343 vaultBackend.EXPECT().Ping().Return(nil), 344 ) 345 346 args := params.ModelSet{ 347 Config: map[string]interface{}{"secret-backend": "backend-1"}, 348 } 349 err := s.api.ModelSet(args) 350 c.Assert(err, jc.ErrorIsNil) 351 result, err := s.api.ModelGet() 352 c.Assert(err, jc.ErrorIsNil) 353 c.Assert(result.Config["secret-backend"].Value, gc.Equals, "backend-1") 354 } 355 356 func (s *modelconfigSuite) TestSetSecretBackendExternalValidationFailed(c *gc.C) { 357 ctrl := gomock.NewController(c) 358 defer ctrl.Finish() 359 360 vaultProvider := mocks.NewMockSecretBackendProvider(ctrl) 361 s.PatchValue(&commonsecrets.GetProvider, func(string) (secretsprovider.SecretBackendProvider, error) { return vaultProvider, nil }) 362 vaultBackend := mocks.NewMockSecretsBackend(ctrl) 363 364 gomock.InOrder( 365 vaultProvider.EXPECT().Type().Return("vault"), 366 vaultProvider.EXPECT().NewBackend(&secretsprovider.ModelBackendConfig{ 367 BackendConfig: secretsprovider.BackendConfig{ 368 BackendType: "vault", 369 Config: s.backend.secretBackend.Config, 370 }, 371 }).Return(vaultBackend, nil), 372 vaultBackend.EXPECT().Ping().Return(errors.New("not reachable")), 373 ) 374 375 args := params.ModelSet{ 376 Config: map[string]interface{}{"secret-backend": "backend-1"}, 377 } 378 err := s.api.ModelSet(args) 379 c.Assert(err, gc.ErrorMatches, `cannot ping backend "backend-1": not reachable`) 380 } 381 382 func (s *modelconfigSuite) TestModelUnset(c *gc.C) { 383 err := s.backend.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil) 384 c.Assert(err, jc.ErrorIsNil) 385 386 args := params.ModelUnset{Keys: []string{"abc"}} 387 err = s.api.ModelUnset(args) 388 c.Assert(err, jc.ErrorIsNil) 389 s.assertConfigValueMissing(c, "abc") 390 } 391 392 func (s *modelconfigSuite) TestBlockModelUnset(c *gc.C) { 393 err := s.backend.UpdateModelConfig(map[string]interface{}{"abc": 123}, nil) 394 c.Assert(err, jc.ErrorIsNil) 395 s.blockAllChanges(c, "TestBlockModelUnset") 396 397 args := params.ModelUnset{Keys: []string{"abc"}} 398 err = s.api.ModelUnset(args) 399 s.assertBlocked(c, err, "TestBlockModelUnset") 400 } 401 402 func (s *modelconfigSuite) TestModelUnsetMissing(c *gc.C) { 403 // It's okay to unset a non-existent attribute. 404 args := params.ModelUnset{Keys: []string{"not_there"}} 405 err := s.api.ModelUnset(args) 406 c.Assert(err, jc.ErrorIsNil) 407 } 408 409 func (s *modelconfigSuite) TestSetSupportCredentals(c *gc.C) { 410 err := s.api.SetSLALevel(params.ModelSLA{ 411 ModelSLAInfo: params.ModelSLAInfo{Level: "level", Owner: "bob"}, 412 Credentials: []byte("foobar"), 413 }) 414 c.Assert(err, jc.ErrorIsNil) 415 } 416 417 func (s *modelconfigSuite) TestClientSetModelConstraints(c *gc.C) { 418 // Set constraints for the model. 419 cons, err := constraints.Parse("mem=4096", "cores=2") 420 c.Assert(err, jc.ErrorIsNil) 421 err = s.api.SetModelConstraints(params.SetConstraints{ 422 ApplicationName: "app", 423 Constraints: cons, 424 }) 425 c.Assert(err, jc.ErrorIsNil) 426 c.Assert(s.backend.cons, gc.DeepEquals, cons) 427 } 428 429 func (s *modelconfigSuite) assertSetModelConstraintsBlocked(c *gc.C, msg string) { 430 // Set constraints for the model. 431 cons, err := constraints.Parse("mem=4096", "cores=2") 432 c.Assert(err, jc.ErrorIsNil) 433 err = s.api.SetModelConstraints(params.SetConstraints{ 434 ApplicationName: "app", 435 Constraints: cons, 436 }) 437 s.assertBlocked(c, err, msg) 438 } 439 440 func (s *modelconfigSuite) TestBlockChangesClientSetModelConstraints(c *gc.C) { 441 s.blockAllChanges(c, "TestBlockChangesClientSetModelConstraints") 442 s.assertSetModelConstraintsBlocked(c, "TestBlockChangesClientSetModelConstraints") 443 } 444 445 func (s *modelconfigSuite) TestClientGetModelConstraints(c *gc.C) { 446 // Set constraints for the model. 447 cons, err := constraints.Parse("mem=4096", "cores=2") 448 c.Assert(err, jc.ErrorIsNil) 449 s.backend.cons = cons 450 obtained, err := s.api.GetModelConstraints() 451 c.Assert(err, jc.ErrorIsNil) 452 c.Assert(obtained.Constraints, gc.DeepEquals, cons) 453 } 454 455 type mockBackend struct { 456 cfg config.ConfigValues 457 old *config.Config 458 b state.BlockType 459 msg string 460 cons constraints.Value 461 secretBackend *coresecrets.SecretBackend 462 } 463 464 func (m *mockBackend) SetModelConstraints(value constraints.Value) error { 465 m.cons = value 466 return nil 467 } 468 469 func (m *mockBackend) ModelConstraints() (constraints.Value, error) { 470 return m.cons, nil 471 } 472 473 func (m *mockBackend) ModelConfigValues() (config.ConfigValues, error) { 474 return m.cfg, nil 475 } 476 477 func (m *mockBackend) Sequences() (map[string]int, error) { 478 return nil, nil 479 } 480 481 func (m *mockBackend) UpdateModelConfig(update map[string]interface{}, remove []string, 482 validate ...state.ValidateConfigFunc) error { 483 for _, validateFunc := range validate { 484 if err := validateFunc(update, remove, m.old); err != nil { 485 return err 486 } 487 } 488 for k, v := range update { 489 m.cfg[k] = config.ConfigValue{Value: v, Source: "model"} 490 } 491 for _, n := range remove { 492 delete(m.cfg, n) 493 } 494 return nil 495 } 496 497 func (m *mockBackend) GetBlockForType(t state.BlockType) (state.Block, bool, error) { 498 if m.b == t { 499 return &mockBlock{t: t, m: m.msg}, true, nil 500 } else { 501 return nil, false, nil 502 } 503 } 504 505 func (m *mockBackend) ModelTag() names.ModelTag { 506 return names.NewModelTag("deadbeef-2f18-4fd2-967d-db9663db7bea") 507 } 508 509 func (m *mockBackend) ControllerTag() names.ControllerTag { 510 return names.NewControllerTag("deadbeef-babe-4fd2-967d-db9663db7bea") 511 } 512 513 func (m *mockBackend) SetSLA(level, owner string, credentials []byte) error { 514 return nil 515 } 516 517 func (m *mockBackend) SLALevel() (string, error) { 518 return "mock-level", nil 519 } 520 521 func (m *mockBackend) SpaceByName(string) error { 522 return nil 523 } 524 525 func (m *mockBackend) GetSecretBackend(name string) (*coresecrets.SecretBackend, error) { 526 if name == "invalid" { 527 return nil, errors.NotFoundf("invalid") 528 } 529 return m.secretBackend, nil 530 } 531 532 type mockBlock struct { 533 state.Block 534 t state.BlockType 535 m string 536 } 537 538 func (m mockBlock) Id() string { return "" } 539 540 func (m mockBlock) Tag() (names.Tag, error) { return names.NewModelTag("mocktesting"), nil } 541 542 func (m mockBlock) Type() state.BlockType { return m.t } 543 544 func (m mockBlock) Message() string { return m.m } 545 546 func (m mockBlock) ModelUUID() string { return "" }