Skip to content

Commit

Permalink
Merge pull request #36 from vshn/ignore-names
Browse files Browse the repository at this point in the history
New property: IgnoreNames in NamespaceSelector
  • Loading branch information
ccremer authored Oct 14, 2020
2 parents f2c0513 + 16fe4c0 commit 7d4ddc4
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 7d4ddc4

Please sign in to comment.