diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 548b246b3..0b22f2eba 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -88,7 +88,7 @@ func main() { { secretExports := sharing.NewSecretExportsWarmedUp( - sharing.NewSecretExports(log.WithName("secretexports"))) + sharing.NewSecretExports(mgr.GetClient(), log.WithName("secretexports"))) secretExportReconciler := sharing.NewSecretExportReconciler( mgr.GetClient(), secretExports, log.WithName("secexp")) diff --git a/config/package-bundle/config/crds.yml b/config/package-bundle/config/crds.yml index 49924bdaf..17f67346a 100644 --- a/config/package-bundle/config/crds.yml +++ b/config/package-bundle/config/crds.yml @@ -40,6 +40,30 @@ spec: items: type: string type: array + dangerousToNamespacesSelector: + type: array + items: + properties: + key: + type: string + description: Property to target the resource for the match. It supports 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 462db5dd5..c0e69f24b 100644 --- a/docs/secret-export.md +++ b/docs/secret-export.md @@ -20,6 +20,13 @@ apiVersion: v1 kind: Namespace metadata: name: user2 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: user3 + annotations: + field.cattle.io/projectId: "cluster1:project1" #! generate user-password secret upon creation --- @@ -29,7 +36,7 @@ metadata: name: user-password namespace: user1 -#! offer user-password to user2 namespace +#! offer user-password to user2 namespace and namespace with specified annotations (in this case user3) --- apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretExport @@ -38,6 +45,10 @@ metadata: namespace: user1 spec: toNamespace: user2 + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: In + value: "cluster1:project1" #! allow user-password to be created in user2 namespace --- @@ -48,6 +59,17 @@ metadata: namespace: user2 spec: fromNamespace: user1 + +#! allow user-password to be created in namespace user3 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: user-password + namespace: user3 +spec: + fromNamespace: user1 + ``` Above configuration results in a `user-password` Secret created within `user2` namespace: @@ -75,6 +97,16 @@ 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. +- `dangerousToNamespacesSelector` (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. Based on kubernetes JSONPath syntax. + - `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: Namespace must include a label with the specified key (the value isn't relevant). When using this operator, the values field should not be specified. + - DoesNotExist: Namespace 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 9d22fe832..ba0c476b6 100644 --- a/examples/secret-export.yml +++ b/examples/secret-export.yml @@ -90,3 +90,33 @@ metadata: namespace: user3 spec: fromNamespace: user1 + +#! export user-password-multi secret from user1 to namespaces containing specific annotations +--- +apiVersion: secretgen.k14s.io/v1alpha1 +kind: Password +metadata: + name: scoped-user-password-multi + namespace: user1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretExport +metadata: + name: scoped-user-password-multi + namespace: user1 +spec: + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: In + value: "cluster1:project1" +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: scoped-user-password-multi + namespace: user2 + annotations: + field.cattle.io/projectId: "cluster1:project1" +spec: + fromNamespace: user1 + diff --git a/pkg/apis/secretgen2/v1alpha1/secret_export.go b/pkg/apis/secretgen2/v1alpha1/secret_export.go index 7f06df54d..249328c9b 100644 --- a/pkg/apis/secretgen2/v1alpha1/secret_export.go +++ b/pkg/apis/secretgen2/v1alpha1/secret_export.go @@ -39,11 +39,31 @@ type SecretExportList struct { Items []SecretExport `json:"items"` } +// SelectorOperator is a part of SelectorMatchField +type SelectorOperator string + +// SelectorOperator values +const ( + SelectorOperatorIn SelectorOperator = "In" + SelectorOperatorNotIn = "NotIn" + SelectorOperatorExists = "Exists" + SelectorOperatorDoesNotExist = "DoesNotExist" +) + +// SelectorMatchField is a selector field to match against namespace definition +type SelectorMatchField struct { + Key string `json:"key,omitempty"` + Operator SelectorOperator `json:"operator,omitempty"` + Values []string `json:"values,omitempty"` +} + type SecretExportSpec struct { // +optional ToNamespace string `json:"toNamespace,omitempty"` // +optional ToNamespaces []string `json:"toNamespaces,omitempty"` + // +optional + ToNamespacesSelector []SelectorMatchField `json:"dangerousToNamespacesSelector,omitempty"` } type SecretExportStatus struct { @@ -68,15 +88,28 @@ func (e SecretExport) Validate() error { var errs []error toNses := e.StaticToNamespaces() + toSmf := e.Spec.ToNamespacesSelector - if len(toNses) == 0 { - errs = append(errs, fmt.Errorf("Expected to have at least one non-empty to namespace")) + 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) } diff --git a/pkg/sharing/import_secret_test.go b/pkg/sharing/import_secret_test.go index 59938e4ef..2ee1c16fd 100644 --- a/pkg/sharing/import_secret_test.go +++ b/pkg/sharing/import_secret_test.go @@ -69,7 +69,7 @@ func secretImportFor(sourceSecret corev1.Secret) sg2v1alpha1.SecretImport { func importReconcilers(objects ...runtime.Object) (secretExportReconciler *sharing.SecretExportReconciler, secretImportReconciler *sharing.SecretImportReconciler, k8sClient client.Client) { k8sClient = fakeClient.NewFakeClient(objects...) - secretExports := sharing.NewSecretExportsWarmedUp(sharing.NewSecretExports(testLogr)) + secretExports := sharing.NewSecretExportsWarmedUp(sharing.NewSecretExports(k8sClient, testLogr)) secretExportReconciler = sharing.NewSecretExportReconciler(k8sClient, secretExports, testLogr) secretExports.WarmUpFunc = secretExportReconciler.WarmUp secretImportReconciler = sharing.NewSecretImportReconciler(k8sClient, secretExports, testLogr) diff --git a/pkg/sharing/placeholder_secret_test.go b/pkg/sharing/placeholder_secret_test.go index 2ad68185d..f817827a4 100644 --- a/pkg/sharing/placeholder_secret_test.go +++ b/pkg/sharing/placeholder_secret_test.go @@ -220,7 +220,7 @@ func Test_SecretReconciler_updatesStatus(t *testing.T) { } func placeholderReconcilers(objects ...runtime.Object) (secretExportReconciler *sharing.SecretExportReconciler, secretReconciler *sharing.SecretReconciler, k8sClient client.Client) { k8sClient = fakeClient.NewFakeClient(objects...) - secretExports := sharing.NewSecretExportsWarmedUp(sharing.NewSecretExports(testLogr)) + secretExports := sharing.NewSecretExportsWarmedUp(sharing.NewSecretExports(k8sClient, testLogr)) secretExportReconciler = sharing.NewSecretExportReconciler(k8sClient, secretExports, testLogr) secretExports.WarmUpFunc = secretExportReconciler.WarmUp secretReconciler = sharing.NewSecretReconciler(k8sClient, secretExports, testLogr) diff --git a/pkg/sharing/secret_exports.go b/pkg/sharing/secret_exports.go index faa1f6196..05cf346b7 100644 --- a/pkg/sharing/secret_exports.go +++ b/pkg/sharing/secret_exports.go @@ -4,6 +4,9 @@ package sharing import ( + "bytes" + "context" + "encoding/json" "fmt" "sort" "strconv" @@ -12,6 +15,9 @@ import ( "github.com/go-logr/logr" sg2v1alpha1 "github.com/vmware-tanzu/carvel-secretgen-controller/pkg/apis/secretgen2/v1alpha1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/jsonpath" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -38,17 +44,23 @@ type SecretExportsProvider interface { // (SecretExports is used by SecretExportReconciler to export/unexport secrets; // SecretExports is used by SecretReconciler to determine imported secrets.) type SecretExports struct { - log logr.Logger + log logr.Logger + k8sReader K8sReader exportedSecretsLock sync.RWMutex exportedSecrets map[string]exportedSecret } +// K8sReader is an interface for reading Kubernetes resources. +type K8sReader interface { + Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error +} + var _ SecretExportsProvider = &SecretExports{} // NewSecretExports constructs new SecretExports cache. -func NewSecretExports(log logr.Logger) *SecretExports { - return &SecretExports{log: log, exportedSecrets: map[string]exportedSecret{}} +func NewSecretExports(k8sReader K8sReader, log logr.Logger) *SecretExports { + return &SecretExports{log: log, k8sReader: k8sReader, exportedSecrets: map[string]exportedSecret{}} } // Export adds the in-memory representation (cached) @@ -85,6 +97,75 @@ type SecretMatcher struct { Subject string SecretType corev1.SecretType + + Ctx context.Context +} + +// NamespacesMatcher allows to specify criteria for matching exported secrets based on namespaces fields. +type NamespacesMatcher struct { + Selectors []sg2v1alpha1.SelectorMatchField +} + +// MatchNamespace returns true if the given SecretMatcher matches one of the Selectors. +func (nm NamespacesMatcher) MatchNamespace(matcher SecretMatcher, log logr.Logger, k8sReader K8sReader) bool { + + selectors := nm.Selectors + nsName := matcher.ToNamespace + query := types.NamespacedName{ + Name: nsName, + } + namespace := corev1.Namespace{} + err := k8sReader.Get(matcher.Ctx, query, &namespace) + + jsonNsString, _ := json.Marshal(namespace) + var jsonNsObject interface{} + json.Unmarshal(jsonNsString, &jsonNsObject) + + if err != nil { + log.Error(err, fmt.Sprintf("failed to get namespace %s", nsName)) + } + for _, s := range selectors { + jp := jsonpath.New("jsonpath") + + jsonPathKey := "{." + s.Key + "}" + err := jp.Parse(jsonPathKey) + if err != nil { + log.Error(err, fmt.Sprintf("invalid jsonpath: %s", jsonPathKey)) + return false + } + var valueBuffer bytes.Buffer + err = jp.Execute(&valueBuffer, jsonNsObject) + value := valueBuffer.String() + + switch s.Operator { + case sg2v1alpha1.SelectorOperatorIn: + found := false + for _, svalue := range s.Values { + if svalue == value { + found = true + break + } + } + if !found { + return false + } + case sg2v1alpha1.SelectorOperatorNotIn: + for _, svalue := range s.Values { + if svalue == value { + return false + } + } + case sg2v1alpha1.SelectorOperatorExists: + if value == "" { + return false + } + case sg2v1alpha1.SelectorOperatorDoesNotExist: + if value != "" { + return false + } + } + } + return true } // MatchedSecretsForImport filters secrets export cache by the given criteria. @@ -104,7 +185,7 @@ func (se *SecretExports) MatchedSecretsForImport(matcher SecretMatcher, nsIsExcl var matched []exportedSecret for _, exportedSec := range se.exportedSecrets { - if exportedSec.Matches(matcher, nsIsExcludedFromWildcard, se.log) { + if exportedSec.Matches(matcher, nsIsExcludedFromWildcard, se.log, se.k8sReader) { matched = append(matched, exportedSec) } } @@ -124,8 +205,9 @@ func (se *SecretExports) MatchedSecretsForImport(matcher SecretMatcher, nsIsExcl // exportedSecret is used for keeping track export->secret pair. type exportedSecret struct { - export *sg2v1alpha1.SecretExport - secret *corev1.Secret + export *sg2v1alpha1.SecretExport + secret *corev1.Secret + namespacesMatcher NamespacesMatcher } func newExportedSecret(export *sg2v1alpha1.SecretExport, secret *corev1.Secret) exportedSecret { @@ -144,7 +226,10 @@ func newExportedSecret(export *sg2v1alpha1.SecretExport, secret *corev1.Secret) } secret = secret.DeepCopy() } - return exportedSecret{export.DeepCopy(), secret} + + namespacesMatcher := NamespacesMatcher{Selectors: export.Spec.ToNamespacesSelector} + + return exportedSecret{export.DeepCopy(), secret, namespacesMatcher} } func (es exportedSecret) Key() string { @@ -155,12 +240,14 @@ func (es exportedSecret) Secret() *corev1.Secret { return es.secret.DeepCopy() } -func (es exportedSecret) Matches(matcher SecretMatcher, nsIsExcludedFromWildcard NamespaceWildcardExclusionCheck, log logr.Logger) bool { +func (es exportedSecret) Matches(matcher SecretMatcher, nsIsExcludedFromWildcard NamespaceWildcardExclusionCheck, log logr.Logger, k8sReader K8sReader) bool { + if matcher.Subject != "" { // TODO we currently do not match by subject log.Info("Warning: Matcher has empty subject and will never match any secret") return false } + if len(matcher.SecretType) > 0 { if matcher.SecretType != es.secret.Type { return false @@ -171,15 +258,24 @@ func (es exportedSecret) Matches(matcher SecretMatcher, nsIsExcludedFromWildcard return false } } + if len(matcher.FromNamespace) > 0 { if matcher.FromNamespace != es.secret.Namespace { return false } } - if !es.matchesNamespace(matcher.ToNamespace, nsIsExcludedFromWildcard) { - return false + + namespacesMatcher := es.namespacesMatcher + + if es.matchesNamespace(matcher.ToNamespace, nsIsExcludedFromWildcard) { + return true } - return true + + if len(namespacesMatcher.Selectors) > 0 { + return namespacesMatcher.MatchNamespace(matcher, log, k8sReader) + } + + return false } func (es exportedSecret) matchesNamespace(nsToMatch string, nsIsExcludedFromWildcard NamespaceWildcardExclusionCheck) bool { diff --git a/pkg/sharing/secret_exports_test.go b/pkg/sharing/secret_exports_test.go index 79cbd39dd..c9273ed10 100644 --- a/pkg/sharing/secret_exports_test.go +++ b/pkg/sharing/secret_exports_test.go @@ -13,12 +13,12 @@ import ( "github.com/vmware-tanzu/carvel-secretgen-controller/pkg/sharing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestSecretExports(t *testing.T) { - t.Run("matching", func(t *testing.T) { - se := sharing.NewSecretExports(logr.Discard()) + t.Run("matching", func(t *testing.T) { // Namespace does not match secret1 := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: "secret1", Namespace: "ns1"}, @@ -31,7 +31,6 @@ func TestSecretExports(t *testing.T) { ToNamespace: "wrong-ns", }, } - se.Export(export1, secret1) // Secret type does not match secret2 := &corev1.Secret{ @@ -45,6 +44,12 @@ func TestSecretExports(t *testing.T) { ToNamespace: "dst-ns", }, } + + k8sClient := fakeClient.NewFakeClient(secret1, secret2, export1, export2) + se := sharing.NewSecretExports(k8sClient, logr.Discard()) + + se.Export(export1, secret1) + se.Export(export2, secret2) nsCheck := func(string) bool { return false } @@ -177,10 +182,13 @@ func TestSecretExports(t *testing.T) { }) t.Run("returns secrets in specific order (last secret is most preferred)", func(t *testing.T) { - se := sharing.NewSecretExports(logr.Discard()) // higher weight, but name comes earlier - se.Export(&sg2v1alpha1.SecretExport{ + secret1 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret1-highest-weight", Namespace: "ns1"}, + Type: "Opaque", + } + export1 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret1-highest-weight", Namespace: "ns1", @@ -189,13 +197,14 @@ func TestSecretExports(t *testing.T) { }, }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "*"}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret1-highest-weight", Namespace: "ns1"}, - Type: "Opaque", - }) + } // higher weight, but name comes later - se.Export(&sg2v1alpha1.SecretExport{ + secret2 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret1a-highest-weight", Namespace: "ns1"}, + Type: "Opaque", + } + export2 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret1a-highest-weight", Namespace: "ns1", @@ -204,12 +213,13 @@ func TestSecretExports(t *testing.T) { }, }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "*"}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret1a-highest-weight", Namespace: "ns1"}, - Type: "Opaque", - }) + } - se.Export(&sg2v1alpha1.SecretExport{ + secret3 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret2-low-weight", Namespace: "ns1"}, + Type: "Opaque", + } + export3 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret2-low-weight", Namespace: "ns1", @@ -218,58 +228,76 @@ func TestSecretExports(t *testing.T) { }, }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "*"}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret2-low-weight", Namespace: "ns1"}, - Type: "Opaque", - }) + } // Wildcard ns match - se.Export(&sg2v1alpha1.SecretExport{ + secret4 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret3-diff-ns-wildcard-ns", Namespace: "ns1"}, + Type: "Opaque", + } + export4 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret3-diff-ns-wildcard-ns", Namespace: "ns1", }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "*"}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret3-diff-ns-wildcard-ns", Namespace: "ns1"}, - Type: "Opaque", - }) + } // Specific ns match (even though there is a wildcard as well) - se.Export(&sg2v1alpha1.SecretExport{ + secret5 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret4-diff-ns-specific-ns", Namespace: "ns1"}, + Type: "Opaque", + } + export5 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret4-diff-ns-specific-ns", Namespace: "ns1", }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "dst-ns", ToNamespaces: []string{"*"}}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret4-diff-ns-specific-ns", Namespace: "ns1"}, - Type: "Opaque", - }) + } + + k8sClient := fakeClient.NewFakeClient(secret1, secret2, export1, export2) + se := sharing.NewSecretExports(k8sClient, logr.Discard()) // Wildcard ns match in same namespace - se.Export(&sg2v1alpha1.SecretExport{ + secret6 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret5-same-ns-wildcard-ns", Namespace: "dst-ns"}, + Type: "Opaque", + } + export6 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret5-same-ns-wildcard-ns", Namespace: "dst-ns", }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "*"}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret5-same-ns-wildcard-ns", Namespace: "dst-ns"}, - Type: "Opaque", - }) + } // Specific ns match (even though there is a wildcard as well) - se.Export(&sg2v1alpha1.SecretExport{ + secret7 := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "secret6-same-ns-specific-ns", Namespace: "dst-ns"}, + Type: "Opaque", + } + export7 := &sg2v1alpha1.SecretExport{ ObjectMeta: metav1.ObjectMeta{ Name: "secret6-same-ns-specific-ns", Namespace: "dst-ns", }, Spec: sg2v1alpha1.SecretExportSpec{ToNamespace: "dst-ns", ToNamespaces: []string{"*"}}, - }, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: "secret6-same-ns-specific-ns", Namespace: "dst-ns"}, - Type: "Opaque", - }) + } + + se.Export(export1, secret1) + + se.Export(export2, secret2) + + se.Export(export3, secret3) + + se.Export(export4, secret4) + + se.Export(export5, secret5) + + se.Export(export6, secret6) + + se.Export(export7, secret7) result := se.MatchedSecretsForImport(sharing.SecretMatcher{ ToNamespace: "dst-ns", diff --git a/pkg/sharing/secret_import_reconciler.go b/pkg/sharing/secret_import_reconciler.go index a18c28aa6..941d7bafb 100644 --- a/pkg/sharing/secret_import_reconciler.go +++ b/pkg/sharing/secret_import_reconciler.go @@ -184,6 +184,7 @@ func (r *SecretImportReconciler) reconcile( FromName: secretImport.Name, FromNamespace: secretImport.Spec.FromNamespace, ToNamespace: secretImport.Namespace, + 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 5afed1596..7af76bc38 100644 --- a/test/e2e/secret_exports_test.go +++ b/test/e2e/secret_exports_test.go @@ -10,8 +10,8 @@ import ( "time" "github.com/ghodss/yaml" - corev1 "k8s.io/api/core/v1" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" ) func TestExportSuccessful(t *testing.T) { @@ -37,6 +37,47 @@ metadata: name: sg-test3 --- apiVersion: v1 +kind: Namespace +metadata: + name: sg-test4 + annotations: + field.cattle.io/projectId: "cluster1:project1" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: sg-test5 + annotations: + field.cattle.io/projectId: "cluster2:project3" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: sg-test6 + annotations: + field.cattle.io/projectId: "whatever:whatever" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: sg-test7 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: sg-test8 + annotations: + one: "1" + two: "2" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: sg-test8-unmatched + annotations: + one: "1" +--- +apiVersion: v1 kind: Secret metadata: name: secret @@ -47,6 +88,50 @@ stringData: key2: val2 key3: val3 --- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test5 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1 + key2: val2 + key3: val3 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test6 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1 + key2: val2 + key3: val3 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test7 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1 + key2: val2 + key3: val3 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test8 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1 + key2: val2 + key3: val3 +--- apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretExport metadata: @@ -56,6 +141,59 @@ spec: toNamespaces: - sg-test2 - sg-test3 + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: In + values: + - "cluster1:project1" +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretExport +metadata: + name: secret-test5 + namespace: sg-test1 +spec: + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: NotIn + values: + - "cluster1:project1" +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretExport +metadata: + name: secret-test6 + namespace: sg-test1 +spec: + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: Exists +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretExport +metadata: + name: secret-test7 + namespace: sg-test1 +spec: + dangerousToNamespacesSelector: + - key: "metadata.annotations['field\\.cattle\\.io/projectId']" + operator: DoesNotExist +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretExport +metadata: + name: secret-test8 + namespace: sg-test1 +spec: + dangerousToNamespacesSelector: + - key: "metadata.annotations.one" + operator: In + values: + - "1" + - key: "metadata.annotations.two" + operator: In + values: + - "2" --- apiVersion: secretgen.carvel.dev/v1alpha1 kind: SecretImport @@ -72,6 +210,54 @@ metadata: namespace: sg-test3 spec: fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret + namespace: sg-test4 +spec: + fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret-test5 + namespace: sg-test5 +spec: + fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret-test6 + namespace: sg-test6 +spec: + fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret-test7 + namespace: sg-test7 +spec: + fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret-test8 + namespace: sg-test8 +spec: + fromNamespace: sg-test1 +--- +apiVersion: secretgen.carvel.dev/v1alpha1 +kind: SecretImport +metadata: + name: secret-test8 + namespace: sg-test8-unmatched +spec: + fromNamespace: sg-test1 ` yaml2 := ` @@ -87,6 +273,50 @@ stringData: # key2 deleted key3: val3 # keep key4: val4 # new +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test5 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1.1 + key3: val3 + key4: val4 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test6 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1.1 + key3: val3 + key4: val4 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test7 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1.1 + key3: val3 + key4: val4 +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-test8 + namespace: sg-test1 +type: Opaque +stringData: + key1: val1.1 + key3: val3 + key4: val4 ` name := "test-export-successful" @@ -97,14 +327,29 @@ stringData: cleanUp() defer cleanUp() + getSecretName := func(ns string) string { + switch ns { + case "sg-test5": + return "secret-test5" + case "sg-test6": + return "secret-test6" + case "sg-test7": + return "secret-test7" + case "sg-test8": + return "secret-test8" + default: + return "secret" + } + } + logger.Section("Deploy", func() { kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name}, RunOpts{StdinReader: strings.NewReader(yaml1)}) }) logger.Section("Check imported secrets were created", func() { - for _, ns := range []string{"sg-test2", "sg-test3"} { - out := waitForSecretInNs(t, kubectl, ns, "secret") + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5", "sg-test6", "sg-test7", "sg-test8"} { + out := waitForSecretInNs(t, kubectl, ns, getSecretName(ns)) var secret corev1.Secret @@ -127,6 +372,15 @@ stringData: } }) + logger.Section("Check secrets should not be created", func() { + for _, ns := range []string{"sg-test8-unmatched"} { + notInNs := waitForSecretNotInNs(kubectl, ns, getSecretName(ns)) + if !notInNs { + t.Fatalf("Secret should not be created in ns: %s", ns) + } + } + }) + logger.Section("Update secret", func() { kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "-p"}, RunOpts{StdinReader: strings.NewReader(yaml2)}) @@ -136,8 +390,8 @@ stringData: // TODO proper waiting time.Sleep(5 * time.Second) - for _, ns := range []string{"sg-test2", "sg-test3"} { - out := waitForSecretInNs(t, kubectl, ns, "secret") + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5", "sg-test6", "sg-test7", "sg-test8"} { + out := waitForSecretInNs(t, kubectl, ns, getSecretName(ns)) var secret corev1.Secret @@ -161,14 +415,16 @@ stringData: }) logger.Section("Delete export to see exported secrets deleted", func() { - kubectl.RunWithOpts([]string{"delete", "secretexport.secretgen.carvel.dev", "secret", "-n", "sg-test1"}, - RunOpts{NoNamespace: true}) + for _, secretName := range []string{"secret", "secret-test5", "secret-test6", "secret-test7", "secret-test8"} { + kubectl.RunWithOpts([]string{"delete", "secretexport.secretgen.carvel.dev", secretName, "-n", "sg-test1"}, + RunOpts{NoNamespace: true}) + } // TODO proper waiting time.Sleep(5 * time.Second) - for _, ns := range []string{"sg-test2", "sg-test3"} { - _, err := kubectl.RunWithOpts([]string{"get", "secret", "secret", "-n", ns}, + for _, ns := range []string{"sg-test2", "sg-test3", "sg-test4", "sg-test5", "sg-test6", "sg-test7", "sg-test8"} { + _, err := kubectl.RunWithOpts([]string{"get", "secret", getSecretName(ns), "-n", ns}, RunOpts{AllowError: true, NoNamespace: true}) require.Error(t, err) diff --git a/test/e2e/wait.go b/test/e2e/wait.go index 9f6c6900a..3ab416151 100644 --- a/test/e2e/wait.go +++ b/test/e2e/wait.go @@ -40,6 +40,28 @@ func waitForSecretInNs(t *testing.T, kubectl Kubectl, nsName, name string) strin panic("Unreachable") } +func waitForSecretNotInNs(kubectl Kubectl, nsName, name string) bool { + var lastErr error + + args := []string{"get", "secret", name, "-o", "yaml"} + noNs := false + + if len(nsName) > 0 { + args = append(args, []string{"-n", nsName}...) + noNs = true + } + + for i := 0; i < 30; i++ { + _, lastErr = kubectl.RunWithOpts(args, RunOpts{AllowError: true, NoNamespace: noNs}) + if lastErr == nil { + return false + } + time.Sleep(time.Second) + } + + return true +} + func waitUntilSecretInNsPopulated(t *testing.T, kubectl Kubectl, nsName, name string, checkFun func(*corev1.Secret) bool) string { var secret corev1.Secret