github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/endpoint_bindings.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 9 "github.com/juju/charm/v12" 10 "github.com/juju/collections/set" 11 "github.com/juju/errors" 12 "github.com/juju/mgo/v3" 13 "github.com/juju/mgo/v3/bson" 14 "github.com/juju/mgo/v3/txn" 15 jujutxn "github.com/juju/txn/v3" 16 17 "github.com/juju/juju/core/network" 18 "github.com/juju/juju/mongo/utils" 19 ) 20 21 // defaultEndpointName is the key in the bindings map that stores the 22 // space name that endpoints should be bound to if they aren't found 23 // individually. 24 const defaultEndpointName = "" 25 26 // endpointBindingsDoc represents how a application's endpoints are bound to spaces. 27 // The DocID field contains the applications's global key, so there is always one 28 // endpointBindingsDoc per application. 29 type endpointBindingsDoc struct { 30 // DocID is always the same as a application's global key. 31 DocID string `bson:"_id"` 32 33 // Bindings maps an application endpoint name to the space ID it is bound to. 34 Bindings bindingsMap `bson:"bindings"` 35 36 // TxnRevno is used to assert the collection have not changed since this 37 // document was fetched. 38 TxnRevno int64 `bson:"txn-revno"` 39 } 40 41 // bindingsMap is the underlying type stored in mongo for bindings. 42 type bindingsMap map[string]string 43 44 // SetBSON ensures any special characters ($ or .) are unescaped in keys after 45 // unmarshalling the raw BSON coming from the stored document. 46 func (b *bindingsMap) SetBSON(raw bson.Raw) error { 47 rawMap := make(map[string]string) 48 if err := raw.Unmarshal(rawMap); err != nil { 49 return err 50 } 51 for key, value := range rawMap { 52 newKey := utils.UnescapeKey(key) 53 if newKey != key { 54 delete(rawMap, key) 55 } 56 rawMap[newKey] = value 57 } 58 *b = bindingsMap(rawMap) 59 return nil 60 } 61 62 // GetBSON ensures any special characters ($ or .) are escaped in keys before 63 // marshalling the map into BSON and storing in mongo. 64 func (b bindingsMap) GetBSON() (interface{}, error) { 65 if b == nil || len(b) == 0 { 66 // We need to return a non-nil map otherwise bson.Unmarshal 67 // call will fail when reading the doc back. 68 return make(map[string]string), nil 69 } 70 rawMap := make(map[string]string, len(b)) 71 for key, value := range b { 72 newKey := utils.EscapeKey(key) 73 rawMap[newKey] = value 74 } 75 76 return rawMap, nil 77 } 78 79 // Merge the default bindings based on the given charm metadata with the 80 // current bindings, overriding with mergeWith values (for the same keys). 81 // Current values and mergeWith are both optional and will ignored when 82 // empty. The current object contains the combined finalized bindings. 83 // Returns true/false if there are any actual differences. 84 func (b *Bindings) Merge(mergeWith map[string]string, meta *charm.Meta) (bool, error) { 85 // Verify the bindings to be merged, and ensure we're merging with 86 // space ids. 87 merge, err := NewBindings(b.st, mergeWith) 88 if err != nil { 89 return false, errors.Trace(err) 90 } 91 mergeMap := merge.Map() 92 93 defaultsMap, err := DefaultEndpointBindingsForCharm(b.st, meta) 94 if err != nil { 95 return false, errors.Trace(err) 96 } 97 98 defaultBinding, mergeOK := b.bindingsMap[defaultEndpointName] 99 if !mergeOK { 100 var err error 101 defaultBinding, err = b.st.DefaultEndpointBindingSpace() 102 if err != nil { 103 return false, errors.Trace(err) 104 } 105 } 106 if newDefaultBinding, newOk := mergeMap[defaultEndpointName]; newOk { 107 // new default binding supersedes the old default binding 108 defaultBinding = newDefaultBinding 109 } 110 111 // defaultsMap contains all endpoints that must be bound for the given charm 112 // metadata, but we need to figure out which value to use for each key. 113 updated := make(map[string]string) 114 updated[defaultEndpointName] = defaultBinding 115 for key, defaultValue := range defaultsMap { 116 effectiveValue := defaultValue 117 118 currentValue, hasCurrent := b.bindingsMap[key] 119 if hasCurrent { 120 if currentValue != effectiveValue { 121 effectiveValue = currentValue 122 } 123 } else { 124 // current didn't talk about this value, but maybe we have a default 125 effectiveValue = defaultBinding 126 } 127 128 mergeValue, hasMerge := mergeMap[key] 129 if hasMerge && mergeValue != effectiveValue && mergeValue != "" { 130 effectiveValue = mergeValue 131 } 132 133 updated[key] = effectiveValue 134 } 135 136 // Any other bindings in mergeWith Map are most likely extraneous, but add them 137 // anyway and let the validation handle them. 138 for key, newValue := range mergeMap { 139 if _, defaultExists := defaultsMap[key]; !defaultExists { 140 updated[key] = newValue 141 } 142 } 143 isModified := false 144 if len(updated) != len(b.bindingsMap) { 145 isModified = true 146 } else { 147 // If the len() is identical, then we know as long as we iterate all entries, then there is no way to 148 // miss an entry. Either they have identical keys and we check all the values, or there is an identical 149 // number of new keys and missing keys and we'll notice a missing key. 150 for key, val := range updated { 151 if oldVal, existed := b.bindingsMap[key]; !existed || oldVal != val { 152 isModified = true 153 break 154 } 155 } 156 } 157 logger.Debugf("merged endpoint bindings modified: %t, default: %v, current: %v, mergeWith: %v, after: %v", 158 isModified, defaultsMap, b.bindingsMap, mergeMap, updated) 159 if isModified { 160 b.bindingsMap = updated 161 } 162 return isModified, nil 163 } 164 165 // createOp returns the op needed to create new endpoint bindings using the 166 // optional current bindings and the specified charm metadata to for 167 // determining defaults and to validate the effective bindings. 168 func (b *Bindings) createOp(bindings map[string]string, meta *charm.Meta) (txn.Op, error) { 169 if b.app == nil { 170 return txn.Op{}, errors.Trace(errors.New("programming error: app is a nil pointer")) 171 } 172 // No existing map to Merge, just use the defaults. 173 _, err := b.Merge(bindings, meta) 174 if err != nil { 175 return txn.Op{}, errors.Trace(err) 176 } 177 178 // Validate the bindings before inserting. 179 if err := b.validateForCharm(meta); err != nil { 180 return txn.Op{}, errors.Trace(err) 181 } 182 183 return txn.Op{ 184 C: endpointBindingsC, 185 Id: b.app.globalKey(), 186 Assert: txn.DocMissing, 187 Insert: endpointBindingsDoc{ 188 Bindings: b.Map(), 189 }, 190 }, nil 191 } 192 193 // updateOps returns an op list to update the endpoint bindings for an application. 194 // The implementation calculates the final set of bindings by merging the provided 195 // newMap into the existing set of bindings. The final bindings are validated 196 // in two ways: 197 // 1. we make sure that the endpoint names in the binding map are all present 198 // in the provided charm metadata and that the space IDs actually exist. 199 // 2. we check that all existing units for the application are executing on 200 // machines that have an address in each space we are binding to. This 201 // check can be circumvented by setting the force argument to true. 202 // 203 // The returned operation list includes additional operations that perform 204 // the following assertions: 205 // - assert that the unit count has not changed while the txn is in progress. 206 // - assert that the spaces we are binding to have not been deleted. 207 // - assert that the existing bindings we used for calculating the merged set 208 // of bindings have not changed while the txn is in progress. 209 func (b *Bindings) updateOps(txnRevno int64, newMap map[string]string, newMeta *charm.Meta, force bool) ([]txn.Op, error) { 210 var ops []txn.Op 211 212 if b.app == nil { 213 return ops, errors.Trace(errors.New("programming error: app is a nil pointer")) 214 } 215 216 useTxnRevno := len(b.bindingsMap) > 0 217 218 // Merge existing with new as needed. 219 isModified, err := b.Merge(newMap, newMeta) 220 if err != nil { 221 return ops, errors.Trace(err) 222 } 223 224 if !isModified { 225 return ops, jujutxn.ErrNoOperations 226 } 227 228 // Validate the bindings before updating. 229 if err := b.validateForCharm(newMeta); err != nil { 230 return ops, errors.Trace(err) 231 } 232 233 // Make sure that all machines which run units of this application 234 // contain addresses in the spaces we are trying to bind to. 235 if !force { 236 if err := b.validateForMachines(); err != nil { 237 return ops, errors.Trace(err) 238 } 239 } 240 241 // Ensure that the spaceIDs needed for the bindings exist. 242 spIdMap := set.NewStrings() 243 for _, spID := range b.Map() { 244 sp, err := b.st.Space(spID) 245 if err != nil { 246 return ops, errors.Trace(err) 247 } 248 if spIdMap.Contains(spID) { 249 continue 250 } 251 ops = append(ops, txn.Op{ 252 C: spacesC, 253 Id: sp.doc.DocId, 254 Assert: txn.DocExists, 255 }) 256 spIdMap.Add(spID) 257 } 258 259 // To avoid a potential race where units may suddenly appear on a new 260 // machine that does not have addresses for all the required spaces 261 // while we are applying the txn, we define an assertion on the unit 262 // count for the current application. 263 ops = append(ops, txn.Op{ 264 C: applicationsC, 265 Id: b.app.doc.DocID, 266 Assert: bson.D{{"unitcount", b.app.UnitCount()}}, 267 }) 268 269 // Prepare the update operations. 270 escaped := make(bson.M, len(b.Map())) 271 for endpoint, space := range b.Map() { 272 escaped[utils.EscapeKey(endpoint)] = space 273 } 274 275 _, bindingsErr := readEndpointBindingsDoc(b.app.st, b.app.globalKey()) 276 if bindingsErr != nil && !errors.IsNotFound(err) { 277 return nil, errors.Trace(err) 278 } 279 if err != nil { 280 // No bindings to update. 281 return ops, nil 282 } 283 updateOp := txn.Op{ 284 C: endpointBindingsC, 285 Id: b.app.globalKey(), 286 Assert: txn.DocExists, 287 Update: bson.M{"$set": bson.M{"bindings": escaped}}, 288 } 289 if useTxnRevno { 290 // Only assert existing haven't changed when they actually exist. 291 updateOp.Assert = bson.D{{"txn-revno", txnRevno}} 292 } 293 294 return append(ops, updateOp), nil 295 } 296 297 // validateForMachines ensures that the current set of endpoint to space ID 298 // bindings (including the default space ID for the app) are feasible given the 299 // the network configuration settings of the machines where application units 300 // are already running. 301 func (b *Bindings) validateForMachines() error { 302 if b.app == nil { 303 return errors.Trace(errors.New("programming error: app is a nil pointer")) 304 } 305 // Get a list of deployed machines and create a map where we track the 306 // count of deployed machines for each space. 307 machineCountInSpace := make(map[string]int) 308 deployedMachines, err := b.app.DeployedMachines() 309 if err != nil { 310 return err 311 } 312 313 for _, m := range deployedMachines { 314 machineSpaces, err := m.AllSpaces() 315 if err != nil { 316 return errors.Annotatef(err, "unable to get space assignments for machine %q", m.Id()) 317 } 318 for spID := range machineSpaces { 319 machineCountInSpace[spID]++ 320 } 321 } 322 323 // We only need to validate changes to the default space ID for the 324 // application if the operator is trying to change it to something 325 // other than network.DefaultSpaceID 326 if newDefaultSpaceIDForApp, defined := b.bindingsMap[defaultEndpointName]; defined && newDefaultSpaceIDForApp != network.AlphaSpaceId { 327 if machineCountInSpace[newDefaultSpaceIDForApp] != len(deployedMachines) { 328 msg := "changing default space to %q is not feasible: one or more deployed machines lack an address in this space" 329 return b.spaceNotFeasibleError(msg, newDefaultSpaceIDForApp) 330 } 331 } 332 333 for epName, spID := range b.bindingsMap { 334 if epName == "" { 335 continue 336 } 337 // TODO(achilleasa): this check is a temporary workaround 338 // to allow upgrading charms that define new endpoints 339 // which we automatically bind to the default space if 340 // the operator does not explicitly try to bind them 341 // to a space. 342 // 343 // If we deploy a charm with a "spaces=xxx" constraint, 344 // it will not have a provider address in the default 345 // space so the machine-count check below would 346 // otherwise fail. 347 if spID == network.AlphaSpaceId { 348 continue 349 } 350 351 // Ensure that all currently deployed machines have an address 352 // in the requested space for this binding 353 if machineCountInSpace[spID] != len(deployedMachines) { 354 msg := fmt.Sprintf("binding endpoint %q to ", epName) 355 return b.spaceNotFeasibleError(msg+"space %q is not feasible: one or more deployed machines lack an address in this space", spID) 356 } 357 } 358 359 return nil 360 } 361 362 func (b *Bindings) spaceNotFeasibleError(msg, id string) error { 363 space, err := b.st.Space(id) 364 if err != nil { 365 logger.Errorf(msg, id) 366 return errors.Annotatef(err, "cannot get space name for id %q", id) 367 } 368 return errors.Errorf(msg, space.Name()) 369 } 370 371 // removeEndpointBindingsOp returns an op removing the bindings for the given 372 // key, without asserting they exist in the first place. 373 func removeEndpointBindingsOp(key string) txn.Op { 374 return txn.Op{ 375 C: endpointBindingsC, 376 Id: key, 377 Remove: true, 378 } 379 } 380 381 // readEndpointBindings returns the stored bindings and TxnRevno for the given 382 // application global key, or an error satisfying errors.IsNotFound() otherwise. 383 func readEndpointBindings(st *State, key string) (map[string]string, int64, error) { 384 doc, err := readEndpointBindingsDoc(st, key) 385 if err != nil { 386 return nil, 0, err 387 } 388 return doc.Bindings, doc.TxnRevno, nil 389 } 390 391 // readEndpointBindingsDoc returns the endpoint bindings document for the 392 // specified key. 393 func readEndpointBindingsDoc(st *State, key string) (*endpointBindingsDoc, error) { 394 endpointBindings, closer := st.db().GetCollection(endpointBindingsC) 395 defer closer() 396 397 var doc endpointBindingsDoc 398 err := endpointBindings.FindId(key).One(&doc) 399 if err == mgo.ErrNotFound { 400 return nil, errors.NotFoundf("endpoint bindings for %q", key) 401 } 402 if err != nil { 403 return nil, errors.Annotatef(err, "cannot get endpoint bindings for %q", key) 404 } 405 406 return &doc, nil 407 } 408 409 // validateForCharm verifies that all endpoint names in bindings 410 // are valid for the given charm metadata, and each endpoint is bound to a known 411 // space - otherwise an error satisfying errors.IsNotValid() will be returned. 412 func (b *Bindings) validateForCharm(charmMeta *charm.Meta) error { 413 if b.bindingsMap == nil { 414 return errors.NotValidf("nil bindings") 415 } 416 if charmMeta == nil { 417 return errors.NotValidf("nil charm metadata") 418 } 419 420 spaceInfos, err := b.st.AllSpaceInfos() 421 if err != nil { 422 return errors.Trace(err) 423 } 424 425 allBindings, err := DefaultEndpointBindingsForCharm(b.st, charmMeta) 426 if err != nil { 427 return errors.Trace(err) 428 } 429 endpointsNamesSet := set.NewStrings() 430 for name := range allBindings { 431 endpointsNamesSet.Add(name) 432 } 433 434 // Ensure there are no unknown endpoints and/or spaces specified. 435 // 436 // TODO(dimitern): This assumes spaces cannot be deleted when they are used 437 // in bindings. In follow-up, this will be enforced by using refcounts on 438 // spaces. 439 for endpoint, space := range b.bindingsMap { 440 if endpoint != defaultEndpointName && !endpointsNamesSet.Contains(endpoint) { 441 return errors.NotValidf("unknown endpoint %q", endpoint) 442 } 443 if !spaceInfos.ContainsID(space) { 444 return errors.NotValidf("unknown space %q", space) 445 } 446 } 447 return nil 448 } 449 450 // DefaultEndpointBindingSpace returns the current space ID to be used for 451 // the default endpoint binding. 452 func (st *State) DefaultEndpointBindingSpace() (string, error) { 453 model, err := st.Model() 454 if err != nil { 455 return "", errors.Trace(err) 456 } 457 458 cfg, err := model.Config() 459 if err != nil { 460 return "", errors.Trace(err) 461 } 462 463 defaultBinding := network.AlphaSpaceId 464 465 space, err := st.SpaceByName(cfg.DefaultSpace()) 466 if err != nil && !errors.IsNotFound(err) { 467 return "", errors.Trace(err) 468 } 469 if err == nil { 470 defaultBinding = space.Id() 471 } 472 473 return defaultBinding, nil 474 } 475 476 // DefaultEndpointBindingsForCharm populates a bindings map containing each 477 // endpoint of the given charm metadata (relation name or extra-binding name) 478 // bound to an empty space. 479 func DefaultEndpointBindingsForCharm(st EndpointBinding, charmMeta *charm.Meta) (map[string]string, error) { 480 defaultBindingSpaceID, err := st.DefaultEndpointBindingSpace() 481 if err != nil { 482 return nil, err 483 } 484 allRelations := charmMeta.CombinedRelations() 485 bindings := make(map[string]string, len(allRelations)+len(charmMeta.ExtraBindings)) 486 for name := range allRelations { 487 bindings[name] = defaultBindingSpaceID 488 } 489 for name := range charmMeta.ExtraBindings { 490 bindings[name] = defaultBindingSpaceID 491 } 492 return bindings, nil 493 } 494 495 // EndpointBinding are the methods necessary for exported methods of 496 // Bindings to work. 497 // 498 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/endpointbinding_mock.go github.com/juju/juju/state EndpointBinding 499 type EndpointBinding interface { 500 network.SpaceLookup 501 DefaultEndpointBindingSpace() (string, error) 502 Space(id string) (*Space, error) 503 } 504 505 // Bindings are EndpointBindings. 506 type Bindings struct { 507 st EndpointBinding 508 app *Application 509 bindingsMap 510 } 511 512 // NewBindings returns a bindings guaranteed to be in space id format. 513 func NewBindings(st EndpointBinding, givenMap map[string]string) (*Bindings, error) { 514 // namesErr and idError are only problems if both are not nil. 515 spaceInfos, err := st.AllSpaceInfos() 516 if err != nil { 517 return nil, errors.Trace(err) 518 } 519 520 // If givenMap contains space names empty values are allowed (e.g. they 521 // may be present when migrating a model from a 2.6.x controller). 522 namesErr := allOfOne(spaceInfos.ContainsName, givenMap, true) 523 524 // If givenMap contains empty values then the map most probably contains 525 // space names. Therefore, we want allOfOne to be strict and bail out 526 // if it sees any empty values. 527 idErr := allOfOne(spaceInfos.ContainsID, givenMap, false) 528 529 // Ensure the spaces values are all names OR ids in the 530 // given map. 531 var newMap map[string]string 532 switch { 533 case namesErr == nil && idErr == nil: 534 // The givenMap is empty or has empty endpoints 535 if len(givenMap) > 0 { 536 newMap = givenMap 537 break 538 } 539 newMap = make(map[string]string, len(givenMap)) 540 541 case namesErr == nil && idErr != nil: 542 newMap, err = newBindingsFromNames(spaceInfos, givenMap) 543 case idErr == nil && namesErr != nil: 544 newMap, err = newBindingsFromIDs(spaceInfos, givenMap) 545 default: 546 logger.Errorf("%s", namesErr) 547 logger.Errorf("%s", idErr) 548 return nil, errors.NotFoundf("space") 549 } 550 551 return &Bindings{st: st, bindingsMap: newMap}, err 552 } 553 554 func allOfOne(foundValue func(string) bool, givenMap map[string]string, allowEmptyValues bool) error { 555 for k, v := range givenMap { 556 if !foundValue(v) && (v != "" || (v == "" && !allowEmptyValues)) { 557 return errors.NotFoundf("endpoint %q, value %q, space name or id", k, v) 558 } 559 } 560 return nil 561 } 562 563 func newBindingsFromNames(spaceInfos network.SpaceInfos, givenMap map[string]string) (map[string]string, error) { 564 newMap := make(map[string]string, len(givenMap)) 565 for epName, name := range givenMap { 566 if name == "" { 567 newMap[epName] = network.AlphaSpaceId 568 continue 569 } 570 // check that the name is valid and get id. 571 info := spaceInfos.GetByName(name) 572 if info == nil { 573 return nil, errors.NotFoundf("programming error: epName %q space name value %q", epName, name) 574 } 575 newMap[epName] = info.ID 576 } 577 return newMap, nil 578 } 579 580 func newBindingsFromIDs(spaceInfos network.SpaceInfos, givenMap map[string]string) (map[string]string, error) { 581 newMap := make(map[string]string, len(givenMap)) 582 for epName, id := range givenMap { 583 if id == "" { 584 // This is most probably a set of bindings to space names. 585 return nil, errors.NotValidf("bindings map with empty space ID") 586 } 587 // check that the id is valid. 588 if !spaceInfos.ContainsID(id) { 589 return nil, errors.NotFoundf("programming error: epName %q space id value %q", epName, id) 590 } 591 592 newMap[epName] = id 593 } 594 return newMap, nil 595 } 596 597 // MapWithSpaceNames returns the current bindingMap with space names rather than ids. 598 func (b *Bindings) MapWithSpaceNames(lookup network.SpaceInfos) (map[string]string, error) { 599 // Handle the fact that space name lookup can be nil or empty. 600 if lookup == nil || (len(b.bindingsMap) > 0 && len(lookup) == 0) { 601 return nil, errors.NotValidf("empty space lookup") 602 } 603 604 retVal := make(map[string]string, len(b.bindingsMap)) 605 606 // Assume that b.bindings is always in space id format due to 607 // Bindings constructor. 608 for k, v := range b.bindingsMap { 609 spaceInfo := lookup.GetByID(v) 610 if spaceInfo == nil { 611 return nil, errors.NotFoundf("space with ID %q", v) 612 } 613 retVal[k] = string(spaceInfo.Name) 614 } 615 return retVal, nil 616 } 617 618 // Map returns the current bindingMap with space ids. 619 func (b *Bindings) Map() map[string]string { 620 return b.bindingsMap 621 }