Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GEP-26] CredentialsBinding validation via admission webhook #845

Merged
merged 3 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ rules:
- get
- list
- watch
- apiGroups:
- security.gardener.cloud
resources:
- credentialsbindings
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down
2 changes: 2 additions & 0 deletions cmd/gardener-extension-admission-openstack/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
webhookcmd "github.com/gardener/gardener/extensions/pkg/webhook/cmd"
"github.com/gardener/gardener/pkg/apis/core/install"
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
securityinstall "github.com/gardener/gardener/pkg/apis/security/install"
gardenerhealthz "github.com/gardener/gardener/pkg/healthz"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -122,6 +123,7 @@ func NewAdmissionCommand(ctx context.Context) *cobra.Command {
}

install.Install(mgr.GetScheme())
securityinstall.Install(mgr.GetScheme())

if err := openstackinstall.AddToScheme(mgr.GetScheme()); err != nil {
return fmt.Errorf("could not update manager scheme: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ In this document we are describing how this configuration looks like for OpenSta

## Provider Secret Data

Every shoot cluster references a `SecretBinding` which itself references a `Secret`, and this `Secret` contains the provider credentials of your OpenStack tenant.
Every shoot cluster references a `SecretBinding` or a `CredentialsBinding` which itself references a `Secret`, and this `Secret` contains the provider credentials of your OpenStack tenant.
This `Secret` must look as follows:

```yaml
Expand Down
62 changes: 62 additions & 0 deletions pkg/admission/validator/credentialsbinding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package validator

import (
"context"
"fmt"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/security"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"

openstackvalidation "github.com/gardener/gardener-extension-provider-openstack/pkg/apis/openstack/validation"
)

type credentialsBinding struct {
apiReader client.Reader
}

// NewCredentialsBindingValidator returns a new instance of a credentials binding validator.
func NewCredentialsBindingValidator(mgr manager.Manager) extensionswebhook.Validator {
return &credentialsBinding{
apiReader: mgr.GetAPIReader(),
}
}

// Validate checks whether the given CredentialsBinding refers to valid OpenStack credentials.
func (cb *credentialsBinding) Validate(ctx context.Context, newObj, oldObj client.Object) error {
credentialsBinding, ok := newObj.(*security.CredentialsBinding)
if !ok {
return fmt.Errorf("wrong object type %T", newObj)
}

if oldObj != nil {
_, ok := oldObj.(*security.CredentialsBinding)
if !ok {
return fmt.Errorf("wrong object type %T for old object", oldObj)
}

// The relevant fields of the credentials binding are immutable so we can exit early on update
return nil
}

// Explicitly use the client.Reader to prevent controller-runtime to start Informer for Secrets
// under the hood. The latter increases the memory usage of the component.
var credentialsKey = client.ObjectKey{Namespace: credentialsBinding.CredentialsRef.Namespace, Name: credentialsBinding.CredentialsRef.Name}
switch {
case credentialsBinding.CredentialsRef.APIVersion == corev1.SchemeGroupVersion.String() && credentialsBinding.CredentialsRef.Kind == "Secret":
secret := &corev1.Secret{}
if err := cb.apiReader.Get(ctx, credentialsKey, secret); err != nil {
return err
}

return openstackvalidation.ValidateCloudProviderSecret(secret)
default:
return fmt.Errorf("unsupported credentials reference: version %q, kind %q", credentialsBinding.CredentialsRef.APIVersion, credentialsBinding.CredentialsRef.Kind)
}
}
129 changes: 129 additions & 0 deletions pkg/admission/validator/credentialsbinding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0

package validator_test

import (
"context"
"errors"
"fmt"

extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/security"
mockclient "github.com/gardener/gardener/third_party/mock/controller-runtime/client"
mockmanager "github.com/gardener/gardener/third_party/mock/controller-runtime/manager"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/mock/gomock"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/gardener/gardener-extension-provider-openstack/pkg/admission/validator"
"github.com/gardener/gardener-extension-provider-openstack/pkg/openstack"
)

var _ = Describe("CredentialsBinding validator", func() {
Describe("#Validate", func() {
const (
namespace = "garden-dev"
name = "my-provider-account"
)

var (
credentialsBindingValidator extensionswebhook.Validator

ctrl *gomock.Controller
mgr *mockmanager.MockManager
apiReader *mockclient.MockReader

ctx = context.TODO()
credentialsBinding *security.CredentialsBinding

fakeErr = fmt.Errorf("fake err")
)

BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())

mgr = mockmanager.NewMockManager(ctrl)

apiReader = mockclient.NewMockReader(ctrl)
mgr.EXPECT().GetAPIReader().Return(apiReader)

credentialsBindingValidator = validator.NewCredentialsBindingValidator(mgr)

credentialsBinding = &security.CredentialsBinding{
CredentialsRef: corev1.ObjectReference{
Name: name,
Namespace: namespace,
Kind: "Secret",
APIVersion: "v1",
},
}
})

AfterEach(func() {
ctrl.Finish()
})

It("should return err when obj is not a CredentialsBinding", func() {
err := credentialsBindingValidator.Validate(ctx, &corev1.Secret{}, nil)
Expect(err).To(MatchError("wrong object type *v1.Secret"))
})

It("should return err when oldObj is not a CredentialsBinding", func() {
err := credentialsBindingValidator.Validate(ctx, &security.CredentialsBinding{}, &corev1.Secret{})
Expect(err).To(MatchError("wrong object type *v1.Secret for old object"))
})

It("should return err if the CredentialsBinding references unknown credentials type", func() {
credentialsBinding.CredentialsRef.APIVersion = "unknown"
err := credentialsBindingValidator.Validate(ctx, credentialsBinding, nil)
Expect(err).To(MatchError(errors.New(`unsupported credentials reference: version "unknown", kind "Secret"`)))
})

It("should return err if it fails to get the corresponding Secret", func() {
apiReader.EXPECT().Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, gomock.AssignableToTypeOf(&corev1.Secret{})).Return(fakeErr)

err := credentialsBindingValidator.Validate(ctx, credentialsBinding, nil)
Expect(err).To(MatchError(fakeErr))
})

It("should return err when the corresponding Secret is not valid", func() {
apiReader.EXPECT().Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, gomock.AssignableToTypeOf(&corev1.Secret{})).
DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *corev1.Secret, _ ...client.GetOption) error {
secret := &corev1.Secret{Data: map[string][]byte{
"foo": []byte("bar"),
}}
*obj = *secret
return nil
})

err := credentialsBindingValidator.Validate(ctx, credentialsBinding, nil)
Expect(err).To(HaveOccurred())
})

It("should succeed when the corresponding Secret is valid", func() {
apiReader.EXPECT().Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, gomock.AssignableToTypeOf(&corev1.Secret{})).
DoAndReturn(func(_ context.Context, _ client.ObjectKey, obj *corev1.Secret, _ ...client.GetOption) error {
secret := &corev1.Secret{Data: map[string][]byte{
openstack.DomainName: []byte("domain"),
openstack.TenantName: []byte("tenant"),
openstack.UserName: []byte("user"),
openstack.Password: []byte("password"),
}}
*obj = *secret
return nil
})

Expect(credentialsBindingValidator.Validate(ctx, credentialsBinding, nil)).To(Succeed())
})

It("should return nil when the CredentialsBinding did not change", func() {
old := credentialsBinding.DeepCopy()

Expect(credentialsBindingValidator.Validate(ctx, credentialsBinding, old)).To(Succeed())
})
})
})
33 changes: 22 additions & 11 deletions pkg/admission/validator/shoot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/gardener/gardener/pkg/apis/core"
gardencorehelper "github.com/gardener/gardener/pkg/apis/core/helper"
gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
securityv1alpha1 "github.com/gardener/gardener/pkg/apis/security/v1alpha1"
kutil "github.com/gardener/gardener/pkg/utils/kubernetes"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
Expand Down Expand Up @@ -73,7 +74,7 @@ func (s *shoot) Validate(ctx context.Context, new, old client.Object) error {
}

var credentials *openstack.Credentials
if shoot.Spec.SecretBindingName != nil {
if shoot.Spec.SecretBindingName != nil || shoot.Spec.CredentialsBindingName != nil {
secret, err := s.getCloudProviderSecretForShoot(ctx, shoot)
if err != nil {
return fmt.Errorf("failed to get cloud provider credentials: %v", err)
Expand Down Expand Up @@ -212,20 +213,30 @@ func newValidationContext(ctx context.Context, decoder runtime.Decoder, c client
}

func (s *shoot) getCloudProviderSecretForShoot(ctx context.Context, shoot *core.Shoot) (*corev1.Secret, error) {
var (
secretBinding = &gardencorev1beta1.SecretBinding{}
secretBindingKey = client.ObjectKey{Namespace: shoot.Namespace, Name: *shoot.Spec.SecretBindingName}
)
if err := kutil.LookupObject(ctx, s.client, s.apiReader, secretBindingKey, secretBinding); err != nil {
return nil, err
var secretKey client.ObjectKey
if shoot.Spec.SecretBindingName != nil {
var (
bindingKey = client.ObjectKey{Namespace: shoot.Namespace, Name: *shoot.Spec.SecretBindingName}
secretBinding = &gardencorev1beta1.SecretBinding{}
)
if err := kutil.LookupObject(ctx, s.client, s.apiReader, bindingKey, secretBinding); err != nil {
return nil, err
}
secretKey = client.ObjectKey{Namespace: secretBinding.SecretRef.Namespace, Name: secretBinding.SecretRef.Name}
} else {
var (
bindingKey = client.ObjectKey{Namespace: shoot.Namespace, Name: *shoot.Spec.CredentialsBindingName}
credentialsBinding = &securityv1alpha1.CredentialsBinding{}
)
if err := kutil.LookupObject(ctx, s.client, s.apiReader, bindingKey, credentialsBinding); err != nil {
return nil, err
}
secretKey = client.ObjectKey{Namespace: credentialsBinding.CredentialsRef.Namespace, Name: credentialsBinding.CredentialsRef.Name}
}

var (
secret = &corev1.Secret{}
secretKey = client.ObjectKey{Namespace: secretBinding.SecretRef.Namespace, Name: secretBinding.SecretRef.Name}
)
// Explicitly use the client.Reader to prevent controller-runtime to start Informer for Secrets
// under the hood. The latter increases the memory usage of the component.
secret := &corev1.Secret{}
if err := s.apiReader.Get(ctx, secretKey, secret); err != nil {
return nil, err
}
Expand Down
6 changes: 5 additions & 1 deletion pkg/admission/validator/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
extensionspredicate "github.com/gardener/gardener/extensions/pkg/predicate"
extensionswebhook "github.com/gardener/gardener/extensions/pkg/webhook"
"github.com/gardener/gardener/pkg/apis/core"
"github.com/gardener/gardener/pkg/apis/security"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -26,7 +27,7 @@ const (

var logger = log.Log.WithName("openstack-validator-webhook")

// New creates a new webhook that validates Shoot and CloudProfile resources.
// New creates a new webhook that validates Shoot, CloudProfile, SecretBinding and CredentialsBinding resources.
func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
logger.Info("Setting up webhook", "name", Name)

Expand All @@ -39,6 +40,9 @@ func New(mgr manager.Manager) (*extensionswebhook.Webhook, error) {
NewShootValidator(mgr): {{Obj: &core.Shoot{}}},
NewCloudProfileValidator(mgr): {{Obj: &core.CloudProfile{}}},
NewSecretBindingValidator(mgr): {{Obj: &core.SecretBinding{}}},
// TODO(dimityrmirchev): Uncomment this line once this extension uses a g/g version that contains https://github.com/gardener/gardener/pull/10499
// Predicates: []predicate.Predicate{predicate.Or(extensionspredicate.GardenCoreProviderType(openstack.Type), extensionspredicate.GardenSecurityProviderType(openstack.Type))},
NewCredentialsBindingValidator(mgr): {{Obj: &security.CredentialsBinding{}}},
},
Target: extensionswebhook.TargetSeed,
ObjectSelector: &metav1.LabelSelector{
Expand Down
Loading