get.porter.sh/porter@v1.3.0/pkg/yaml/yq.go (about) 1 package yaml 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "sync" 10 11 "get.porter.sh/porter/pkg" 12 "github.com/carolynvs/aferox" 13 "github.com/mikefarah/yq/v3/pkg/yqlib" 14 "gopkg.in/op/go-logging.v1" 15 "gopkg.in/yaml.v3" 16 ) 17 18 var ( 19 // Only initialize the yq logging backend a single time, in a thread-safe way 20 yqLogInit sync.Once 21 ) 22 23 // Editor can modify the yaml in a Porter manifest. 24 type Editor struct { 25 filesystem aferox.Aferox 26 yq yqlib.YqLib 27 node *yaml.Node 28 } 29 30 func NewEditor(filesystem aferox.Aferox) *Editor { 31 e := &Editor{ 32 filesystem: filesystem, 33 } 34 yqLogInit.Do(e.suppressYqLogging) 35 return e 36 } 37 38 // Hide yq log statements 39 func (e *Editor) suppressYqLogging() { 40 // TODO: We could improve the logging and yq libs to be parallel friendly 41 // This turns off all logging from yq 42 // yq doesn't return errors from its api, and logs them instead (awkward) 43 // The yq logger is global, unless we seriously edit yq, we can't separate 44 // logging for once instance of a yq run from any parallel runs. It just 45 // seemed better to turn it off. 46 // yq has moved to v4 which is a very large api change, so it would be 47 // a lot of work though. 48 49 // The yq lib that we use makes frequent calls to a logger that are by default 50 // printed directly to stderr 51 var backend = logging.AddModuleLevel(logging.NewLogBackend(io.Discard, "", 0)) 52 backend.SetLevel(logging.ERROR, "yq") 53 logging.SetBackend(backend) 54 } 55 56 func (e *Editor) Read(data []byte) (n int, err error) { 57 e.yq = yqlib.NewYqLib() 58 e.node = &yaml.Node{} 59 60 var decoder = yaml.NewDecoder(bytes.NewReader(data)) 61 err = decoder.Decode(e.node) 62 if err != nil { 63 return len(data), fmt.Errorf("could not parse manifest:\n%s: %w", string(data), err) 64 } 65 66 return len(data), nil 67 } 68 69 func (e *Editor) ReadFile(src string) error { 70 contents, err := e.filesystem.ReadFile(src) 71 if err != nil { 72 return fmt.Errorf("could not read the manifest at %q: %w", src, err) 73 } 74 _, err = e.Read(contents) 75 if err != nil { 76 return fmt.Errorf("could not parse the manifest at %q: %w", src, err) 77 } 78 79 return nil 80 } 81 82 func (e *Editor) WriteFile(dest string) error { 83 destFile, err := e.filesystem.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, pkg.FileModeWritable) 84 if err != nil { 85 return fmt.Errorf("could not open destination manifest location %s: %w", dest, err) 86 } 87 defer destFile.Close() 88 89 // Encode the updated manifest to the proper location 90 // yqlib.NewYamlEncoder takes: dest (io.Writer), indent spaces (int), colorized output (bool) 91 var encoder = yqlib.NewYamlEncoder(destFile, 2, false) 92 err = encoder.Encode(e.node) 93 if err != nil { 94 return fmt.Errorf("unable to write the manifest to %s: %w", dest, err) 95 } 96 97 return nil 98 } 99 100 func (e *Editor) SetValue(path string, value string) error { 101 var valueParser = yqlib.NewValueParser() 102 // valueParser.Parse takes: argument (string), custom tag (string), 103 // custom style (string), anchor name (string), create alias (bool) 104 var parsedValue = valueParser.Parse(value, "", "", "", false) 105 cmd := yqlib.UpdateCommand{Command: "update", Path: path, Value: parsedValue, Overwrite: true} 106 err := e.yq.Update(e.node, cmd, true) 107 if err != nil { 108 return fmt.Errorf("could not update path %q with value %q: %w", path, value, err) 109 } 110 111 return nil 112 } 113 114 func (e *Editor) GetValue(path string) (string, error) { 115 n, err := e.GetNode(path) 116 if err != nil { 117 return "", err 118 } 119 120 return n.Value, nil 121 } 122 123 func (e *Editor) DeleteNode(path string) error { 124 cmd := yqlib.UpdateCommand{Command: "delete", Path: path} 125 err := e.yq.Update(e.node, cmd, true) 126 if err != nil { 127 return fmt.Errorf("could not delete path %q: %w", path, err) 128 } 129 130 return nil 131 } 132 133 // GetNode evaluates the specified yaml path to a single node. 134 // Returns an error if a node isn't found, or more than one is found. 135 func (e *Editor) GetNode(path string) (*yaml.Node, error) { 136 results, err := e.yq.Get(e.node, path) 137 if err != nil { 138 return nil, err 139 } 140 141 switch len(results) { 142 case 0: 143 return nil, fmt.Errorf("no matching nodes found for %s", path) 144 case 1: 145 return results[0].Node, nil 146 default: 147 return nil, fmt.Errorf("multiple nodes matched the path %s", path) 148 } 149 } 150 151 // WalkNodes executes f for all yaml nodes found in path. 152 // If an error is returned from f, the WalkNodes function will return the error and stop iterating through 153 // the rest of the nodes. 154 func (e *Editor) WalkNodes(ctx context.Context, path string, f func(ctx context.Context, nc *yqlib.NodeContext) error) error { 155 nodes, err := e.yq.Get(e.node, path) 156 if err != nil { 157 return fmt.Errorf("failed to find nodes with path %s: %w", path, err) 158 } 159 160 for _, node := range nodes { 161 if node.Node.IsZero() { 162 continue 163 } 164 if err := f(ctx, node); err != nil { 165 return err 166 } 167 168 } 169 170 return nil 171 }