// Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package wrappy import ( "fmt" "log" "regexp" "strconv" "strings" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/kyaml/yaml" ) // WNode implements ifc.Kunstructured using yaml.RNode. // // It exists only to help manage a switch from // kunstruct.UnstructAdapter to yaml.RNode as the core // representation of KRM objects in kustomize. // // It's got a silly name because we don't want it around for long, // and want its use to be obvious. type WNode struct { node *yaml.RNode } var _ ifc.Kunstructured = (*WNode)(nil) func NewWNode() *WNode { return FromRNode(yaml.NewRNode(nil)) } func FromMap(m map[string]interface{}) (*WNode, error) { n, err := yaml.FromMap(m) if err != nil { return nil, err } return FromRNode(n), nil } func FromRNode(node *yaml.RNode) *WNode { return &WNode{node: node} } func (wn *WNode) AsRNode() *yaml.RNode { return wn.node } func (wn *WNode) demandMetaData(label string) yaml.ResourceMeta { meta, err := wn.node.GetMeta() if err != nil { // Log and die since interface doesn't allow error. log.Fatalf("for %s', expected valid resource: %v", label, err) } return meta } // Copy implements ifc.Kunstructured. func (wn *WNode) Copy() ifc.Kunstructured { return &WNode{node: wn.node.Copy()} } // GetAnnotations implements ifc.Kunstructured. func (wn *WNode) GetAnnotations() map[string]string { return wn.demandMetaData("GetAnnotations").Annotations } // convertSliceIndex traverses the items in `fields` and find // if there is a slice index in the item and change it to a // valid Lookup field path. For example, 'ports[0]' will be // converted to 'ports' and '0'. func convertSliceIndex(fields []string) []string { var res []string for _, s := range fields { if !strings.HasSuffix(s, "]") { res = append(res, s) continue } re := regexp.MustCompile(`^(.*)\[(\d+)\]$`) groups := re.FindStringSubmatch(s) if len(groups) == 0 { // no match, add to result res = append(res, s) continue } if groups[1] != "" { res = append(res, groups[1]) } res = append(res, groups[2]) } return res } // GetFieldValue implements ifc.Kunstructured. func (wn *WNode) GetFieldValue(path string) (interface{}, error) { fields := convertSliceIndex(strings.Split(path, ".")) rn, err := wn.node.Pipe(yaml.Lookup(fields...)) if err != nil { return nil, err } if rn == nil { return nil, NoFieldError{path} } yn := rn.YNode() // If this is an alias node, resolve it if yn.Kind == yaml.AliasNode { yn = yn.Alias } // Return value as map for DocumentNode and MappingNode kinds if yn.Kind == yaml.DocumentNode || yn.Kind == yaml.MappingNode { var result map[string]interface{} if err := yn.Decode(&result); err != nil { return nil, err } return result, err } // Return value as slice for SequenceNode kind if yn.Kind == yaml.SequenceNode { var result []interface{} if err := yn.Decode(&result); err != nil { return nil, err } return result, nil } if yn.Kind != yaml.ScalarNode { return nil, fmt.Errorf("expected ScalarNode, got Kind=%d", yn.Kind) } // TODO: When doing kustomize var replacement, which is likely a // a primary use of this function and the reason it returns interface{} // rather than string, we do conversion from Nodes to Go types and back // to nodes. We should figure out how to do replacement using raw nodes, // assuming we keep the var feature in kustomize. // The other end of this is: refvar.go:updateNodeValue. switch yn.Tag { case yaml.NodeTagString: return yn.Value, nil case yaml.NodeTagInt: return strconv.Atoi(yn.Value) case yaml.NodeTagFloat: return strconv.ParseFloat(yn.Value, 64) case yaml.NodeTagBool: return strconv.ParseBool(yn.Value) default: // Possibly this should be an error or log. return yn.Value, nil } } // GetGvk implements ifc.Kunstructured. func (wn *WNode) GetGvk() resid.Gvk { meta := wn.demandMetaData("GetGvk") g, v := resid.ParseGroupVersion(meta.APIVersion) return resid.Gvk{Group: g, Version: v, Kind: meta.Kind} } // GetDataMap implements ifc.Kunstructured. func (wn *WNode) GetDataMap() map[string]string { return wn.node.GetDataMap() } // SetDataMap implements ifc.Kunstructured. func (wn *WNode) SetDataMap(m map[string]string) { wn.node.SetDataMap(m) } // GetBinaryDataMap implements ifc.Kunstructured. func (wn *WNode) GetBinaryDataMap() map[string]string { return wn.node.GetBinaryDataMap() } // SetBinaryDataMap implements ifc.Kunstructured. func (wn *WNode) SetBinaryDataMap(m map[string]string) { wn.node.SetBinaryDataMap(m) } // GetKind implements ifc.Kunstructured. func (wn *WNode) GetKind() string { return wn.demandMetaData("GetKind").Kind } // GetLabels implements ifc.Kunstructured. func (wn *WNode) GetLabels() map[string]string { return wn.demandMetaData("GetLabels").Labels } // GetName implements ifc.Kunstructured. func (wn *WNode) GetName() string { return wn.demandMetaData("GetName").Name } // GetSlice implements ifc.Kunstructured. func (wn *WNode) GetSlice(path string) ([]interface{}, error) { value, err := wn.GetFieldValue(path) if err != nil { return nil, err } if sliceValue, ok := value.([]interface{}); ok { return sliceValue, nil } return nil, fmt.Errorf("node %s is not a slice", path) } // GetSlice implements ifc.Kunstructured. func (wn *WNode) GetString(path string) (string, error) { value, err := wn.GetFieldValue(path) if err != nil { return "", err } if v, ok := value.(string); ok { return v, nil } return "", fmt.Errorf("node %s is not a string: %v", path, value) } // Map implements ifc.Kunstructured. func (wn *WNode) Map() (map[string]interface{}, error) { return wn.node.Map() } // MarshalJSON implements ifc.Kunstructured. func (wn *WNode) MarshalJSON() ([]byte, error) { return wn.node.MarshalJSON() } // MatchesAnnotationSelector implements ifc.Kunstructured. func (wn *WNode) MatchesAnnotationSelector(selector string) (bool, error) { return wn.node.MatchesAnnotationSelector(selector) } // MatchesLabelSelector implements ifc.Kunstructured. func (wn *WNode) MatchesLabelSelector(selector string) (bool, error) { return wn.node.MatchesLabelSelector(selector) } // SetAnnotations implements ifc.Kunstructured. func (wn *WNode) SetAnnotations(annotations map[string]string) { if err := wn.node.SetAnnotations(annotations); err != nil { log.Fatal(err) // interface doesn't allow error. } } // SetGvk implements ifc.Kunstructured. func (wn *WNode) SetGvk(gvk resid.Gvk) { wn.setMapField(yaml.NewScalarRNode(gvk.Kind), yaml.KindField) wn.setMapField(yaml.NewScalarRNode(gvk.ApiVersion()), yaml.APIVersionField) } // SetLabels implements ifc.Kunstructured. func (wn *WNode) SetLabels(labels map[string]string) { if err := wn.node.SetLabels(labels); err != nil { log.Fatal(err) // interface doesn't allow error. } } // SetName implements ifc.Kunstructured. func (wn *WNode) SetName(name string) { wn.setMapField(yaml.NewScalarRNode(name), yaml.MetadataField, yaml.NameField) } // SetNamespace implements ifc.Kunstructured. func (wn *WNode) SetNamespace(ns string) { if err := wn.node.SetNamespace(ns); err != nil { log.Fatal(err) // interface doesn't allow error. } } func (wn *WNode) setMapField(value *yaml.RNode, path ...string) { if err := wn.node.SetMapField(value, path...); err != nil { // Log and die since interface doesn't allow error. log.Fatalf("failed to set field %v: %v", path, err) } } // UnmarshalJSON implements ifc.Kunstructured. func (wn *WNode) UnmarshalJSON(data []byte) error { return wn.node.UnmarshalJSON(data) } type NoFieldError struct { Field string } func (e NoFieldError) Error() string { return fmt.Sprintf("no field named '%s'", e.Field) }