AB#2098 versioned & strict yaml reading (#157)

This commit is contained in:
Fabian Kammel 2022-05-18 18:10:57 +02:00 committed by GitHub
parent 7c2d1c3490
commit 135c787001
5 changed files with 134 additions and 32 deletions

View File

@ -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
} }

View File

@ -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."

View File

@ -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!")
}

View File

@ -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.

View File

@ -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