github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/core/assumes/sat_checker.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package assumes
     5  
     6  import (
     7  	chassumes "github.com/juju/charm/v12/assumes"
     8  	"github.com/juju/errors"
     9  )
    10  
    11  // A link to a web page with additional information about features,
    12  // the Juju versions that support them etc.
    13  const featureDocsURL = "https://juju.is/docs/olm/supported-features"
    14  
    15  // satisfyExpr checks whether the feature set contents satisfy the provided
    16  // "assumes" expression. The function can process either feature or composite
    17  // expressions.
    18  func satisfyExpr(fs FeatureSet, expr chassumes.Expression, exprTreeDepth int) error {
    19  	switch expr := expr.(type) {
    20  	case chassumes.FeatureExpression:
    21  		return satisfyFeatureExpr(fs, expr)
    22  	case chassumes.CompositeExpression:
    23  		return satisfyCompositeExpr(fs, expr, exprTreeDepth)
    24  	default:
    25  		return errors.NotSupportedf("assumes expression type %q", expr.Type())
    26  	}
    27  }
    28  
    29  // satisfyExpr checks whether the feature set contents satisfy the provided
    30  // "assumes" feature expression.
    31  //
    32  // The expression is matched if the feature set contains the required feature
    33  // name and any of the following conditions is true:
    34  // a) The feature set entry OR the assumes expression does not specify a version.
    35  //  2. Both the feature set entry AND the assumes expression specify versions
    36  //     AND the required version constraint (>= or <) is satisfied.
    37  func satisfyFeatureExpr(fs FeatureSet, expr chassumes.FeatureExpression) error {
    38  	supported, defined := fs.Get(expr.Name)
    39  	if !defined {
    40  		featDescr := UserFriendlyFeatureDescriptions[expr.Name]
    41  		return featureError(
    42  			expr.Name, featDescr,
    43  			"charm requires feature %q but model does not support it", expr.Name,
    44  		)
    45  	}
    46  
    47  	// If the "assumes" feature expression does not specify a version or the
    48  	// provided feature omits its version, then the expression is always
    49  	// satisfied.
    50  	if expr.Version == nil || supported.Version == nil {
    51  		return nil
    52  	}
    53  
    54  	// Compare versions
    55  	var satisfied bool
    56  	switch expr.Constraint {
    57  	case chassumes.VersionGTE:
    58  		satisfied = supported.Version.Compare(*expr.Version) >= 0
    59  	case chassumes.VersionLT:
    60  		satisfied = supported.Version.Compare(*expr.Version) < 0
    61  	}
    62  
    63  	if satisfied {
    64  		return nil
    65  	}
    66  
    67  	var featDescr = supported.Description
    68  	if featDescr == "" {
    69  		// The feature set should always have a feature description.
    70  		// Try the fallback descriptions if it is missing.
    71  		featDescr = UserFriendlyFeatureDescriptions[featDescr]
    72  	}
    73  	return featureError(
    74  		expr.Name, featDescr,
    75  		"charm requires feature %q (version %s %s) but model currently supports version %s",
    76  		expr.Name, expr.Constraint, expr.Version, supported.Version,
    77  	)
    78  }
    79  
    80  // satisfyCompositeExpr checks whether the feature set contents satisfy the
    81  // provided "assumes" composite expression.
    82  //
    83  // For an any-of kind of expression, the sub-expression evaluation will be
    84  // short-circuited when the first satisfied sub-expression is found. For all-of
    85  // kind of expressions, all sub-expressions must be matched.
    86  //
    87  // If the expression cannot be satisfied, the function returns a multi-error
    88  // value listing any detected conflicts.
    89  func satisfyCompositeExpr(fs FeatureSet, expr chassumes.CompositeExpression, exprTreeDepth int) error {
    90  	var errList = make([]error, 0, len(expr.SubExpressions))
    91  	for _, subExpr := range expr.SubExpressions {
    92  		err := satisfyExpr(fs, subExpr, exprTreeDepth+1)
    93  		if err == nil && expr.ExprType == chassumes.AnyOfExpression {
    94  			// We can short-circuit the check if this is an any-of
    95  			// expression and we found a matching subexpression.
    96  			return nil
    97  		} else if err != nil {
    98  			errList = append(errList, err)
    99  		}
   100  	}
   101  
   102  	if len(errList) == 0 {
   103  		return nil
   104  	}
   105  
   106  	// The root of the expression tree is always an implicit all-of
   107  	// expression. To improve UX, we should avoid using the switch statement
   108  	// below which introduces yet another indentation level and instead
   109  	// emit a top-level descriptive message.
   110  	if exprTreeDepth == 0 {
   111  		return requirementsNotSatisfied("Charm feature requirements cannot be met:", errList)
   112  	}
   113  
   114  	switch expr.Type() {
   115  	case chassumes.AllOfExpression:
   116  		return notSatisfiedError("charm requires all of the following:", errList...)
   117  	case chassumes.AnyOfExpression:
   118  		return notSatisfiedError("charm requires at least one of the following:", errList...)
   119  	default:
   120  		return notSatisfiedError("charm requires "+string(expr.Type())+" of the following:", errList...)
   121  	}
   122  }