diff --git a/k8sdeps/kunstruct/kunstruct.go b/k8sdeps/kunstruct/kunstruct.go index 24cad01e1..dc3a100d6 100644 --- a/k8sdeps/kunstruct/kunstruct.go +++ b/k8sdeps/kunstruct/kunstruct.go @@ -20,6 +20,8 @@ package kunstruct import ( "encoding/json" "fmt" + + "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/kustomize/v3/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -267,3 +269,19 @@ func (fs *UnstructAdapter) GetMap(path string) (map[string]interface{}, error) { } return nil, types.NoFieldError{Field: path} } + +func (fs *UnstructAdapter) MatchesLabelSelector(selector string) (bool, error) { + s, err := labels.Parse(selector) + if err != nil { + return false, err + } + return s.Matches(labels.Set(fs.GetLabels())), nil +} + +func (fs *UnstructAdapter) MatchesAnnotationSelector(selector string) (bool, error) { + s, err := labels.Parse(selector) + if err != nil { + return false, err + } + return s.Matches(labels.Set(fs.GetAnnotations())), nil +} diff --git a/pkg/ifc/ifc.go b/pkg/ifc/ifc.go index 190d77aaa..3c839d962 100644 --- a/pkg/ifc/ifc.go +++ b/pkg/ifc/ifc.go @@ -61,6 +61,8 @@ type Kunstructured interface { SetLabels(map[string]string) GetAnnotations() map[string]string SetAnnotations(map[string]string) + MatchesLabelSelector(selector string) (bool, error) + MatchesAnnotationSelector(selector string) (bool, error) } // KunstructuredFactory makes instances of Kunstructured. diff --git a/pkg/resmap/resmap.go b/pkg/resmap/resmap.go index 3d09595ad..9c9e194fc 100644 --- a/pkg/resmap/resmap.go +++ b/pkg/resmap/resmap.go @@ -8,6 +8,8 @@ package resmap import ( "bytes" "fmt" + "regexp" + "github.com/pkg/errors" "sigs.k8s.io/kustomize/v3/pkg/resid" @@ -160,6 +162,10 @@ type ResMap interface { // Debug prints the ResMap. Debug(title string) + + // Select returns a list of resources that + // are selected by a Selector + Select(types.Selector) ([]*resource.Resource, error) } // resWrangler holds the content manipulated by kustomize. @@ -600,3 +606,66 @@ func (m *resWrangler) appendReplaceOrMerge( } return nil } + +// Select returns a list of resources that +// are selected by a Selector +func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { + ns := regexp.MustCompile(s.Namespace) + nm := regexp.MustCompile(s.Name) + var result []*resource.Resource + for _, r := range m.Resources() { + curId := r.CurId() + orgId := r.OrgId() + + // matches the namespace when namespace is not empty in the selector + // It first tries to match with the original namespace + // then matches with the current namespace + if r.GetNamespace() != "" { + matched := ns.MatchString(orgId.EffectiveNamespace()) + if !matched { + matched = ns.MatchString(curId.EffectiveNamespace()) + if !matched { + continue + } + } + } + + // matches the name when name is not empty in the selector + // It first tries to match with the original name + // then matches with the current name + if r.GetName() != "" { + matched := nm.MatchString(orgId.Name) + if !matched { + matched = nm.MatchString(curId.Name) + if !matched { + continue + } + } + } + + // matches the GVK + if !r.GetGvk().IsSelected(&s.Gvk) { + continue + } + + // matches the label selector + matched, err := r.MatchesLabelSelector(s.LabelSelector) + if err != nil { + return nil, err + } + if !matched { + continue + } + + // matches the annotation selector + matched, err = r.MatchesAnnotationSelector(s.AnnotationSelector) + if err != nil { + return nil, err + } + if !matched { + continue + } + result = append(result, r) + } + return result, nil +} diff --git a/pkg/resmap/selector_test.go b/pkg/resmap/selector_test.go new file mode 100644 index 000000000..e1b078e09 --- /dev/null +++ b/pkg/resmap/selector_test.go @@ -0,0 +1,130 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2. + +package resmap_test + +import ( + "testing" + + "sigs.k8s.io/kustomize/v3/pkg/gvk" + "sigs.k8s.io/kustomize/v3/pkg/resmap" + "sigs.k8s.io/kustomize/v3/pkg/types" +) + +func setupRMForPatchTargets(t *testing.T) resmap.ResMap { + result, err := rmF.NewResMapFromBytes([]byte(` +apiVersion: group1/v1 +kind: Kind1 +metadata: + name: name1 + namespace: ns1 + labels: + app: name1 + annotations: + foo: bar +--- +apiVersion: group1/v1 +kind: Kind1 +metadata: + name: name2 + namespace: default + labels: + app: name2 + annotations: + foo: bar +--- +apiVersion: group1/v1 +kind: Kind2 +metadata: + name: name3 + labels: + app: name3 + annotations: + bar: baz +`)) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + return result +} + +func TestFindPatchTargets(t *testing.T) { + rm := setupRMForPatchTargets(t) + testcases := []struct { + target types.Selector + count int + }{ + { + target: types.Selector{ + Name: "name*", + }, + count: 3, + }, + { + target: types.Selector{ + Name: "name*", + AnnotationSelector: "foo=bar", + }, + count: 2, + }, + { + target: types.Selector{ + LabelSelector: "app=name1", + }, + count: 1, + }, + { + target: types.Selector{ + Gvk: gvk.Gvk{ + Kind: "Kind1", + }, + Name: "name*", + }, + count: 2, + }, + { + target: types.Selector{ + Name: "NotMatched", + }, + count: 0, + }, + { + target: types.Selector{ + Name: "", + }, + count: 3, + }, + { + target: types.Selector{ + Namespace: "default", + }, + count: 2, + }, + { + target: types.Selector{ + Namespace: "", + }, + count: 3, + }, + { + target: types.Selector{ + Namespace: "default", + Name: "name*", + Gvk: gvk.Gvk{ + Kind: "Kind1", + }, + }, + count: 1, + }, + } + for _, testcase := range testcases { + actual, err := rm.Select(testcase.target) + if err != nil { + t.Errorf("unexpected error %v", err) + } + if len(actual) != testcase.count { + t.Errorf("expected %d objects, but got %d:\n%v", testcase.count, len(actual), actual) + } + } + +} diff --git a/pkg/types/kustomization.go b/pkg/types/kustomization.go index 6e61606ab..0d46fd90d 100644 --- a/pkg/types/kustomization.go +++ b/pkg/types/kustomization.go @@ -386,13 +386,13 @@ type Patch struct { Patch string `json:"patch,omitempty" yaml:"patch,omitempty"` // Target points to the resources that the patch is applied to - Target Targets `json:"target,omitempty" yaml:"target,omitempty"` + Target Selector `json:"target,omitempty" yaml:"target,omitempty"` } -// Targets specifies a set of resources. +// Selector specifies a set of resources. // Any resource that matches intersection of all conditions // is included in this set. -type Targets struct { +type Selector struct { gvk.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"`