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 ¬SatisfiedErr{ 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 }