diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0fe5534 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module github.com/cloudogu/blueprint-lib + +go 1.23.4 + +require ( + github.com/cloudogu/ces-commons-lib v0.2.0 + github.com/cloudogu/cesapp-lib v0.18.0 + github.com/cloudogu/k8s-blueprint-operator/v2 v2.2.2 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/cloudogu/k8s-registry-lib v0.5.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gammazero/toposort v0.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/sys v0.29.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.31.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0cbb80c --- /dev/null +++ b/go.sum @@ -0,0 +1,95 @@ +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/cloudogu/ces-commons-lib v0.2.0 h1:yOEZWFl4W9N3J/6fok4svE3UufK5GQQtyxvwtIF5AdM= +github.com/cloudogu/ces-commons-lib v0.2.0/go.mod h1:4rvR2RTDDaz5a6OZ1fW27G0MOnl5I3ackeiHxt4gn3o= +github.com/cloudogu/cesapp-lib v0.18.0 h1:9VsWJLXiyhcKlbOcQ/M3O6xvZqpOfKlRqdkjwqAmOy4= +github.com/cloudogu/cesapp-lib v0.18.0/go.mod h1:J05eXFxnz4enZblABlmiVTZaUtJ+LIhlJ2UF6l9jpDw= +github.com/cloudogu/k8s-blueprint-operator/v2 v2.2.2 h1:Yok8Uq0wrN8WhcIYSfTgwpEXfs9rizm8MJDyDBrQsQU= +github.com/cloudogu/k8s-blueprint-operator/v2 v2.2.2/go.mod h1:xlpK+7giaTr9iEcWw96NKYEXInaRhysvDNwK3NSPj8E= +github.com/cloudogu/k8s-registry-lib v0.5.1 h1:gbdrhETUm53GP65LoljrS1kekDDl/onBPfrOQTQpt1s= +github.com/cloudogu/k8s-registry-lib v0.5.1/go.mod h1:mdMOgknEOrGQH1zc/3K859iPhwpqwtzigK9QrjM3Vk0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= diff --git a/v2/blueprint.go b/v2/blueprint.go new file mode 100644 index 0000000..f600c3e --- /dev/null +++ b/v2/blueprint.go @@ -0,0 +1,18 @@ +package v2 + +// Blueprint describes an abstraction of CES components that should be absent or present within one or more CES +// instances. When the same Blueprint is applied to two different CES instances it is required to leave two equal +// instances in terms of the components. +// +// In general additions without changing the version are fine, as long as they don't change semantics. Removal or +// renaming are breaking changes and require a new blueprint API version. +type Blueprint struct { + // Dogus contains a set of exact dogu versions which should be present or absent in the CES instance after which this + // blueprint was applied. Optional. + Dogus []Dogu + // Components contains a set of exact components versions which should be present or absent in the CES instance after which + // this blueprint was applied. Optional. + Components []Component + // Config contains all config entries to set via blueprint. + Config Config +} diff --git a/v2/blueprintMask.go b/v2/blueprintMask.go new file mode 100644 index 0000000..0e0148f --- /dev/null +++ b/v2/blueprintMask.go @@ -0,0 +1,10 @@ +package v2 + +// BlueprintMask describes an abstraction of CES components that should alter a blueprint definition before +// applying it to a CES system via a blueprint upgrade. The blueprint mask does not change the blueprint +// itself, but is applied to the information in it to generate a new, effective blueprint. +type BlueprintMask struct { + // Dogus contains a set of dogus which alters the states of the dogus in the blueprint this mask is applied on. + // The names and target states of all dogus must not be empty. + Dogus []MaskDogu +} diff --git a/v2/component.go b/v2/component.go new file mode 100644 index 0000000..ee8b76b --- /dev/null +++ b/v2/component.go @@ -0,0 +1,37 @@ +package v2 + +import ( + "errors" + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/ecosystem" +) + +// Component represents a CES component (e.g. operators), its version, and the installation state in which it is supposed to be +// after a blueprint was applied. +type Component struct { + // Name defines the name and namespace of the component. Must not be empty. + Name QualifiedComponentName + // Version defines the version of the package that is to be installed. Must not be empty if the targetState is + // "present"; otherwise it is optional and is not going to be interpreted. + Version *semver.Version + // TargetState defines a state of installation of this package. Optional field, but defaults to "TargetStatePresent" + TargetState TargetState + // DeployConfig defines generic properties for the component. This field is optional. + DeployConfig ecosystem.DeployConfig +} + +// Validate checks if the component is semantically correct. +func (component *Component) Validate() error { + nameError := component.Name.Validate() + + var versionErr error + if component.TargetState == TargetStatePresent { + if component.Version == nil { + versionErr = fmt.Errorf("version of component %q must not be empty", component.Name) + } + } + + return errors.Join(versionErr, nameError) +} diff --git a/v2/componentName.go b/v2/componentName.go new file mode 100644 index 0000000..618e857 --- /dev/null +++ b/v2/componentName.go @@ -0,0 +1,52 @@ +package v2 + +import ( + "errors" + "fmt" + "strings" +) + +type QualifiedComponentName struct { + Namespace ComponentNamespace + SimpleName SimpleComponentName +} + +type ComponentNamespace string +type SimpleComponentName string + +func NewQualifiedComponentName(namespace ComponentNamespace, simpleName SimpleComponentName) (QualifiedComponentName, error) { + componentName := QualifiedComponentName{Namespace: namespace, SimpleName: simpleName} + err := componentName.Validate() + if err != nil { + return QualifiedComponentName{}, err + } + return QualifiedComponentName{Namespace: namespace, SimpleName: simpleName}, nil +} + +func (componentName QualifiedComponentName) Validate() error { + var errorList []error + if componentName.Namespace == "" { + errorList = append(errorList, fmt.Errorf("namespace of component %q must not be empty", componentName.SimpleName)) + } + if componentName.SimpleName == "" { + errorList = append(errorList, fmt.Errorf("component name must not be empty: '%s/%s'", componentName.Namespace, componentName.SimpleName)) + } + return errors.Join(errorList...) +} + +// String returns the component name with namespace, e.g. k8s/k8s-dogu-operator +func (componentName QualifiedComponentName) String() string { + return fmt.Sprintf("%s/%s", componentName.Namespace, componentName.SimpleName) +} + +// QualifiedComponentNameFromString converts a qualified component as a string, e.g. "k8s/k8s-dogu-operator", to a dedicated QualifiedComponentName or raises an error if this is not possible. +func QualifiedComponentNameFromString(qualifiedName string) (QualifiedComponentName, error) { + splitName := strings.Split(qualifiedName, "/") + if len(splitName) != 2 { + return QualifiedComponentName{}, fmt.Errorf("component name needs to be in the form 'namespace/component' but is '%s'", qualifiedName) + } + return NewQualifiedComponentName( + ComponentNamespace(splitName[0]), + SimpleComponentName(splitName[1]), + ) +} diff --git a/v2/componentName_test.go b/v2/componentName_test.go new file mode 100644 index 0000000..0b45347 --- /dev/null +++ b/v2/componentName_test.go @@ -0,0 +1,31 @@ +package v2 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestQualifiedComponentNameFromString(t *testing.T) { + tests := []struct { + test string + given string + expected QualifiedComponentName + wantErr assert.ErrorAssertionFunc + }{ + {test: "ok", given: "k8s/k8s-dogu-operator", expected: QualifiedComponentName{ComponentNamespace("k8s"), SimpleComponentName("k8s-dogu-operator")}, wantErr: assert.NoError}, + {test: "no ns", given: "k8s-dogu-operator", expected: QualifiedComponentName{}, wantErr: assert.Error}, + {test: "no name", given: "k8s/", expected: QualifiedComponentName{}, wantErr: assert.Error}, + {test: "double namespace", given: "k8s/test/k8s-dogu-operator", expected: QualifiedComponentName{}, wantErr: assert.Error}, + } + for _, tt := range tests { + t.Run(tt.test, func(t *testing.T) { + got, err := QualifiedComponentNameFromString(tt.given) + if !tt.wantErr(t, err, fmt.Sprintf("TestQualifiedComponentNameFromString(%v)", tt.given)) { + return + } + assert.Equalf(t, tt.expected, got, "TestQualifiedComponentNameFromString(%v)", tt.given) + }) + } +} diff --git a/v2/component_test.go b/v2/component_test.go new file mode 100644 index 0000000..b3283be --- /dev/null +++ b/v2/component_test.go @@ -0,0 +1,79 @@ +package v2 + +import ( + "testing" + + "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cloudogu/cesapp-lib/core" +) + +var ( + compVersion123 = semver.MustParse("1.2.3") + version123, _ = core.ParseVersion("1.2.3") +) + +func TestComponent_Validate(t *testing.T) { + t.Run("errorOnMissingComponentVersion", func(t *testing.T) { + component := Component{Name: testComponentName, TargetState: TargetStatePresent} + + err := component.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), `version of component "k8s/my-component" must not be empty`) + }) + + t.Run("errorOnEmptyComponentVersion", func(t *testing.T) { + component := Component{Name: testComponentName, Version: nil, TargetState: TargetStatePresent} + + err := component.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "version of component \"k8s/my-component\" must not be empty") + }) + + t.Run("errorOnMissingComponentName", func(t *testing.T) { + component := Component{Version: compVersion123, TargetState: TargetStatePresent} + + err := component.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "component name must not be empty") + }) + + t.Run("errorOnEmptyComponentNamespace", func(t *testing.T) { + component := Component{Name: v2.QualifiedComponentName{Namespace: "", SimpleName: "test"}, Version: compVersion123, TargetState: TargetStatePresent} + err := component.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "namespace of component \"test\" must not be empty") + }) + + t.Run("errorOnEmptyComponentName", func(t *testing.T) { + component := Component{Name: v2.QualifiedComponentName{Namespace: "k8s"}, Version: compVersion123, TargetState: TargetStatePresent} + + err := component.Validate() + + require.Error(t, err) + assert.Contains(t, err.Error(), "component name must not be empty") + }) + + t.Run("emptyComponentStateDefaultsToPresent", func(t *testing.T) { + component := Component{Name: testComponentName, Version: compVersion123} + + err := component.Validate() + + require.NoError(t, err) + assert.Equal(t, TargetState(TargetStatePresent), component.TargetState) + }) + + t.Run("missingComponentVersionOkayForAbsent", func(t *testing.T) { + component := Component{Name: testComponentName, TargetState: TargetStateAbsent} + + err := component.Validate() + + require.NoError(t, err) + }) +} diff --git a/v2/config.go b/v2/config.go new file mode 100644 index 0000000..9601dbb --- /dev/null +++ b/v2/config.go @@ -0,0 +1,213 @@ +package v2 + +import ( + "errors" + "fmt" + "golang.org/x/exp/maps" + "slices" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" +) + +type Config struct { + Dogus map[cescommons.SimpleName]CombinedDoguConfig + Global GlobalConfig +} + +type CombinedDoguConfig struct { + DoguName cescommons.SimpleName + Config DoguConfig + SensitiveConfig SensitiveDoguConfig +} + +type DoguConfig struct { + Present map[common.DoguConfigKey]common.DoguConfigValue + Absent []common.DoguConfigKey +} + +type SensitiveDoguConfig = DoguConfig + +type GlobalConfig struct { + Present map[common.GlobalConfigKey]common.GlobalConfigValue + Absent []common.GlobalConfigKey +} + +func (config GlobalConfig) GetGlobalConfigKeys() []common.GlobalConfigKey { + var keys []common.GlobalConfigKey + keys = append(keys, maps.Keys(config.Present)...) + keys = append(keys, config.Absent...) + return keys +} + +func (config Config) GetDoguConfigKeys() []common.DoguConfigKey { + var keys []common.DoguConfigKey + for _, doguConfig := range config.Dogus { + keys = append(keys, maps.Keys(doguConfig.Config.Present)...) + keys = append(keys, doguConfig.Config.Absent...) + } + return keys +} + +func (config Config) GetSensitiveDoguConfigKeys() []common.SensitiveDoguConfigKey { + var keys []common.SensitiveDoguConfigKey + for _, doguConfig := range config.Dogus { + keys = append(keys, maps.Keys(doguConfig.SensitiveConfig.Present)...) + keys = append(keys, doguConfig.SensitiveConfig.Absent...) + } + return keys +} + +// GetDogusWithChangedConfig returns a list of dogus for which possible config changes are needed. +func (config Config) GetDogusWithChangedConfig() []cescommons.SimpleName { + var dogus []cescommons.SimpleName + for dogu, doguConfig := range config.Dogus { + if len(doguConfig.Config.Present) != 0 || len(doguConfig.Config.Absent) != 0 { + dogus = append(dogus, dogu) + } + } + return dogus +} + +// GetDogusWithChangedSensitiveConfig returns a list of dogus for which possible sensitive config changes are needed. +func (config Config) GetDogusWithChangedSensitiveConfig() []cescommons.SimpleName { + var dogus []cescommons.SimpleName + for dogu, doguConfig := range config.Dogus { + if len(doguConfig.SensitiveConfig.Present) != 0 || len(doguConfig.SensitiveConfig.Absent) != 0 { + dogus = append(dogus, dogu) + } + } + return dogus +} + +// censorValues censors all sensitive configuration data to make them unrecognisable. +func (config Config) censorValues() Config { + for _, doguConfig := range config.Dogus { + for k := range doguConfig.SensitiveConfig.Present { + doguConfig.SensitiveConfig.Present[k] = censorValue + } + } + return config +} + +func (config Config) validate() error { + var errs []error + for doguName, doguConfig := range config.Dogus { + if doguName != doguConfig.DoguName { + errs = append(errs, fmt.Errorf("dogu name %q in map and dogu name %q in value are not equal", doguName, doguConfig.DoguName)) + } + errs = append(errs, doguConfig.validate()) + } + errs = append(errs, config.Global.validate()) + + return errors.Join(errs...) +} + +func (config CombinedDoguConfig) validate() error { + err := errors.Join( + config.Config.validate(config.DoguName), + config.SensitiveConfig.validate(config.DoguName), + config.validateConflictingConfigKeys(), + ) + + if err != nil { + return fmt.Errorf("config for dogu %q is invalid: %w", config.DoguName, err) + } + return nil +} + +// validateConflictingConfigKeys checks that there are no conflicting keys in normal config and sensitive config. +// This is a problem as both config types are loaded via the same API in dogus at the moment. +func (config CombinedDoguConfig) validateConflictingConfigKeys() error { + var normalKeys []common.DoguConfigKey + normalKeys = append(normalKeys, maps.Keys(config.Config.Present)...) + normalKeys = append(normalKeys, config.Config.Absent...) + var sensitiveKeys []common.SensitiveDoguConfigKey + sensitiveKeys = append(sensitiveKeys, maps.Keys(config.SensitiveConfig.Present)...) + sensitiveKeys = append(sensitiveKeys, config.SensitiveConfig.Absent...) + + var errorList []error + + for _, sensitiveKey := range sensitiveKeys { + keyToSearch := common.DoguConfigKey{ + DoguName: sensitiveKey.DoguName, + Key: sensitiveKey.Key, + } + if slices.Contains(normalKeys, keyToSearch) { + errorList = append(errorList, fmt.Errorf("dogu config key %s cannot be in normal and sensitive configuration at the same time", keyToSearch)) + } + } + return errors.Join(errorList...) +} + +func (config DoguConfig) validate(referencedDoguName cescommons.SimpleName) error { + var errs []error + + for configKey := range config.Present { + err := configKey.Validate() + if err != nil { + errs = append(errs, fmt.Errorf("present dogu config key invalid: %w", err)) + } + + // validate that all keys are of the same dogu + if referencedDoguName != configKey.DoguName { + errs = append(errs, fmt.Errorf("present %s does not match superordinate dogu name %q", configKey, referencedDoguName)) + } + } + + for _, configKey := range config.Absent { + err := configKey.Validate() + if err != nil { + errs = append(errs, fmt.Errorf("absent dogu config key invalid: %w", err)) + } + + // absent keys cannot be present + _, isPresent := config.Present[configKey] + if isPresent { + errs = append(errs, fmt.Errorf("%s cannot be present and absent at the same time", configKey)) + } + + // validate that all keys are of the same dogu + if referencedDoguName != configKey.DoguName { + errs = append(errs, fmt.Errorf("absent %s does not match superordinate dogu name %q", configKey, referencedDoguName)) + } + } + + absentDuplicates := util.GetDuplicates(config.Absent) + if len(absentDuplicates) > 0 { + errs = append(errs, fmt.Errorf("absent dogu config should not contain duplicate keys: %v", absentDuplicates)) + } + + return errors.Join(errs...) +} + +func (config GlobalConfig) validate() error { + var errs []error + for configKey := range config.Present { + + // empty key is not allowed + if string(configKey) == "" { + errs = append(errs, fmt.Errorf("key for present global config should not be empty")) + } + } + + for _, configKey := range config.Absent { + + // empty key is not allowed + if string(configKey) == "" { + errs = append(errs, fmt.Errorf("key for absent global config should not be empty")) + } + + // absent keys cannot be present + _, isPresent := config.Present[configKey] + if isPresent { + errs = append(errs, fmt.Errorf("global config key %q cannot be present and absent at the same time", configKey)) + } + } + + absentDuplicates := util.GetDuplicates(config.Absent) + if len(absentDuplicates) > 0 { + errs = append(errs, fmt.Errorf("absent global config should not contain duplicate keys: %v", absentDuplicates)) + } + + return errors.Join(errs...) +} diff --git a/v2/configNames.go b/v2/configNames.go new file mode 100644 index 0000000..ec92104 --- /dev/null +++ b/v2/configNames.go @@ -0,0 +1,43 @@ +package v2 + +import ( + "errors" + "fmt" + + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/k8s-registry-lib/config" +) + +type GlobalConfigKey = config.Key + +type DoguConfigKey struct { + DoguName cescommons.SimpleName + Key config.Key +} + +func (k DoguConfigKey) Validate() error { + var errs []error + if k.DoguName == "" { + errs = append(errs, fmt.Errorf("dogu name for dogu config key %q should not be empty", k.Key)) + } + if string(k.Key) == "" { + errs = append(errs, fmt.Errorf("key for dogu config of dogu %q should not be empty", k.DoguName)) + } + + return errors.Join(errs...) +} + +func (k DoguConfigKey) String() string { + return fmt.Sprintf("key %q of dogu %q", k.Key, k.DoguName) +} + +type SensitiveDoguConfigKey = DoguConfigKey + +// GlobalConfigValue is a single global config value +type GlobalConfigValue = config.Value + +// DoguConfigValue is a single dogu config value, which is no sensitive configuration +type DoguConfigValue = config.Value + +// SensitiveDoguConfigValue is a single unencrypted sensitive dogu config value +type SensitiveDoguConfigValue = config.Value diff --git a/v2/config_test.go b/v2/config_test.go new file mode 100644 index 0000000..6a3c27b --- /dev/null +++ b/v2/config_test.go @@ -0,0 +1,572 @@ +package v2 + +import ( + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + domain2 "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain" + "github.com/cloudogu/k8s-blueprint-operator/v2/pkg/domain/common" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGlobalConfig_validate(t *testing.T) { + t.Run("empty config is ok", func(t *testing.T) { + config := GlobalConfig{} + err := config.validate() + assert.NoError(t, err) + }) + t.Run("config is ok", func(t *testing.T) { + config := GlobalConfig{ + Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ + "my/key1": "", //empty values are ok + "my/key2": "test", + }, + Absent: []common.GlobalConfigKey{ + "key3", + }, + } + + err := config.validate() + + assert.NoError(t, err) + }) + t.Run("no empty present keys", func(t *testing.T) { + config := GlobalConfig{ + Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ + "": "", + }, + } + + err := config.validate() + + assert.ErrorContains(t, err, "key for present global config should not be empty") + }) + t.Run("no empty absent keys", func(t *testing.T) { + config := GlobalConfig{ + Absent: []common.GlobalConfigKey{""}, + } + + err := config.validate() + + assert.ErrorContains(t, err, "key for absent global config should not be empty") + }) + t.Run("not present and absent at the same time", func(t *testing.T) { + config := GlobalConfig{ + Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ + "my/key1": "test", + }, + Absent: []common.GlobalConfigKey{ + "my/key1", + }, + } + + err := config.validate() + + assert.ErrorContains(t, err, "config key \"my/key1\" cannot be present and absent at the same time") + }) + + t.Run("combine errors", func(t *testing.T) { + config := GlobalConfig{ + Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ + "": "", + "my/key1": "test", + }, + Absent: []common.GlobalConfigKey{ + "my/key1", + }, + } + + err := config.validate() + + assert.ErrorContains(t, err, "key for present global config should not be empty") + assert.ErrorContains(t, err, "config key \"my/key1\" cannot be present and absent at the same time") + }) + t.Run("not same key multiple times", func(t *testing.T) { + config := GlobalConfig{ + Absent: []common.GlobalConfigKey{"my/key", "my/key"}, + } + err := config.validate() + assert.ErrorContains(t, err, "absent global config should not contain duplicate keys") + }) +} + +func TestDoguConfig_validate(t *testing.T) { + t.Run("empty is ok", func(t *testing.T) { + config := DoguConfig{} + err := config.validate("dogu1") + assert.NoError(t, err) + }) + t.Run("config is ok", func(t *testing.T) { + config := DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + common.DoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", + }, + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: "my/key2"}, + }, + } + err := config.validate("dogu1") + assert.NoError(t, err) + }) + t.Run("not absent and present at the same time", func(t *testing.T) { + config := DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + common.DoguConfigKey{DoguName: "dogu1", Key: "my/key"}: "value1", + }, + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "key \"my/key\" of dogu \"dogu1\" cannot be present and absent at the same time") + }) + t.Run("not same key multiple times", func(t *testing.T) { + config := DoguConfig{ + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "absent dogu config should not contain duplicate keys: [key \"my/key\" of dogu \"dogu1\"]") + }) + t.Run("only one referenced dogu name", func(t *testing.T) { + config := DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + common.DoguConfigKey{DoguName: "dogu1", Key: "test"}: "value1", + }, + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu2") + assert.ErrorContains(t, err, "present key \"test\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + assert.ErrorContains(t, err, "absent key \"my/key\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + }) + t.Run("combine errors", func(t *testing.T) { + config := DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + common.DoguConfigKey{DoguName: "dogu1", Key: ""}: "value1", + }, + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: ""}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "present dogu config key invalid") + assert.ErrorContains(t, err, "absent dogu config key invalid") + }) +} + +func TestSensitiveDoguConfig_validate(t *testing.T) { + t.Run("empty is ok", func(t *testing.T) { + config := SensitiveDoguConfig{} + err := config.validate("") + assert.NoError(t, err) + }) + t.Run("config is ok", func(t *testing.T) { + config := SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", + }, + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: "my/key2"}, + }, + } + err := config.validate("dogu1") + assert.NoError(t, err) + }) + t.Run("not absent and present at the same time", func(t *testing.T) { + config := SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key"}: "value1", + }, + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "key \"my/key\" of dogu \"dogu1\" cannot be present and absent at the same time") + }) + t.Run("not same key multiple times", func(t *testing.T) { + config := SensitiveDoguConfig{ + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "absent dogu config should not contain duplicate keys: [key \"my/key\" of dogu \"dogu1\"]") + }) + t.Run("only one referenced dogu name", func(t *testing.T) { + config := SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "test"}: "value1", + }, + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: "my/key"}, + }, + } + err := config.validate("dogu2") + assert.ErrorContains(t, err, "present key \"test\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + assert.ErrorContains(t, err, "absent key \"my/key\" of dogu \"dogu1\" does not match superordinate dogu name \"dogu2\"") + }) + t.Run("combine errors", func(t *testing.T) { + config := SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: ""}: "value1", + }, + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: ""}, + }, + } + err := config.validate("dogu1") + assert.ErrorContains(t, err, "present dogu config key invalid") + assert.ErrorContains(t, err, "absent dogu config key invalid") + }) +} + +func TestConfig_validate(t *testing.T) { + t.Run("succeed for empty config", func(t *testing.T) { + // given + sut := Config{} + + // when + err := sut.validate() + + // then + assert.NoError(t, err) + }) + t.Run("fail if dogu name in dogu config does not match dogu key", func(t *testing.T) { + // given + sut := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + "some-name": {DoguName: "another-name"}, + }, + } + + // when + err := sut.validate() + + // then + assert.ErrorContains(t, err, "dogu name \"some-name\" in map and dogu name \"another-name\" in value are not equal") + }) + t.Run("fail with multiple errors", func(t *testing.T) { + // given + sut := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + "some-name": { + DoguName: "another-name", + Config: DoguConfig{ + Absent: []common.DoguConfigKey{{DoguName: ""}}, + }, + SensitiveConfig: SensitiveDoguConfig{ + Absent: []common.SensitiveDoguConfigKey{{DoguName: ""}}, + }, + }, + }, + Global: GlobalConfig{Absent: []common.GlobalConfigKey{""}}, + } + + // when + err := sut.validate() + + // then + assert.ErrorContains(t, err, "dogu name \"some-name\" in map and dogu name \"another-name\" in value are not equal") + assert.ErrorContains(t, err, "config for dogu \"another-name\" is invalid") + assert.ErrorContains(t, err, "key for absent global config should not be empty") + }) +} + +func TestGlobalConfig_GetGlobalConfigKeys(t *testing.T) { + var ( + globalKey1 = common.GlobalConfigKey("key1") + globalKey2 = common.GlobalConfigKey("key2") + ) + config := GlobalConfig{ + Present: map[common.GlobalConfigKey]common.GlobalConfigValue{ + globalKey1: "value", + }, + Absent: []common.GlobalConfigKey{ + globalKey2, + }, + } + + keys := config.GetGlobalConfigKeys() + + assert.ElementsMatch(t, keys, []common.GlobalConfigKey{globalKey1, globalKey2}) +} + +func TestConfig_GetDoguConfigKeys(t *testing.T) { + var ( + nginx = cescommons.SimpleName("nginx") + postfix = cescommons.SimpleName("postfix") + nginxKey1 = common.DoguConfigKey{DoguName: nginx, Key: "key1"} + nginxKey2 = common.DoguConfigKey{DoguName: nginx, Key: "key2"} + postfixKey1 = common.DoguConfigKey{DoguName: postfix, Key: "key1"} + postfixKey2 = common.DoguConfigKey{DoguName: postfix, Key: "key2"} + ) + config := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + nginx: { + DoguName: nginx, + Config: DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + nginxKey1: "value", + }, + Absent: []common.DoguConfigKey{ + nginxKey2, + }, + }, + SensitiveConfig: SensitiveDoguConfig{}, + }, + postfix: { + DoguName: postfix, + Config: DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + postfixKey1: "value", + }, + Absent: []common.DoguConfigKey{ + postfixKey2, + }, + }, + SensitiveConfig: SensitiveDoguConfig{}, + }, + }, + } + + keys := config.GetDoguConfigKeys() + + assert.ElementsMatch(t, keys, []common.DoguConfigKey{nginxKey1, nginxKey2, postfixKey1, postfixKey2}) +} + +func TestConfig_GetSensitiveDoguConfigKeys(t *testing.T) { + var ( + nginx = cescommons.SimpleName("nginx") + postfix = cescommons.SimpleName("postfix") + nginxKey1 = common.SensitiveDoguConfigKey{DoguName: nginx, Key: "key1"} + nginxKey2 = common.SensitiveDoguConfigKey{DoguName: nginx, Key: "key2"} + postfixKey1 = common.SensitiveDoguConfigKey{DoguName: postfix, Key: "key1"} + postfixKey2 = common.SensitiveDoguConfigKey{DoguName: postfix, Key: "key2"} + ) + config := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + nginx: { + DoguName: nginx, + SensitiveConfig: SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + nginxKey1: "value", + }, + Absent: []common.SensitiveDoguConfigKey{ + nginxKey2, + }, + }, + }, + postfix: { + DoguName: postfix, + SensitiveConfig: SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + postfixKey1: "value", + }, + Absent: []common.SensitiveDoguConfigKey{ + postfixKey2, + }, + }, + }, + }, + } + + keys := config.GetSensitiveDoguConfigKeys() + + assert.ElementsMatch(t, keys, []common.SensitiveDoguConfigKey{nginxKey1, nginxKey2, postfixKey1, postfixKey2}) +} + +func TestCombinedDoguConfig_validate(t *testing.T) { + normalConfig := DoguConfig{ + Present: map[common.DoguConfigKey]common.DoguConfigValue{ + common.DoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", + }, + Absent: []common.DoguConfigKey{ + {DoguName: "dogu1", Key: "my/key2"}, + }, + } + sensitiveConfig := SensitiveDoguConfig{ + Present: map[common.SensitiveDoguConfigKey]common.SensitiveDoguConfigValue{ + common.SensitiveDoguConfigKey{DoguName: "dogu1", Key: "my/key1"}: "value1", + }, + Absent: []common.SensitiveDoguConfigKey{ + {DoguName: "dogu1", Key: "my/key2"}, + }, + } + + config := CombinedDoguConfig{ + DoguName: "dogu1", + Config: normalConfig, + SensitiveConfig: sensitiveConfig, + } + + err := config.validate() + + assert.ErrorContains(t, err, "dogu config key key \"my/key1\" of dogu \"dogu1\" cannot be in normal and sensitive configuration at the same time") +} + +func TestConfig_GetDogusWithChangedConfig(t *testing.T) { + presentConfig := map[common.DoguConfigKey]common.DoguConfigValue{ + dogu1Key1: "val", + } + AbsentConfig := []common.DoguConfigKey{ + dogu1Key1, + } + emptyPresentConfig := map[common.DoguConfigKey]common.DoguConfigValue{} + var emptyAbsentConfig []common.DoguConfigKey + + type args struct { + doguConfig DoguConfig + withDogu2Change bool + } + + var emptyResult []cescommons.SimpleName + var tests = []struct { + name string + args args + want []cescommons.SimpleName + }{ + { + name: "should get multiple Dogus", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}, withDogu2Change: true}, + want: []cescommons.SimpleName{dogu1, dogu2}, + }, + { + name: "should get Dogus with changed present and absent config", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should get Dogus with changed present config", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: emptyAbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should get Dogus with changed absent config", + args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: AbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should not get Dogus with no config changes", + args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: emptyAbsentConfig}}, + want: emptyResult, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + emptyDoguConfig := struct { + Present map[common.DoguConfigKey]common.DoguConfigValue + Absent []common.DoguConfigKey + }{} + config := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + dogu1: { + DoguName: dogu1, + Config: tt.args.doguConfig, + SensitiveConfig: emptyDoguConfig, + }, + }, + Global: GlobalConfig{}, + } + + if tt.args.withDogu2Change { + config.Dogus[dogu2] = CombinedDoguConfig{ + DoguName: dogu2, + Config: tt.args.doguConfig, + SensitiveConfig: emptyDoguConfig, + } + } + + changedDogus := config.GetDogusWithChangedConfig() + assert.Len(t, changedDogus, len(tt.want)) + for _, doguName := range tt.want { + assert.Contains(t, changedDogus, doguName) + } + }) + } +} + +func TestConfig_GetDogusWithChangedSensitiveConfig(t *testing.T) { + presentConfig := map[common.DoguConfigKey]common.DoguConfigValue{ + dogu1Key1: "val", + } + AbsentConfig := []common.DoguConfigKey{ + dogu1Key1, + } + emptyPresentConfig := map[common.DoguConfigKey]common.DoguConfigValue{} + var emptyAbsentConfig []common.DoguConfigKey + + type args struct { + doguConfig DoguConfig + withDogu2Change bool + } + + var emptyResult []cescommons.SimpleName + var tests = []struct { + name string + args args + want []cescommons.SimpleName + }{ + { + name: "should get multiple Dogus", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}, withDogu2Change: true}, + want: []cescommons.SimpleName{dogu1, dogu2}, + }, + { + name: "should get Dogus with changed present and absent config", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: AbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should get Dogus with changed present config", + args: args{doguConfig: DoguConfig{Present: presentConfig, Absent: emptyAbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should get Dogus with changed absent config", + args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: AbsentConfig}}, + want: []cescommons.SimpleName{dogu1}, + }, + { + name: "should not get Dogus with no config changes", + args: args{doguConfig: DoguConfig{Present: emptyPresentConfig, Absent: emptyAbsentConfig}}, + want: emptyResult, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + emptyDoguConfig := struct { + Present map[common.DoguConfigKey]common.DoguConfigValue + Absent []common.DoguConfigKey + }{} + config := Config{ + Dogus: map[cescommons.SimpleName]CombinedDoguConfig{ + dogu1: { + DoguName: dogu1, + Config: emptyDoguConfig, + SensitiveConfig: tt.args.doguConfig, + }, + }, + Global: GlobalConfig{}, + } + + if tt.args.withDogu2Change { + config.Dogus[dogu2] = CombinedDoguConfig{ + DoguName: dogu2, + Config: emptyDoguConfig, + SensitiveConfig: tt.args.doguConfig, + } + } + + changedDogus := config.GetDogusWithChangedSensitiveConfig() + assert.Len(t, changedDogus, len(tt.want)) + for _, doguName := range tt.want { + assert.Contains(t, changedDogus, doguName) + } + }) + } +} diff --git a/v2/dogu.go b/v2/dogu.go new file mode 100644 index 0000000..c41f785 --- /dev/null +++ b/v2/dogu.go @@ -0,0 +1,23 @@ +package v2 + +import ( + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/cesapp-lib/core" +) + +// Dogu defines a Dogu, its version, and the installation state in which it is supposed to be after a blueprint +// was applied. +type Dogu struct { + // Name defines the name of the dogu, e.g. "official/postgresql" + Name cescommons.QualifiedName + // Version defines the version of the dogu that is to be installed. Must not be empty if the targetState is "present"; + // otherwise it is optional and is not going to be interpreted. + Version core.Version + // TargetState defines a state of installation of this dogu. Optional field, but defaults to "TargetStatePresent" + TargetState TargetState + // MinVolumeSize is the minimum storage of the dogu. This field is optional and can be nil to indicate that no + // storage is needed. + MinVolumeSize VolumeSize + // ReverseProxyConfig defines configuration for the ecosystem reverse proxy. This field is optional. + ReverseProxyConfig ReverseProxyConfig +} diff --git a/v2/doguShared.go b/v2/doguShared.go new file mode 100644 index 0000000..dfb994e --- /dev/null +++ b/v2/doguShared.go @@ -0,0 +1,14 @@ +package v2 + +import "k8s.io/apimachinery/pkg/api/resource" + +type VolumeSize = resource.Quantity +type ReverseProxyConfig struct { + MaxBodySize *BodySize + RewriteTarget RewriteTarget + AdditionalConfig AdditionalConfig +} + +type BodySize = resource.Quantity +type RewriteTarget string +type AdditionalConfig string diff --git a/v2/maskDogu.go b/v2/maskDogu.go new file mode 100644 index 0000000..60ef99c --- /dev/null +++ b/v2/maskDogu.go @@ -0,0 +1,18 @@ +package v2 + +import ( + cescommons "github.com/cloudogu/ces-commons-lib/dogu" + "github.com/cloudogu/cesapp-lib/core" +) + +// MaskDogu defines a Dogu, its version, and the installation state in which it is supposed to be after a blueprint +// was applied for a blueprintMask. +type MaskDogu struct { + // Name is the qualified name of the dogu. + Name cescommons.QualifiedName + // Version defines the version of the dogu that is to be installed. This version is optional and overrides + // the version of the dogu from the blueprint. + Version core.Version + // TargetState defines a state of installation of this dogu. Optional field, but defaults to "TargetStatePresent" + TargetState TargetState +} diff --git a/v2/targetState.go b/v2/targetState.go new file mode 100644 index 0000000..14ea4ed --- /dev/null +++ b/v2/targetState.go @@ -0,0 +1,27 @@ +package v2 + +// TargetState defines an enum of values that determines a state of installation. +type TargetState int + +const ( + // TargetStatePresent is the default state. If selected the chosen item must be present after the blueprint was + // applied. + TargetStatePresent = iota + // TargetStateAbsent sets the state of the item to absent. If selected the chosen item must be absent after the + // blueprint was applied. + TargetStateAbsent +) + +var PossibleTargetStates = []TargetState{ + TargetStatePresent, TargetStateAbsent, +} + +// String returns a string representation of the given TargetState enum value. +func (state TargetState) String() string { + return toString[state] +} + +var toString = map[TargetState]string{ + TargetStatePresent: "present", + TargetStateAbsent: "absent", +} diff --git a/v2/targetState_test.go b/v2/targetState_test.go new file mode 100644 index 0000000..dc3611e --- /dev/null +++ b/v2/targetState_test.go @@ -0,0 +1,31 @@ +package v2 + +import ( + "testing" +) + +func TestTargetState_String(t *testing.T) { + tests := []struct { + name string + state TargetState + want string + }{ + { + "map present enum value to string", + TargetStatePresent, + "present", + }, + { + "map absent enum value to string", + TargetStateAbsent, + "absent", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.state.String(); got != tt.want { + t.Errorf("TargetState.String() = %v, want %v", got, tt.want) + } + }) + } +}