github.com/tooploox/oya@v0.0.21-0.20230524103240-1cda1861aad6/pkg/raw/oyafile.go (about)

     1  package raw
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  
    11  	"github.com/pkg/errors"
    12  	"github.com/tooploox/oya/pkg/secrets"
    13  	"github.com/tooploox/oya/pkg/template"
    14  	yaml "gopkg.in/yaml.v2"
    15  )
    16  
    17  const DefaultName = "Oyafile"
    18  const ValueFileExt = ".oya"
    19  
    20  // Oyafile represents an unparsed Oyafile.
    21  type Oyafile struct {
    22  	Path            string // Path contains normalized absolute path to the Oyafile.
    23  	Dir             string // Dir contains normalized absolute path to the containing directory.
    24  	RootDir         string // RootDir is the absolute, normalized path to the project root directory.
    25  	oyafileContents []byte // file contains the main Oyafile contents.
    26  }
    27  
    28  // DecodedOyafile is an Oyafile that has been loaded from YAML
    29  // but hasn't been parsed yet.
    30  type DecodedOyafile template.Scope
    31  
    32  func (lhs DecodedOyafile) Merge(rhs DecodedOyafile) DecodedOyafile {
    33  	return DecodedOyafile(template.Scope(lhs).Merge(template.Scope(rhs)))
    34  }
    35  
    36  func Load(oyafilePath, rootDir string) (*Oyafile, bool, error) {
    37  	raw, err := New(oyafilePath, rootDir)
    38  	if err != nil {
    39  		return nil, false, nil
    40  	}
    41  	return raw, true, nil
    42  }
    43  
    44  func LoadFromDir(dirPath, rootDir string) (*Oyafile, bool, error) {
    45  	oyafilePath := fullPath(dirPath, "")
    46  	fi, err := os.Stat(oyafilePath)
    47  	if err != nil {
    48  		if os.IsNotExist(err) {
    49  			return nil, false, nil
    50  		}
    51  		return nil, false, err
    52  	}
    53  	if fi.IsDir() {
    54  		return nil, false, nil
    55  	}
    56  	return Load(oyafilePath, rootDir)
    57  }
    58  
    59  func New(oyafilePath, rootDir string) (*Oyafile, error) {
    60  	file, err := ioutil.ReadFile(oyafilePath)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  	normalizedOyafilePath := filepath.Clean(oyafilePath)
    65  	return &Oyafile{
    66  		Path:            normalizedOyafilePath,
    67  		Dir:             filepath.Dir(normalizedOyafilePath),
    68  		RootDir:         rootDir,
    69  		oyafileContents: file,
    70  	}, nil
    71  }
    72  
    73  func (raw *Oyafile) Decode() (DecodedOyafile, error) {
    74  	mainOyafile, err := decodeOyafile(raw)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	paths, err := listFiles(raw.Dir, ValueFileExt)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	for _, path := range paths {
    84  		fullPath := filepath.Join(raw.Dir, path)
    85  		rawValueFile, found, err := Load(fullPath, raw.RootDir)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		if !found {
    90  			return nil, errors.Errorf("Internal error: %s file not found while loading", path)
    91  		}
    92  		valueFile, err := decodeOyafile(rawValueFile)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		values, ok := template.ParseScope(map[interface{}]interface{}(valueFile))
    97  		if !ok {
    98  			return nil, errors.Errorf("Internal: error parsing scope trying to merge values, unexpected type: %T", valueFile)
    99  		}
   100  		if err := mergeValues(&mainOyafile, values); err != nil {
   101  			return nil, err
   102  		}
   103  	}
   104  	return mainOyafile, nil
   105  }
   106  
   107  func listFiles(path, ext string) ([]string, error) {
   108  	var files []string
   109  	fileInfo, err := ioutil.ReadDir(path)
   110  	if err != nil {
   111  		return files, err
   112  	}
   113  	for _, file := range fileInfo {
   114  		path := file.Name()
   115  		if !file.IsDir() && filepath.Ext(path) == ext {
   116  			files = append(files, path)
   117  		}
   118  	}
   119  	return files, nil
   120  }
   121  
   122  func decodeOyafile(raw *Oyafile) (DecodedOyafile, error) {
   123  	decrypted, found, err := secrets.Decrypt(raw.Path)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	if found {
   128  		decodedSecrets, err := decodeYaml(decrypted)
   129  		if err != nil {
   130  			return nil, errors.Wrapf(err, "error parsing secret file %q", raw.Path)
   131  		}
   132  		return decodedSecrets, nil
   133  	}
   134  
   135  	// YAML parser does not handle files without at least one node.
   136  	empty, err := isEmptyYAML(raw.Path)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	if empty {
   141  		return make(DecodedOyafile), nil
   142  	}
   143  	decodedOyafileI, err := decodeYaml(raw.oyafileContents)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	return DecodedOyafile(decodedOyafileI), nil
   148  }
   149  
   150  func decodeYaml(content []byte) (map[interface{}]interface{}, error) {
   151  	reader := bytes.NewReader(content)
   152  	decoder := yaml.NewDecoder(reader)
   153  	var of map[interface{}]interface{}
   154  	err := decoder.Decode(&of)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	return of, nil
   159  }
   160  
   161  func (raw *Oyafile) Project() (interface{}, bool, error) {
   162  	of, err := decodeOyafile(raw)
   163  	if err != nil {
   164  		return nil, false, err
   165  	}
   166  	val, ok := of["Project"]
   167  	return val, ok, nil
   168  }
   169  
   170  func (raw *Oyafile) IsRoot() (bool, error) {
   171  	_, hasProject, err := raw.Project()
   172  	if err != nil {
   173  		return false, err
   174  	}
   175  
   176  	rel, err := filepath.Rel(raw.RootDir, raw.Path)
   177  	if err != nil {
   178  		return false, err
   179  	}
   180  	return hasProject && rel == DefaultName, nil
   181  }
   182  
   183  // isEmptyYAML returns true if the Oyafile contains only blank characters
   184  // or YAML comments.
   185  func isEmptyYAML(oyafilePath string) (bool, error) {
   186  	file, err := os.Open(oyafilePath)
   187  	if err != nil {
   188  		return false, err
   189  	}
   190  	defer file.Close()
   191  
   192  	scanner := bufio.NewScanner(file)
   193  	for scanner.Scan() {
   194  		if isNode(scanner.Text()) {
   195  			return false, nil
   196  		}
   197  	}
   198  
   199  	return true, scanner.Err()
   200  }
   201  
   202  func isNode(line string) bool {
   203  	for _, c := range line {
   204  		switch c {
   205  		case '#':
   206  			return false
   207  		case ' ', '\t', '\n', '\f', '\r':
   208  			continue
   209  		default:
   210  			return true
   211  		}
   212  	}
   213  	return false
   214  }
   215  
   216  func fullPath(projectDir, name string) string {
   217  	if len(name) == 0 {
   218  		name = DefaultName
   219  	}
   220  	return path.Join(projectDir, name)
   221  }
   222  
   223  func mergeValues(of *DecodedOyafile, secrets template.Scope) error {
   224  	var values template.Scope
   225  	valuesI, ok := (*of)["Values"]
   226  	if ok {
   227  		values, ok = template.ParseScope(valuesI)
   228  		if !ok {
   229  			return errors.Errorf("Internal: error parsing scope")
   230  		}
   231  	}
   232  	(*of)["Values"] = map[interface{}]interface{}(values.Merge(secrets))
   233  	return nil
   234  }