Repository: avenga/couper
Pull request #567 (2022-08-29 15:07:12)

No blocks in backend refinement
* document backend refinement
* throw error for more blocks used in backend refinement

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/config/configload/backend.go b/config/configload/backend.go
index 9295e11d..760c6978 100644
--- a/config/configload/backend.go
+++ b/config/configload/backend.go
@@ -83,6 +83,8 @@ func PrepareBackend(helper *helper, attrName, attrValue string, block config.Inl
 			backendBody = hclbody.MergeBodies(
 				hclbody.New(hclbody.NewContentWithAttrName("name", reference)),
 				backendBody)
+			// no child blocks are allowed, so no need to try to wrap with oauth2 or token request
+			return backendBody, nil
 		}
 	} else { // anonymous backend block
 		var labelRange *hcl.Range
diff --git a/config/configload/validate.go b/config/configload/validate.go
index 0ad87f0f..443de1b5 100644
--- a/config/configload/validate.go
+++ b/config/configload/validate.go
@@ -248,11 +248,19 @@ func verifyResponseBodyAttrs(b hcl.Body) error {
 }
 
 var invalidAttributes = []string{"disable_certificate_validation", "disable_connection_reuse", "http2", "max_connections"}
-var openApiBlockSchema = &hcl.BodySchema{
+var forbiddenInRefinedBackendBlockSchema = &hcl.BodySchema{
 	Blocks: []hcl.BlockHeaderSchema{
 		{
 			Type: "openapi",
 		},
+		config.OAuthBlockHeaderSchema,
+		{
+			Type: "beta_health",
+		},
+		config.TokenRequestBlockHeaderSchema,
+		{
+			Type: "beta_rate_limit",
+		},
 	},
 }
 
@@ -269,9 +277,9 @@ func invalidRefinement(body hcl.Body) error {
 		}
 	}
 
-	content, _, _ := body.PartialContent(openApiBlockSchema)
-	if content != nil && len(content.Blocks.OfType("openapi")) > 0 {
-		return newDiagErr(&content.Blocks.OfType("openapi")[0].DefRange, fmt.Sprintf(message, "openapi"))
+	content, _, _ := body.PartialContent(forbiddenInRefinedBackendBlockSchema)
+	if content != nil && len(content.Blocks) > 0 {
+		return newDiagErr(&content.Blocks[0].DefRange, fmt.Sprintf(message, content.Blocks[0].Type))
 	}
 
 	return nil
diff --git a/config/oauth2ra.go b/config/oauth2ra.go
index a374b8a2..c51e73b4 100644
--- a/config/oauth2ra.go
+++ b/config/oauth2ra.go
@@ -11,11 +11,12 @@ const (
 	Password          = "password"
 )
 
+var OAuthBlockHeaderSchema = hcl.BlockHeaderSchema{
+	Type: "oauth2",
+}
 var OAuthBlockSchema = &hcl.BodySchema{
 	Blocks: []hcl.BlockHeaderSchema{
-		{
-			Type: "oauth2",
-		},
+		OAuthBlockHeaderSchema,
 	},
 }
 
diff --git a/config/token_request.go b/config/token_request.go
index 099e6d41..41a3d2ed 100644
--- a/config/token_request.go
+++ b/config/token_request.go
@@ -11,13 +11,14 @@ var (
 	_ Inline           = &TokenRequest{}
 )
 
+var TokenRequestBlockHeaderSchema = hcl.BlockHeaderSchema{
+	Type:          "beta_token_request",
+	LabelNames:    []string{"name"},
+	LabelOptional: true,
+}
 var TokenRequestBlockSchema = &hcl.BodySchema{
 	Blocks: []hcl.BlockHeaderSchema{
-		{
-			Type:          "beta_token_request",
-			LabelNames:    []string{"name"},
-			LabelOptional: true,
-		},
+		TokenRequestBlockHeaderSchema,
 	},
 }
 
diff --git a/docs/website/content/2.configuration/4.block/backend.md b/docs/website/content/2.configuration/4.block/backend.md
index bba29d5b..4d54a97f 100644
--- a/docs/website/content/2.configuration/4.block/backend.md
+++ b/docs/website/content/2.configuration/4.block/backend.md
@@ -63,13 +63,13 @@ values: [
     "name": "disable_certificate_validation",
     "type": "bool",
     "default": "false",
-    "description": "Disables the peer certificate validation."
+    "description": "Disables the peer certificate validation. Must not be used in backend refinement."
   },
   {
     "name": "disable_connection_reuse",
     "type": "bool",
     "default": "false",
-    "description": "Disables reusage of connections to the origin."
+    "description": "Disables reusage of connections to the origin. Must not be used in backend refinement."
   },
   {
     "name": "hostname",
@@ -81,13 +81,13 @@ values: [
     "name": "http2",
     "type": "bool",
     "default": "false",
-    "description": "Enables the HTTP2 support."
+    "description": "Enables the HTTP2 support. Must not be used in backend refinement."
   },
   {
     "name": "max_connections",
     "type": "number",
     "default": "0",
-    "description": "The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin."
+    "description": "The maximum number of concurrent connections in any state (_active_ or _idle_) to the origin. Must not be used in backend refinement."
   },
   {
     "name": "origin",
@@ -187,6 +187,38 @@ values: [
   }
 ]
 
+## Refining a referenced backend
+
+Referenced backends may be "refined" by using a labeled `backend` block in places where an unlabeled `backend` block would also be allowed, e.g. in a `proxy` block:
+
+```hcl
+    proxy {
+      backend "ref_be" {      # refine referenced backend
+        path = "/b"           # override existing attribute value
+        add_form_params = {   # set new attribute
+          # ...
+        }
+      }
+    }
+
+# ...
+
+definitions {
+  backend "ref_be" {
+    origin = "https://example.com"
+    path = "/a"
+  }
+}
+```
+
+If an attribute is set in both the _referenced_ and the _refining_ block, the value in the _refining_ block is used.
+
+**Note:** Child _blocks_ and the following _attributes_ are not allowed in refining `backend` blocks:
+* `disable_certificate_validation`,
+* `disable_connection_reuse`,
+* `http2` and
+* `max_connections`.
+
 ---
 ::
 


Test patch:
diff --git a/config/configload/load_test.go b/config/configload/load_test.go
index 3e7ed214..12d5da0e 100644
--- a/config/configload/load_test.go
+++ b/config/configload/load_test.go
@@ -66,14 +66,63 @@ definitions {
 	}
 }`
 
-	_, err := configload.LoadBytes([]byte(fmt.Sprintf(config, `openapi { file = ""}`)), "test.hcl")
-	if err == nil {
-		t.Fatal("expected an error")
+	tests := []struct {
+		name string
+		hcl  string
+	}{
+		{
+			"openapi",
+			`openapi { file = ""}`,
+		},
+		{
+			"oauth2",
+			`oauth2 {
+  grant_type = "client_credentials"
+  token_endpoint = ""
+  client_id = "asdf"
+  client_secret = "asdf"
+}`,
+		},
+		{
+			"beta_health",
+			`beta_health {
+}`,
+		},
+		{
+			"beta_token_request",
+			`beta_token_request {
+  token = "asdf"
+  ttl = "1s"
+}`,
+		},
+		{
+			"beta_token_request",
+			`beta_token_request "name" {
+  token = "asdf"
+  ttl = "1s"
+}`,
+		},
+		{
+			"beta_rate_limit",
+			`beta_rate_limit {
+  per_period = 10
+  period = "10s"
+}`,
+		},
 	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(subT *testing.T) {
+			_, err := configload.LoadBytes([]byte(fmt.Sprintf(config, tt.hcl)), "test.hcl")
+			if err == nil {
+				subT.Error("expected an error")
+				return
+			}
 
-	if !strings.HasSuffix(err.Error(),
-		fmt.Sprintf("backend reference: refinement for %q is not permitted; ", "openapi")) {
-		t.Error(err)
+			if !strings.HasSuffix(err.Error(),
+				fmt.Sprintf("backend reference: refinement for %q is not permitted; ", tt.name)) {
+				subT.Error(err)
+			}
+		})
 	}
 }