Skip to content

Commit

Permalink
New property: IgnoreNames in NamespaceSelector
Browse files Browse the repository at this point in the history
To allow specif namespaces to be ignored even if they were included via labels or matchNames.
This can be useful to ignore system-relevant namespaces
  • Loading branch information
ccremer committed Oct 14, 2020
1 parent f2c0513 commit 16fe4c0
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 122 deletions.
11 changes: 8 additions & 3 deletions api/v1alpha1/syncconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ type (
NamespaceSelector struct {
// LabelSelector of namespaces to be targeted. Can be combined with MatchNames to include unlabelled namespaces.
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
// MatchNames lists namespace names to be targeted. Each entry can be a Regex pattern. A namespace is included
// if at least one pattern matches. Invalid patterns will cause the sync to be cancelled and the status conditions
// will contain the error message.
// MatchNames lists namespace names to be targeted. Each entry can be a Regex pattern.
// A namespace is included if at least one pattern matches.
// Invalid patterns will cause the sync to be cancelled and the status conditions will contain the error message.
MatchNames []string `json:"matchNames,omitempty"`
// IgnoreNames lists namespace names to be ignored. Each entry can be a Regex pattern and if they match
// the namespaces will be excluded from the sync even if matching in "matchNames" or via LabelSelector.
// A namespace is ignored if at least one pattern matches.
// Invalid patterns will cause the sync to be cancelled and the status conditions will contain the error message.
IgnoreNames []string `json:"ignoreNames,omitempty"`
}

// SyncConfigStatus defines the observed state of SyncConfig
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions config/crd/bases/sync.appuio.ch_syncconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ spec:
namespaceSelector:
description: NamespaceSelector defines which namespaces should be targeted
properties:
ignoreNames:
description: IgnoreNames lists namespace names to be ignored. Each
entry can be a Regex pattern and if they match the namespaces
will be excluded from the sync even if matching in "matchNames"
or via LabelSelector. A namespace is ignored if at least one pattern
matches. Invalid patterns will cause the sync to be cancelled
and the status conditions will contain the error message.
items:
type: string
type: array
labelSelector:
description: LabelSelector of namespaces to be targeted. Can be
combined with MatchNames to include unlabelled namespaces.
Expand Down
46 changes: 10 additions & 36 deletions controllers/syncconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ type (
}
// ReconciliationContext holds the parameters of a single SyncConfig reconciliation
ReconciliationContext struct {
ctx context.Context
cfg *syncv1alpha1.SyncConfig
conditions map[syncv1alpha1.SyncConfigConditionType]syncv1alpha1.SyncConfigCondition
matchNamesRegex []*regexp.Regexp
syncCount int64
deleteCount int64
failCount int64
ctx context.Context
cfg *syncv1alpha1.SyncConfig
conditions map[syncv1alpha1.SyncConfigConditionType]syncv1alpha1.SyncConfigCondition
matchNamesRegex []*regexp.Regexp
ignoreNamesRegex []*regexp.Regexp
nsSelector labels.Selector
syncCount int64
deleteCount int64
failCount int64
}
)

Expand Down Expand Up @@ -229,24 +231,7 @@ func (r *SyncConfigReconciler) getNamespaces(rc *ReconciliationContext) (namespa
return []corev1.Namespace{}, err
}

if rc.cfg.Spec.NamespaceSelector == nil {
r.Log.Info("spec.namespaceSelector was not given, using namespace from SyncConfig", getLoggingKeysAndValuesForSyncConfig(rc.cfg)...)
namespaces = []corev1.Namespace{namespaceFromString(rc.cfg.Namespace)}
return namespaces, nil
}
namespaces = includeNamespacesByNames(rc, namespaceList.Items)

labelSelector, err := metav1.LabelSelectorAsSelector(rc.cfg.Spec.NamespaceSelector.LabelSelector)
if err != nil {
return namespaces, err
}

for _, ns := range namespaceList.Items {
if labelSelector.Matches(labels.Set(ns.GetLabels())) {
namespaces = append(namespaces, ns)
}
}
return namespaces, err
return filterNamespaces(rc, namespaceList.Items), nil
}

