Skip to content

Commit

Permalink
Support multinamespace informer filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
jkhelil committed Jul 24, 2024
1 parent be7c1ba commit a27a24a
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 122 deletions.
19 changes: 14 additions & 5 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package main

import (
"flag"
"strings"

"github.com/tektoncd/chains/pkg/reconciler/pipelinerun"
"github.com/tektoncd/chains/pkg/reconciler/taskrun"
Expand All @@ -24,6 +25,7 @@ import (
"knative.dev/pkg/controller"
"knative.dev/pkg/injection"
"knative.dev/pkg/injection/sharedmain"
"knative.dev/pkg/logging"
"knative.dev/pkg/signals"

// Run with all of the upstream providers.
Expand All @@ -41,11 +43,21 @@ import (

func main() {
flag.IntVar(&controller.DefaultThreadsPerController, "threads-per-controller", controller.DefaultThreadsPerController, "Threads (goroutines) to create per controller")
namespace := flag.String("namespace", "", "Namespace to restrict informer to. Optional, defaults to all namespaces.")
namespaceList := flag.String("namespace", "", "Comma-separated list of namespaces to restrict informer to. Optional, if empty defaults to all namespaces.")

// This also calls flag.Parse().
cfg := injection.ParseAndGetRESTConfigOrDie()

ctx := signals.NewContext()
logger := logging.FromContext(ctx)

var namespaces []string
if *namespaceList != "" {
// Remove any whitespace from the namespaces string and split it
namespaces = strings.Split(strings.ReplaceAll(*namespaceList, " ", ""), ",")
logger.Infof("controller is scoped to the following namespaces: %s\n", namespaces)
}

if cfg.QPS == 0 {
cfg.QPS = 2 * rest.DefaultQPS
}
Expand All @@ -57,8 +69,5 @@ func main() {
cfg.QPS = 2 * cfg.QPS
cfg.Burst = 2 * cfg.Burst

flag.Parse()
ctx := injection.WithNamespaceScope(signals.NewContext(), *namespace)

sharedmain.MainWithConfig(ctx, "watcher", cfg, taskrun.NewController, pipelinerun.NewController)
sharedmain.MainWithConfig(ctx, "watcher", cfg, taskrun.NewNamespacesScopedController(namespaces), pipelinerun.NewNamespacesScopedController(namespaces))
}
20 changes: 17 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ linkTitle: "Chains Configuration"
weight: 20
---
-->

# Chains Configuration

`Chains` works by observing `TaskRun` and `PipelineRun` executions, capturing relevant information, and storing it in a cryptographically-signed format.

`TaskRuns` and `PipelineRuns` can indicate inputs and outputs which are then captured and surfaced in the `Chains` payload formats, where relevant.
Expand Down Expand Up @@ -158,4 +156,20 @@ chains.tekton.dev/transparency-upload: "true"
> the path specified by `signers.kms.auth.token-dir`.

> [!IMPORTANT]
> To project the latest token values without needing to recreate the pod, avoid using `subPath` in volume mount.
> To project the latest token values without needing to recreate the pod, avoid using `subPath` in volume mount.

## Namespaces Restrictions in Chains Controller
This feature allows you to specify a list of namespaces for the controller to monitor, providing granular control over its operation. If no namespaces are specified, the controller defaults to monitoring all namespaces.

### Usage
To restrict the Chains Controller to specific namespaces, pass a comma-separated list of namespaces as an argument to the controller using the --namespace flag. The controller will then limit its watch to the specified namespaces, filtering the PipelineRun and TaskRun objects accordingly.

### Argument Description
--namespace: A comma-separated list of namespaces that the controller should monitor. If this argument is not provided or is left empty, the controller will watch all namespaces by default.

### Example
To restrict the controller to the dev and test namespaces, you would start the controller with the following argument:
```shell
--namespace=dev,test
```
In this example, the controller will only monitor resources within the dev and test namespaces.
80 changes: 80 additions & 0 deletions pkg/reconciler/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
Copyright 2024 The Tekton 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 reconciler

import (
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"knative.dev/pkg/controller"
)

// PipelineRunInformerFilterFunc returns a filter function
// for PipelineRuns ensuring PipelineRuns are filtered by list of namespaces membership
func PipelineRunInformerFilterFunc(namespaces []string) func(obj interface{}) bool {
return func(obj interface{}) bool {
// Namespace filter
if len(namespaces) == 0 {
return true
}
if pr, ok := obj.(*v1.PipelineRun); ok {
for _, ns := range namespaces {
if pr.Namespace == ns {
return true
}
}
}
return false
}
}

// TaskRunInformerFilterFunc returns a filter function
// for TaskRuns ensuring TaskRuns are filtered by list of namespaces membership
func TaskRunInformerFilterFunc(namespaces []string) func(obj interface{}) bool {
return func(obj interface{}) bool {
// Namespace filter
if len(namespaces) == 0 {
return true
}
if tr, ok := obj.(*v1.TaskRun); ok {
for _, ns := range namespaces {
if tr.Namespace == ns {
return true
}
}
}
return false
}
}

// TaskRunInformerFilterFuncWithOwnership returns a filter function
// for TaskRuns ensuring Ownership by a PipelineRun and filtered by list of namespaces membership and
func TaskRunInformerFilterFuncWithOwnership(namespaces []string) func(obj interface{}) bool {
return func(obj interface{}) bool {
// Ownership filter
if !controller.FilterController(&v1.PipelineRun{})(obj) {
return false
}
// Namespace filter
if len(namespaces) == 0 {
return true
}
if tr, ok := obj.(*v1.TaskRun); ok {
for _, ns := range namespaces {
if tr.Namespace == ns {
return true
}
}
}
return false
}
}
173 changes: 173 additions & 0 deletions pkg/reconciler/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
Copyright 2024 The Tekton 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 reconciler

import (
"testing"

v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TestPipelineRunInformerFilterFunc tests the PipelineRunInformerFilterFunc
func TestPipelineRunInformerFilterFunc(t *testing.T) {
tests := []struct {
name string
namespaces []string
obj interface{}
expected bool
}{
{
name: "Empty namespaces, should match",
namespaces: []string{},
obj: &v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: true,
},
{
name: "Matching namespace",
namespaces: []string{"default", "test"},
obj: &v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: true,
},
{
name: "Non-matching namespace",
namespaces: []string{"test"},
obj: &v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: false,
},
{
name: "Non PipelineRun object",
namespaces: []string{"default"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filterFunc := PipelineRunInformerFilterFunc(tt.namespaces)
result := filterFunc(tt.obj)
if result != tt.expected {
t.Errorf("Reconciler.PipelineRunInformerFilterFunc() result = %v, wanted %v", result, tt.expected)
}
})
}
}

// TestTaskRunInformerFilterFunc tests the TaskRunInformerFilterFunc
func TestTaskRunInformerFilterFunc(t *testing.T) {
tests := []struct {
name string
namespaces []string
obj interface{}
expected bool
}{
{
name: "Matching namespace",
namespaces: []string{"default", "test"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: true,
},
{
name: "Empty namespaces, should match",
namespaces: []string{},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: true,
},
{
name: "Non-matching namespace",
namespaces: []string{"test"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: false,
},
{
name: "Non TaskRun object",
namespaces: []string{"default"},
obj: &v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filterFunc := TaskRunInformerFilterFunc(tt.namespaces)
result := filterFunc(tt.obj)
if result != tt.expected {
t.Errorf("Reconciler.TaskRunInformerFilterFunc() result = %v, wanted %v", result, tt.expected)
}
})
}
}

// TestTaskRunInformerFilterFuncWithOwnership tests the TaskRunInformerFilterFuncWithOwnership
func TestTaskRunInformerFilterFuncWithOwnership(t *testing.T) {
boolValue := true
tests := []struct {
name string
namespaces []string
obj interface{}
expected bool
}{
{
name: "Empty namespaces and ownership, should match",
namespaces: []string{},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{APIVersion: "tekton.dev/v1", Kind: "PipelineRun", Controller: &boolValue},
},
}},
expected: true,
},
{
name: "Matching namespace and ownership",
namespaces: []string{"default", "test"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{APIVersion: "tekton.dev/v1", Kind: "PipelineRun", Controller: &boolValue},
},
}},
expected: true,
},
{
name: "Non-matching namespace and ownership",
namespaces: []string{"test"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
OwnerReferences: []metav1.OwnerReference{
{APIVersion: "tekton.dev/v1", Kind: "PipelineRun", Controller: &boolValue},
},
}},
expected: false,
},
{
name: "No ownership",
namespaces: []string{"default"},
obj: &v1.TaskRun{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filterFunc := TaskRunInformerFilterFuncWithOwnership(tt.namespaces)
result := filterFunc(tt.obj)
if result != tt.expected {
t.Errorf("Reconciler.TaskRunInformerFilterFuncWithOwnership() result = %v, wanted %v", result, tt.expected)
}
})
}
}
Loading

0 comments on commit a27a24a

Please sign in to comment.