store.go
package goss
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"gopkg.in/yaml.v2"
"github.com/aelsabbahy/goss/resource"
)
const (
UNSET = iota
JSON
YAML
)
var outStoreFormat = UNSET
var currentTemplateFilter TemplateFilter
var debug = false
func getStoreFormatFromFileName(f string) (int, error) {
ext := filepath.Ext(f)
switch ext {
case ".json":
return JSON, nil
case ".yaml", ".yml":
return YAML, nil
default:
return 0, fmt.Errorf("unknown file extension: %v", ext)
}
}
func getStoreFormatFromData(data []byte) (int, error) {
var v interface{}
if err := unmarshalJSON(data, &v); err == nil {
return JSON, nil
}
if err := unmarshalYAML(data, &v); err == nil {
return YAML, nil
}
return 0, fmt.Errorf("unable to determine format from content")
}
// ReadJSON Reads json file returning GossConfig
func ReadJSON(filePath string) (GossConfig, error) {
file, err := ioutil.ReadFile(filePath)
if err != nil {
return GossConfig{}, fmt.Errorf("file error: %v", err)
}
return ReadJSONData(file, false)
}
type TmplVars struct {
Vars map[string]interface{}
}
func (t *TmplVars) Env() map[string]string {
env := make(map[string]string)
for _, i := range os.Environ() {
sep := strings.Index(i, "=")
env[i[0:sep]] = i[sep+1:]
}
return env
}
func loadVars(varsFile string, varsInline string) (map[string]interface{}, error) {
vars, err := varsFromFile(varsFile)
if err != nil {
return nil, fmt.Errorf("Error: loading vars file '%s'\n%w", varsFile, err)
}
varsExtra, err := varsFromString(varsInline)
if err != nil {
return nil, fmt.Errorf("Error: loading inline vars\n%w", err)
}
for k, v := range varsExtra {
vars[k] = v
}
return vars, nil
}
func varsFromFile(varsFile string) (map[string]interface{}, error) {
vars := make(map[string]interface{})
if varsFile == "" {
return vars, nil
}
data, err := ioutil.ReadFile(varsFile)
if err != nil {
return vars, err
}
format, err := getStoreFormatFromData(data)
if err != nil {
return nil, err
}
if err := unmarshal(data, &vars, format); err != nil {
return vars, err
}
return vars, nil
}
func varsFromString(varsString string) (map[string]interface{}, error) {
vars := make(map[string]interface{})
if varsString == "" {
return vars, nil
}
data := []byte(varsString)
format, err := getStoreFormatFromData(data)
if err != nil {
return nil, err
}
if err := unmarshal(data, &vars, format); err != nil {
return vars, err
}
return vars, nil
}
// ReadJSONData Reads json byte array returning GossConfig
func ReadJSONData(data []byte, detectFormat bool) (GossConfig, error) {
var err error
if currentTemplateFilter != nil {
data, err = currentTemplateFilter(data)
if err != nil {
return GossConfig{}, err
}
if debug {
fmt.Println("DEBUG: file after text/template render")
fmt.Println(string(data))
}
}
format := outStoreFormat
if detectFormat == true {
format, err = getStoreFormatFromData(data)
if err != nil {
return GossConfig{}, err
}
}
gossConfig := NewGossConfig()
// Horrible, but will do for now
if err := unmarshal(data, gossConfig, format); err != nil {
return *gossConfig, err
}
return *gossConfig, nil
}
// Reads json file recursively returning string
func RenderJSON(c *RuntimeConfig) (string, error) {
var err error
debug = c.Debug
currentTemplateFilter, err = NewTemplateFilter(c.Vars, c.VarsInline)
if err != nil {
return "", err
}
outStoreFormat, err = getStoreFormatFromFileName(c.Spec)
if err != nil {
return "", err
}
j, err := ReadJSON(c.Spec)
if err != nil {
return "", err
}
gossConfig, err := mergeJSONData(j, 0, filepath.Dir(c.Spec))
if err != nil {
return "", err
}
b, err := marshal(gossConfig)
if err != nil {
return "", fmt.Errorf("rendering failed: %v", err)
}
return string(b), nil
}
func mergeJSONData(gossConfig GossConfig, depth int, path string) (GossConfig, error) {
depth++
if depth >= 50 {
return GossConfig{}, fmt.Errorf("max depth of 50 reached, possibly due to dependency loop in goss file")
}
// Our return gossConfig
ret := *NewGossConfig()
ret = mergeGoss(ret, gossConfig)
// Sort the gossfiles to ensure consistent ordering
var keys []string
for k := range gossConfig.Gossfiles {
keys = append(keys, k)
}
sort.Strings(keys)
// Merge gossfiles in sorted order
for _, k := range keys {
g := gossConfig.Gossfiles[k]
var fpath string
if strings.HasPrefix(g.ID(), "/") {
fpath = g.ID()
} else {
fpath = filepath.Join(path, g.ID())
}
matches, err := filepath.Glob(fpath)
if err != nil {
return ret, fmt.Errorf("error in expanding glob pattern: %q", err)
}
if matches == nil {
return ret, fmt.Errorf("no matched files were found: %q", fpath)
}
for _, match := range matches {
fdir := filepath.Dir(match)
j, err := ReadJSON(match)
if err != nil {
return GossConfig{}, fmt.Errorf("could not read json data in %s: %s", match, err)
}
j, err = mergeJSONData(j, depth, fdir)
if err != nil {
return ret, fmt.Errorf("could not write json data: %s", err)
}
ret = mergeGoss(ret, j)
}
}
return ret, nil
}
func WriteJSON(filePath string, gossConfig GossConfig) error {
jsonData, err := marshal(gossConfig)
if err != nil {
return fmt.Errorf("failed to write %s: %s", filePath, err)
}
// check if the auto added json data is empty before writing to file.
emptyConfig := *NewGossConfig()
emptyData, err := marshal(emptyConfig)
if err != nil {
return fmt.Errorf("failed to write %s: %s", filePath, err)
}
if string(emptyData) == string(jsonData) {
log.Printf("Can't write empty configuration file. Please check resource name(s).")
return nil
}
if err := ioutil.WriteFile(filePath, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write %s: %s", filePath, err)
}
return nil
}
func resourcePrint(fileName string, res resource.ResourceRead) {
resMap := map[string]resource.ResourceRead{res.ID(): res}
oj, _ := marshal(resMap)
typ := reflect.TypeOf(res)
typs := strings.Split(typ.String(), ".")[1]
fmt.Printf("Adding %s to '%s':\n\n%s\n\n", typs, fileName, string(oj))
}
func marshal(gossConfig interface{}) ([]byte, error) {
switch outStoreFormat {
case JSON:
return marshalJSON(gossConfig)
case YAML:
return marshalYAML(gossConfig)
default:
return nil, fmt.Errorf("StoreFormat unset")
}
}
func unmarshal(data []byte, v interface{}, storeFormat int) error {
switch storeFormat {
case JSON:
return unmarshalJSON(data, v)
case YAML:
return unmarshalYAML(data, v)
default:
return fmt.Errorf("StoreFormat unset")
}
}
func marshalJSON(gossConfig interface{}) ([]byte, error) {
return json.MarshalIndent(gossConfig, "", " ")
}
func unmarshalJSON(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
func marshalYAML(gossConfig interface{}) ([]byte, error) {
return yaml.Marshal(gossConfig)
}
func unmarshalYAML(data []byte, v interface{}) error {
err := yaml.Unmarshal(data, v)
if err != nil {
return fmt.Errorf("could not unmarshal %q as YAML data: %s", string(data), err)
}
return nil
}