diff --git a/pkg/commands/build/build.go b/pkg/commands/build/build.go index 3aa6d0e43..76e549d27 100644 --- a/pkg/commands/build/build.go +++ b/pkg/commands/build/build.go @@ -202,22 +202,41 @@ func NewCmdBuildPrune( func writeIndividualFiles( fSys fs.FileSystem, folderPath string, m resmap.ResMap) error { - for _, res := range m.Resources() { - filename := filepath.Join( - folderPath, - fmt.Sprintf( + + byNamespace := m.GroupedByNamespace() + nsNeeded := len(byNamespace) > 1 + for namespace, nresources := range byNamespace { + for _, res := range nresources { + basename := fmt.Sprintf( "%s_%s.yaml", strings.ToLower(res.GetGvk().String()), strings.ToLower(res.GetName()), - ), - ) - out, err := yaml.Marshal(res.Map()) - if err != nil { - return err - } - err = fSys.WriteFile(filename, out) - if err != nil { - return err + ) + + // Preserve backward compatibility with kustomize 2.1.0. + // No need to cluter filename with namespace if all the objects + // are in the same namespace. + if (nsNeeded) && (namespace != "cluster-wide") { + basename = fmt.Sprintf( + "%s_%s", + strings.ToLower(namespace), + strings.ToLower(basename), + ) + } + + filename := filepath.Join( + folderPath, + basename, + ) + + out, err := yaml.Marshal(res.Map()) + if err != nil { + return err + } + err = fSys.WriteFile(filename, out) + if err != nil { + return err + } } } return nil diff --git a/pkg/gvk/gvk.go b/pkg/gvk/gvk.go index 553e8d0a5..7cca2d944 100644 --- a/pkg/gvk/gvk.go +++ b/pkg/gvk/gvk.go @@ -180,3 +180,40 @@ func (x Gvk) IsClusterKind() bool { } return false } + +var notNamespaceableKinds = []string{ + "APIService", + "CSIDriver", + "CSINode", + "CertificateSigningRequest", + "ClusterRole", + "ClusterRoleBinding", + "ComponentStatus", + "CustomResourceDefinition", + "MutatingWebhookConfiguration", + "Namespace", + "Node", + "PersistentVolume", + "PodSecurityPolicy", + "PodSecurityPolicy", + "PriorityClass", + "RuntimeClass", + "SelfSubjectAccessReview", + "SelfSubjectRulesReview", + "StorageClass", + "SubjectAccessReview", + "TokenReview", + "ValidatingWebhookConfiguration", + "VolumeAttachment", +} + +// IsNamespaceableKind returns true if x is a namespable Gvk +// Implements https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#not-all-objects-are-in-a-namespace +func (x Gvk) IsNamespaceableKind() bool { + for _, k := range notNamespaceableKinds { + if k == x.Kind { + return false + } + } + return true +} diff --git a/pkg/resid/resid.go b/pkg/resid/resid.go index 264f8fa4e..31b55d4a5 100644 --- a/pkg/resid/resid.go +++ b/pkg/resid/resid.go @@ -93,5 +93,25 @@ func (id ResId) GvknEquals(o ResId) bool { // Equals returns true if the other id matches // namespace/Group/Version/Kind/name. func (id ResId) Equals(o ResId) bool { - return id.Namespace == o.Namespace && id.GvknEquals(o) + return id.IsNsEquals(o) && id.GvknEquals(o) +} + +// IsNsEquals returns true if the other id matches namespace +// or both are in the default namespace +// or both are not namespaceable id. +func (id ResId) IsNsEquals(o ResId) bool { + return id.Namespace == o.Namespace || + (id.IsInDefaultNs() && o.IsInDefaultNs()) || + (!id.IsNamespaceable() && !o.IsNamespaceable()) +} + +// IsInDefaultNs returns true if id is a namespable ResId and the Namespace +// is either not set or set to "default" +func (id ResId) IsInDefaultNs() bool { + return id.IsNamespaceable() && (id.Namespace == "" || id.Namespace == "default") +} + +// IsNamespaceable returns true if id is a namespable ResId +func (id ResId) IsNamespaceable() bool { + return id.IsNamespaceableKind() } diff --git a/pkg/resmap/resmap.go b/pkg/resmap/resmap.go index f2e2d4a79..41a9dfc95 100644 --- a/pkg/resmap/resmap.go +++ b/pkg/resmap/resmap.go @@ -93,6 +93,13 @@ type ResMap interface { // Same as GetByOriginalId. GetById(resid.ResId) (*resource.Resource, error) + // GroupedByNamespace provides map of discardable slice + // of resource pointer + // The not namespaceable resources are added to the "cluster-wide" key. + // The resources in the default namespace are added to the "default" key. + // The rest of the resources are grouped in their respectiv namespace. + GroupedByNamespace() map[string][]*resource.Resource + // AllIds returns all CurrentIds. AllIds() []resid.ResId @@ -343,6 +350,29 @@ func (m *resWrangler) GetById(id resid.ResId) (*resource.Resource, error) { return m.GetByCurrentId(id) } +// GroupedByNamespace implements ResMap.GroupByNamespace +func (m *resWrangler) GroupedByNamespace() map[string][]*resource.Resource { + byNamespace := make(map[string][]*resource.Resource) + for _, res := range m.rList { + namespace := "cluster-wide" + + if res.OrgId().IsNamespaceable() { + namespace = res.OrgId().Namespace + if res.OrgId().IsInDefaultNs() { + namespace = "default" + } + } + + if _, found := byNamespace[namespace]; !found { + byNamespace[namespace] = make([]*resource.Resource, 0) + } + + byNamespace[namespace] = append(byNamespace[namespace], res) + } + + return byNamespace +} + // AsYaml implements ResMap. func (m *resWrangler) AsYaml() ([]byte, error) { firstObj := true @@ -504,7 +534,7 @@ func (m *resWrangler) appendReplaceOrMerge( res *resource.Resource) error { id := res.CurId() // Maybe also try by current id if nothing matches? - matches := m.GetMatchingResourcesByOriginalId(id.GvknEquals) + matches := m.GetMatchingResourcesByOriginalId(id.Equals) switch len(matches) { case 0: switch res.Behavior() { diff --git a/pkg/target/generatormergeandreplace_test.go b/pkg/target/generatormergeandreplace_test.go index d3afdcbb0..a1751fe88 100644 --- a/pkg/target/generatormergeandreplace_test.go +++ b/pkg/target/generatormergeandreplace_test.go @@ -486,18 +486,108 @@ func TestGeneratingIntoNamespaces(t *testing.T) { th.WriteK("/app", ` configMapGenerator: - name: test - namespace: bob + namespace: default + literals: + - key=value +- name: test + namespace: kube-system + literals: + - key=value +secretGenerator: +- name: test + namespace: default + literals: + - username=admin + - password=somepw +- name: test + namespace: kube-system + literals: + - username=admin + - password=somepw +`) + m, err := th.MakeKustTarget().MakeCustomizedResMap() + if err != nil { + t.Fatalf("Err: %v", err) + } + th.AssertActualEqualsExpected(m, ` +apiVersion: v1 +data: + key: value +kind: ConfigMap +metadata: + name: test-t5t4md8fdm + namespace: default +--- +apiVersion: v1 +data: + key: value +kind: ConfigMap +metadata: + name: test-t5t4md8fdm + namespace: kube-system +--- +apiVersion: v1 +data: + password: c29tZXB3 + username: YWRtaW4= +kind: Secret +metadata: + name: test-h65t9hg6kc + namespace: default +type: Opaque +--- +apiVersion: v1 +data: + password: c29tZXB3 + username: YWRtaW4= +kind: Secret +metadata: + name: test-h65t9hg6kc + namespace: kube-system +type: Opaque +`) +} + +// Valid that conflict is detected is the name are identical +// and namespace left to default +func TestConfigMapGeneratingIntoSameNamespace(t *testing.T) { + th := kusttest_test.NewKustTestHarness(t, "/app") + th.WriteK("/app", ` +configMapGenerator: +- name: test + namespace: default literals: - key=value - name: test - namespace: kube-system literals: - key=value `) _, err := th.MakeKustTarget().MakeCustomizedResMap() - // Document #1155 - // This actually should be nil; it should work, and - // have some expected output. + if err == nil { + t.Fatalf("expected error") + } + if !strings.Contains(err.Error(), "must merge or replace") { + t.Fatalf("unexpected error %v", err) + } +} + +// Valid that conflict is detected is the name are identical +// and namespace left to default +func TestSecretGeneratingIntoSameNamespace(t *testing.T) { + th := kusttest_test.NewKustTestHarness(t, "/app") + th.WriteK("/app", ` +secretGenerator: +- name: test + namespace: default + literals: + - username=admin + - password=somepw +- name: test + literals: + - username=admin + - password=somepw +`) + _, err := th.MakeKustTarget().MakeCustomizedResMap() if err == nil { t.Fatalf("expected error") }