mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
AB#2098 versioned & strict yaml reading (#157)
This commit is contained in:
parent
7c2d1c3490
commit
135c787001
@ -13,8 +13,15 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/internal/file"
|
"github.com/edgelesssys/constellation/internal/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version1 = "v1"
|
||||||
|
)
|
||||||
|
|
||||||
// Config defines configuration used by CLI.
|
// Config defines configuration used by CLI.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// description: |
|
||||||
|
// Schema version of this configuration file.
|
||||||
|
Version string `yaml:"version"`
|
||||||
// description: |
|
// description: |
|
||||||
// Minimum number of nodes in autoscaling group.
|
// Minimum number of nodes in autoscaling group.
|
||||||
// worker nodes.
|
// worker nodes.
|
||||||
@ -154,6 +161,7 @@ type QEMUConfig struct {
|
|||||||
// Default returns a struct with the default config.
|
// Default returns a struct with the default config.
|
||||||
func Default() *Config {
|
func Default() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
|
Version: Version1,
|
||||||
AutoscalingNodeGroupsMin: 1,
|
AutoscalingNodeGroupsMin: 1,
|
||||||
AutoscalingNodeGroupsMax: 10,
|
AutoscalingNodeGroupsMax: 10,
|
||||||
StateDiskSizeGB: 30,
|
StateDiskSizeGB: 30,
|
||||||
@ -245,11 +253,14 @@ func FromFile(fileHandler file.Handler, name string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var emptyConf Config
|
var emptyConf Config
|
||||||
if err := fileHandler.ReadYAML(name, &emptyConf); err != nil {
|
if err := fileHandler.ReadYAMLStrict(name, &emptyConf); err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("unable to find %s - use `constellation config generate` to generate it first", name)
|
return nil, fmt.Errorf("unable to find %s - use `constellation config generate` to generate it first", name)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("could not load config from file %s: %w", name, err)
|
return nil, fmt.Errorf("could not load config from file %s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
if emptyConf.Version != Version1 {
|
||||||
|
return nil, fmt.Errorf("config version (%s) is not supported - only version %s is supported", emptyConf.Version, Version1)
|
||||||
|
}
|
||||||
return &emptyConf, nil
|
return &emptyConf, nil
|
||||||
}
|
}
|
||||||
|
@ -24,46 +24,51 @@ func init() {
|
|||||||
ConfigDoc.Type = "Config"
|
ConfigDoc.Type = "Config"
|
||||||
ConfigDoc.Comments[encoder.LineComment] = "Config defines configuration used by CLI."
|
ConfigDoc.Comments[encoder.LineComment] = "Config defines configuration used by CLI."
|
||||||
ConfigDoc.Description = "Config defines configuration used by CLI."
|
ConfigDoc.Description = "Config defines configuration used by CLI."
|
||||||
ConfigDoc.Fields = make([]encoder.Doc, 7)
|
ConfigDoc.Fields = make([]encoder.Doc, 8)
|
||||||
ConfigDoc.Fields[0].Name = "autoscalingNodeGroupsMin"
|
ConfigDoc.Fields[0].Name = "version"
|
||||||
ConfigDoc.Fields[0].Type = "int"
|
ConfigDoc.Fields[0].Type = "string"
|
||||||
ConfigDoc.Fields[0].Note = ""
|
ConfigDoc.Fields[0].Note = ""
|
||||||
ConfigDoc.Fields[0].Description = "Minimum number of nodes in autoscaling group.\nworker nodes."
|
ConfigDoc.Fields[0].Description = "Schema version of this configuration file."
|
||||||
ConfigDoc.Fields[0].Comments[encoder.LineComment] = "Minimum number of nodes in autoscaling group."
|
ConfigDoc.Fields[0].Comments[encoder.LineComment] = "Schema version of this configuration file."
|
||||||
ConfigDoc.Fields[1].Name = "autoscalingNodeGroupsMax"
|
ConfigDoc.Fields[1].Name = "autoscalingNodeGroupsMin"
|
||||||
ConfigDoc.Fields[1].Type = "int"
|
ConfigDoc.Fields[1].Type = "int"
|
||||||
ConfigDoc.Fields[1].Note = ""
|
ConfigDoc.Fields[1].Note = ""
|
||||||
ConfigDoc.Fields[1].Description = "Maximum number of nodes in autoscaling group.\nworker nodes."
|
ConfigDoc.Fields[1].Description = "Minimum number of nodes in autoscaling group.\nworker nodes."
|
||||||
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Maximum number of nodes in autoscaling group."
|
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Minimum number of nodes in autoscaling group."
|
||||||
ConfigDoc.Fields[2].Name = "stateDisksizeGB"
|
ConfigDoc.Fields[2].Name = "autoscalingNodeGroupsMax"
|
||||||
ConfigDoc.Fields[2].Type = "int"
|
ConfigDoc.Fields[2].Type = "int"
|
||||||
ConfigDoc.Fields[2].Note = ""
|
ConfigDoc.Fields[2].Note = ""
|
||||||
ConfigDoc.Fields[2].Description = "Size (in GB) of data disk used for nodes."
|
ConfigDoc.Fields[2].Description = "Maximum number of nodes in autoscaling group.\nworker nodes."
|
||||||
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Size (in GB) of data disk used for nodes."
|
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Maximum number of nodes in autoscaling group."
|
||||||
ConfigDoc.Fields[3].Name = "ingressFirewall"
|
ConfigDoc.Fields[3].Name = "stateDisksizeGB"
|
||||||
ConfigDoc.Fields[3].Type = "Firewall"
|
ConfigDoc.Fields[3].Type = "int"
|
||||||
ConfigDoc.Fields[3].Note = ""
|
ConfigDoc.Fields[3].Note = ""
|
||||||
ConfigDoc.Fields[3].Description = "Ingress firewall rules for node network."
|
ConfigDoc.Fields[3].Description = "Size (in GB) of data disk used for nodes."
|
||||||
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Ingress firewall rules for node network."
|
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Size (in GB) of data disk used for nodes."
|
||||||
ConfigDoc.Fields[4].Name = "egressFirewall"
|
ConfigDoc.Fields[4].Name = "ingressFirewall"
|
||||||
ConfigDoc.Fields[4].Type = "Firewall"
|
ConfigDoc.Fields[4].Type = "Firewall"
|
||||||
ConfigDoc.Fields[4].Note = ""
|
ConfigDoc.Fields[4].Note = ""
|
||||||
ConfigDoc.Fields[4].Description = "Egress firewall rules for node network."
|
ConfigDoc.Fields[4].Description = "Ingress firewall rules for node network."
|
||||||
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Egress firewall rules for node network."
|
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Ingress firewall rules for node network."
|
||||||
|
ConfigDoc.Fields[5].Name = "egressFirewall"
|
||||||
ConfigDoc.Fields[4].AddExample("", Firewall{{Name: "rule#1", Description: "the first rule", Protocol: "tcp", IPRange: "0.0.0.0/0", FromPort: 443, ToPort: 443}})
|
ConfigDoc.Fields[5].Type = "Firewall"
|
||||||
ConfigDoc.Fields[5].Name = "provider"
|
|
||||||
ConfigDoc.Fields[5].Type = "ProviderConfig"
|
|
||||||
ConfigDoc.Fields[5].Note = ""
|
ConfigDoc.Fields[5].Note = ""
|
||||||
ConfigDoc.Fields[5].Description = "Supported cloud providers & their specific configurations."
|
ConfigDoc.Fields[5].Description = "Egress firewall rules for node network."
|
||||||
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Supported cloud providers & their specific configurations."
|
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Egress firewall rules for node network."
|
||||||
ConfigDoc.Fields[6].Name = "sshUsers"
|
|
||||||
ConfigDoc.Fields[6].Type = "[]UserKey"
|
|
||||||
ConfigDoc.Fields[6].Note = ""
|
|
||||||
ConfigDoc.Fields[6].Description = "Create SSH users on Constellation nodes."
|
|
||||||
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Create SSH users on Constellation nodes."
|
|
||||||
|
|
||||||
ConfigDoc.Fields[6].AddExample("", []UserKey{{Username: "Alice", PublicKey: "ssh-rsa AAAAB3NzaC...5QXHKW1rufgtJeSeJ8= alice@domain.com"}})
|
ConfigDoc.Fields[5].AddExample("", Firewall{{Name: "rule#1", Description: "the first rule", Protocol: "tcp", IPRange: "0.0.0.0/0", FromPort: 443, ToPort: 443}})
|
||||||
|
ConfigDoc.Fields[6].Name = "provider"
|
||||||
|
ConfigDoc.Fields[6].Type = "ProviderConfig"
|
||||||
|
ConfigDoc.Fields[6].Note = ""
|
||||||
|
ConfigDoc.Fields[6].Description = "Supported cloud providers & their specific configurations."
|
||||||
|
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Supported cloud providers & their specific configurations."
|
||||||
|
ConfigDoc.Fields[7].Name = "sshUsers"
|
||||||
|
ConfigDoc.Fields[7].Type = "[]UserKey"
|
||||||
|
ConfigDoc.Fields[7].Note = ""
|
||||||
|
ConfigDoc.Fields[7].Description = "Create SSH users on Constellation nodes."
|
||||||
|
ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Create SSH users on Constellation nodes."
|
||||||
|
|
||||||
|
ConfigDoc.Fields[7].AddExample("", []UserKey{{Username: "Alice", PublicKey: "ssh-rsa AAAAB3NzaC...5QXHKW1rufgtJeSeJ8= alice@domain.com"}})
|
||||||
|
|
||||||
UserKeyDoc.Type = "UserKey"
|
UserKeyDoc.Type = "UserKey"
|
||||||
UserKeyDoc.Comments[encoder.LineComment] = "UserKey describes a user that should be created with corresponding public SSH key."
|
UserKeyDoc.Comments[encoder.LineComment] = "UserKey describes a user that should be created with corresponding public SSH key."
|
||||||
|
@ -46,11 +46,13 @@ func TestFromFile(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"custom config from default file": {
|
"custom config from default file": {
|
||||||
config: &Config{
|
config: &Config{
|
||||||
|
Version: Version1,
|
||||||
AutoscalingNodeGroupsMin: 42,
|
AutoscalingNodeGroupsMin: 42,
|
||||||
AutoscalingNodeGroupsMax: 1337,
|
AutoscalingNodeGroupsMax: 1337,
|
||||||
},
|
},
|
||||||
configName: constants.ConfigFilename,
|
configName: constants.ConfigFilename,
|
||||||
wantResult: &Config{
|
wantResult: &Config{
|
||||||
|
Version: Version1,
|
||||||
AutoscalingNodeGroupsMin: 42,
|
AutoscalingNodeGroupsMin: 42,
|
||||||
AutoscalingNodeGroupsMax: 1337,
|
AutoscalingNodeGroupsMax: 1337,
|
||||||
},
|
},
|
||||||
@ -94,6 +96,51 @@ func TestFromFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromFileStrictErrors(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
yamlConfig string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"valid config": {
|
||||||
|
yamlConfig: `
|
||||||
|
autoscalingNodeGroupsMin: 5
|
||||||
|
autoscalingNodeGroupsMax: 10
|
||||||
|
stateDisksizeGB: 25
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"typo": {
|
||||||
|
yamlConfig: `
|
||||||
|
autoscalingNodeGroupsMini: 5
|
||||||
|
autoscalingNodeGroupsMax: 10
|
||||||
|
stateDisksizeGB: 25
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"unsupported version": {
|
||||||
|
yamlConfig: `
|
||||||
|
version: v5
|
||||||
|
autoscalingNodeGroupsMin: 1
|
||||||
|
autoscalingNodeGroupsMax: 10
|
||||||
|
stateDisksizeGB: 30
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
err := fileHandler.Write(constants.ConfigFilename, []byte(tc.yamlConfig), file.OptNone)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
_, err = FromFile(fileHandler, constants.ConfigFilename)
|
||||||
|
assert.Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigRemoveProviderExcept(t *testing.T) {
|
func TestConfigRemoveProviderExcept(t *testing.T) {
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
removeExcept cloudprovider.Provider
|
removeExcept cloudprovider.Provider
|
||||||
@ -134,3 +181,8 @@ func TestConfigRemoveProviderExcept(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigGeneratedDocsFresh(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Len(ConfigDoc.Fields, 8, "remember to re-generate config docs!")
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ and file system abstraction.
|
|||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@ -99,11 +100,23 @@ func (h *Handler) WriteJSON(name string, content any, options Option) error {
|
|||||||
// ReadYAML reads a YAML file from name and unmarshals it into the content interface.
|
// ReadYAML reads a YAML file from name and unmarshals it into the content interface.
|
||||||
// The interface content must be a pointer to a YAML marchalable object.
|
// The interface content must be a pointer to a YAML marchalable object.
|
||||||
func (h *Handler) ReadYAML(name string, content any) error {
|
func (h *Handler) ReadYAML(name string, content any) error {
|
||||||
|
return h.readYAML(name, content, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadYAMLStrict does the same as ReadYAML, but fails if YAML contains fields
|
||||||
|
// that are not specified in content.
|
||||||
|
func (h *Handler) ReadYAMLStrict(name string, content any) error {
|
||||||
|
return h.readYAML(name, content, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) readYAML(name string, content any, strict bool) error {
|
||||||
data, err := h.Read(name)
|
data, err := h.Read(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return yaml.Unmarshal(data, content)
|
decoder := yaml.NewDecoder(bytes.NewBuffer(data))
|
||||||
|
decoder.KnownFields(strict)
|
||||||
|
return decoder.Decode(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteYAML marshals the content interface to YAML and writes it to the path with the given name.
|
// WriteYAML marshals the content interface to YAML and writes it to the path with the given name.
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/internal/constants"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -211,6 +212,26 @@ func TestReadYAML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadYAMLStrictUnknownFieldFails(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
type SampleConfig struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
yamlConfig := `
|
||||||
|
version: "1.0.0"
|
||||||
|
value: "foobar"
|
||||||
|
sneakyValue: "superSecret"
|
||||||
|
`
|
||||||
|
|
||||||
|
handler := NewHandler(afero.NewMemMapFs())
|
||||||
|
assert.NoError(handler.Write(constants.ConfigFilename, []byte(yamlConfig), OptNone))
|
||||||
|
|
||||||
|
var readInConfig SampleConfig
|
||||||
|
assert.Error(handler.ReadYAMLStrict(constants.ConfigFilename, &readInConfig))
|
||||||
|
}
|
||||||
|
|
||||||
func TestWriteYAML(t *testing.T) {
|
func TestWriteYAML(t *testing.T) {
|
||||||
type testContent struct {
|
type testContent struct {
|
||||||
First string
|
First string
|
||||||
|
Loading…
Reference in New Issue
Block a user