Skip to content

Commit

Permalink
Add genruntime support for Dynamic secrets and configmap exports (#4314)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthchr authored Oct 2, 2024
1 parent c7a92c3 commit cb5ffca
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 23 deletions.
14 changes: 6 additions & 8 deletions v2/pkg/genruntime/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,17 @@ func (s NamespacedConfigMapReference) String() string {
}

// ConfigMapDestination describes the location to store a single configmap value
// Note: This is similar to SecretDestination in secrets.go. Changes to one should likely also be made to the other.
// Note: This is similar to: SecretDestination in secrets.go.
// Changes to one may need to be made to the others as well.
type ConfigMapDestination struct {
// Note: We could embed ConfigMapReference here, but it makes our life harder because then our reflection based tools will "find" ConfigMapReferences's
// inside of ConfigMapDestination and try to resolve them. It also gives a worse experience when using the Go Types (the YAML is the same either way).

// Name is the name of the Kubernetes ConfigMap being referenced.
// The ConfigMap must be in the same namespace as the resource
// Name is the name of the Kubernetes ConfigMap to write to.
// The ConfigMap will be created in the same namespace as the resource.
// +kubebuilder:validation:Required
Name string `json:"name"`
Name string `json:"name,omitempty"`

// Key is the key in the ConfigMap being referenced
// +kubebuilder:validation:Required
Key string `json:"key"`
Key string `json:"key,omitempty"`

// This is a type separate from ConfigMapReference as in the future we may want to support things like
// customizable annotations or labels, instructions to not delete the ConfigMap when the resource is
Expand Down
15 changes: 15 additions & 0 deletions v2/pkg/genruntime/configmaps/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package configmaps

import (
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
)

// Exporter defines an interface for exporting ConfigMaps based on CEL expressions.
type Exporter interface {
ConfigMapDestinationExpressions() []*core.DestinationExpression
}
31 changes: 31 additions & 0 deletions v2/pkg/genruntime/configmaps/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/Azure/azure-service-operator/v2/internal/set"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
)

type keyPair struct {
Expand All @@ -21,6 +22,14 @@ type keyPair struct {
// ValidateDestinations checks that no two destinations are writing to the same configmap/key, as that could cause
// those values to overwrite one another.
func ValidateDestinations(destinations []*genruntime.ConfigMapDestination) (admission.Warnings, error) {
return ValidateDestinationsExt(destinations, nil)
}

// TODO: ValidateDestinationsExt will replace ValidateDestinations in a future PR.
func ValidateDestinationsExt(
destinations []*genruntime.ConfigMapDestination,
destinationExpressions []*core.DestinationExpression,
) (admission.Warnings, error) {
locations := set.Make[keyPair]()

for _, dest := range destinations {
Expand All @@ -39,6 +48,28 @@ func ValidateDestinations(destinations []*genruntime.ConfigMapDestination) (admi
locations.Add(pair)
}

for _, dest := range destinationExpressions {
if dest == nil {
continue
}

if dest.Key == "" {
// TODO: Key may be empty because of map[string]string supported exports.
// TODO: We should validate that in more depth but need a CEL parser to do so.
continue
}

pair := keyPair{
name: dest.Name,
key: dest.Key,
}
if locations.Contains(pair) {
return nil, errors.Errorf("cannot write more than one configmap value to destination %s", dest.String())
}

locations.Add(pair)
}

return nil, nil
}

Expand Down
177 changes: 177 additions & 0 deletions v2/pkg/genruntime/configmaps/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package configmaps_test

import (
"testing"

. "github.com/onsi/gomega"

"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/configmaps"
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
)

func Test_ValidateConfigMapDestination_EmptyListValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

warnings, err := configmaps.ValidateDestinationsExt(nil, nil)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestination_ListWithNilElementsValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
nil,
nil,
}

warnings, err := configmaps.ValidateDestinationsExt(destinations, nil)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestinationExpressions_ListWithNilElementsValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*core.DestinationExpression{
nil,
nil,
}

warnings, err := configmaps.ValidateDestinationsExt(nil, destinations)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestination_LengthOneListValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
{Name: "n1", Key: "key1"},
}

warnings, err := configmaps.ValidateDestinationsExt(destinations, nil)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestinationExpressions_LengthOneListValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*core.DestinationExpression{
{Name: "n1", Key: "key1", Value: "resource.status.id"},
}

warnings, err := configmaps.ValidateDestinationsExt(nil, destinations)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestination_ListWithoutCollisionsValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
{Name: "n1", Key: "key1"},
{Name: "n1", Key: "key2"},
{Name: "n1", Key: "key3"},
{Name: "n1", Key: "key4"},
}

warnings, err := configmaps.ValidateDestinationsExt(destinations, nil)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestinationExpressions_ListWithoutCollisionsValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*core.DestinationExpression{
{Name: "n1", Key: "key1", Value: "resource.status.id"},
{Name: "n1", Key: "key2", Value: "resource.status.id"},
{Name: "n1", Key: "key3", Value: "resource.status.id"},
{Name: "n1", Key: "key4", Value: "resource.status.id"},
}

warnings, err := configmaps.ValidateDestinationsExt(nil, destinations)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestination_ListWithDifferentCasesValidates(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
{Name: "n1", Key: "key1"},
{Name: "n1", Key: "Key1"},
{Name: "n1", Key: "key3"},
{Name: "n1", Key: "key4"},
}

warnings, err := configmaps.ValidateDestinationsExt(destinations, nil)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}

