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  }