func (r *SyncConfigReconciler) recreateObject(rc *ReconciliationContext, obj *unstructured.Unstructured) error {
Expand All @@ -267,14 +252,3 @@ func (r *SyncConfigReconciler) recreateObject(rc *ReconciliationContext, obj *un

return nil
}

func (r *SyncConfigReconciler) validateSpec(rc *ReconciliationContext) error {
for _, pattern := range rc.cfg.Spec.NamespaceSelector.MatchNames {
rgx, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf(".spec.matchNames pattern invalid: %w", err)
}
rc.matchNamesRegex = append(rc.matchNamesRegex, rgx)
}
return nil
}
18 changes: 7 additions & 11 deletions controllers/syncconfig_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ var _ = Describe("SyncConfig controller", func() {

By("reconciling sync config")
result, err := syncConfigReconciler.Reconcile(ctrl.Request{
NamespacedName: types.NamespacedName{Name: sc.Name, Namespace: sc.Namespace},
NamespacedName: toObjectKey(sc),
})
Expect(err).ToNot(HaveOccurred())
Expect(result).ToNot(BeNil())
Expand Down Expand Up @@ -152,7 +152,7 @@ var _ = Describe("SyncConfig controller", func() {

By("reconciling sync config")
result, err := syncConfigReconciler.Reconcile(ctrl.Request{
NamespacedName: types.NamespacedName{Name: sc.Name, Namespace: sc.Namespace},
NamespacedName: toObjectKey(sc),
})
Expect(err).ToNot(HaveOccurred())
Expect(result).ToNot(BeNil())
Expand Down Expand Up @@ -206,25 +206,21 @@ var _ = Describe("SyncConfig controller", func() {
syncConfigReconciler.WatchNamespace = ""
})

It("should not reconcile SyncConfig spec with invalid matchNames pattern", func() {
It("should not reconcile SyncConfig upon failed validation", func() {

By("setting up test resources")
ns := "invalid-regex"
ns := "validation-fail"
sourceNs := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}
sc := &SyncConfig{
ObjectMeta: metav1.ObjectMeta{Name: "test-syncconfig", Namespace: sourceNs.Name},
Spec: SyncConfigSpec{
NamespaceSelector: &NamespaceSelector{
MatchNames: []string{"["},
},
},
Spec: SyncConfigSpec{},
}
Expect(k8sClient.Create(context.Background(), sourceNs)).ToNot(HaveOccurred())
Expect(k8sClient.Create(context.Background(), sc)).ToNot(HaveOccurred())

By("reconciling sync config")
result, err := syncConfigReconciler.Reconcile(ctrl.Request{
NamespacedName: types.NamespacedName{Name: sc.Name, Namespace: sc.Namespace},
NamespacedName: toObjectKey(sc),
})
Expect(result.Requeue).To(BeFalse())

Expand All @@ -233,7 +229,7 @@ var _ = Describe("SyncConfig controller", func() {
err = k8sClient.Get(context.Background(), toObjectKey(newSC), newSC)
Expect(err).ToNot(HaveOccurred())
conditions := mapConditionsToType(newSC.Status.Conditions)
Expect(conditions[SyncConfigInvalid].Message).To(ContainSubstring("error parsing regexp"))
Expect(conditions).To(HaveKey(SyncConfigInvalid))
})
})

Expand Down
78 changes: 78 additions & 0 deletions controllers/syncconfig_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package controllers

import (
"fmt"
"github.com/vshn/espejo/api/v1alpha1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"regexp"
)

func (r *SyncConfigReconciler) validateSpec(rc *ReconciliationContext) error {
spec := rc.cfg.Spec
if hasNoNamespaceSelector(rc.cfg.Spec) {
return fmt.Errorf("either .spec.namespaceSelector.matchNames or .spec.namespaceSelector.labelSelector is required")
}
if len(spec.DeleteItems) == 0 && len(spec.SyncItems) == 0 {
return fmt.Errorf("either spec.deleteItems or .spec.syncItems is required")
}
for _, pattern := range spec.NamespaceSelector.MatchNames {
rgx, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf(".spec.namespaceSelector.matchNames pattern invalid: %w", err)
}
rc.matchNamesRegex = append(rc.matchNamesRegex, rgx)
}
for _, pattern := range spec.NamespaceSelector.IgnoreNames {
rgx, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf(".spec.namespaceSelector.ignoreNames pattern invalid: %w", err)
}
rc.ignoreNamesRegex = append(rc.ignoreNamesRegex, rgx)
}
if rc.cfg.Spec.NamespaceSelector.LabelSelector != nil {
labelSelector, err := metav1.LabelSelectorAsSelector(rc.cfg.Spec.NamespaceSelector.LabelSelector)
if err != nil {
return fmt.Errorf(".spec.namespaceSelector.labelSelector is invalid: %w", err)
}
rc.nsSelector = labelSelector
}

return nil
}

func filterNamespaces(rc *ReconciliationContext, namespaceList []v1.Namespace) (namespaces []v1.Namespace) {
NamespaceLoop:
for _, ns := range namespaceList {
for _, regex := range rc.ignoreNamesRegex {
if regex.MatchString(ns.Name) {
continue NamespaceLoop
}
}
if rc.nsSelector != nil && rc.nsSelector.Matches(labels.Set(ns.GetLabels())) {
namespaces = append(namespaces, ns)
continue NamespaceLoop
}
for _, regex := range rc.matchNamesRegex {
if regex.MatchString(ns.Name) {
namespaces = append(namespaces, ns)
continue NamespaceLoop
}
}
}
return namespaces
}

// isReconcileFailed returns true if no objects could be synced or deleted and failedCount is > 0
func isReconcileFailed(rc *ReconciliationContext) bool {
return rc.syncCount == 0 && rc.deleteCount == 0 && rc.failCount > 0
}

// hasNoNamespaceSelector will return true if the SyncConfigSpec does not have a valid namespace selector
func hasNoNamespaceSelector(spec v1alpha1.SyncConfigSpec) bool {
if spec.NamespaceSelector == nil {
return true
}
return spec.NamespaceSelector.LabelSelector == nil && len(spec.NamespaceSelector.MatchNames) == 0
}
92 changes: 92 additions & 0 deletions controllers/syncconfig_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package controllers

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/vshn/espejo/api/v1alpha1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"regexp"
)

var _ = Describe("SyncConfig utils", func() {
It("should include regular namespace name in selector", func() {
ns := "match-regular-namespace"

sourceNs := []v1.Namespace{namespaceFromString(ns)}
rc := ReconciliationContext{matchNamesRegex: []*regexp.Regexp{toRegex(ns)}}

result := filterNamespaces(&rc, sourceNs)
Expect(result).To(ConsistOf(sourceNs))
})

It("should include namespace that match a pattern", func() {
ns := "match-regex-namespace"

sourceNs := []v1.Namespace{namespaceFromString(ns)}

rc := ReconciliationContext{matchNamesRegex: []*regexp.Regexp{toRegex("match-.*")}}

result := filterNamespaces(&rc, sourceNs)
Expect(result).To(ConsistOf(sourceNs))
})

It("should exclude namespace that does not match a pattern", func() {
ns := "match-regex-namespace"

sourceNs := []v1.Namespace{namespaceFromString(ns)}

rc := ReconciliationContext{matchNamesRegex: []*regexp.Regexp{toRegex("match\\snamespaceWithSpace")}}

result := filterNamespaces(&rc, sourceNs)
Expect(result).To(BeEmpty())
})

It("should fail validation if invalid regex is specified in matchNames", func() {
cfg := &v1alpha1.SyncConfig{Spec: v1alpha1.SyncConfigSpec{
NamespaceSelector: &v1alpha1.NamespaceSelector{
MatchNames: []string{"["},
},
SyncItems: []unstructured.Unstructured{toUnstructured(&v1.ConfigMap{})},
}}
rc := ReconciliationContext{cfg: cfg}

err := syncConfigReconciler.validateSpec(&rc)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("error parsing regexp"))
})

It("should fail validation if invalid regex is specified in ignoreNames", func() {
cfg := &v1alpha1.SyncConfig{Spec: v1alpha1.SyncConfigSpec{
NamespaceSelector: &v1alpha1.NamespaceSelector{
IgnoreNames: []string{"["},
MatchNames: []string{".*"},
},
SyncItems: []unstructured.Unstructured{toUnstructured(&v1.ConfigMap{})},
}}
rc := ReconciliationContext{cfg: cfg}

err := syncConfigReconciler.validateSpec(&rc)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("error parsing regexp"))
})

It("should fail validation if no namespace selector is given", func() {
cfg := &v1alpha1.SyncConfig{Spec: v1alpha1.SyncConfigSpec{
NamespaceSelector: &v1alpha1.NamespaceSelector{
MatchNames: []string{},
},
}}
rc := ReconciliationContext{cfg: cfg}

err := syncConfigReconciler.validateSpec(&rc)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("labelSelector is required"))
})

})

func toRegex(pattern string) *regexp.Regexp {
rgx, _ := regexp.Compile(pattern)
return rgx
}
18 changes: 0 additions & 18 deletions controllers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,3 @@ func namespaceFromString(namespace string) v1.Namespace {
ObjectMeta: v12.ObjectMeta{Name: namespace},
}
}

func includeNamespacesByNames(rc *ReconciliationContext, namespaceList []v1.Namespace) (namespaces []v1.Namespace) {
NamespaceLoop:
for _, ns := range namespaceList {
for _, regex := range rc.matchNamesRegex {
if regex.MatchString(ns.Name) {
namespaces = append(namespaces, ns)
continue NamespaceLoop
}
}
}
return namespaces
}

// isReconcileFailed returns true if no objects could be synced or deleted and failedCount is > 0
func isReconcileFailed(rc *ReconciliationContext) bool {
return rc.syncCount == 0 && rc.deleteCount == 0 && rc.failCount > 0
}
Loading

0 comments on commit 16fe4c0

Please sign in to comment.