diff --git a/config/package-bundle/config/crds.yml b/config/package-bundle/config/crds.yml index 769dabb30..75be57b63 100644 --- a/config/package-bundle/config/crds.yml +++ b/config/package-bundle/config/crds.yml @@ -40,12 +40,30 @@ spec: items: type: string type: array - toNamespaceAnnotation: - type: object - additionalProperties: true - toNamespaceAnnotations: - type: object - additionalProperties: true + toSelectorMatchFields: + type: array + items: + properties: + key: + type: string + description: Property to target on the resource for the match. It's support dot notation. + operator: + type: string + description: Type of comparison. + enum: + - In + - NotIn + - Exists + - DoesNotExist + values: + description: Values to match on the resource key using the comparison operator. + type: array + items: + type: string + type: object + required: + - key + - operator type: object status: properties: diff --git a/docs/secret-export.md b/docs/secret-export.md index 6c2e44238..ee451aeb7 100644 --- a/docs/secret-export.md +++ b/docs/secret-export.md @@ -45,9 +45,10 @@ metadata: namespace: user1 spec: toNamespace: user2 - toNamespaceAnnotations: - field.cattle.io/projectId: - - "cluster1:project1" + toSelectorMatchFields: + - key: metadata.annotations.field\\.cattle\\.io/projectId: + operator: In + value: "cluster1:project1" #! allow user-password to be created in user2 namespace --- @@ -96,8 +97,18 @@ SecretExport CRD allows to "offer" secrets for export. - `toNamespace` (optional; string) Destination namespace for offer. Use `*` to indicate all namespaces. - `toNamespaces` (optional; array of strings) List of destination namespaces for offer. -- `toNamespaceAnnotation` (optional; annotation map with single string value) List of destination namespaces annotations key/value for offer. -- `toNamespaceAnnotations` (optional; annotation map with array of strings value) List of destination namespaces annotations key/values for offer. +- `toSelectorMatchFields` (optional; array of selector objects) List of matchers for destination namespaces. If multiple expressions are specified, all those expressions must evaluate to true for the selector to match a namespace. The selector object is composed as follows: + - `key` (required; string) Property to target on the resource for the match. It's support dot notation from [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md). + - `operator` (required; enum string) Type of comparison. Must be one of `In`, `NotIn`, `Exists`, `DoesNotExist`. + Operator explanations: + - In: Label's value must match one of the specified values. + - NotIn: Label's value must not match any of the specified values. + - Exists: Pod must include a label with the specified key (the value isn't important). When using this operator, the values field should not be specified. + - NotIn: Label's value must not match any of the specified values. + - Exists: Pod must include a label with the specified key (the value isn't important). When using this operator, the values field should not be specified. + - DoesNotExist: Pod must not include a label with the specified key. The values property must not be specified. + + - `values` (optional; array if string) Values to match on the resource key using the comparison operator. ### SecretImport diff --git a/examples/secret-export.yml b/examples/secret-export.yml index 7f1883b00..acf8c0363 100644 --- a/examples/secret-export.yml +++ b/examples/secret-export.yml @@ -105,11 +105,10 @@ metadata: name: scoped-user-password-multi namespace: user1 spec: - toNamespaceAnnotation: - field.cattle.io/projectId: "cluster1:project1" - toNamespaceAnnotations: - field.cattle.io/projectId: - - "cluster1:project2" + toSelectorMatchFields: + - key: metadata.annotations.field\\.cattle\\.io/projectId + operator: In + value: "cluster1:project1" --- apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretImport @@ -120,15 +119,4 @@ metadata: field.cattle.io/projectId: "cluster1:project1" spec: fromNamespace: user1 ---- -apiVersion: secretgen.carvel.dev/v1alpha1 -kind: SecretImport -metadata: - name: scoped-user-password-multi - namespace: user3 - annotations: - field.cattle.io/projectId: "cluster1:project2" -spec: - fromNamespace: user1 - diff --git a/pkg/apis/secretgen2/v1alpha1/secret_export.go b/pkg/apis/secretgen2/v1alpha1/secret_export.go index e65248ed1..814bdeec1 100644 --- a/pkg/apis/secretgen2/v1alpha1/secret_export.go +++ b/pkg/apis/secretgen2/v1alpha1/secret_export.go @@ -39,15 +39,28 @@ type SecretExportList struct { Items []SecretExport `json:"items"` } +type SelectorOperator string + +const ( + SelectorOperatorIn SelectorOperator = "In" + SelectorOperatorNotIn = "NotIn" + SelectorOperatorExists = "Exists" + SelectorOperatorDoesNotExist = "DoesNotExist" +) + +type SelectorMatchField struct { + Key string + Operator SelectorOperator + Values []string +} + type SecretExportSpec struct { // +optional ToNamespace string `json:"toNamespace,omitempty"` // +optional ToNamespaces []string `json:"toNamespaces,omitempty"` // +optional - ToNamespaceAnnotation map[string]string `json:"toNamespaceAnnotation,omitempty"` - // +optional - ToNamespaceAnnotations map[string][]string `json:"toNamespaceAnnotations,omitempty"` + ToSelectorMatchFields []SelectorMatchField `json:"toSelectorMatchFields,omitempty"` } type SecretExportStatus struct { @@ -74,33 +87,13 @@ func (e SecretExport) StaticToNamespaces() []string { return result } -// StaticToNamespacesAnnotations aggregate toNamespaceAnnotation and toNamespaceAnnotations as a single slice -func (e SecretExport) StaticToNamespacesAnnotations() []*SecretExportAnnotation { - var result []*SecretExportAnnotation - for k, v := range e.Spec.ToNamespaceAnnotation { - result = append(result, &SecretExportAnnotation{ - Key: k, - Value: v, - }) - } - for k, v := range e.Spec.ToNamespaceAnnotations { - for _, value := range v { - result = append(result, &SecretExportAnnotation{ - Key: k, - Value: value, - }) - } - } - return result -} - func (e SecretExport) Validate() error { var errs []error toNses := e.StaticToNamespaces() - toNsesA := e.StaticToNamespacesAnnotations() + toSmf := e.Spec.ToSelectorMatchFields - if len(toNses) == 0 && len(toNsesA) == 0 { + if len(toNses) == 0 && len(toSmf) == 0 { errs = append(errs, fmt.Errorf("Expected to have at least one non-empty to namespace or to namespace annotation")) } for _, ns := range toNses { @@ -108,6 +101,18 @@ func (e SecretExport) Validate() error { errs = append(errs, fmt.Errorf("Expected to namespace to be non-empty")) } } + for _, s := range toSmf { + switch s.Operator { + case SelectorOperatorIn, SelectorOperatorNotIn: + if len(s.Values) == 0 { + errs = append(errs, fmt.Errorf("Values must be specified when `operator` is 'In' or 'NotIn'")) + } + case SelectorOperatorExists, SelectorOperatorDoesNotExist: + if len(s.Values) > 0 { + errs = append(errs, fmt.Errorf("Values may not be specified when `operator` is 'Exists' or 'DoesNotExist'")) + } + } + } return combinedErrs("Validation errors", errs) } diff --git a/pkg/sharing/secret_exports.go b/pkg/sharing/secret_exports.go index c97b54e85..1ce2e4ab1 100644 --- a/pkg/sharing/secret_exports.go +++ b/pkg/sharing/secret_exports.go @@ -4,14 +4,18 @@ package sharing import ( + "context" + "encoding/json" "fmt" "sort" "strconv" "sync" "github.com/go-logr/logr" + "github.com/tidwall/gjson" sg2v1alpha1 "github.com/vmware-tanzu/carvel-secretgen-controller/pkg/apis/secretgen2/v1alpha1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" ) const ( @@ -81,12 +85,13 @@ type SecretMatcher struct { FromName string FromNamespace string - FromNamespaceAnnotations map[string]string - ToNamespace string Subject string SecretType corev1.SecretType + + SecretImportReconciler *SecretImportReconciler + Ctx context.Context } // MatchedSecretsForImport filters secrets export cache by the given criteria. @@ -182,14 +187,51 @@ func (es exportedSecret) Matches(matcher SecretMatcher, nsIsExcludedFromWildcard } } - nsAnnotations := es.export.StaticToNamespacesAnnotations() - if len(nsAnnotations) > 0 { - for _, nsAnnotation := range nsAnnotations { - if matcher.FromNamespaceAnnotations[nsAnnotation.Key] == nsAnnotation.Value { - return true + selectors := es.export.Spec.ToSelectorMatchFields + if len(selectors) > 0 { + nsName := matcher.ToNamespace + query := types.NamespacedName{ + Name: nsName, + } + namespace := corev1.Namespace{} + err := matcher.SecretImportReconciler.client.Get(matcher.Ctx, query, &namespace) + if err == nil { + jsonNs, _ := json.Marshal(namespace) + for _, s := range selectors { + switch s.Operator { + case sg2v1alpha1.SelectorOperatorIn: + value := gjson.GetBytes(jsonNs, s.Key).String() + found := false + for _, svalue := range s.Values { + if svalue == value { + found = true + break + } + } + if !found { + return false + } + case sg2v1alpha1.SelectorOperatorNotIn: + value := gjson.GetBytes(jsonNs, s.Key).String() + for _, svalue := range s.Values { + if svalue == value { + return false + } + } + case sg2v1alpha1.SelectorOperatorExists: + if !gjson.GetBytes(jsonNs, s.Key).Exists() { + return false + } + case sg2v1alpha1.SelectorOperatorDoesNotExist: + if gjson.GetBytes(jsonNs, s.Key).Exists() { + return false + } + } } + return true } } + if !es.matchesNamespace(matcher.ToNamespace, nsIsExcludedFromWildcard) { return false } diff --git a/pkg/sharing/secret_import_reconciler.go b/pkg/sharing/secret_import_reconciler.go index e2987ed2d..625f5216a 100644 --- a/pkg/sharing/secret_import_reconciler.go +++ b/pkg/sharing/secret_import_reconciler.go @@ -180,22 +180,12 @@ func (r *SecretImportReconciler) reconcile( log.Info("Reconciling") - nsName := secretImport.Namespace - query := types.NamespacedName{ - Name: nsName, - } - namespace := corev1.Namespace{} - err = r.client.Get(ctx, query, &namespace) - var fromNamespaceAnnotations map[string]string - if err == nil { - fromNamespaceAnnotations = namespace.GetAnnotations() - } - matcher := SecretMatcher{ - FromName: secretImport.Name, - FromNamespace: secretImport.Spec.FromNamespace, - FromNamespaceAnnotations: fromNamespaceAnnotations, - ToNamespace: secretImport.Namespace, + FromName: secretImport.Name, + FromNamespace: secretImport.Spec.FromNamespace, + ToNamespace: secretImport.Namespace, + SecretImportReconciler: r, + Ctx: ctx, } nscheck := makeNamespaceWildcardExclusionCheck(ctx, r.client, log) diff --git a/test/e2e/secret_exports_test.go b/test/e2e/secret_exports_test.go index 2b6278a0e..49bb3289f 100644 --- a/test/e2e/secret_exports_test.go +++ b/test/e2e/secret_exports_test.go @@ -44,13 +44,6 @@ metadata: field.cattle.io/projectId: "cluster1:project1" --- apiVersion: v1 -kind: Namespace -metadata: - name: sg-test5 - annotations: - field.cattle.io/projectId: "cluster1:project2" ---- -apiVersion: v1 kind: Secret metadata: name: secret @@ -70,11 +63,11 @@ spec: toNamespaces: - sg-test2 - sg-test3 - toNamespaceAnnotation: - field.cattle.io/projectId: "cluster1:project1" - toNamespaceAnnotations: - field.cattle.io/projectId: - - "cluster1:project2" + toSelectorMatchFields: + - key: "metadata.annotations.field\\.cattle\\.io/projectId" + operator: In + values: + - "cluster1:project1" --- apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretImport @@ -99,14 +92,6 @@ metadata: namespace: sg-test4 spec: fromNamespace: sg-test1 ---- -apiVersion: secretgen.carvel.dev/v1alpha1 -kind: SecretImport -metadata: - name: secret - namespace: sg-test5 -spec: - fromNamespace: sg-test1 ` yaml2 := ` @@ -138,7 +123,7 @@ stringData: }) logger.Section("Check imported secrets were created", func() { - for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5"} { + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4"} { out := waitForSecretInNs(t, kubectl, ns, "secret") var secret corev1.Secret @@ -171,7 +156,7 @@ stringData: // TODO proper waiting time.Sleep(5 * time.Second) - for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5"} { + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4"} { out := waitForSecretInNs(t, kubectl, ns, "secret") var secret corev1.Secret @@ -202,7 +187,7 @@ stringData: // TODO proper waiting time.Sleep(5 * time.Second) - for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5"} { + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4"} { _, err := kubectl.RunWithOpts([]string{"get", "secret", "secret", "-n", ns}, RunOpts{AllowError: true, NoNamespace: true}) require.Error(t, err)