Skip to content

Commit

Permalink
Merge pull request #1325 from keleustes/residequals-namereference
Browse files Browse the repository at this point in the history
NameReference Transformer needs to account for namespace and cluster wide objects.
  • Loading branch information
k8s-ci-robot committed Jul 17, 2019
2 parents 3a843f1 + 9b40f8a commit 30b378a
Show file tree
Hide file tree
Showing 9 changed files with 646 additions and 66 deletions.
2 changes: 1 addition & 1 deletion pkg/commands/build/build.go
Expand Up @@ -201,7 +201,7 @@ func NewCmdBuildPrune(

func writeIndividualFiles(
fSys fs.FileSystem, folderPath string, m resmap.ResMap) error {
byNamespace := m.GroupedByNamespace()
byNamespace := m.GroupedByCurrentNamespace()
for namespace, resList := range byNamespace {
for _, res := range resList {
fName := fileName(res)
Expand Down
38 changes: 31 additions & 7 deletions pkg/resmap/resmap.go
Expand Up @@ -112,13 +112,18 @@ type ResMap interface {
// match.
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.
// Resources for whom IsNamespaceableKind is false are
// are not included at all (see NonNamespaceable).
// Resources with an empty namespace are placed
// 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
// cannot be placed in a namespace, e.g.
Expand Down Expand Up @@ -390,19 +395,19 @@ func demandOneMatch(
return nil, fmt.Errorf("no matches for %sId %s", s, id)
}

// GroupedByNamespace implements ResMap.GroupByNamespace
func (m *resWrangler) GroupedByNamespace() map[string][]*resource.Resource {
items := m.groupedByNamespace()
// GroupedByCurrentNamespace implements ResMap.GroupByCurrentNamespace
func (m *resWrangler) GroupedByCurrentNamespace() map[string][]*resource.Resource {
items := m.groupedByCurrentNamespace()
delete(items, resid.TotallyNotANamespace)
return items
}

// NonNamespaceable implements ResMap.NonNamespaceable
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)
for _, res := range m.rList {
namespace := res.CurId().EffectiveNamespace()
Expand All @@ -414,6 +419,25 @@ func (m *resWrangler) groupedByNamespace() map[string][]*resource.Resource {
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.
func (m *resWrangler) AsYaml() ([]byte, error) {
firstObj := true
Expand Down
8 changes: 8 additions & 0 deletions pkg/resmaptest/rmbuilder.go
Expand Up @@ -62,6 +62,14 @@ func (rm *rmBuilder) AddWithNs(ns string, m map[string]interface{}) *rmBuilder {
return rm
}

func (rm *rmBuilder) AddWithNsAndName(ns string, n string, m map[string]interface{}) *rmBuilder {
err := rm.m.Append(rm.rf.FromMapWithNamespaceAndName(ns, n, m))
if err != nil {
rm.t.Fatalf("test setup failure: %v", err)
}
return rm
}

func (rm *rmBuilder) ReplaceResource(m map[string]interface{}) *rmBuilder {
r := rm.rf.FromMap(m)
_, err := rm.m.Replace(r)
Expand Down
5 changes: 5 additions & 0 deletions pkg/resource/factory.go
Expand Up @@ -43,6 +43,11 @@ func (rf *Factory) FromMapWithNamespace(n string, m map[string]interface{}) *Res
return rf.makeOne(rf.kf.FromMap(m), nil).setOriginalNs(n)
}

// FromMapWithNamespaceAndName returns a new instance with the given "original" namespace.
func (rf *Factory) FromMapWithNamespaceAndName(ns string, n string, m map[string]interface{}) *Resource {
return rf.makeOne(rf.kf.FromMap(m), nil).setOriginalNs(ns).setOriginalName(n)
}

// FromMapAndOption returns a new instance of Resource with given options.
func (rf *Factory) FromMapAndOption(
m map[string]interface{}, args *types.GeneratorArgs, option *types.GeneratorOptions) *Resource {
Expand Down
2 changes: 1 addition & 1 deletion pkg/resource/resource.go
Expand Up @@ -127,7 +127,7 @@ func (r *Resource) GetOutermostNameSuffix() string {
}

func (r *Resource) InSameFuzzyNamespace(o *Resource) bool {
return r.GetNamespace() == o.GetNamespace() &&
return r.CurId().IsNsEquals(o.CurId()) &&
r.GetOutermostNamePrefix() == o.GetOutermostNamePrefix() &&
r.GetOutermostNameSuffix() == o.GetOutermostNameSuffix()
}
Expand Down
48 changes: 37 additions & 11 deletions pkg/target/namespaces_test.go
Expand Up @@ -5,7 +5,6 @@ package target_test

import (
"sigs.k8s.io/kustomize/v3/pkg/kusttest"
"strings"
"testing"
)

Expand Down Expand Up @@ -51,19 +50,46 @@ resources:
- role.yaml
`)

_, err := th.MakeKustTarget().MakeCustomizedResMap()
// TODO: Fix #1044
// This should not be an error -
m, err := th.MakeKustTarget().MakeCustomizedResMap()
// This validates Fix #1444. This should not be an error anymore -
// the secrets have the same name but are in different namespaces.
// The ClusterRole (by def) is not in a namespace,
// an in this case applies to *any* Secret resource
// named "dummy"
if err == nil {
t.Fatalf("unexpected lack of error")
}
if !strings.Contains(
err.Error(),
"slice case - multiple matches for ~G_v1_Secret|default|dummy") {
t.Fatalf("unexpected error: %s", err)
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
dummy: ""
kind: Secret
metadata:
name: dummy
namespace: default
type: Opaque
---
apiVersion: v1
data:
dummy: ""
kind: Secret
metadata:
name: dummy
namespace: kube-system
type: Opaque
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dummy
rules:
- apiGroups:
- ""
resourceNames:
- dummy
resources:
- secrets
verbs:
- get
`)
}
6 changes: 4 additions & 2 deletions pkg/transformers/config/defaultconfig/namereference.go
Expand Up @@ -299,10 +299,10 @@ nameReference:
- kind: ServiceAccount
version: v1
fieldSpecs:
- path: subjects/name
- path: subjects
kind: RoleBinding
group: rbac.authorization.k8s.io
- path: subjects/name
- path: subjects
kind: ClusterRoleBinding
group: rbac.authorization.k8s.io
- path: spec/serviceAccountName
Expand Down Expand Up @@ -343,6 +343,8 @@ nameReference:
fieldSpecs:
- path: spec/volumeName
kind: PersistentVolumeClaim
- path: rules/resourceNames
kind: ClusterRole
- kind: StorageClass
version: v1
Expand Down
168 changes: 126 additions & 42 deletions pkg/transformers/namereference.go
Expand Up @@ -106,6 +106,102 @@ func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
return nil
}

// selectReferral picks the referral among a subset of candidates.
// It returns the current name and namespace of the selected candidate.
// Note that the content of the referricalCandidateSubset slice is most of the time
// identical to the referralCandidates resmap. Still in some cases, such
// as ClusterRoleBinding, the subset only contains the resources of a specific
// namespace.
func (o *nameReferenceTransformer) selectReferral(
oldName string,
referrer *resource.Resource,
target gvk.Gvk,
referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (interface{}, interface{}, error) {

for _, res := range referralCandidateSubset {
id := res.OrgId()
if id.IsSelected(&target) && res.GetOriginalName() == oldName {
matches := referralCandidates.GetMatchingResourcesByOriginalId(id.Equals)
// If there's more than one match, there's no way
// to know which one to pick, so emit error.
if len(matches) > 1 {
return nil, nil, fmt.Errorf(
"multiple matches for %s:\n %v",
id, getIds(matches))
}
// In the resource, note that it is referenced
// by the referrer.
res.AppendRefBy(referrer.CurId())
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
return res.GetName(), res.GetNamespace(), nil
}
}

return oldName, nil, nil
}

// utility function to replace a simple string by the new name
func (o *nameReferenceTransformer) getSimpleNameField(
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{}
// and leverage the namespace field.
func (o *nameReferenceTransformer) getNameAndNsStruct(
inMap map[string]interface{},
referrer *resource.Resource,
target gvk.Gvk,
referralCandidates resmap.ResMap) (interface{}, error) {

// Example:
if _, ok := inMap["name"]; !ok {
return nil, fmt.Errorf(
"%#v is expected to contain a name field", inMap)
}
oldName, ok := inMap["name"].(string)
if !ok {
return nil, fmt.Errorf(
"%#v is expected to contain a name field of type string", oldName)
}

subset := referralCandidates.Resources()
if namespacevalue, ok := inMap["namespace"]; ok {
namespace := namespacevalue.(string)
bynamespace := referralCandidates.GroupedByOriginalNamespace()
if _, ok := bynamespace[namespace]; !ok {
return inMap, nil
}
subset = bynamespace[namespace]
}

newname, newnamespace, err := o.selectReferral(oldName, referrer, target,
referralCandidates, subset)
if err != nil {
return nil, err
}

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

}

func (o *nameReferenceTransformer) getNewNameFunc(
referrer *resource.Resource,
target gvk.Gvk,
Expand All @@ -114,52 +210,40 @@ func (o *nameReferenceTransformer) getNewNameFunc(
switch in.(type) {
case string:
oldName, _ := in.(string)
for _, res := range referralCandidates.Resources() {
id := res.OrgId()
if id.IsSelected(&target) && res.GetOriginalName() == oldName {
matches := referralCandidates.GetMatchingResourcesByOriginalId(id.GvknEquals)
// If there's more than one match, there's no way
// to know which one to pick, so emit error.
if len(matches) > 1 {
return nil, fmt.Errorf(
"string case - multiple matches for %s:\n %v",
id, getIds(matches))
}
// In the resource, note that it is referenced
// by the referrer.
res.AppendRefBy(referrer.CurId())
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
return res.GetName(), nil
}
}
return in, nil
return o.getSimpleNameField(oldName, referrer, target,
referralCandidates, referralCandidates.Resources())
case []interface{}:
l, _ := in.([]interface{})
var names []string
for _, item := range l {
name, ok := item.(string)
if !ok {
return nil, fmt.Errorf(
"%#v is expected to be %T", item, name)
}
names = append(names, name)
}
for _, res := range referralCandidates.Resources() {
indexes := indexOf(res.GetOriginalName(), names)
id := res.OrgId()
if id.IsSelected(&target) && len(indexes) > 0 {
matches := referralCandidates.GetMatchingResourcesByOriginalId(id.GvknEquals)
if len(matches) > 1 {
return nil, fmt.Errorf(
"slice case - multiple matches for %s:\n %v",
id, getIds(matches))
for idx, item := range l {
switch item.(type) {
case string:
// Kind: Role/ClusterRole
// FieldSpec is rules.resourceNames
oldName, _ := item.(string)
newName, err := o.getSimpleNameField(oldName, referrer, target,
referralCandidates, referralCandidates.Resources())
if err != nil {
return nil, err
}
for _, index := range indexes {
l[index] = res.GetName()
l[idx] = newName
case map[string]interface{}:
// Kind: RoleBinding/ClusterRoleBinding
// FieldSpec is subjects
// Note: The corresponding fieldSpec had been changed from
// from path: subjects/name to just path: subjects. This is
// what get mutatefield to request the mapping of the whole
// map containing namespace and name instead of just a simple
// string field containing the name
oldMap, _ := item.(map[string]interface{})
newMap, err := o.getNameAndNsStruct(oldMap, referrer, target,
referralCandidates)
if err != nil {
return nil, err
}
res.AppendRefBy(referrer.CurId())
return l, nil
l[idx] = newMap
default:
return nil, fmt.Errorf(
"%#v is expected to be either a []string or a []map[string]interface{}", in)
}
}
return in, nil
Expand Down

0 comments on commit 30b378a

Please sign in to comment.