github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/core/assumes/error.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  	"bytes"
     8  	"fmt"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/juju/collections/set"
    13  	"github.com/juju/errors"
    14  )
    15  
    16  var (
    17  	indentRegex = regexp.MustCompile("(?m)^")
    18  )
    19  
    20  // RequirementsNotSatisfiedError indicates that the set of features supported
    21  // by a model cannot satisfy an "assumes" expression tree as specified in the
    22  // charm metadata.
    23  type RequirementsNotSatisfiedError struct {
    24  	message string
    25  }
    26  
    27  // IsRequirementsNotSatisfiedError returns true if err is a
    28  // RequirementsNotSatisfiedError.
    29  func IsRequirementsNotSatisfiedError(err error) bool {
    30  	_, is := errors.Cause(err).(*RequirementsNotSatisfiedError)
    31  	return is
    32  }
    33  
    34  // requirementsNotSatisfied constructs a RequirementsNotSatisfiedError error
    35  // by combining the specified error message and a pretty-printed list of error
    36  // messages that correspond to conflicts between the supported features and
    37  // the required set of features from an "assumes" expression.
    38  //
    39  // The constructor recursively visits the error list items and collects the
    40  // unique set of feature names and descriptions referenced by each error. A
    41  // sorted list of feature name/description pairs will be appended at the end
    42  // of the error to make it more user-friendly.
    43  func requirementsNotSatisfied(message string, errList []error) *RequirementsNotSatisfiedError {
    44  	var buf bytes.Buffer
    45  	buf.WriteString(
    46  		notSatisfiedError(message, errList...).Error(),
    47  	)
    48  
    49  	notSatFeatureDescrs := make(map[string]string)
    50  	for _, nestedErr := range errList {
    51  		for featName, featDescr := range notSatisfiedFeatureSet(nestedErr) {
    52  			notSatFeatureDescrs[featName] = featDescr
    53  		}
    54  	}
    55  
    56  	// Create a footer section where we list description for the features
    57  	// names that were not matched.
    58  	var featNames = set.NewStrings()
    59  	for featName, featDescr := range notSatFeatureDescrs {
    60  		if featDescr != "" {
    61  			featNames.Add(featName)
    62  		}
    63  	}
    64  
    65  	if len(featNames) != 0 {
    66  		buf.WriteString("\nFeature descriptions:\n")
    67  		for _, featName := range featNames.SortedValues() {
    68  			buf.WriteString(fmt.Sprintf("  - %q: %s\n", featName, notSatFeatureDescrs[featName]))
    69  		}
    70  	}
    71  
    72  	buf.WriteString("\nFor additional information please see: " + featureDocsURL)
    73  
    74  	return &RequirementsNotSatisfiedError{
    75  		message: buf.String(),
    76  	}
    77  }
    78  
    79  // notSatisfiedFeatureSet recursive scans an error value (which should be either
    80  // a notSatisfiedErr or featureErr value) and returns a set with the feature
    81  // names that could not be satisfied and their user-friendly descriptions.
    82  func notSatisfiedFeatureSet(err error) map[string]string {
    83  	set := make(map[string]string)
    84  	switch t := err.(type) {
    85  	case *notSatisfiedErr:
    86  		for _, nestedErr := range t.errList {
    87  			for featName, featDescr := range notSatisfiedFeatureSet(nestedErr) {
    88  				set[featName] = featDescr
    89  			}
    90  		}
    91  	case *featureErr:
    92  		set[t.featureName] = t.featureDescr
    93  	}
    94  
    95  	return set
    96  }
    97  
    98  // Error returns the error message associated with this error.
    99  func (err *RequirementsNotSatisfiedError) Error() string {
   100  	return err.message
   101  }
   102  
   103  // notSatisfiedErr is a type of multi-error that pretty-prints the nested
   104  // error list when its Error() method is invoked.
   105  type notSatisfiedErr struct {
   106  	message string
   107  	errList []error
   108  }
   109  
   110  func notSatisfiedError(message string, errors ...error) *notSatisfiedErr {
   111  	return &notSatisfiedErr{
   112  		message: message,
   113  		errList: errors,
   114  	}
   115  }
   116  
   117  func (err *notSatisfiedErr) Error() string {
   118  	if len(err.errList) == 0 {
   119  		return err.message
   120  	}
   121  
   122  	var buf bytes.Buffer
   123  	buf.WriteString(err.message)
   124  	buf.WriteRune('\n')
   125  
   126  	for _, nestedErr := range err.errList {
   127  		// Stringify and indent each error
   128  		indentedErr := indentRegex.ReplaceAllString(
   129  			"- "+strings.Trim(
   130  				nestedErr.Error(),
   131  				"\n",
   132  			),
   133  			"  ",
   134  		)
   135  		buf.WriteString(indentedErr)
   136  		buf.WriteRune('\n')
   137  	}
   138  
   139  	return buf.String()
   140  }
   141  
   142  // featureErr indicates a feature-level conflict (i.e. feature not present or
   143  // feature version mismatch) that prevents an assumes feature expression from
   144  // being satisfied.
   145  type featureErr struct {
   146  	message      string
   147  	featureName  string
   148  	featureDescr string
   149  }
   150  
   151  func featureError(featureName, featureDescr, format string, args ...interface{}) *featureErr {
   152  	return &featureErr{
   153  		message:      fmt.Sprintf(format, args...),
   154  		featureName:  featureName,
   155  		featureDescr: featureDescr,
   156  	}
   157  }
   158  
   159  func (err *featureErr) Error() string {
   160  	return err.message
   161  }