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"
|
||||
)
|
||||
|
||||
const (
|
||||
Version1 = "v1"
|
||||
)
|
||||
|
||||
// Config defines configuration used by CLI.
|
||||
type Config struct {
|
||||
// description: |
|
||||
// Schema version of this configuration file.
|
||||
Version string `yaml:"version"`
|
||||
// description: |
|
||||
// Minimum number of nodes in autoscaling group.
|
||||
// worker nodes.
|
||||
@ -154,6 +161,7 @@ type QEMUConfig struct {
|
||||
// Default returns a struct with the default config.
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
Version: Version1,
|
||||
AutoscalingNodeGroupsMin: 1,
|
||||
AutoscalingNodeGroupsMax: 10,
|
||||
StateDiskSizeGB: 30,
|
||||
@ -245,11 +253,14 @@ func FromFile(fileHandler file.Handler, name string) (*Config, error) {
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -24,46 +24,51 @@ func init() {
|
||||
ConfigDoc.Type = "Config"
|
||||
ConfigDoc.Comments[encoder.LineComment] = "Config defines configuration used by CLI."
|
||||
ConfigDoc.Description = "Config defines configuration used by CLI."
|
||||
ConfigDoc.Fields = make([]encoder.Doc, 7)
|
||||
ConfigDoc.Fields[0].Name = "autoscalingNodeGroupsMin"
|
||||
ConfigDoc.Fields[0].Type = "int"
|
||||
ConfigDoc.Fields = make([]encoder.Doc, 8)
|
||||
ConfigDoc.Fields[0].Name = "version"
|
||||
ConfigDoc.Fields[0].Type = "string"
|
||||
ConfigDoc.Fields[0].Note = ""
|
||||
ConfigDoc.Fields[0].Description = "Minimum number of nodes in autoscaling group.\nworker nodes."
|
||||
ConfigDoc.Fields[0].Comments[encoder.LineComment] = "Minimum number of nodes in autoscaling group."
|
||||
ConfigDoc.Fields[1].Name = "autoscalingNodeGroupsMax"
|
||||
ConfigDoc.Fields[0].Description = "Schema version of this configuration file."
|
||||
ConfigDoc.Fields[0].Comments[encoder.LineComment] = "Schema version of this configuration file."
|
||||
ConfigDoc.Fields[1].Name = "autoscalingNodeGroupsMin"
|
||||
ConfigDoc.Fields[1].Type = "int"
|
||||
ConfigDoc.Fields[1].Note = ""
|
||||
ConfigDoc.Fields[1].Description = "Maximum number of nodes in autoscaling group.\nworker nodes."
|
||||
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Maximum number of nodes in autoscaling group."
|
||||
ConfigDoc.Fields[2].Name = "stateDisksizeGB"
|
||||
ConfigDoc.Fields[1].Description = "Minimum number of nodes in autoscaling group.\nworker nodes."
|
||||
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Minimum number of nodes in autoscaling group."
|
||||
ConfigDoc.Fields[2].Name = "autoscalingNodeGroupsMax"
|
||||
ConfigDoc.Fields[2].Type = "int"
|
||||
ConfigDoc.Fields[2].Note = ""
|
||||
ConfigDoc.Fields[2].Description = "Size (in GB) of data disk used for nodes."
|
||||
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Size (in GB) of data disk used for nodes."
|
||||
ConfigDoc.Fields[3].Name = "ingressFirewall"
|
||||
ConfigDoc.Fields[3].Type = "Firewall"
|
||||
ConfigDoc.Fields[2].Description = "Maximum number of nodes in autoscaling group.\nworker nodes."
|
||||
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Maximum number of nodes in autoscaling group."
|
||||
ConfigDoc.Fields[3].Name = "stateDisksizeGB"
|
||||
ConfigDoc.Fields[3].Type = "int"
|
||||
ConfigDoc.Fields[3].Note = ""
|
||||
ConfigDoc.Fields[3].Description = "Ingress firewall rules for node network."
|
||||
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Ingress firewall rules for node network."
|
||||
ConfigDoc.Fields[4].Name = "egressFirewall"
|
||||
ConfigDoc.Fields[3].Description = "Size (in GB) of data disk used for nodes."
|
||||
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Size (in GB) of data disk used for nodes."
|
||||
ConfigDoc.Fields[4].Name = "ingressFirewall"
|
||||
ConfigDoc.Fields[4].Type = "Firewall"
|
||||
ConfigDoc.Fields[4].Note = ""
|
||||
ConfigDoc.Fields[4].Description = "Egress firewall rules for node network."
|
||||
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Egress firewall rules for node network."
|
||||
|
||||
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].Name = "provider"
|
||||
ConfigDoc.Fields[5].Type = "ProviderConfig"
|
||||
ConfigDoc.Fields[4].Description = "Ingress firewall rules for node network."
|
||||
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Ingress firewall rules for node network."
|
||||
ConfigDoc.Fields[5].Name = "egressFirewall"
|
||||
ConfigDoc.Fields[5].Type = "Firewall"
|
||||
ConfigDoc.Fields[5].Note = ""
|
||||
ConfigDoc.Fields[5].Description = "Supported cloud providers & their specific configurations."
|
||||
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Supported cloud providers & their specific configurations."
|
||||
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[5].Description = "Egress firewall rules for node network."
|
||||
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Egress firewall rules for node network."
|
||||
|
||||
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.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": {
|
||||
config: &Config{
|
||||
Version: Version1,
|
||||
AutoscalingNodeGroupsMin: 42,
|
||||
AutoscalingNodeGroupsMax: 1337,
|
||||
},
|
||||
configName: constants.ConfigFilename,
|
||||
wantResult: &Config{
|
||||
Version: Version1,
|
||||
AutoscalingNodeGroupsMin: 42,
|
||||
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) {
|
||||
testCases := map[string]struct {
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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.
|
||||
// The interface content must be a pointer to a YAML marchalable object.
|
||||
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)
|
||||
if err != nil {
|
||||
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.
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
type testContent struct {
|
||||
First string
|
||||
|
Loading…
Reference in New Issue
Block a user