Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cmd/operator/app/thread_chi.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import (
"context"
"time"

meta "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sLabels "k8s.io/apimachinery/pkg/labels"
kubeinformers "k8s.io/client-go/informers"

log "github.com/altinity/clickhouse-operator/pkg/announcer"
"github.com/altinity/clickhouse-operator/pkg/chop"
chopinformers "github.com/altinity/clickhouse-operator/pkg/client/informers/externalversions"
"github.com/altinity/clickhouse-operator/pkg/controller/chi"
chiLabeler "github.com/altinity/clickhouse-operator/pkg/model/chi/tags/labeler"
commonLabeler "github.com/altinity/clickhouse-operator/pkg/model/common/tags/labeler"
)

// Prometheus exporter defaults
Expand All @@ -43,6 +47,16 @@ var (
func init() {
}

// chopGeneratedObjectsLabelSelector builds a label selector that matches objects generated by the operator.
// This is the server-side counterpart of the client-side Controller.isTrackedObject() check, which requires
// Labeler.IsCHOPGeneratedObject(). Both are built out of the same labeler, so that the two can not diverge.
func chopGeneratedObjectsLabelSelector() string {
labeler := chiLabeler.New(nil)
return k8sLabels.SelectorFromSet(k8sLabels.Set{
labeler.Get(commonLabeler.LabelAppName): labeler.Get(commonLabeler.LabelAppValue),
}).String()
}

var chiController *chi.Controller

// initClickHouse is an entry point of the application
Expand Down Expand Up @@ -90,10 +104,20 @@ func initClickHouse(ctx context.Context) {
}

// Create Informers
// Kube objects are filtered server-side down to operator-generated objects only. GetInformerNamespace()
// falls back to all namespaces as soon as watched namespaces are specified as anything other than a single
// literal namespace name, so without this selector the informer caches - and the DeltaFIFO feeding them -
// scale with the size of the whole cluster instead of with the number of managed installations.
// CHOp custom resource informers below are intentionally left unfiltered.
kubeObjectsLabelSelector := chopGeneratedObjectsLabelSelector()
log.V(1).F().Info("Kube informers label selector: '%s'", kubeObjectsLabelSelector)
kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(
kubeClient,
kubeInformerFactoryResyncPeriod,
kubeinformers.WithNamespace(chop.Config().GetInformerNamespace()),
kubeinformers.WithTweakListOptions(func(options *meta.ListOptions) {
options.LabelSelector = kubeObjectsLabelSelector
}),
)
chopInformerFactory := chopinformers.NewSharedInformerFactoryWithOptions(
chopClient,
Expand Down
92 changes: 92 additions & 0 deletions cmd/operator/app/thread_chi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package app

import (
"testing"

meta "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sLabels "k8s.io/apimachinery/pkg/labels"

chiLabeler "github.com/altinity/clickhouse-operator/pkg/model/chi/tags/labeler"
)

// The kube informer factory applies chopGeneratedObjectsLabelSelector() server-side. Handlers still apply
// the client-side Controller.isTrackedObject() check, which requires Labeler.IsCHOPGeneratedObject().
// If the two ever disagree, the operator silently stops seeing objects it is supposed to reconcile, so
// pin down both the literal selector and its equivalence to the client-side predicate.

func Test_chopGeneratedObjectsLabelSelector(t *testing.T) {
want := "clickhouse.altinity.com/app=chop"
if got := chopGeneratedObjectsLabelSelector(); got != want {
t.Errorf("chopGeneratedObjectsLabelSelector() = %q, want %q", got, want)
}
}

func Test_chopGeneratedObjectsLabelSelectorMatchesIsCHOPGeneratedObject(t *testing.T) {
tests := []struct {
name string
labels map[string]string
want bool
}{
{
name: "operator-generated object",
labels: map[string]string{"clickhouse.altinity.com/app": "chop"},
want: true,
},
{
name: "operator-generated object among unrelated labels",
labels: map[string]string{
"clickhouse.altinity.com/app": "chop",
"clickhouse.altinity.com/chi": "some-chi",
"kubernetes.io/service-name": "some-svc",
"app.kubernetes.io/managed-by": "Helm",
},
want: true,
},
{
name: "no labels at all",
labels: nil,
want: false,
},
{
name: "unrelated labels only",
labels: map[string]string{"app": "clickhouse"},
want: false,
},
{
name: "right key, wrong value",
labels: map[string]string{"clickhouse.altinity.com/app": "not-chop"},
want: false,
},
{
name: "bare app label is not enough",
labels: map[string]string{"app": "chop"},
want: false,
},
{
// CHK-generated objects carry the clickhouse-keeper API group key, and isTrackedObject()
// uses the CHI labeler, so they are not tracked by the CHI controller either way.
name: "keeper-generated object",
labels: map[string]string{"clickhouse-keeper.altinity.com/app": "chop"},
want: false,
},
}

selector, err := k8sLabels.Parse(chopGeneratedObjectsLabelSelector())
if err != nil {
t.Fatalf("unable to parse selector: %v", err)
}
labeler := chiLabeler.New(nil)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Server-side: what the informer factory asks the API server for
if got := selector.Matches(k8sLabels.Set(tt.labels)); got != tt.want {
t.Errorf("selector.Matches() = %v, want %v", got, tt.want)
}
// Client-side: what the event handlers gate on
if got := labeler.IsCHOPGeneratedObject(&meta.ObjectMeta{Labels: tt.labels}); got != tt.want {
t.Errorf("IsCHOPGeneratedObject() = %v, want %v", got, tt.want)
}
})
}
}