github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/machine_ports_ops.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "github.com/juju/collections/set" 8 "github.com/juju/errors" 9 "github.com/juju/mgo/v3/bson" 10 "github.com/juju/mgo/v3/txn" 11 "github.com/juju/names/v5" 12 jujutxn "github.com/juju/txn/v3" 13 14 "github.com/juju/juju/core/network" 15 ) 16 17 var _ ModelOperation = (*openClosePortRangesOperation)(nil) 18 19 type openClosePortRangesOperation struct { 20 mpr *machinePortRanges 21 22 // unitSelector allows us to specify a unit name and limit the scope 23 // of changes to that particular unit only. 24 unitSelector string 25 26 // The following fields are populated when the operation steps are being 27 // assembled. 28 openedPortRangeToUnit map[network.PortRange]string 29 endpointsNamesByApp map[string]set.Strings 30 updatedUnitPortRanges map[string]network.GroupedPortRanges 31 } 32 33 // Build implements ModelOperation. 34 func (op *openClosePortRangesOperation) Build(attempt int) ([]txn.Op, error) { 35 if err := checkModelNotDead(op.mpr.st); err != nil { 36 return nil, errors.Annotate(err, "cannot open/close ports") 37 } 38 39 var createDoc = !op.mpr.docExists 40 if attempt > 0 { 41 if err := op.mpr.Refresh(); err != nil { 42 if !errors.IsNotFound(err) { 43 return nil, errors.Annotate(err, "cannot open/close ports") 44 } 45 46 // Doc not found; we need to create it. 47 createDoc = true 48 } 49 } 50 51 ops := []txn.Op{ 52 assertModelNotDeadOp(op.mpr.st.ModelUUID()), 53 assertMachineNotDeadOp(op.mpr.st, op.mpr.doc.MachineID), 54 } 55 56 // Start with a clean copy of the existing opened port ranges and set 57 // up an auxiliary Portrange->unitName map for detecting port conflicts 58 // in a more efficient manner. 59 op.cloneExistingUnitPortRanges() 60 op.buildPortRangeToUnitMap() 61 62 // Find the endpoints for the applications with existing opened ports 63 // and the applications with pending open port requests. 64 if err := op.lookupUnitEndpoints(); err != nil { 65 return nil, errors.Annotate(err, "cannot open/close ports") 66 } 67 68 // Ensure that the pending request list does not contain any bogus endpoints. 69 if err := op.validatePendingChanges(); err != nil { 70 return nil, errors.Annotate(err, "cannot open/close ports") 71 } 72 73 // Append docs for opening each one of the pending port ranges. 74 portListModified, err := op.mergePendingOpenPortRanges() 75 if err != nil { 76 return nil, errors.Trace(err) 77 } 78 79 // Scan the port ranges for each unit and prune endpoint-specific 80 // entries for which we already have a rule in the wildcard endpoint 81 // section. 82 portListModified = op.pruneOpenPorts() || portListModified 83 84 // Remove entries that match the pending close port requests. 85 modified, err := op.mergePendingClosePortRanges() 86 if err != nil { 87 return nil, errors.Trace(err) 88 } 89 portListModified = portListModified || modified 90 91 // Run a final prune pass and remove empty sections 92 portListModified = op.pruneEmptySections() || portListModified 93 94 // Bail out if we don't need to mutate the DB document. 95 if !portListModified || (createDoc && len(op.updatedUnitPortRanges) == 0) { 96 return nil, jujutxn.ErrNoOperations 97 } 98 99 // Ensure that none of the units with open port ranges are dead and 100 // that all are assigned to this machine. 101 for unitName := range op.updatedUnitPortRanges { 102 ops = append(ops, 103 assertUnitNotDeadOp(op.mpr.st, unitName), 104 assertUnitAssignedToMachineOp(op.mpr.st, unitName, op.mpr.doc.MachineID), 105 ) 106 } 107 108 if createDoc { 109 assert := txn.DocMissing 110 ops = append(ops, insertPortsDocOps(op.mpr.st, &op.mpr.doc, assert, op.updatedUnitPortRanges)...) 111 } else if len(op.updatedUnitPortRanges) == 0 { 112 // Port list is empty; get rid of ports document. 113 ops = append(ops, op.mpr.removeOps()...) 114 } else { 115 assert := bson.D{{"txn-revno", op.mpr.doc.TxnRevno}} 116 ops = append(ops, updatePortsDocOps(op.mpr.st, &op.mpr.doc, assert, op.updatedUnitPortRanges)...) 117 } 118 119 return ops, nil 120 } 121 122 // Done implements ModelOperation. 123 func (op *openClosePortRangesOperation) Done(err error) error { 124 if err != nil { 125 return err 126 } 127 128 // Document has been persisted to state. 129 op.mpr.docExists = true 130 op.mpr.doc.UnitRanges = op.updatedUnitPortRanges 131 132 // If we applied all pending changes, clean up the pending maps 133 if op.unitSelector == "" { 134 op.mpr.pendingOpenRanges = nil 135 op.mpr.pendingCloseRanges = nil 136 } else { 137 // Just remove the map entries for the selected unit. 138 if op.mpr.pendingOpenRanges != nil { 139 delete(op.mpr.pendingOpenRanges, op.unitSelector) 140 } 141 if op.mpr.pendingCloseRanges != nil { 142 delete(op.mpr.pendingCloseRanges, op.unitSelector) 143 } 144 } 145 return nil 146 } 147 148 func (op *openClosePortRangesOperation) cloneExistingUnitPortRanges() { 149 op.updatedUnitPortRanges = make(map[string]network.GroupedPortRanges) 150 for unitName, existingDoc := range op.mpr.doc.UnitRanges { 151 newDoc := make(network.GroupedPortRanges) 152 for endpointName, portRanges := range existingDoc { 153 newDoc[endpointName] = append([]network.PortRange(nil), portRanges...) 154 } 155 op.updatedUnitPortRanges[unitName] = newDoc 156 } 157 } 158 159 func (op *openClosePortRangesOperation) buildPortRangeToUnitMap() { 160 op.openedPortRangeToUnit = make(map[network.PortRange]string) 161 for existingUnitName, existingRangeDoc := range op.updatedUnitPortRanges { 162 for _, existingRanges := range existingRangeDoc { 163 for _, portRange := range existingRanges { 164 op.openedPortRangeToUnit[portRange] = existingUnitName 165 } 166 } 167 } 168 } 169 170 // lookupUnitEndpoints loads the bound endpoints for any applications already 171 // deployed to the target machine as well as any additional applications that 172 // have pending open port requests. 173 func (op *openClosePortRangesOperation) lookupUnitEndpoints() error { 174 // Find the unique set of applications with opened port ranges on the machine. 175 appsWithOpenedPorts := set.NewStrings() 176 for unitName := range op.mpr.doc.UnitRanges { 177 appName, err := names.UnitApplication(unitName) 178 if err != nil { 179 return errors.Trace(err) 180 } 181 appsWithOpenedPorts.Add(appName) 182 } 183 184 // Augment list with the applications in the pending open port list. 185 for unitName := range op.mpr.pendingOpenRanges { 186 appName, err := names.UnitApplication(unitName) 187 if err != nil { 188 return errors.Trace(err) 189 } 190 appsWithOpenedPorts.Add(appName) 191 } 192 193 // Lookup the endpoint bindings for each application 194 op.endpointsNamesByApp = make(map[string]set.Strings) 195 for appName := range appsWithOpenedPorts { 196 appGlobalID := applicationGlobalKey(appName) 197 endpointToSpaceIDMap, _, err := readEndpointBindings(op.mpr.st, appGlobalID) 198 if err != nil { 199 return errors.Trace(err) 200 } 201 202 appEndpoints := set.NewStrings() 203 for endpointName := range endpointToSpaceIDMap { 204 if endpointName == "" { 205 continue 206 } 207 appEndpoints.Add(endpointName) 208 } 209 op.endpointsNamesByApp[appName] = appEndpoints 210 } 211 212 return nil 213 } 214 215 // validatePendingChanges ensures that the none of the pending open/close 216 // entries specifies an endpoint that is not defined by the unit's charm 217 // metadata. 218 func (op *openClosePortRangesOperation) validatePendingChanges() error { 219 for unitName, pendingRangesByEndpoint := range op.mpr.pendingOpenRanges { 220 // Already verified; ignore error 221 appName, _ := names.UnitApplication(unitName) 222 for pendingEndpointName := range pendingRangesByEndpoint { 223 if pendingEndpointName != "" && !op.endpointsNamesByApp[appName].Contains(pendingEndpointName) { 224 return errors.NotFoundf("open port range: endpoint %q for unit %q", pendingEndpointName, unitName) 225 } 226 } 227 } 228 for unitName, pendingRangesByEndpoint := range op.mpr.pendingCloseRanges { 229 // Already verified; ignore error 230 appName, _ := names.UnitApplication(unitName) 231 for pendingEndpointName := range pendingRangesByEndpoint { 232 if pendingEndpointName != "" && !op.endpointsNamesByApp[appName].Contains(pendingEndpointName) { 233 return errors.NotFoundf("close port range: endpoint %q for unit %q", pendingEndpointName, unitName) 234 } 235 } 236 } 237 238 return nil 239 } 240 241 // mergePendingOpenPortRanges compares the set of new port ranges to open to 242 // the set of currently opened port ranges and appends a new entry for each 243 // port range that is not present in the current list and does not conflict 244 // with any pre-existing entries. The method returns a boolean value to 245 // indicate whether new documents were generated. 246 func (op *openClosePortRangesOperation) mergePendingOpenPortRanges() (bool, error) { 247 var portListModified bool 248 for pendingUnitName, pendingRangesByEndpoint := range op.mpr.pendingOpenRanges { 249 // If we are only interested in the changes for a particular 250 // unit only, exclude any pending changes for other units. 251 if op.unitSelector != "" && op.unitSelector != pendingUnitName { 252 continue 253 } 254 for pendingEndpointName, pendingRanges := range pendingRangesByEndpoint { 255 for _, pendingRange := range pendingRanges { 256 // If this port range has already been opened by the same unit this is a no-op 257 // when the range is opened for all endpoints. Otherwise, we still need to add 258 // an entry for the appropriate endpoint. 259 if op.openedPortRangeToUnit[pendingRange] == pendingUnitName { 260 if op.rangeExistsForEndpoint(pendingUnitName, "", pendingRange) || op.rangeExistsForEndpoint(pendingUnitName, pendingEndpointName, pendingRange) { 261 continue 262 } 263 264 // Still need to add an entry for the specified endpoint. 265 } else if err := op.checkForPortRangeConflict(pendingUnitName, pendingRange); err != nil { 266 return false, errors.Annotatef(err, "cannot open ports %v", pendingRange) 267 } 268 269 // We can safely add the new port range to the updated port list. 270 if op.updatedUnitPortRanges[pendingUnitName] == nil { 271 op.updatedUnitPortRanges[pendingUnitName] = make(network.GroupedPortRanges) 272 } 273 op.updatedUnitPortRanges[pendingUnitName][pendingEndpointName] = append( 274 op.updatedUnitPortRanges[pendingUnitName][pendingEndpointName], 275 pendingRange, 276 ) 277 op.openedPortRangeToUnit[pendingRange] = pendingUnitName 278 portListModified = true 279 } 280 } 281 } 282 283 return portListModified, nil 284 } 285 286 // pruneOpenPorts examines the open ports for each unit and removes any 287 // endpoint-specific ranges that are also present in the wildcard (all 288 // endpoints) section for the unit. The method returns a boolean value to 289 // indicate whether any ranges where pruned. 290 func (op *openClosePortRangesOperation) pruneOpenPorts() bool { 291 var portListModified bool 292 for unitName, unitRangeDoc := range op.updatedUnitPortRanges { 293 for endpointName, portRanges := range unitRangeDoc { 294 if endpointName == "" { 295 continue 296 } 297 298 for i := 0; i < len(portRanges); i++ { 299 for _, wildcardPortRange := range unitRangeDoc[""] { 300 if portRanges[i] != wildcardPortRange { 301 continue 302 } 303 304 // This port is redundant as it already 305 // exists in the wildcard section. 306 // Remove it from the port range list. 307 portRanges[i] = portRanges[len(portRanges)-1] 308 portRanges = portRanges[:len(portRanges)-1] 309 portListModified = true 310 i-- 311 break 312 } 313 } 314 unitRangeDoc[endpointName] = portRanges 315 } 316 op.updatedUnitPortRanges[unitName] = unitRangeDoc 317 } 318 return portListModified 319 } 320 321 // mergePendingClosePortRanges compares the set of port ranges to close to the 322 // set of currently opened port range documents and removes the entries that 323 // correspond to the port ranges that should be closed. 324 // 325 // The implementation contains additional logic to detect cases where a port 326 // range is currently opened for all endpoints and we attempt to close it 327 // for a specific endpoint. In this case, the port range will be removed from 328 // the wildcard slot of the unitPortRanges document and new entries will be 329 // added for all bound endpoints except the one where the port range is closed. 330 // 331 // The method returns a boolean value to indicate whether any changes were made. 332 func (op *openClosePortRangesOperation) mergePendingClosePortRanges() (bool, error) { 333 var portListModified bool 334 for pendingUnitName, pendingRangesByEndpoint := range op.mpr.pendingCloseRanges { 335 // If we are only interested in the changes for a particular 336 // unit only, exclude any pending changes for other units. 337 if op.unitSelector != "" && op.unitSelector != pendingUnitName { 338 continue 339 } 340 for pendingEndpointName, pendingRanges := range pendingRangesByEndpoint { 341 for _, pendingRange := range pendingRanges { 342 // If the port range has not been opened by 343 // this unit we only need to ensure that it 344 // doesn't cause a conflict with port ranges 345 // opened by other units. 346 if op.openedPortRangeToUnit[pendingRange] != pendingUnitName { 347 if err := op.checkForPortRangeConflict(pendingUnitName, pendingRange); err != nil { 348 return false, errors.Annotatef(err, "cannot close ports %v", pendingRange) 349 } 350 351 // This port range is not open so this is a no-op. 352 continue 353 } 354 355 portListModified = op.removePortRange(pendingUnitName, pendingEndpointName, pendingRange) || portListModified 356 } 357 } 358 } 359 360 return portListModified, nil 361 } 362 363 func (op *openClosePortRangesOperation) removePortRange(unitName, endpointName string, portRange network.PortRange) bool { 364 var portListModified bool 365 366 // Sanity check 367 if len(op.updatedUnitPortRanges[unitName]) == 0 { 368 return false 369 } 370 371 // If we target all endpoints, remove the range from the wildcard entry 372 // as well as any other endpoint-specific entries (if present) 373 if endpointName == "" { 374 delete(op.openedPortRangeToUnit, portRange) 375 for existingEndpointName, existingRanges := range op.updatedUnitPortRanges[unitName] { 376 for i := 0; i < len(existingRanges); i++ { 377 if existingRanges[i] != portRange { 378 continue 379 } 380 381 // Remove entry from list 382 existingRanges[i] = existingRanges[len(existingRanges)-1] 383 op.updatedUnitPortRanges[unitName][existingEndpointName] = existingRanges[:len(existingRanges)-1] 384 portListModified = true 385 break 386 } 387 } 388 389 return portListModified 390 } 391 392 // If we target a specific endpoint, start by removing the port from 393 // the specified endpoint (if the range is present). 394 if existingRanges := op.updatedUnitPortRanges[unitName][endpointName]; len(existingRanges) != 0 { 395 for i := 0; i < len(existingRanges); i++ { 396 if existingRanges[i] != portRange { 397 continue 398 } 399 400 // Remove entry from list 401 existingRanges[i] = existingRanges[len(existingRanges)-1] 402 op.updatedUnitPortRanges[unitName][endpointName] = existingRanges[:len(existingRanges)-1] 403 portListModified = true 404 break 405 } 406 } 407 408 // If the port range is instead present in the wildcard slot, we 409 // need to remove it and replace it with entries for each bound endpoint 410 // except the one we just closed the port to. 411 if existingRanges := op.updatedUnitPortRanges[unitName][""]; len(existingRanges) != 0 { 412 for i := 0; i < len(existingRanges); i++ { 413 if existingRanges[i] != portRange { 414 continue 415 } 416 417 // Remove entry from list 418 existingRanges[i] = existingRanges[len(existingRanges)-1] 419 op.updatedUnitPortRanges[unitName][""] = existingRanges[:len(existingRanges)-1] 420 portListModified = true 421 422 // This has already been checked during endpoint lookup. 423 // The error can be safely ignored here. 424 appName, _ := names.UnitApplication(unitName) 425 426 // Iterate the set of application endpoints 427 for appEndpoint := range op.endpointsNamesByApp[appName] { 428 if appEndpoint == endpointName { 429 continue // the port is closed for this endpoint 430 } 431 432 // The port should remain open for the remaining endpoints. 433 op.updatedUnitPortRanges[unitName][appEndpoint] = append( 434 op.updatedUnitPortRanges[unitName][appEndpoint], 435 portRange, 436 ) 437 } 438 439 break 440 } 441 } 442 443 // Finally, check if the port range is still open for any other endpoint. 444 // If not, remove it from the openedPortRangeToUnit map. 445 for endpointName := range op.updatedUnitPortRanges[unitName] { 446 if op.rangeExistsForEndpoint(unitName, endpointName, portRange) { 447 return portListModified 448 } 449 } 450 451 delete(op.openedPortRangeToUnit, portRange) 452 return portListModified 453 } 454 455 func (op *openClosePortRangesOperation) rangeExistsForEndpoint(unitName, endpointName string, portRange network.PortRange) bool { 456 if len(op.updatedUnitPortRanges[unitName]) == 0 || len(op.updatedUnitPortRanges[unitName][endpointName]) == 0 { 457 return false 458 } 459 460 for _, existingPortRange := range op.updatedUnitPortRanges[unitName][endpointName] { 461 if existingPortRange == portRange { 462 return true 463 } 464 } 465 466 return false 467 } 468 469 // pruneEmptySections removes empty port range sections from the updated unit 470 // port range documents and removes the docs themselves if they end up empty. 471 // The method returns a boolean value to indicate whether any changes where 472 // made. 473 func (op *openClosePortRangesOperation) pruneEmptySections() bool { 474 var portListModified bool 475 for unitName, unitRangeDoc := range op.updatedUnitPortRanges { 476 for endpointName, portRanges := range unitRangeDoc { 477 if len(portRanges) == 0 { 478 delete(unitRangeDoc, endpointName) 479 portListModified = true 480 } 481 } 482 if len(unitRangeDoc) == 0 { 483 delete(op.updatedUnitPortRanges, unitName) 484 portListModified = true 485 continue 486 } 487 op.updatedUnitPortRanges[unitName] = unitRangeDoc 488 } 489 return portListModified 490 } 491 492 // checkForPortRangeConflict returns an error if a pending port range conflicts 493 // with any already opeend port range. 494 func (op *openClosePortRangesOperation) checkForPortRangeConflict(pendingUnitName string, pendingRange network.PortRange) error { 495 if err := pendingRange.Validate(); err != nil { 496 return errors.Trace(err) 497 } 498 499 for existingRange, existingUnitName := range op.openedPortRangeToUnit { 500 if pendingRange.ConflictsWith(existingRange) { 501 return errors.Errorf("port ranges %v (%q) and %v (%q) conflict", existingRange, existingUnitName, pendingRange, pendingUnitName) 502 } 503 } 504 505 return nil 506 }