github.com/openshift-online/ocm-sdk-go@v0.1.473/configuration/tags.go (about)

     1  /*
     2  Copyright (c) 2020 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // This file contains the code that is used to process tags like `!variable` and `!file` inside
    18  // configuration files.
    19  
    20  package configuration
    21  
    22  import (
    23  	"bytes"
    24  	"os"
    25  	"os/exec"
    26  	"strings"
    27  
    28  	"gopkg.in/yaml.v3"
    29  )
    30  
    31  // tagRegistryEntry stores the description of one tag. When a tag is detected in a node the
    32  // corresponding function will be called passing the node, so that the function can modify
    33  // it as needed.
    34  type tagRegistryEntry struct {
    35  	name    string
    36  	kind    yaml.Kind
    37  	process func(*yaml.Node) error
    38  }
    39  
    40  // registerTag adds a tag to the registry.
    41  func (b *Builder) registerTag(name string, kind yaml.Kind, process func(*yaml.Node) error) {
    42  	b.tags = append(b.tags, tagRegistryEntry{
    43  		name:    name,
    44  		kind:    kind,
    45  		process: process,
    46  	})
    47  }
    48  
    49  // lookupTag tries to find an entry in the tag registry corresponding to the given name.
    50  func (b *Builder) lookupTag(name string) (result tagRegistryEntry, ok bool) {
    51  	for _, entry := range b.tags {
    52  		if strings.HasPrefix(entry.name, name) {
    53  			result = entry
    54  			ok = true
    55  			break
    56  		}
    57  	}
    58  	return
    59  }
    60  
    61  // processTreeTags process recursively the tags present in the given YAML tree and returns the
    62  // result.
    63  func (b *Builder) processTreeTags(tree *yaml.Node) error {
    64  	err := b.processNodeTags(tree)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	for _, node := range tree.Content {
    69  		err = b.processTreeTags(node)
    70  		if err != nil {
    71  			return err
    72  		}
    73  	}
    74  	return nil
    75  }
    76  
    77  // processNodeTags process the tags present in the given YAML node.
    78  func (b *Builder) processNodeTags(node *yaml.Node) error {
    79  	tag := node.ShortTag()
    80  	if strings.HasPrefix(tag, "!!") {
    81  		return nil
    82  	}
    83  	names := b.parseTag(tag)
    84  	for _, name := range names {
    85  		entry, ok := b.lookupTag(name)
    86  		if ok {
    87  			if entry.kind != node.Kind {
    88  				return b.nodeError(
    89  					node,
    90  					"tag '%s' expects %s node but found %s",
    91  					name,
    92  					b.kindString(entry.kind),
    93  					b.kindString(node.Kind),
    94  				)
    95  			}
    96  			err := entry.process(node)
    97  			if err != nil {
    98  				return err
    99  			}
   100  		} else {
   101  			return b.nodeError(node, "unsupported tag '%s'", name)
   102  		}
   103  	}
   104  	return nil
   105  }
   106  
   107  // processVariableTag is the implementation fo the `variable` tag: replaces an environment variable
   108  // reference with its value.
   109  func (b *Builder) processVariableTag(node *yaml.Node) error {
   110  	variable := node.Value
   111  	result, ok := os.LookupEnv(variable)
   112  	if !ok {
   113  		return b.nodeError(node, "can't find environment variable '%s'", variable)
   114  	}
   115  	node.SetString(result)
   116  	return nil
   117  }
   118  
   119  // processFileTag is the implementation of the `file` tag: replaces a file reference with the
   120  // content of the file.
   121  func (b *Builder) processFileTag(node *yaml.Node) error {
   122  	file := node.Value
   123  	data, err := os.ReadFile(file) // #nosec G304
   124  	if err != nil {
   125  		return b.nodeError(node, "%w", err)
   126  	}
   127  	result := string(data)
   128  	node.SetString(result)
   129  	b.titleTree(file, node)
   130  	return nil
   131  }
   132  
   133  // processScriptTag is the implementation of the `script` tag: replaces a script script with
   134  // the result of executing it.
   135  func (b *Builder) processScriptTag(node *yaml.Node) error {
   136  	shell, ok := os.LookupEnv("SHELL")
   137  	if !ok {
   138  		shell = "/bin/sh"
   139  	}
   140  	script := node.Value
   141  	stdin := strings.NewReader(script)
   142  	stdout := &bytes.Buffer{}
   143  	stderr := &bytes.Buffer{}
   144  	cmd := &exec.Cmd{
   145  		Path:   shell,
   146  		Stdin:  stdin,
   147  		Stdout: stdout,
   148  		Stderr: stderr,
   149  	}
   150  	err := cmd.Run()
   151  	_, exit := err.(*exec.ExitError)
   152  	if exit {
   153  		err = nil
   154  	}
   155  	if err != nil {
   156  		return b.nodeError(node, "%w", err)
   157  	}
   158  	code := cmd.ProcessState.ExitCode()
   159  	if code != 0 || stderr.Len() > 0 {
   160  		return b.nodeError(
   161  			node,
   162  			"script '%s' finished with exit code %d, wrote '%s' to stdout and '%s' "+
   163  				"to stderr",
   164  			b.quoteForError(script),
   165  			code,
   166  			b.quoteForError(stdout.String()),
   167  			b.quoteForError(stderr.String()),
   168  		)
   169  	}
   170  	result := stdout.String()
   171  	node.SetString(result)
   172  	return nil
   173  }
   174  
   175  // processTrimTag is the implementation of the `trim` tag: trims leading and trailing white
   176  // space, including line breaks and tabs.
   177  func (b *Builder) processTrimTag(node *yaml.Node) error {
   178  	value := node.Value
   179  	value = strings.TrimSpace(value)
   180  	node.SetString(value)
   181  	return nil
   182  }
   183  
   184  // processYamlTag is the implementation of the `yaml` tag: parses the value as a YAML
   185  // document and replaces the current node with the result.
   186  func (b *Builder) processYamlTag(node *yaml.Node) error {
   187  	buffer := []byte(node.Value)
   188  	var parsed yaml.Node
   189  	err := yaml.Unmarshal(buffer, &parsed)
   190  	if err != nil {
   191  		return b.nodeError(node, "can't parse '%s': %w", buffer, err)
   192  	}
   193  	b.titleTree(b.titles[node], &parsed)
   194  	err = b.processTreeTags(&parsed)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	*node = *parsed.Content[0]
   199  	return nil
   200  }
   201  
   202  // processStrTag is the implementation of the `string` tag: it explicitly sets the node kind
   203  // to `!!str`.
   204  func (b *Builder) processStringTag(node *yaml.Node) error {
   205  	node.Tag = "!!str"
   206  	return nil
   207  }
   208  
   209  // processIntegerTag is the implementation of the `integer` tag: it explicitly sets the node kind
   210  // to `!!int`.
   211  func (b *Builder) processIntegerTag(node *yaml.Node) error {
   212  	node.Tag = "!!int"
   213  	return nil
   214  }
   215  
   216  // processBooleanTag is the implementation of the `bool` tag: it explicitly sets the node kind
   217  // to `!!bool`.
   218  func (b *Builder) processBooleanTag(node *yaml.Node) error {
   219  	node.Tag = "!!bool"
   220  	return nil
   221  }
   222  
   223  // processFloatTag is the implementation of the `float` tag: it explicitly sets the node type
   224  // to `!!float`.
   225  func (b *Builder) processFloatTag(node *yaml.Node) error {
   226  	node.Tag = "!!float"
   227  	return nil
   228  }
   229  
   230  // parseTag extracts the coponents from the given tags. For example, the tag `!file/int` will be
   231  // parsed as a slice containing `file` and `int`.
   232  func (b *Builder) parseTag(tag string) []string {
   233  	if !strings.HasPrefix(tag, "!") {
   234  		return nil
   235  	}
   236  	return strings.Split(tag[1:], "/")
   237  }