Skip to content

Commit

Permalink
feat: toSelectorMatchFields
Browse files Browse the repository at this point in the history
  • Loading branch information
devthejo committed May 31, 2023
1 parent 7f3712a commit 2fc50f2
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 97 deletions.
30 changes: 24 additions & 6 deletions config/package-bundle/config/crds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 16 additions & 5 deletions docs/secret-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down Expand Up @@ -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

Expand Down
20 changes: 4 additions & 16 deletions examples/secret-export.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


55 changes: 30 additions & 25 deletions pkg/apis/secretgen2/v1alpha1/secret_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -74,40 +87,32 @@ 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 {
if len(ns) == 0 {
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)
}
56 changes: 49 additions & 7 deletions pkg/sharing/secret_exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
20 changes: 5 additions & 15 deletions pkg/sharing/secret_import_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 8 additions & 23 deletions test/e2e/secret_exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 := `
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 2fc50f2

Please sign in to comment.