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

View File

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

View File

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

View File

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

View File

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