summaryrefslogtreecommitdiff
path: root/config/reader.go
blob: 4386aa78e0e895e02fd4e561a7568dd4edd3e5c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package config

import (
	"errors"
	"fmt"
	"io/ioutil"

	"gopkg.in/yaml.v2"
)

// DefaultConfig contains YAML that is applied before the user's
// configuration.
//
const DefaultConfig = `---
lives:
  in: /srv/app
  as: somebody
  uid: 65533
  gid: 65533
runs:
  as: runuser
  uid: 900
  gid: 900`

// ResolveIncludes iterates over and recurses through a given variant's
// includes to build a flat slice of variant names in the correct order by
// which they should be expanded/merged. It checks for both the existence of
// included variants and maintains a recursion stack to protect against
// infinite loops.
//
// Variant names found at a greater depth of recursion are first and siblings
// last, the order in which config should be merged.
//
func ResolveIncludes(config *Config, name string) ([]string, error) {
	stack := map[string]bool{}
	includes := []string{}

	var resolve func(string) error

	resolve = func(name string) error {
		if instack, found := stack[name]; found && instack {
			return errors.New("variant expansion detected loop")
		}

		stack[name] = true
		defer func() { stack[name] = false }()

		variant, found := config.Variants[name]

		if !found {
			return fmt.Errorf("variant '%s' does not exist", name)
		}

		for _, include := range variant.Includes {
			if err := resolve(include); err != nil {
				return err
			}
		}

		// Appending _after_ recursion ensures the correct ordering
		includes = append(includes, name)

		return nil
	}

	err := resolve(name)

	return includes, err
}

// ExpandVariant merges a named variant with a config. It also attempts to
// recursively expand any included variants in the expanded variant.
//
func ExpandVariant(config *Config, name string) (*VariantConfig, error) {
	expanded := new(VariantConfig)
	expanded.CommonConfig.Merge(config.CommonConfig)

	includes, err := ResolveIncludes(config, name)

	if err != nil {
		return nil, err
	}

	for _, include := range includes {
		expanded.Merge(config.Variants[include])
	}

	return expanded, nil
}

// ReadConfig unmarshals the given YAML bytes into a new Config struct.
//
func ReadConfig(data []byte) (*Config, error) {
	var (
		version VersionConfig
		config  Config
	)

	// Unmarshal (un-strictly) config version first for pre-validation
	err := yaml.Unmarshal(data, &version)

	if err != nil {
		return nil, err
	}

	if err = Validate(version); err != nil {
		return nil, err
	}

	// Unmarshal the default config
	yaml.Unmarshal([]byte(DefaultConfig), &config)

	// And finally strictly unmarshal the entire user-provided config
	err = yaml.UnmarshalStrict(data, &config)

	if err != nil {
		return nil, err
	}

	err = Validate(config)

	return &config, err
}

// ReadConfigFile unmarshals the given YAML file contents into a Config
// struct.
//
func ReadConfigFile(path string) (*Config, error) {
	data, err := ioutil.ReadFile(path)

	if err != nil {
		return nil, err
	}

	return ReadConfig(data)
}