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  }