github.com/avenga/couper@v1.12.2/config/configload/schema.go (about) 1 package configload 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "strings" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/gohcl" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/avenga/couper/config" 15 "github.com/avenga/couper/config/configload/collect" 16 "github.com/avenga/couper/config/meta" 17 "github.com/avenga/couper/internal/seetie" 18 ) 19 20 const ( 21 noLabelForErrorHandler = "No labels are expected for error_handler blocks." 22 summUnsupportedAttr = "Unsupported argument" 23 ) 24 25 var reFetchUnexpectedArg = regexp.MustCompile(`An argument named (.*) is not expected here\.`) 26 27 func ValidateConfigSchema(body hcl.Body, obj interface{}) hcl.Diagnostics { 28 blocks, diags := getSchemaComponents(body, obj) 29 diags = enhanceErrors(diags, obj) 30 31 for _, block := range blocks { 32 diags = diags.Extend(checkObjectFields(block, obj)) 33 } 34 35 return uniqueErrors(diags) 36 } 37 38 // enhanceErrors enhances diagnostics e.g. by providing a hint how to solve the issue 39 func enhanceErrors(diags hcl.Diagnostics, obj interface{}) hcl.Diagnostics { 40 _, isEndpoint := obj.(*config.Endpoint) 41 _, isProxy := obj.(*config.Proxy) 42 for _, err := range diags { 43 if err.Summary == summUnsupportedAttr && (isEndpoint || isProxy) { 44 if matches := reFetchUnexpectedArg.FindStringSubmatch(err.Detail); matches != nil && matches[1] == `"path"` { 45 err.Detail = err.Detail + ` Use the "path" attribute in a backend block instead.` 46 } 47 } 48 } 49 return diags 50 } 51 52 func checkObjectFields(block *hcl.Block, obj interface{}) hcl.Diagnostics { 53 var errors hcl.Diagnostics 54 var checked bool 55 56 typ := reflect.TypeOf(obj) 57 if typ.Kind() == reflect.Ptr { 58 typ = typ.Elem() 59 } 60 61 val := reflect.ValueOf(obj) 62 if val.Kind() == reflect.Ptr { 63 val = val.Elem() 64 } 65 66 for i := 0; i < typ.NumField(); i++ { 67 field := typ.Field(i) 68 69 if field.Anonymous { 70 o := reflect.New(field.Type).Interface() 71 errors = errors.Extend(checkObjectFields(block, o)) 72 73 continue 74 } 75 76 // TODO: How to implement this automatically? 77 if field.Type.String() != "*config.OAuth2ReqAuth" || block.Type != "oauth2" || typ.String() == "config.Backend" { 78 if _, ok := field.Tag.Lookup("hcl"); !ok { 79 continue 80 } 81 if field.Tag.Get("hcl") != block.Type+",block" { 82 continue 83 } 84 } 85 86 checked = true 87 88 if field.Type.Kind() == reflect.Ptr { 89 o := reflect.New(field.Type.Elem()).Interface() 90 errors = errors.Extend(ValidateConfigSchema(block.Body, o)) 91 92 continue 93 } else if field.Type.Kind() == reflect.Slice { 94 tp := reflect.TypeOf(val.Field(i).Interface()) 95 if tp.Kind() == reflect.Slice { 96 tp = tp.Elem() 97 } 98 99 vl := reflect.ValueOf(tp) 100 if vl.Kind() == reflect.Ptr { 101 vl = vl.Elem() 102 } 103 104 if vl.Kind() == reflect.Struct { 105 var elem reflect.Type 106 107 if tp.Kind() == reflect.Struct { 108 elem = tp 109 } else if tp.Kind() == reflect.Ptr { 110 elem = tp.Elem() 111 } else { 112 errors = errors.Append(&hcl.Diagnostic{ 113 Severity: hcl.DiagError, 114 Summary: "Unsupported type.Kind '" + tp.Kind().String() + "' for: " + field.Name, 115 }) 116 117 continue 118 } 119 120 o := reflect.New(elem).Interface() 121 errors = errors.Extend(ValidateConfigSchema(block.Body, o)) 122 123 continue 124 } 125 } 126 127 errors = errors.Append(&hcl.Diagnostic{ 128 Severity: hcl.DiagError, 129 Summary: "A block without config test found: " + field.Name, 130 }) 131 } 132 133 if !checked { 134 if i, ok := obj.(config.Inline); ok { 135 errors = errors.Extend(checkObjectFields(block, i.Inline())) 136 } 137 } 138 139 return errors 140 } 141 142 func getSchemaComponents(body hcl.Body, obj interface{}) (hcl.Blocks, hcl.Diagnostics) { 143 var ( 144 blocks hcl.Blocks 145 errors hcl.Diagnostics 146 ) 147 148 schema, _ := gohcl.ImpliedBodySchema(obj) 149 150 typ := reflect.TypeOf(obj) 151 if typ.Kind() == reflect.Ptr { 152 typ = typ.Elem() 153 } 154 155 // TODO: How to implement this automatically? 156 if typ.String() == "config.Backend" { 157 meta.MergeSchemas(schema, config.OAuthBlockSchema, config.TokenRequestBlockSchema) 158 } 159 160 if _, ok := obj.(collect.ErrorHandlerSetter); ok { 161 schema = config.WithErrorHandlerSchema(schema) 162 } 163 164 if i, ok := obj.(config.Inline); ok { 165 inlineSchema := i.Schema(true) 166 schema.Attributes = append(schema.Attributes, inlineSchema.Attributes...) 167 schema.Blocks = append(schema.Blocks, inlineSchema.Blocks...) 168 } 169 170 blocks, errors = completeSchemaComponents(body, schema, blocks, errors) 171 172 return blocks, errors 173 } 174 175 func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, 176 blocks hcl.Blocks, errors hcl.Diagnostics) (hcl.Blocks, hcl.Diagnostics) { 177 178 content, diags := body.Content(schema) 179 180 errorHandlerCompleted := false 181 182 for _, diag := range diags { 183 // TODO: How to implement this block automatically? 184 if diag.Detail == noLabelForErrorHandler { 185 if errorHandlerCompleted { 186 continue 187 } 188 189 bodyContent := bodyToContent(body.(*hclsyntax.Body)) 190 191 for _, block := range bodyContent.Blocks { 192 if block.Type == errorHandler && len(block.Labels) > 0 { 193 blocks = append(blocks, block) 194 } 195 } 196 197 errorHandlerCompleted = true 198 } else { 199 errors = errors.Append(diag) 200 } 201 } 202 203 if content != nil { 204 for name, attr := range content.Attributes { 205 if expr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr); ok { 206 207 value, _ := attr.Expr.Value(nil) 208 if value.CanIterateElements() { 209 unique := make(map[string]struct{}) 210 211 iter := value.ElementIterator() 212 213 for { 214 if !iter.Next() { 215 break 216 } 217 218 k, _ := iter.Element() 219 if k.Type() != cty.String { 220 continue 221 } 222 223 keyName := seetie.ValueToString(k) 224 switch name { 225 case "add_request_headers", "add_response_headers", "required_permission", "headers", "set_request_headers", "set_response_headers": 226 // header field names, method names: handle object keys case-insensitively 227 keyName = strings.ToLower(keyName) 228 } 229 if _, ok := unique[keyName]; ok { 230 errors = errors.Append(&hcl.Diagnostic{ 231 Subject: &expr.SrcRange, 232 Severity: hcl.DiagError, 233 Summary: fmt.Sprintf("key in an attribute must be unique: '%s'", keyName), 234 Detail: "Key must be unique for " + keyName + ".", 235 }) 236 } 237 238 unique[keyName] = struct{}{} 239 } 240 } 241 } 242 } 243 244 blocks = append(blocks, content.Blocks...) 245 } 246 247 return blocks, errors 248 } 249 250 func uniqueErrors(errors hcl.Diagnostics) hcl.Diagnostics { 251 var unique hcl.Diagnostics 252 253 for _, diag := range errors { 254 var contains bool 255 256 for _, is := range unique { 257 if reflect.DeepEqual(diag, is) { 258 contains = true 259 break 260 } 261 } 262 263 if !contains { 264 unique = unique.Append(diag) 265 } 266 } 267 268 return unique 269 } 270 271 func bodyToContent(b *hclsyntax.Body) *hcl.BodyContent { 272 content := &hcl.BodyContent{ 273 MissingItemRange: *getRange(b), 274 } 275 276 if len(b.Attributes) > 0 { 277 content.Attributes = make(hcl.Attributes) 278 } 279 for name, attr := range b.Attributes { 280 content.Attributes[name] = &hcl.Attribute{ 281 Name: attr.Name, 282 Expr: attr.Expr, 283 Range: attr.Range(), 284 NameRange: attr.NameRange, 285 } 286 } 287 288 for _, block := range b.Blocks { 289 content.Blocks = append(content.Blocks, &hcl.Block{ 290 Body: block.Body, 291 DefRange: block.DefRange(), 292 LabelRanges: block.LabelRanges, 293 Labels: block.Labels, 294 Type: block.Type, 295 TypeRange: block.TypeRange, 296 }) 297 } 298 299 return content 300 } 301 302 func getRange(body *hclsyntax.Body) *hcl.Range { 303 if body == nil { 304 return &hcl.Range{} 305 } 306 307 return &body.SrcRange 308 }