Repository: replicatedhq/kots
Pull request #239 (2020-01-07 00:35:52)

accept bools or strings in config yaml
'true' is translated to the string '"1"', 'false' into '""'

Those were the values that we previously suggested for the defaults of boolean config options, so this works with existing kotsadm config rendering code.


Patch:
diff --git a/go.mod b/go.mod
index d02a0b83a..5cd03bee6 100644
--- a/go.mod
+++ b/go.mod
@@ -19,13 +19,9 @@ require (
 	github.com/containers/storage v1.12.13 // indirect
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/docker/distribution v2.7.1+incompatible
-	github.com/docker/docker v1.13.1 // indirect
 	github.com/docker/docker-credential-helpers v0.6.3 // indirect
-	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/docker/go-units v0.4.0
 	github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
-	github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
-	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect
 	github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect
 	github.com/etcd-io/bbolt v1.3.3 // indirect
@@ -33,7 +29,7 @@ require (
 	github.com/frankban/quicktest v1.4.1 // indirect
 	github.com/ghodss/yaml v1.0.0
 	github.com/golang/protobuf v1.3.2 // indirect
-	github.com/golang/snappy v0.0.1 // indirect
+	github.com/google/gofuzz v1.0.0
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/mux v1.7.3 // indirect
 	github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
@@ -48,8 +44,6 @@ require (
 	github.com/mistifyio/go-zfs v2.1.1+incompatible // indirect
 	github.com/mtrmac/gpgme v0.0.0-20170102180018-b2432428689c // indirect
 	github.com/nicksnyder/go-i18n v0.0.0-00010101000000-000000000000 // indirect
-	github.com/nwaples/rardecode v1.0.0 // indirect
-	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/opencontainers/runc v1.0.0-rc8 // indirect
 	github.com/opencontainers/selinux v1.2.2 // indirect
@@ -70,7 +64,6 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.1.0 // indirect
-	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 	golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7
 	golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect
 	gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
diff --git a/kotskinds/apis/kots/v1beta1/config_types.go b/kotskinds/apis/kots/v1beta1/config_types.go
index ce586c8d7..946a40d03 100644
--- a/kotskinds/apis/kots/v1beta1/config_types.go
+++ b/kotskinds/apis/kots/v1beta1/config_types.go
@@ -18,34 +18,36 @@ package v1beta1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/replicatedhq/kots/kotskinds/multitype"
 )
 
 type ConfigChildItem struct {
-	Name        string `json:"name"`
-	Title       string `json:"title"`
-	Recommended bool   `json:"recommended,omitempty"`
-	Default     string `json:"default,omitempty"`
-	Value       string `son:"value,omitempty"`
+	Name        string                 `json:"name"`
+	Title       string                 `json:"title"`
+	Recommended bool                   `json:"recommended,omitempty"`
+	Default     multitype.BoolOrString `json:"default,omitempty"`
+	Value       multitype.BoolOrString `son:"value,omitempty"`
 }
 
 type ConfigItem struct {
-	Name        string            `json:"name"`
-	Type        string            `json:"type"`
-	Title       string            `json:"title,omitempty"`
-	HelpText    string            `json:"help_text,omitempty"`
-	Recommended bool              `json:"recommended,omitempty"`
-	Default     string            `json:"default,omitempty"`
-	Value       string            `json:"value,omitempty"`
-	MultiValue  []string          `json:"multi_value,omitempty"`
-	ReadOnly    bool              `json:"readonly,omitempty"`
-	WriteOnce   bool              `json:"write_once,omitempty"`
-	When        string            `json:"when,omitempty"`
-	Multiple    bool              `json:"multiple,omitempty"`
-	Hidden      bool              `json:"hidden,omitempty"`
-	Position    int               `json:"-"`
-	Affix       string            `json:"affix,omitempty"`
-	Required    bool              `json:"required,omitempty"`
-	Items       []ConfigChildItem `json:"items,omitempty"`
+	Name        string                 `json:"name"`
+	Type        string                 `json:"type"`
+	Title       string                 `json:"title,omitempty"`
+	HelpText    string                 `json:"help_text,omitempty"`
+	Recommended bool                   `json:"recommended,omitempty"`
+	Default     multitype.BoolOrString `json:"default,omitempty"`
+	Value       multitype.BoolOrString `json:"value,omitempty"`
+	MultiValue  []string               `json:"multi_value,omitempty"`
+	ReadOnly    bool                   `json:"readonly,omitempty"`
+	WriteOnce   bool                   `json:"write_once,omitempty"`
+	When        string                 `json:"when,omitempty"`
+	Multiple    bool                   `json:"multiple,omitempty"`
+	Hidden      bool                   `json:"hidden,omitempty"`
+	Position    int                    `json:"-"`
+	Affix       string                 `json:"affix,omitempty"`
+	Required    bool                   `json:"required,omitempty"`
+	Items       []ConfigChildItem      `json:"items,omitempty"`
 	// Props       map[string]interface{} `json:"props,omitempty"`
 	// DefaultCmd  *ConfigItemCmd         `json:"default_cmd,omitempty"`
 	// ValueCmd    *ConfigItemCmd         `json:"value_cmd,omitempty"`
diff --git a/kotskinds/multitype/boolstring.go b/kotskinds/multitype/boolstring.go
new file mode 100644
index 000000000..7dbf5fb77
--- /dev/null
+++ b/kotskinds/multitype/boolstring.go
@@ -0,0 +1,105 @@
+// Based on https://github.com/kubernetes/apimachinery/blob/455a99f/pkg/util/intstr/intstr.go
+
+package multitype
+
+import (
+	"encoding/json"
+	"fmt"
+
+	fuzz "github.com/google/gofuzz"
+)
+
+// BoolOrString is a type that can hold an bool or a string.  When used in
+// JSON or YAML marshalling and unmarshalling, it produces or consumes the
+// inner type.  This allows you to have, for example, a JSON field that can
+// accept a booolean string or raw bool.
+//
+// +protobuf=true
+// +protobuf.options.(gogoproto.goproto_stringer)=false
+// +k8s:openapi-gen=true
+type BoolOrString struct {
+	Type    BoolOrStringType `protobuf:"varbool,1,opt,name=type,casttype=Type"`
+	BoolVal bool             `protobuf:"varbool,2,opt,name=boolVal"`
+	StrVal  string           `protobuf:"bytes,3,opt,name=strVal"`
+}
+
+// Type represents the stored type of BoolOrString.
+type BoolOrStringType int
+
+const (
+	Bool   BoolOrStringType = iota // The BoolOrString holds an bool.
+	String                         // The BoolOrString holds a string.
+)
+
+// FromBool creates an BoolOrString object with a bool value.
+func FromBool(val bool) BoolOrString {
+	return BoolOrString{Type: Bool, BoolVal: val}
+}
+
+// FromString creates an BoolOrString object with a string value.
+func FromString(val string) BoolOrString {
+	return BoolOrString{Type: String, StrVal: val}
+}
+
+// Parse the given string
+func Parse(val string) BoolOrString {
+	return FromString(val)
+}
+
+// UnmarshalJSON implements the json.Unmarshaller boolerface.
+func (boolstr *BoolOrString) UnmarshalJSON(value []byte) error {
+	if value[0] == '"' {
+		boolstr.Type = String
+		return json.Unmarshal(value, &boolstr.StrVal)
+	}
+	boolstr.Type = Bool
+	return json.Unmarshal(value, &boolstr.BoolVal)
+}
+
+// String returns the string value, '1' for true, or '' for false.
+func (boolstr *BoolOrString) String() string {
+	if boolstr.Type == String {
+		return boolstr.StrVal
+	} else if boolstr.BoolVal {
+		return "1"
+	} else {
+		return ""
+	}
+}
+
+// MarshalJSON implements the json.Marshaller interface.
+func (boolstr BoolOrString) MarshalJSON() ([]byte, error) {
+	switch boolstr.Type {
+	case Bool:
+		return json.Marshal(boolstr.BoolVal)
+	case String:
+		return json.Marshal(boolstr.StrVal)
+	default:
+		return []byte{}, fmt.Errorf("impossible BoolOrString.Type")
+	}
+}
+
+// OpenAPISchemaType is used by the kube-openapi generator when constructing
+// the OpenAPI spec of this type.
+//
+// See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators
+func (BoolOrString) OpenAPISchemaType() []string { return []string{"string"} }
+
+// OpenAPISchemaFormat is used by the kube-openapi generator when constructing
+// the OpenAPI spec of this type.
+func (BoolOrString) OpenAPISchemaFormat() string { return "bool-or-string" }
+
+func (boolstr *BoolOrString) Fuzz(c fuzz.Continue) {
+	if boolstr == nil {
+		return
+	}
+	if c.RandBool() {
+		boolstr.Type = Bool
+		c.Fuzz(&boolstr.BoolVal)
+		boolstr.StrVal = ""
+	} else {
+		boolstr.Type = String
+		boolstr.BoolVal = false
+		c.Fuzz(&boolstr.StrVal)
+	}
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 9ce21e192..72310d8df 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/kotskinds/multitype"
 	"github.com/replicatedhq/kots/pkg/base"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/replicatedhq/kots/pkg/template"
@@ -21,7 +22,7 @@ func TemplateConfig(log *logger.Logger, configSpecData string, configValuesData
 	// This process will re-order items and discard comments, so it should not be saved.
 
 	decode := scheme.Codecs.UniversalDeserializer().Decode
-	obj, _, err := decode([]byte(configSpecData), nil, nil)
+	obj, _, err := decode([]byte(configSpecData), nil, nil) // TODO fix decode of boolstrings
 	if err != nil {
 		return "", errors.Wrap(err, "failed to decode config data")
 	}
@@ -74,14 +75,14 @@ func ApplyValuesToConfig(config *kotsv1beta1.Config, values map[string]template.
 		for idxI, i := range g.Items {
 			value, ok := values[i.Name]
 			if ok {
-				config.Spec.Groups[idxG].Items[idxI].Value = value.ValueStr()
-				config.Spec.Groups[idxG].Items[idxI].Default = value.DefaultStr()
+				config.Spec.Groups[idxG].Items[idxI].Value = multitype.FromString(value.ValueStr())
+				config.Spec.Groups[idxG].Items[idxI].Default = multitype.FromString(value.DefaultStr())
 			}
 			for idxC, c := range i.Items {
 				value, ok := values[c.Name]
 				if ok {
-					config.Spec.Groups[idxG].Items[idxI].Items[idxC].Value = value.ValueStr()
-					config.Spec.Groups[idxG].Items[idxI].Items[idxC].Default = value.DefaultStr()
+					config.Spec.Groups[idxG].Items[idxI].Items[idxC].Value = multitype.FromString(value.ValueStr())
+					config.Spec.Groups[idxG].Items[idxI].Items[idxC].Default = multitype.FromString(value.DefaultStr())
 				}
 			}
 		}
diff --git a/pkg/template/config_context.go b/pkg/template/config_context.go
index 590407884..d6133684e 100644
--- a/pkg/template/config_context.go
+++ b/pkg/template/config_context.go
@@ -29,8 +29,8 @@ func (b *Builder) NewConfigContext(configGroups []kotsv1beta1.ConfigGroup, templ
 					Default: v.Default,
 				}
 			} else {
-				builtDefault, _ := b.String(configItem.Default)
-				builtValue, _ := b.String(configItem.Value)
+				builtDefault, _ := b.String(configItem.Default.String())
+				builtValue, _ := b.String(configItem.Value.String())
 				itemValue = ItemValue{
 					Value:   builtValue,
 					Default: builtDefault,
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index 00057bd44..ce3900dcb 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -498,12 +498,12 @@ func createConfigValues(applicationName string, config *kotsv1beta1.Config, exis
 				foundValue = prevValue.Value
 			}
 
-			renderedValue, err := builder.RenderTemplate(item.Name, item.Value)
+			renderedValue, err := builder.RenderTemplate(item.Name, item.Value.String())
 			if err != nil {
 				return nil, errors.Wrap(err, "failed to render config item value")
 			}
 
-			renderedDefault, err := builder.RenderTemplate(item.Name, item.Default)
+			renderedDefault, err := builder.RenderTemplate(item.Name, item.Default.String())
 			if err != nil {
 				return nil, errors.Wrap(err, "failed to render config item default")
 			}


Test patch:
diff --git a/pkg/upstream/replicated_test.go b/pkg/upstream/replicated_test.go
index 94c9665b0..efa4a8785 100644
--- a/pkg/upstream/replicated_test.go
+++ b/pkg/upstream/replicated_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/kotskinds/multitype"
 	"github.com/replicatedhq/kots/pkg/upstream/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -170,23 +171,38 @@ func Test_createConfigValues(t *testing.T) {
 					Items: []kotsv1beta1.ConfigItem{
 						// should replace default
 						kotsv1beta1.ConfigItem{
-							Name:    "1_with_default",
-							Type:    "string",
-							Default: "default_1_new",
-							Value:   "",
+							Name: "1_with_default",
+							Type: "string",
+							Default: multitype.BoolOrString{
+								Type:   multitype.String,
+								StrVal: "default_1_new",
+							},
+							Value: multitype.BoolOrString{
+								Type:   multitype.String,
+								StrVal: "",
+							},
 						},
 						// should preserve value and add default
 						kotsv1beta1.ConfigItem{
-							Name:    "2_with_value",
-							Type:    "string",
-							Default: "default_2",
-							Value:   "value_2_new",
+							Name: "2_with_value",
+							Type: "string",
+							Default: multitype.BoolOrString{
+								Type:   multitype.String,
+								StrVal: "default_2",
+							},
+							Value: multitype.BoolOrString{
+								Type:   multitype.String,
+								StrVal: "value_2_new",
+							},
 						},
 						// should add a new item
 						kotsv1beta1.ConfigItem{
-							Name:    "4_with_default",
-							Type:    "string",
-							Default: "default_4",
+							Name: "4_with_default",
+							Type: "string",
+							Default: multitype.BoolOrString{
+								Type:   multitype.String,
+								StrVal: "default_4",
+							},
 						},
 					},
 				},