// Copyright © 2022 Roberto Hidalgo // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "fmt" "strconv" "strings" op "github.com/1Password/connect-sdk-go/onepassword" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) func isNumeric(s string) bool { for _, v := range s { if v < '0' || v > '9' { return false } } return true } type secretValue string // Entry is a configuration entry. // Basically a copy of a yaml.Node with extra methods type Entry struct { Value string Kind yaml.Kind Tag string Path []string Content []*Entry Style yaml.Style FootComment string LineComment string HeadComment string Line int Column int // The ShortTag Type string } func NewEntry(name string, kind yaml.Kind) *Entry { return &Entry{ Content: []*Entry{}, Value: name, Kind: kind, } } func copyFromNode(e *Entry, n *yaml.Node) *Entry { if e.Content == nil { e.Content = []*Entry{} } e.Kind = n.Kind e.Value = n.Value e.Tag = n.Tag e.Style = n.Style e.HeadComment = n.HeadComment e.LineComment = n.LineComment e.FootComment = n.FootComment e.Line = n.Line e.Column = n.Column e.Type = n.ShortTag() return e } func (e *Entry) String() string { return e.Value } func (e *Entry) ChildNamed(name string) *Entry { for _, child := range e.Content { if child.Name() == name { return child } } return nil } func (e *Entry) SetPath(parent []string, current string) { e.Path = append(parent, current) switch e.Kind { case yaml.MappingNode, yaml.DocumentNode: for idx := 0; idx < len(e.Content); idx += 2 { key := e.Content[idx] child := e.Content[idx+1] child.SetPath(e.Path, key.Value) } case yaml.SequenceNode: for idx, child := range e.Content { child.Path = append(e.Path, fmt.Sprintf("%d", idx)) } } } func (e *Entry) UnmarshalYAML(node *yaml.Node) error { copyFromNode(e, node) switch node.Kind { case yaml.SequenceNode, yaml.ScalarNode: for _, n := range node.Content { sub := &Entry{} copyFromNode(sub, n) if err := n.Decode(&sub); err != nil { return err } sub.SetPath(e.Path, n.Value) e.Content = append(e.Content, sub) } case yaml.DocumentNode, yaml.MappingNode: for idx := 0; idx < len(node.Content); idx += 2 { keyNode := node.Content[idx] valueNode := node.Content[idx+1] key := NewEntry("", keyNode.Kind) value := NewEntry(keyNode.Value, keyNode.Kind) if err := keyNode.Decode(key); err != nil { logrus.Errorf("decode map key: %s", keyNode.Value) return err } if err := valueNode.Decode(value); err != nil { logrus.Errorf("decode map key: %s", keyNode.Value) return err } if valueNode.Tag == YAMLTypeMetaConfig { key.Type = YAMLTypeMetaConfig } value.SetPath(e.Path, key.Value) e.Content = append(e.Content, key, value) } default: return fmt.Errorf("unknown yaml type: %v", node.Kind) } return nil } func (e *Entry) IsSecret() bool { return e.Tag == YAMLTypeSecret } func (e *Entry) TypeStr() string { if e.IsSecret() { return "secret" } switch e.Type { case "!!bool": return "bool" case "!!int": return "int" case "!!float": return "float" } return "" } func (e *Entry) asNode() *yaml.Node { return &yaml.Node{ Kind: e.Kind, Style: e.Style, Tag: e.Tag, Value: e.Value, HeadComment: e.HeadComment, LineComment: e.LineComment, FootComment: e.FootComment, Line: e.Line, Column: e.Column, Content: []*yaml.Node{}, } } func (e *Entry) MarshalYAML() (*yaml.Node, error) { n := e.asNode() if n.Kind == yaml.ScalarNode { if redactOutput && e.IsSecret() { return &yaml.Node{ Kind: n.Kind, Style: yaml.TaggedStyle & n.Style, Tag: n.Tag, Value: "", HeadComment: n.HeadComment, LineComment: n.LineComment, FootComment: n.FootComment, Line: n.Line, Column: n.Column, }, nil } return n, nil } for _, v := range e.Content { node, err := v.MarshalYAML() if err != nil { return nil, err } n.Content = append(n.Content, node) } return n, nil } func (e *Entry) FromOP(fields []*op.ItemField) error { annotations := map[string]string{} data := map[string]string{} for i := 0; i < len(fields); i++ { field := fields[i] label := field.Label if field.Section != nil { if field.Section.Label == "~annotations" { annotations[label] = field.Value continue } else { label = field.Section.Label + "." + label } } if label == "password" || label == "notesPlain" { continue } data[label] = field.Value } for label, valueStr := range data { var value any var err error var style yaml.Style var tag string switch annotations[label] { case "bool": value, err = strconv.ParseBool(valueStr) if err != nil { return err } case "int": value, err = strconv.ParseInt(valueStr, 10, 64) if err != nil { return err } case "float": var err error value, err = strconv.ParseFloat(valueStr, 64) if err != nil { return err } case "secret": value = secretValue(value.(string)) style = yaml.TaggedStyle tag = YAMLTypeSecret default: // either no annotation or an unknown value value = valueStr } path := strings.Split(label, ".") container := e for idx, key := range path { if idx == len(path)-1 { container.Content = append(container.Content, &Entry{ Path: path, Kind: yaml.ScalarNode, Value: valueStr, Style: style, Tag: tag, }) break } subContainer := container.ChildNamed(key) if subContainer != nil { container = subContainer } else { kind := yaml.MappingNode if isNumeric(key) { kind = yaml.SequenceNode } child := NewEntry(key, kind) child.Path = append(container.Path, key) container.Content = append(container.Content, child) container = child } } } return nil } func (e *Entry) ToOP() []*op.ItemField { ret := []*op.ItemField{} var section *op.ItemSection if e.Kind == yaml.ScalarNode { name := e.Path[len(e.Path)-1] if len(e.Path) > 1 { section = &op.ItemSection{ID: e.Path[0]} name = strings.Join(e.Path[1:], ".") } fieldType := "STRING" if e.IsSecret() { fieldType = "CONCEALED" } else { if annotationType := e.TypeStr(); annotationType != "" { ret = append(ret, &op.ItemField{ ID: "~annotations." + strings.Join(e.Path, "."), Section: annotationsSection, Label: name, Type: "STRING", Value: annotationType, }) } } ret = append(ret, &op.ItemField{ ID: strings.Join(e.Path, "."), Section: section, Label: name, Type: fieldType, Value: e.Value, }) return ret } if e.Kind == yaml.SequenceNode { ret := []*op.ItemField{} for _, child := range e.Content { ret = append(ret, child.ToOP()...) } return ret } for i := 0; i < len(e.Content); i += 2 { child := e.Content[i+1] if child.Type == YAMLTypeMetaConfig { continue } ret = append(ret, child.ToOP()...) } return ret } func (e *Entry) Name() string { if e.Path == nil || len(e.Path) == 0 { return "" } return e.Path[len(e.Path)-1] } func (e *Entry) AsMap() any { if len(e.Content) == 0 { if redactOutput && e.IsSecret() { return "" } return e.Value } if e.Kind == yaml.SequenceNode { ret := []any{} for _, child := range e.Content { ret = append(ret, child.AsMap()) } return ret } ret := map[string]any{} for idx, child := range e.Content { if idx%2 == 0 { continue } ret[child.Name()] = child.AsMap() } return ret }