func Test_ValidateConfigMapDestination_ListWithCollisionsFailsValidation(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
{Name: "n1", Key: "key1"},
{Name: "n2", Key: "key1"},
{Name: "n3", Key: "key1"},
{Name: "n1", Key: "key1"},
}
_, err := configmaps.ValidateDestinationsExt(destinations, nil)
g.Expect(err).ToNot(BeNil())
g.Expect(err.Error()).To(Equal("cannot write more than one configmap value to destination Name: \"n1\", Key: \"key1\""))
}

func Test_ValidateConfigMapDestinationAndExpressions_CollisionBetweenEachFailsValidation(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*genruntime.ConfigMapDestination{
{Name: "n3", Key: "key1"},
{Name: "n4", Key: "key1"},
{Name: "n5", Key: "key1"},
}

destinationExpressions := []*core.DestinationExpression{
{Name: "n1", Key: "key1", Value: "resource.status.id"},
{Name: "n2", Key: "key1", Value: "resource.status.id"},
{Name: "n3", Key: "key1", Value: "resource.status.id"},
}

_, err := configmaps.ValidateDestinationsExt(destinations, destinationExpressions)
g.Expect(err).ToNot(BeNil())
g.Expect(err.Error()).To(Equal("cannot write more than one configmap value to destination Name: \"n3\", Key: \"key1\", Value: \"resource.status.id\""))
}

func Test_ValidateConfigMapDestinationExpressions_EmptyKeyIgnored(t *testing.T) {
t.Parallel()
g := NewGomegaWithT(t)

destinations := []*core.DestinationExpression{
{Name: "n1", Value: "resource.status.id"},
{Name: "n1", Key: "key1", Value: "resource.status.id"},
}

warnings, err := configmaps.ValidateDestinationsExt(nil, destinations)
g.Expect(warnings).To(BeNil())
g.Expect(err).To(BeNil())
}
35 changes: 35 additions & 0 deletions v2/pkg/genruntime/core/destination_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package core

import (
"fmt"
)

// DestinationExpression is a CEL expression and a destination to store the result in. The destination may
// be a secret or a configmap. The value of the expression is stored at the specified location in
// the destination.
// +kubebuilder:object:generate=true
type DestinationExpression struct {
// Name is the name of the Kubernetes configmap or secret to write to.
// The configmap or secret will be created in the same namespace as the resource.
// +kubebuilder:validation:Required
Name string `json:"name,omitempty"`

// Key is the key in the ConfigMap or Secret being written to. If the CEL expression in Value returns a string
// this is required to identify what key to write to. If the CEL expression in Value returns a map[string]string
// Key must not be set, instead the keys written will be determined dynamically based on the keys of the resulting
// map[string]string.
Key string `json:"key,omitempty"`

// Value is a CEL expression. The CEL expression may return a string or a map[string]string. For more information see TODO (improve this)
// +kubebuilder:validation:Required
Value string `json:"value,omitempty"`
}

func (s DestinationExpression) String() string {
return fmt.Sprintf("Name: %q, Key: %q, Value: %q", s.Name, s.Key, s.Value)
}
25 changes: 25 additions & 0 deletions v2/pkg/genruntime/core/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 7 additions & 9 deletions v2/pkg/genruntime/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,17 @@ func (s NamespacedSecretMapReference) String() string {
}

// SecretDestination describes the location to store a single secret value.
// Note: This is similar to ConfigMapDestination in configmaps.go. Changes to one should likely also be made to the other.
// Note: This is similar to: ConfigMapDestination in configmaps.go.
// Changes to one may need to be made to the others as well.
type SecretDestination struct {
// Note: We could embed SecretReference here, but it makes our life harder because then our reflection based tools will "find" SecretReference's
// inside of SecretDestination and try to resolve them. It also gives a worse experience when using the Go Types (the YAML is the same either way).

// Name is the name of the Kubernetes secret being referenced.
// The secret must be in the same namespace as the resource
// Name is the name of the Kubernetes secret to write to.
// The secret will be created in the same namespace as the resource.
// +kubebuilder:validation:Required
Name string `json:"name"`
Name string `json:"name,omitempty"`

// Key is the key in the Kubernetes secret being referenced
// Key is the key in the Kubernetes secret being referenced.
// +kubebuilder:validation:Required
Key string `json:"key"`
Key string `json:"key,omitempty"`

// This is a type separate from SecretReference as in the future we may want to support things like
// customizable annotations or labels, instructions to not delete the secret when the resource is
Expand Down
15 changes: 15 additions & 0 deletions v2/pkg/genruntime/secrets/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

package secrets

import (
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
)

// Exporter defines an interface for exporting Secrets based on CEL expressions.
type Exporter interface {
SecretDestinationExpressions() []*core.DestinationExpression
}
Loading

0 comments on commit cb5ffca

Please sign in to comment.