diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go
index 107c44ca4e9..b0a999fd921 100644
--- a/apis/projectcontour/v1/httpproxy.go
+++ b/apis/projectcontour/v1/httpproxy.go
@@ -42,6 +42,27 @@ type HTTPProxySpec struct {
IngressClassName string `json:"ingressClassName,omitempty"`
}
+// Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+//
+// This validation is based off of the corresponding Kubernetes validation:
+// https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+//
+// This is used for Namespace name validation here:
+// https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+//
+// Valid values include:
+//
+// * "example"
+//
+// Invalid values include:
+//
+// * "example.com" - "." is an invalid character
+//
+// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
+// +kubebuilder:validation:MinLength=1
+// +kubebuilder:validation:MaxLength=63
+type Namespace string
+
// Include describes a set of policies that can be applied to an HTTPProxy in a namespace.
type Include struct {
// Name of the HTTPProxy
diff --git a/apis/projectcontour/v1/httpproxy_helpers.go b/apis/projectcontour/v1/httpproxy_helpers.go
new file mode 100644
index 00000000000..2716fa01738
--- /dev/null
+++ b/apis/projectcontour/v1/httpproxy_helpers.go
@@ -0,0 +1,22 @@
+// Copyright Project Contour Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+func NamespacesToStrings(ns []Namespace) []string {
+ res := make([]string, len(ns))
+ for i, n := range ns {
+ res[i] = string(n)
+ }
+ return res
+}
diff --git a/apis/projectcontour/v1/httpproxy_helpers_test.go b/apis/projectcontour/v1/httpproxy_helpers_test.go
new file mode 100644
index 00000000000..bacfaa55d4f
--- /dev/null
+++ b/apis/projectcontour/v1/httpproxy_helpers_test.go
@@ -0,0 +1,46 @@
+// Copyright Project Contour Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestNamespacesToStrings(t *testing.T) {
+ testCases := []struct {
+ description string
+ namespaces []Namespace
+ expectStrings []string
+ }{
+ {
+ description: "namespace 1",
+ namespaces: []Namespace{},
+ expectStrings: []string{},
+ },
+ {
+ description: "namespace 2",
+ namespaces: []Namespace{"ns1", "ns2"},
+ expectStrings: []string{"ns1", "ns2"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ if !reflect.DeepEqual(NamespacesToStrings(tc.namespaces), tc.expectStrings) {
+ t.Errorf("expect converted strings %v is the same as %v", NamespacesToStrings(tc.namespaces), tc.expectStrings)
+ }
+ })
+ }
+}
diff --git a/apis/projectcontour/v1alpha1/contourdeployment.go b/apis/projectcontour/v1alpha1/contourdeployment.go
index 7a37ef5cecd..049a098df42 100644
--- a/apis/projectcontour/v1alpha1/contourdeployment.go
+++ b/apis/projectcontour/v1alpha1/contourdeployment.go
@@ -14,6 +14,7 @@
package v1alpha1
import (
+ contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -122,6 +123,14 @@ type ContourSettings struct {
// the annotations for Prometheus will be appended or overwritten with predefined value.
// +optional
PodAnnotations map[string]string `json:"podAnnotations,omitempty"`
+
+ // WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ // to only watch this subset of namespaces.
+ // +optional
+ // +kubebuilder:validation:Type=array
+ // +kubebuilder:validation:MinItems=1
+ // +kubebuilder:validation:MaxItems=42
+ WatchNamespaces []contour_api_v1.Namespace `json:"watchNamespaces,omitempty"`
}
// DeploymentSettings contains settings for Deployment resources.
diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
index 8647d3d36fb..9e64b85c778 100644
--- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
@@ -387,6 +387,11 @@ func (in *ContourSettings) DeepCopyInto(out *ContourSettings) {
(*out)[key] = val
}
}
+ if in.WatchNamespaces != nil {
+ in, out := &in.WatchNamespaces, &out.WatchNamespaces
+ *out = make([]v1.Namespace, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContourSettings.
diff --git a/changelogs/unreleased/6073-lubronzhan-small.md b/changelogs/unreleased/6073-lubronzhan-small.md
new file mode 100644
index 00000000000..f9e86422557
--- /dev/null
+++ b/changelogs/unreleased/6073-lubronzhan-small.md
@@ -0,0 +1 @@
+Allow gatewayProvisioner to create contour that only watch limited namespaces of resources
diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml
index 9787514aa67..adf820dabc2 100644
--- a/examples/contour/01-crds.yaml
+++ b/examples/contour/01-crds.yaml
@@ -1620,6 +1620,28 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
+ watchNamespaces:
+ description: |-
+ WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ to only watch this subset of namespaces.
+ items:
+ description: |-
+ Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+ This validation is based off of the corresponding Kubernetes validation:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+ This is used for Namespace name validation here:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+ Valid values include:
+ * "example"
+ Invalid values include:
+ * "example.com" - "." is an invalid character
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ maxItems: 42
+ minItems: 1
+ type: array
type: object
envoy:
description: |-
diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml
index 1b1a48e937b..b077a0809f9 100644
--- a/examples/render/contour-deployment.yaml
+++ b/examples/render/contour-deployment.yaml
@@ -1839,6 +1839,28 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
+ watchNamespaces:
+ description: |-
+ WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ to only watch this subset of namespaces.
+ items:
+ description: |-
+ Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+ This validation is based off of the corresponding Kubernetes validation:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+ This is used for Namespace name validation here:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+ Valid values include:
+ * "example"
+ Invalid values include:
+ * "example.com" - "." is an invalid character
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ maxItems: 42
+ minItems: 1
+ type: array
type: object
envoy:
description: |-
diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml
index 518e9803a5e..08760885e9d 100644
--- a/examples/render/contour-gateway-provisioner.yaml
+++ b/examples/render/contour-gateway-provisioner.yaml
@@ -1631,6 +1631,28 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
+ watchNamespaces:
+ description: |-
+ WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ to only watch this subset of namespaces.
+ items:
+ description: |-
+ Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+ This validation is based off of the corresponding Kubernetes validation:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+ This is used for Namespace name validation here:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+ Valid values include:
+ * "example"
+ Invalid values include:
+ * "example.com" - "." is an invalid character
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ maxItems: 42
+ minItems: 1
+ type: array
type: object
envoy:
description: |-
diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml
index 718d5710b8b..24af6d41950 100644
--- a/examples/render/contour-gateway.yaml
+++ b/examples/render/contour-gateway.yaml
@@ -1842,6 +1842,28 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
+ watchNamespaces:
+ description: |-
+ WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ to only watch this subset of namespaces.
+ items:
+ description: |-
+ Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+ This validation is based off of the corresponding Kubernetes validation:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+ This is used for Namespace name validation here:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+ Valid values include:
+ * "example"
+ Invalid values include:
+ * "example.com" - "." is an invalid character
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ maxItems: 42
+ minItems: 1
+ type: array
type: object
envoy:
description: |-
diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml
index d0bd9625450..24d055b9103 100644
--- a/examples/render/contour.yaml
+++ b/examples/render/contour.yaml
@@ -1839,6 +1839,28 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
+ watchNamespaces:
+ description: |-
+ WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ to only watch this subset of namespaces.
+ items:
+ description: |-
+ Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+ This validation is based off of the corresponding Kubernetes validation:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+ This is used for Namespace name validation here:
+ https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+ Valid values include:
+ * "example"
+ Invalid values include:
+ * "example.com" - "." is an invalid character
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ maxItems: 42
+ minItems: 1
+ type: array
type: object
envoy:
description: |-
diff --git a/internal/provisioner/controller/gateway.go b/internal/provisioner/controller/gateway.go
index 8907dbea37c..f83c74f396f 100644
--- a/internal/provisioner/controller/gateway.go
+++ b/internal/provisioner/controller/gateway.go
@@ -17,6 +17,7 @@ import (
"context"
"fmt"
+ contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1"
contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
"github.com/projectcontour/contour/internal/gatewayapi"
"github.com/projectcontour/contour/internal/provisioner/model"
@@ -261,6 +262,8 @@ func (r *gatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
contourModel.Spec.KubernetesLogLevel = contourParams.KubernetesLogLevel
+ contourModel.Spec.WatchNamespaces = contour_api_v1.NamespacesToStrings(contourParams.WatchNamespaces)
+
if contourParams.Deployment != nil &&
contourParams.Deployment.Strategy != nil {
contourModel.Spec.ContourDeploymentStrategy = *contourParams.Deployment.Strategy
diff --git a/internal/provisioner/model/model.go b/internal/provisioner/model/model.go
index 026802104b7..35541eb57bc 100644
--- a/internal/provisioner/model/model.go
+++ b/internal/provisioner/model/model.go
@@ -134,6 +134,10 @@ func (c *Contour) EnvoyTolerationsExist() bool {
return false
}
+func (c *Contour) WatchAllNamespaces() bool {
+ return c.Spec.WatchNamespaces == nil || len(c.Spec.WatchNamespaces) == 0
+}
+
// ContourSpec defines the desired state of Contour.
type ContourSpec struct {
// ContourReplicas is the desired number of Contour replicas. If unset,
@@ -245,6 +249,11 @@ type ContourSpec struct {
// If the value is 0, the overload manager is disabled.
// defaults to 0.
EnvoyMaxHeapSizeBytes uint64
+
+ // WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance
+ // to only watch these set of namespaces
+ // default is nil, contour will watch resource of all namespaces
+ WatchNamespaces []string
}
// WorkloadType is the type of Kubernetes workload to use for a component.
diff --git a/internal/provisioner/model/names.go b/internal/provisioner/model/names.go
index ca3aeff89fc..3f82ca1a77f 100644
--- a/internal/provisioner/model/names.go
+++ b/internal/provisioner/model/names.go
@@ -67,7 +67,9 @@ func (c *Contour) ContourRBACNames() RBACNames {
Role: fmt.Sprintf("contour-%s", c.Name),
// this one has a different prefix to differentiate from the certgen role binding (see below).
- RoleBinding: fmt.Sprintf("contour-rolebinding-%s", c.Name),
+ RoleBinding: fmt.Sprintf("contour-rolebinding-%s", c.Name),
+ NamespaceScopedResourceRole: fmt.Sprintf("contour-resources-%s-%s", c.Namespace, c.Name),
+ NamespaceScopedResourceRoleBinding: fmt.Sprintf("contour-resources-%s-%s", c.Namespace, c.Name),
}
}
@@ -129,9 +131,11 @@ func (c *Contour) CommonAnnotations() map[string]string {
// RBACNames holds a set of names of related RBAC resources.
type RBACNames struct {
- ServiceAccount string
- ClusterRole string
- ClusterRoleBinding string
- Role string
- RoleBinding string
+ ServiceAccount string
+ ClusterRole string
+ ClusterRoleBinding string
+ Role string
+ RoleBinding string
+ NamespaceScopedResourceRole string
+ NamespaceScopedResourceRoleBinding string
}
diff --git a/internal/provisioner/objects/deployment/deployment.go b/internal/provisioner/objects/deployment/deployment.go
index 81b5d2234cb..bfc56116bcb 100644
--- a/internal/provisioner/objects/deployment/deployment.go
+++ b/internal/provisioner/objects/deployment/deployment.go
@@ -17,6 +17,8 @@ import (
"context"
"fmt"
"path/filepath"
+ "slices"
+ "strings"
"github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
"github.com/projectcontour/contour/internal/provisioner/equality"
@@ -100,6 +102,14 @@ func DesiredDeployment(contour *model.Contour, image string) *appsv1.Deployment
args = append(args, "--debug")
}
+ if !contour.WatchAllNamespaces() {
+ ns := contour.Spec.WatchNamespaces
+ if !slices.Contains(contour.Spec.WatchNamespaces, contour.Namespace) {
+ ns = append(ns, contour.Namespace)
+ }
+ args = append(args, fmt.Sprintf("--watch-namespaces=%s", strings.Join(ns, ",")))
+ }
+
// Pass the insecure/secure flags to Contour if using non-default ports.
for _, port := range contour.Spec.NetworkPublishing.Envoy.Ports {
switch {
diff --git a/internal/provisioner/objects/deployment/deployment_test.go b/internal/provisioner/objects/deployment/deployment_test.go
index ab5843ce9ff..71d0c81be6f 100644
--- a/internal/provisioner/objects/deployment/deployment_test.go
+++ b/internal/provisioner/objects/deployment/deployment_test.go
@@ -15,6 +15,7 @@ package deployment
import (
"fmt"
+ "strings"
"testing"
"github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
@@ -212,6 +213,37 @@ func TestDesiredDeployment(t *testing.T) {
checkDeploymentHasStrategy(t, deploy, cntr.Spec.ContourDeploymentStrategy)
}
+func TestDesiredDeploymentWhenSettingWatchNamespaces(t *testing.T) {
+ testCases := []struct {
+ description string
+ namespaces []string
+ }{
+ {
+ description: "several valid namespaces",
+ namespaces: []string{"ns1", "ns2"},
+ },
+ {
+ description: "single valid namespace",
+ namespaces: []string{"ns1"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ name := "deploy-test"
+ cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
+ icName := "test-ic"
+ cntr.Spec.IngressClassName = &icName
+ // Change the Contour watch namespaces flag
+ cntr.Spec.WatchNamespaces = tc.namespaces
+ deploy := DesiredDeployment(cntr, "ghcr.io/projectcontour/contour:test")
+ container := checkDeploymentHasContainer(t, deploy, contourContainerName, true)
+ arg := fmt.Sprintf("--watch-namespaces=%s", strings.Join(append(tc.namespaces, cntr.Namespace), ","))
+ checkContainerHasArg(t, container, arg)
+ })
+ }
+}
+
func TestNodePlacementDeployment(t *testing.T) {
name := "selector-test"
cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
diff --git a/internal/provisioner/objects/rbac/clusterrole/cluster_role.go b/internal/provisioner/objects/rbac/clusterrole/cluster_role.go
index b610bc7e687..6371a9202f2 100644
--- a/internal/provisioner/objects/rbac/clusterrole/cluster_role.go
+++ b/internal/provisioner/objects/rbac/clusterrole/cluster_role.go
@@ -21,23 +21,16 @@ import (
"github.com/projectcontour/contour/internal/provisioner/labels"
"github.com/projectcontour/contour/internal/provisioner/model"
"github.com/projectcontour/contour/internal/provisioner/objects"
- corev1 "k8s.io/api/core/v1"
- discoveryv1 "k8s.io/api/discovery/v1"
- networkingv1 "k8s.io/api/networking/v1"
+ "github.com/projectcontour/contour/internal/provisioner/objects/rbac/util"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
- gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
-)
-
-const (
- contourV1GroupName = "projectcontour.io"
)
// EnsureClusterRole ensures a ClusterRole resource exists with the provided name
// and contour namespace/name for the owning contour labels.
-func EnsureClusterRole(ctx context.Context, cli client.Client, name string, contour *model.Contour) error {
- desired := desiredClusterRole(name, contour)
+func EnsureClusterRole(ctx context.Context, cli client.Client, name string, contour *model.Contour, clusterScopedResourceOnly bool) error {
+ desired := desiredClusterRole(name, contour, clusterScopedResourceOnly)
// Enclose contour.
updater := func(ctx context.Context, cli client.Client, current, desired *rbacv1.ClusterRole) error {
@@ -49,22 +42,8 @@ func EnsureClusterRole(ctx context.Context, cli client.Client, name string, cont
// desiredClusterRole constructs an instance of the desired ClusterRole resource with
// the provided name and contour namespace/name for the owning contour labels.
-func desiredClusterRole(name string, contour *model.Contour) *rbacv1.ClusterRole {
- var (
- createGetUpdate = []string{"create", "get", "update"}
- getListWatch = []string{"get", "list", "watch"}
- update = []string{"update"}
- )
-
- policyRuleFor := func(apiGroup string, verbs []string, resources ...string) rbacv1.PolicyRule {
- return rbacv1.PolicyRule{
- Verbs: verbs,
- APIGroups: []string{apiGroup},
- Resources: resources,
- }
- }
-
- return &rbacv1.ClusterRole{
+func desiredClusterRole(name string, contour *model.Contour, clusterScopedResourceOnly bool) *rbacv1.ClusterRole {
+ role := &rbacv1.ClusterRole{
TypeMeta: metav1.TypeMeta{
Kind: "Role",
},
@@ -73,27 +52,15 @@ func desiredClusterRole(name string, contour *model.Contour) *rbacv1.ClusterRole
Labels: contour.CommonLabels(),
Annotations: contour.CommonAnnotations(),
},
- Rules: []rbacv1.PolicyRule{
- // Core Contour-watched resources.
- policyRuleFor(corev1.GroupName, getListWatch, "secrets", "endpoints", "services", "namespaces"),
-
- // Discovery Contour-watched resources.
- policyRuleFor(discoveryv1.GroupName, getListWatch, "endpointslices"),
-
- // Gateway API resources.
- // Note, ReferenceGrant does not currently have a .status field so it's omitted from the status rule.
- policyRuleFor(gatewayv1alpha2.GroupName, getListWatch, "gatewayclasses", "gateways", "httproutes", "tlsroutes", "grpcroutes", "tcproutes", "referencegrants"),
- policyRuleFor(gatewayv1alpha2.GroupName, update, "gatewayclasses/status", "gateways/status", "httproutes/status", "tlsroutes/status", "grpcroutes/status", "tcproutes/status"),
-
- // Ingress resources.
- policyRuleFor(networkingv1.GroupName, getListWatch, "ingresses"),
- policyRuleFor(networkingv1.GroupName, createGetUpdate, "ingresses/status"),
-
- // Contour CRDs.
- policyRuleFor(contourV1GroupName, getListWatch, "httpproxies", "tlscertificatedelegations", "extensionservices", "contourconfigurations"),
- policyRuleFor(contourV1GroupName, createGetUpdate, "httpproxies/status", "extensionservices/status", "contourconfigurations/status"),
- },
+ Rules: util.ClusterScopedResourcePolicyRules(),
+ }
+ if clusterScopedResourceOnly {
+ return role
}
+
+ // add basic rules to role
+ role.Rules = append(role.Rules, util.NamespacedResourcePolicyRules()...)
+ return role
}
// updateClusterRoleIfNeeded updates a ClusterRole resource if current does not match desired,
diff --git a/internal/provisioner/objects/rbac/clusterrole/cluster_role_test.go b/internal/provisioner/objects/rbac/clusterrole/cluster_role_test.go
index 2f6a961d32f..2e48d3edd98 100644
--- a/internal/provisioner/objects/rbac/clusterrole/cluster_role_test.go
+++ b/internal/provisioner/objects/rbac/clusterrole/cluster_role_test.go
@@ -15,6 +15,7 @@ package clusterrole
import (
"fmt"
+ "slices"
"testing"
"github.com/projectcontour/contour/internal/provisioner/model"
@@ -43,14 +44,49 @@ func checkClusterRoleLabels(t *testing.T, cr *rbacv1.ClusterRole, expected map[s
t.Errorf("cluster role has unexpected %q labels", cr.Labels)
}
+func clusterRoleRulesContainOnlyClusterScopeRules(cr *rbacv1.ClusterRole) bool {
+ for _, r := range cr.Rules {
+ if !slices.Contains(r.Resources, "gatewayclasses") &&
+ !slices.Contains(r.Resources, "gatewayclasses/status") &&
+ !slices.Contains(r.Resources, "namespaces") {
+ return false
+ }
+ }
+
+ return true
+}
+
func TestDesiredClusterRole(t *testing.T) {
- name := "test-cr"
- cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
- cr := desiredClusterRole(name, cntr)
- checkClusterRoleName(t, cr, name)
- ownerLabels := map[string]string{
- model.ContourOwningGatewayNameLabel: cntr.Name,
- model.GatewayAPIOwningGatewayNameLabel: cntr.Name,
+ testCases := []struct {
+ description string
+ clusterScopeOnly bool
+ }{
+ {
+ description: "gateway class rule only role",
+ clusterScopeOnly: true,
+ },
+ {
+ description: "generic cluster role include all rules",
+ clusterScopeOnly: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ name := "test-cr"
+ cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
+ cr := desiredClusterRole(name, cntr, tc.clusterScopeOnly)
+ checkClusterRoleName(t, cr, name)
+ ownerLabels := map[string]string{
+ model.ContourOwningGatewayNameLabel: cntr.Name,
+ model.GatewayAPIOwningGatewayNameLabel: cntr.Name,
+ }
+ checkClusterRoleLabels(t, cr, ownerLabels)
+ fmt.Println(cr.Rules)
+ if tc.clusterScopeOnly != clusterRoleRulesContainOnlyClusterScopeRules(cr) {
+ t.Errorf("expect clusterScopeOnly to be %v, but clusterRoleRulesContainOnlyClusterScopeRules shows %v",
+ tc.clusterScopeOnly, clusterRoleRulesContainOnlyClusterScopeRules(cr))
+ }
+ })
}
- checkClusterRoleLabels(t, cr, ownerLabels)
}
diff --git a/internal/provisioner/objects/rbac/rbac.go b/internal/provisioner/objects/rbac/rbac.go
index a582980a349..30b731a7e1d 100644
--- a/internal/provisioner/objects/rbac/rbac.go
+++ b/internal/provisioner/objects/rbac/rbac.go
@@ -24,10 +24,14 @@ import (
"github.com/projectcontour/contour/internal/provisioner/objects/rbac/role"
"github.com/projectcontour/contour/internal/provisioner/objects/rbac/rolebinding"
"github.com/projectcontour/contour/internal/provisioner/objects/rbac/serviceaccount"
+ "github.com/projectcontour/contour/internal/provisioner/slice"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -53,19 +57,48 @@ func ensureContourRBAC(ctx context.Context, cli client.Client, contour *model.Co
return fmt.Errorf("failed to ensure service account %s/%s: %w", contour.Namespace, names.ServiceAccount, err)
}
- // Ensure cluster role & binding.
- if err := clusterrole.EnsureClusterRole(ctx, cli, names.ClusterRole, contour); err != nil {
- return fmt.Errorf("failed to ensure cluster role %s: %w", names.ClusterRole, err)
- }
- if err := clusterrolebinding.EnsureClusterRoleBinding(ctx, cli, names.ClusterRoleBinding, names.ClusterRole, names.ServiceAccount, contour); err != nil {
- return fmt.Errorf("failed to ensure cluster role binding %s: %w", names.ClusterRoleBinding, err)
+ // By default, Contour watches all namespaces, use default cluster role and rolebinding
+ clusterRoleForClusterScopedResourcesOnly := true
+ if contour.WatchAllNamespaces() {
+ // Ensure cluster role & binding.
+ if err := clusterrole.EnsureClusterRole(ctx, cli, names.ClusterRole, contour, !clusterRoleForClusterScopedResourcesOnly); err != nil {
+ return fmt.Errorf("failed to ensure cluster role %s: %w", names.ClusterRole, err)
+ }
+ if err := clusterrolebinding.EnsureClusterRoleBinding(ctx, cli, names.ClusterRoleBinding, names.ClusterRole, names.ServiceAccount, contour); err != nil {
+ return fmt.Errorf("failed to ensure cluster role binding %s: %w", names.ClusterRoleBinding, err)
+ }
+ } else {
+ // validate whether all namespaces exist
+ if err := validateNamespacesExist(ctx, cli, contour.Spec.WatchNamespaces); err != nil {
+ return fmt.Errorf("failed when validating watchNamespaces:%w", err)
+ }
+ // Ensure cluster role & cluster binding for gatewayclass and other cluster scope resource first since it's cluster scope variables
+ if err := clusterrole.EnsureClusterRole(ctx, cli, names.ClusterRole, contour, clusterRoleForClusterScopedResourcesOnly); err != nil {
+ return fmt.Errorf("failed to ensure cluster role %s: %w", names.ClusterRole, err)
+ }
+ if err := clusterrolebinding.EnsureClusterRoleBinding(ctx, cli, names.ClusterRoleBinding, names.ClusterRole, names.ServiceAccount, contour); err != nil {
+ return fmt.Errorf("failed to ensure cluster role binding %s: %w", names.ClusterRoleBinding, err)
+ }
+
+ // includes contour's namespace if it's not inside watchNamespaces
+ ns := contour.Spec.WatchNamespaces
+ if !slice.ContainsString(ns, contour.Namespace) {
+ ns = append(ns, contour.Namespace)
+ }
+ // Ensures role and rolebinding for namespaced scope resources in namespaces specified in contour.spec.watchNamespaces variable and contour's namespace
+ if err := role.EnsureRolesInNamespaces(ctx, cli, names.NamespaceScopedResourceRole, contour, ns); err != nil {
+ return fmt.Errorf("failed to ensure roles in namespace %s: %w", contour.Spec.WatchNamespaces, err)
+ }
+ if err := rolebinding.EnsureRoleBindingsInNamespaces(ctx, cli, names.NamespaceScopedResourceRoleBinding, names.ServiceAccount, names.NamespaceScopedResourceRole, contour, ns); err != nil {
+ return fmt.Errorf("failed to ensure rolebindings in namespace %s: %w", contour.Spec.WatchNamespaces, err)
+ }
}
// Ensure role & binding.
if err := role.EnsureControllerRole(ctx, cli, names.Role, contour); err != nil {
return fmt.Errorf("failed to ensure controller role %s/%s: %w", contour.Namespace, names.Role, err)
}
- if err := rolebinding.EnsureRoleBinding(ctx, cli, names.RoleBinding, names.ServiceAccount, names.Role, contour); err != nil {
+ if err := rolebinding.EnsureControllerRoleBinding(ctx, cli, names.RoleBinding, names.ServiceAccount, names.Role, contour); err != nil {
return fmt.Errorf("failed to ensure controller role binding %s/%s: %w", contour.Namespace, names.RoleBinding, err)
}
return nil
@@ -144,3 +177,21 @@ func EnsureRBACDeleted(ctx context.Context, cli client.Client, contour *model.Co
return nil
}
+
+func validateNamespacesExist(ctx context.Context, cli client.Client, ns []string) error {
+ errs := []error{}
+ for _, n := range ns {
+ namespace := &corev1.Namespace{}
+ // Check if the namespace exists
+ err := cli.Get(ctx, types.NamespacedName{Name: n}, namespace)
+ if err != nil {
+ if apierrors.IsNotFound(err) {
+ errs = append(errs, fmt.Errorf("failed to find namespace %s in watchNamespace. Please make sure it exist", n))
+ } else {
+ errs = append(errs, fmt.Errorf("failed to get namespace %s because of: %w", n, err))
+ }
+ }
+ }
+
+ return kerrors.NewAggregate(errs)
+}
diff --git a/internal/provisioner/objects/rbac/role/role.go b/internal/provisioner/objects/rbac/role/role.go
index d326348036c..f4dbd6b0a8d 100644
--- a/internal/provisioner/objects/rbac/role/role.go
+++ b/internal/provisioner/objects/rbac/role/role.go
@@ -21,10 +21,12 @@ import (
"github.com/projectcontour/contour/internal/provisioner/labels"
"github.com/projectcontour/contour/internal/provisioner/model"
"github.com/projectcontour/contour/internal/provisioner/objects"
+ "github.com/projectcontour/contour/internal/provisioner/objects/rbac/util"
coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -34,15 +36,36 @@ func EnsureControllerRole(ctx context.Context, cli client.Client, name string, c
desired := desiredControllerRole(name, contour)
updater := func(ctx context.Context, cli client.Client, current, desired *rbacv1.Role) error {
- _, err := updateRoleIfNeeded(ctx, cli, contour, current, desired)
+ err := updateRoleIfNeeded(ctx, cli, contour, current, desired)
return err
}
return objects.EnsureObject(ctx, cli, desired, updater, &rbacv1.Role{})
}
+// EnsureRolesInNamespaces ensures a set of Role resources exist in namespaces
+// specified, for contour to manage resources under these namespaces. And
+// contour namespace/name for the owning contour labels for the Contour
+// controller
+func EnsureRolesInNamespaces(ctx context.Context, cli client.Client, name string, contour *model.Contour, namespaces []string) error {
+ errs := []error{}
+ for _, ns := range namespaces {
+ desired := desiredRoleForResourceInNamespace(name, ns, contour)
+
+ updater := func(ctx context.Context, cli client.Client, current, desired *rbacv1.Role) error {
+ err := updateRoleIfNeeded(ctx, cli, contour, current, desired)
+ return err
+ }
+ if err := objects.EnsureObject(ctx, cli, desired, updater, &rbacv1.Role{}); err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ return kerrors.NewAggregate(errs)
+}
+
// desiredControllerRole constructs an instance of the desired Role resource with the
-// provided ns/name and contour namespace/name for the owning contour labels for
+// provided ns/name and using contour namespace/name for the owning contour labels for
// the Contour controller.
func desiredControllerRole(name string, contour *model.Contour) *rbacv1.Role {
role := &rbacv1.Role{
@@ -72,17 +95,34 @@ func desiredControllerRole(name string, contour *model.Contour) *rbacv1.Role {
return role
}
+// desiredRoleForResourceInNamespace constructs an instance of the desired Role resource with the
+// provided ns/name and using contour namespace/name for the corresponding Contour instance
+func desiredRoleForResourceInNamespace(name, namespace string, contour *model.Contour) *rbacv1.Role {
+ return &rbacv1.Role{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Role",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ Labels: contour.CommonLabels(),
+ Annotations: contour.CommonAnnotations(),
+ },
+ Rules: util.NamespacedResourcePolicyRules(),
+ }
+}
+
// updateRoleIfNeeded updates a Role resource if current does not match desired,
// using contour to verify the existence of owner labels.
-func updateRoleIfNeeded(ctx context.Context, cli client.Client, contour *model.Contour, current, desired *rbacv1.Role) (*rbacv1.Role, error) {
+func updateRoleIfNeeded(ctx context.Context, cli client.Client, contour *model.Contour, current, desired *rbacv1.Role) error {
if labels.AnyExist(current, model.OwnerLabels(contour)) {
role, updated := equality.RoleConfigChanged(current, desired)
if updated {
if err := cli.Update(ctx, role); err != nil {
- return nil, fmt.Errorf("failed to update cluster role %s/%s: %w", role.Namespace, role.Name, err)
+ return fmt.Errorf("failed to update cluster role %s/%s: %w", role.Namespace, role.Name, err)
}
- return role, nil
+ return nil
}
}
- return current, nil
+ return nil
}
diff --git a/internal/provisioner/objects/rbac/role/role_test.go b/internal/provisioner/objects/rbac/role/role_test.go
index 4412708bbcc..a42fa7b8712 100644
--- a/internal/provisioner/objects/rbac/role/role_test.go
+++ b/internal/provisioner/objects/rbac/role/role_test.go
@@ -43,6 +43,16 @@ func checkRoleLabels(t *testing.T, role *rbacv1.Role, expected map[string]string
t.Errorf("role has unexpected %q labels", role.Labels)
}
+func checkRoleNamespace(t *testing.T, role *rbacv1.Role, namespace string) {
+ t.Helper()
+
+ if role.Namespace == namespace {
+ return
+ }
+
+ t.Errorf("role has unexpected '%q' namespace", role.Namespace)
+}
+
func TestDesiredControllerRole(t *testing.T) {
name := "role-test"
cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
@@ -54,3 +64,34 @@ func TestDesiredControllerRole(t *testing.T) {
}
checkRoleLabels(t, role, ownerLabels)
}
+
+func TestDesiredRoleForContourInNamespace(t *testing.T) {
+ testCases := []struct {
+ description string
+ namespace string
+ }{
+ {
+ description: "namespace 1",
+ namespace: "ns1",
+ },
+ {
+ description: "namespace 2",
+ namespace: "ns2",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ name := "role-test"
+ cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
+ role := desiredRoleForResourceInNamespace(name, tc.namespace, cntr)
+ checkRoleName(t, role, name)
+ ownerLabels := map[string]string{
+ model.ContourOwningGatewayNameLabel: cntr.Name,
+ model.GatewayAPIOwningGatewayNameLabel: cntr.Name,
+ }
+ checkRoleLabels(t, role, ownerLabels)
+ checkRoleNamespace(t, role, tc.namespace)
+ })
+ }
+}
diff --git a/internal/provisioner/objects/rbac/rolebinding/role_binding.go b/internal/provisioner/objects/rbac/rolebinding/role_binding.go
index 19315dac051..e12c0117ada 100644
--- a/internal/provisioner/objects/rbac/rolebinding/role_binding.go
+++ b/internal/provisioner/objects/rbac/rolebinding/role_binding.go
@@ -21,18 +21,18 @@ import (
"github.com/projectcontour/contour/internal/provisioner/labels"
"github.com/projectcontour/contour/internal/provisioner/model"
"github.com/projectcontour/contour/internal/provisioner/objects"
-
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
)
-// EnsureRoleBinding ensures a RoleBinding resource exists with the provided
-// ns/name and contour namespace/name for the owning contour labels.
+// EnsureControllerRoleBinding ensures a RoleBinding resource exists with the provided
+// ns/name and using contour namespace/name for the owning contour labels.
// The RoleBinding will use svcAct for the subject and role for the role reference.
-func EnsureRoleBinding(ctx context.Context, cli client.Client, name, svcAct, role string, contour *model.Contour) error {
- desired := desiredRoleBinding(name, svcAct, role, contour)
+func EnsureControllerRoleBinding(ctx context.Context, cli client.Client, name, svcAct, role string, contour *model.Contour) error {
+ desired := desiredRoleBindingInNamespace(name, svcAct, role, contour.Namespace, contour)
// Enclose contour.
updater := func(ctx context.Context, cli client.Client, current, desired *rbacv1.RoleBinding) error {
@@ -42,26 +42,46 @@ func EnsureRoleBinding(ctx context.Context, cli client.Client, name, svcAct, rol
return objects.EnsureObject(ctx, cli, desired, updater, &rbacv1.RoleBinding{})
}
-// desiredRoleBinding constructs an instance of the desired RoleBinding resource
-// with the provided name in Contour spec Namespace, using contour namespace/name
+// EnsureRoleBindingsInNamespaces ensures a set of RoleBinding resources exist with the provided
+// namespaces/contour-resource-MatchCondition
+
Namespace
+(
string
alias)
+(Appears on: +ContourSettings) +
++
Namespace refers to a Kubernetes namespace. It must be a RFC 1123 label.
+This validation is based off of the corresponding Kubernetes validation: +https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/util/validation/validation.go#L187
+This is used for Namespace name validation here: +https://github.com/kubernetes/apimachinery/blob/02cfb53916346d085a6c6c7c66f882e3c6b0eca6/pkg/api/validation/generic.go#L63
+Valid values include:
+Invalid values include:
+@@ -6247,6 +6268,22 @@
watchNamespaces
+WatchNamespaces is an array of namespaces. Setting it will instruct the contour instance +to only watch this subset of namespaces.
+