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 }