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  }