Address simultaneous transformation of name and namespace

Namereference handler needs to address simulatenous change of
name and namespace in ClusterRoleBinding for instance.
This commit is contained in:
Jerome Brette
2019-07-14 11:35:26 -05:00
parent b43bd5440d
commit 579995dc8a
5 changed files with 206 additions and 32 deletions

View File

@@ -201,7 +201,7 @@ func NewCmdBuildPrune(
func writeIndividualFiles( func writeIndividualFiles(
fSys fs.FileSystem, folderPath string, m resmap.ResMap) error { fSys fs.FileSystem, folderPath string, m resmap.ResMap) error {
byNamespace := m.GroupedByNamespace() byNamespace := m.GroupedByCurrentNamespace()
for namespace, resList := range byNamespace { for namespace, resList := range byNamespace {
for _, res := range resList { for _, res := range resList {
fName := fileName(res) fName := fileName(res)

View File

@@ -112,13 +112,18 @@ type ResMap interface {
// match. // match.
GetById(resid.ResId) (*resource.Resource, error) GetById(resid.ResId) (*resource.Resource, error)
// GroupedByNamespace returns a map of namespace // GroupedByCurrentNamespace returns a map of namespace
// to a slice of *Resource in that namespace. // to a slice of *Resource in that namespace.
// Resources for whom IsNamespaceableKind is false are // Resources for whom IsNamespaceableKind is false are
// are not included at all (see NonNamespaceable). // are not included at all (see NonNamespaceable).
// Resources with an empty namespace are placed // Resources with an empty namespace are placed
// in the resid.DefaultNamespace entry. // in the resid.DefaultNamespace entry.
GroupedByNamespace() map[string][]*resource.Resource GroupedByCurrentNamespace() map[string][]*resource.Resource
// GroupByOrginalNamespace performs as GroupByNamespace
// but use the original namespace instead of the current
// one to perform the grouping.
GroupedByOriginalNamespace() map[string][]*resource.Resource
// NonNamespaceable returns a slice of resources that // NonNamespaceable returns a slice of resources that
// cannot be placed in a namespace, e.g. // cannot be placed in a namespace, e.g.
@@ -390,19 +395,19 @@ func demandOneMatch(
return nil, fmt.Errorf("no matches for %sId %s", s, id) return nil, fmt.Errorf("no matches for %sId %s", s, id)
} }
// GroupedByNamespace implements ResMap.GroupByNamespace // GroupedByCurrentNamespace implements ResMap.GroupByCurrentNamespace
func (m *resWrangler) GroupedByNamespace() map[string][]*resource.Resource { func (m *resWrangler) GroupedByCurrentNamespace() map[string][]*resource.Resource {
items := m.groupedByNamespace() items := m.groupedByCurrentNamespace()
delete(items, resid.TotallyNotANamespace) delete(items, resid.TotallyNotANamespace)
return items return items
} }
// NonNamespaceable implements ResMap.NonNamespaceable // NonNamespaceable implements ResMap.NonNamespaceable
func (m *resWrangler) NonNamespaceable() []*resource.Resource { func (m *resWrangler) NonNamespaceable() []*resource.Resource {
return m.groupedByNamespace()[resid.TotallyNotANamespace] return m.groupedByCurrentNamespace()[resid.TotallyNotANamespace]
} }
func (m *resWrangler) groupedByNamespace() map[string][]*resource.Resource { func (m *resWrangler) groupedByCurrentNamespace() map[string][]*resource.Resource {
byNamespace := make(map[string][]*resource.Resource) byNamespace := make(map[string][]*resource.Resource)
for _, res := range m.rList { for _, res := range m.rList {
namespace := res.CurId().EffectiveNamespace() namespace := res.CurId().EffectiveNamespace()
@@ -414,6 +419,25 @@ func (m *resWrangler) groupedByNamespace() map[string][]*resource.Resource {
return byNamespace return byNamespace
} }
// GroupedByNamespace implements ResMap.GroupByOrginalNamespace
func (m *resWrangler) GroupedByOriginalNamespace() map[string][]*resource.Resource {
items := m.groupedByOriginalNamespace()
delete(items, resid.TotallyNotANamespace)
return items
}
func (m *resWrangler) groupedByOriginalNamespace() map[string][]*resource.Resource {
byNamespace := make(map[string][]*resource.Resource)
for _, res := range m.rList {
namespace := res.OrgId().EffectiveNamespace()
if _, found := byNamespace[namespace]; !found {
byNamespace[namespace] = []*resource.Resource{}
}
byNamespace[namespace] = append(byNamespace[namespace], res)
}
return byNamespace
}
// AsYaml implements ResMap. // AsYaml implements ResMap.
func (m *resWrangler) AsYaml() ([]byte, error) { func (m *resWrangler) AsYaml() ([]byte, error) {
firstObj := true firstObj := true

View File

@@ -343,6 +343,8 @@ nameReference:
fieldSpecs: fieldSpecs:
- path: spec/volumeName - path: spec/volumeName
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
- path: rules/resourceNames
kind: ClusterRole
- kind: StorageClass - kind: StorageClass
version: v1 version: v1

View File

@@ -107,12 +107,12 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
} }
// utility function to replace a simple string by the new name // utility function to replace a simple string by the new name
func (o *nameReferenceTransformer) mapSimpleNameField( func (o *nameReferenceTransformer) selectReferral(
oldName string, oldName string,
referrer *resource.Resource, referrer *resource.Resource,
target gvk.Gvk, target gvk.Gvk,
referralCandidates resmap.ResMap, referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (interface{}, error) { referralCandidateSubset []*resource.Resource) (interface{}, interface{}, error) {
for _, res := range referralCandidateSubset { for _, res := range referralCandidateSubset {
id := res.OrgId() id := res.OrgId()
@@ -121,8 +121,8 @@ func (o *nameReferenceTransformer) mapSimpleNameField(
// If there's more than one match, there's no way // If there's more than one match, there's no way
// to know which one to pick, so emit error. // to know which one to pick, so emit error.
if len(matches) > 1 { if len(matches) > 1 {
return nil, fmt.Errorf( return nil, nil, fmt.Errorf(
"string case - multiple matches for %s:\n %v", "multiple matches for %s:\n %v",
id, getIds(matches)) id, getIds(matches))
} }
// In the resource, note that it is referenced // In the resource, note that it is referenced
@@ -130,11 +130,25 @@ func (o *nameReferenceTransformer) mapSimpleNameField(
res.AppendRefBy(referrer.CurId()) res.AppendRefBy(referrer.CurId())
// Return transformed name of the object, // Return transformed name of the object,
// complete with prefixes, hashes, etc. // complete with prefixes, hashes, etc.
return res.GetName(), nil return res.GetName(), res.GetNamespace(), nil
} }
} }
return oldName, nil return oldName, nil, nil
}
// utility function to replace a simple string by the new name
func (o *nameReferenceTransformer) mapSimpleNameField(
oldName string,
referrer *resource.Resource,
target gvk.Gvk,
referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (interface{}, error) {
newName, _, err := o.selectReferral(oldName, referrer, target,
referralCandidates, referralCandidateSubset)
return newName, err
} }
// utility function to replace name field within a map[string]interface{} // utility function to replace name field within a map[string]interface{}
@@ -159,20 +173,26 @@ func (o *nameReferenceTransformer) mapNameAndNsStruct(
subset := referralCandidates.Resources() subset := referralCandidates.Resources()
if namespacevalue, ok := inMap["namespace"]; ok { if namespacevalue, ok := inMap["namespace"]; ok {
namespace := namespacevalue.(string) namespace := namespacevalue.(string)
bynamespace := referralCandidates.GroupedByNamespace() bynamespace := referralCandidates.GroupedByOriginalNamespace()
if _, ok := bynamespace[namespace]; !ok { if _, ok := bynamespace[namespace]; !ok {
return inMap, nil return inMap, nil
} }
subset = bynamespace[namespace] subset = bynamespace[namespace]
} }
newname, err := o.mapSimpleNameField(oldName, referrer, target, newname, newnamespace, err := o.selectReferral(oldName, referrer, target,
referralCandidates, subset) referralCandidates, subset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
inMap["name"] = newname inMap["name"] = newname
if newnamespace != "" {
// We don't want value "" to replace value "default" since
// the empty string is handled as a wild card here not default namespace
// by kubernetes.
inMap["namespace"] = newnamespace
}
return inMap, nil return inMap, nil
} }

View File

@@ -647,19 +647,22 @@ func deploymentMap(metadatanamespace string, metadataname string,
return deployment return deployment
} }
// TestNameReferenceNamespace creates configmap, secret, deployment const (
// serviceAccount and clusterRoleBinding object with the same original defaultNs = "default"
// names (uniquename) in different namespaces and with different current Id. ns1 = "ns1"
func TestNameReferenceyNamespace(t *testing.T) { ns2 = "ns2"
defaultNs := "default" ns3 = "ns3"
ns1 := "ns1"
ns2 := "ns2"
orgname := "uniquename" orgname = "uniquename"
prefixedname := "prefix-uniquename" prefixedname = "prefix-uniquename"
suffixedname := "uniquename-suffix" suffixedname = "uniquename-suffix"
modifiedname := "modifiedname" modifiedname = "modifiedname"
)
// TestNameReferenceNamespace creates serviceAccount and clusterRoleBinding
// object with the same original names (uniquename) in different namespaces
// and with different current Id.
func TestNameReferenceNamespace(t *testing.T) {
rf := resource.NewFactory( rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl()) kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf). m := resmaptest_test.NewRmBuilder(t, rf).
@@ -709,7 +712,31 @@ func TestNameReferenceyNamespace(t *testing.T) {
// Add Deployment with the same org name in noNs, "ns1" and "ns2" namespaces // Add Deployment with the same org name in noNs, "ns1" and "ns2" namespaces
AddWithNsAndName(defaultNs, orgname, deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)). AddWithNsAndName(defaultNs, orgname, deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)).
AddWithNsAndName(ns1, orgname, deploymentMap(ns1, prefixedname, orgname, orgname)). AddWithNsAndName(ns1, orgname, deploymentMap(ns1, prefixedname, orgname, orgname)).
AddWithNsAndName(ns2, orgname, deploymentMap(ns2, suffixedname, orgname, orgname)). AddWithNsAndName(ns2, orgname, deploymentMap(ns2, suffixedname, orgname, orgname)).ResMap()
expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource(deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)).
ReplaceResource(deploymentMap(ns1, prefixedname, prefixedname, prefixedname)).
ReplaceResource(deploymentMap(ns2, suffixedname, suffixedname, suffixedname)).ResMap()
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
err := nrt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
// TestNameReferenceNamespace creates serviceAccount and clusterRoleBinding
// object with the same original names (uniquename) in different namespaces
// and with different current Id.
func TestNameReferenceClusterWide(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
// Add ServiceAccount with the same org name in noNs, "ns1" and "ns2" namespaces // Add ServiceAccount with the same org name in noNs, "ns1" and "ns2" namespaces
AddWithName(orgname, map[string]interface{}{ AddWithName(orgname, map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
@@ -731,7 +758,7 @@ func TestNameReferenceyNamespace(t *testing.T) {
"name": suffixedname, "name": suffixedname,
"namespace": ns2, "namespace": ns2,
}}). }}).
// Add a PersisitentVolume to have a clusterwide resources // Add a PersistentVolume to have a clusterwide resource
AddWithName(orgname, map[string]interface{}{ AddWithName(orgname, map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "PersistentVolume", "kind": "PersistentVolume",
@@ -784,9 +811,6 @@ func TestNameReferenceyNamespace(t *testing.T) {
}}).ResMap() }}).ResMap()
expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()). expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource(deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)).
ReplaceResource(deploymentMap(ns1, prefixedname, prefixedname, prefixedname)).
ReplaceResource(deploymentMap(ns2, suffixedname, suffixedname, suffixedname)).
ReplaceResource( ReplaceResource(
map[string]interface{}{ map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1", "apiVersion": "rbac.authorization.k8s.io/v1",
@@ -794,6 +818,9 @@ func TestNameReferenceyNamespace(t *testing.T) {
"metadata": map[string]interface{}{ "metadata": map[string]interface{}{
"name": modifiedname, "name": modifiedname,
}, },
// Behavior of the transformer is still imperfect
// It should use the (resources,apigroup,resourceNames) as
// combination to select the candidates.
"rules": []interface{}{ "rules": []interface{}{
map[string]interface{}{ map[string]interface{}{
"resources": []interface{}{ "resources": []interface{}{
@@ -856,6 +883,107 @@ func TestNameReferenceyNamespace(t *testing.T) {
} }
} }
// TestNameReferenceNamespaceTransformation creates serviceAccount and clusterRoleBinding
// object with the same original names (uniquename) in different namespaces
// and with different current Id.
func TestNameReferenceNamespaceTransformation(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
// Add ServiceAccount with the same org name in "ns1" namespaces
AddWithNsAndName(ns1, orgname, map[string]interface{}{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]interface{}{
"name": prefixedname,
"namespace": ns1,
}}).
// Simulate NamespaceTransformer effect (ns3 transformed in ns2)
AddWithNsAndName(ns3, orgname, map[string]interface{}{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": map[string]interface{}{
"name": suffixedname,
"namespace": ns2,
}}).
AddWithName(orgname, map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": map[string]interface{}{
"name": modifiedname,
}}).
AddWithName(orgname, map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRoleBinding",
"metadata": map[string]interface{}{
"name": modifiedname,
},
"roleRef": map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"name": orgname,
},
"subjects": []interface{}{
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": ns1,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": ns3,
},
}}).ResMap()
expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource(
map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRoleBinding",
"metadata": map[string]interface{}{
"name": modifiedname,
},
"roleRef": map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"name": modifiedname,
},
// The following tests required a change in
// getNameFunc implementation in order to leverage
// the namespace field.
"subjects": []interface{}{
map[string]interface{}{
"kind": "ServiceAccount",
"name": prefixedname,
"namespace": ns1,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": suffixedname,
"namespace": ns2,
},
},
}).ResMap()
clusterRoleId := resid.NewResId(
gvk.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, modifiedname)
clusterRoleBindingId := resid.NewResId(
gvk.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, modifiedname)
clusterRole, _ := expected.GetByCurrentId(clusterRoleId)
clusterRole.AppendRefBy(clusterRoleBindingId)
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
err := nrt.Transform(m)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
// TestNameReferenceNamespace creates configmap, secret, deployment // TestNameReferenceNamespace creates configmap, secret, deployment
// It validates the change done is IsSameFuzzyNamespace which // It validates the change done is IsSameFuzzyNamespace which
// uses the IsNsEquals method instead of the simple == operator. // uses the IsNsEquals method instead of the simple == operator.