Skip to content

Commit

Permalink
Validate Release object deletion
Browse files Browse the repository at this point in the history
Signed-off-by: Andrei Pavlov <[email protected]>
  • Loading branch information
Kshatrix committed Dec 3, 2024
1 parent 4dd408f commit 5ddea6f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 9 deletions.
19 changes: 19 additions & 0 deletions api/v1alpha1/management_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ func GetDefaultProviders() []Provider {
}
}

// Templates returns a list of provider templates explicitly defined in the Management object
func (in *Management) Templates() []string {
templates := []string{}
if in.Spec.Core != nil {
if in.Spec.Core.CAPI.Template != "" {
templates = append(templates, in.Spec.Core.CAPI.Template)
}
if in.Spec.Core.HMC.Template != "" {
templates = append(templates, in.Spec.Core.HMC.Template)
}
}
for _, p := range in.Spec.Providers {
if p.Template != "" {
templates = append(templates, p.Template)
}
}
return templates
}

// ManagementStatus defines the observed state of Management
type ManagementStatus struct {
// For each CAPI provider name holds its compatibility [contract versions]
Expand Down
36 changes: 35 additions & 1 deletion internal/webhook/release_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ package webhook

import (
"context"
"fmt"
"slices"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -52,6 +56,36 @@ func (*ReleaseValidator) ValidateUpdate(_ context.Context, _, _ runtime.Object)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (*ReleaseValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
func (v *ReleaseValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
release, ok := obj.(*hmcv1alpha1.Release)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected Release but got a %T", obj))
}
mgmtList := &hmcv1alpha1.ManagementList{}
if err := v.List(ctx, mgmtList); err != nil {
return nil, err
}
if len(mgmtList.Items) == 0 {
return nil, nil
}
if len(mgmtList.Items) > 1 {
return nil, fmt.Errorf("expected 1 Management object, got %d", len(mgmtList.Items))
}

mgmt := mgmtList.Items[0]
if mgmt.Spec.Release == release.Name {
return nil, fmt.Errorf("release %s is still in use", release.Name)
}

templates := release.Templates()
templatesInUse := []string{}
for _, t := range mgmt.Templates() {
if slices.Contains(templates, t) {
templatesInUse = append(templatesInUse, t)
}
}
if len(templatesInUse) > 0 {
return nil, fmt.Errorf("the following ProviderTemplates associated with the Release are still in use: %s", strings.Join(templatesInUse, ", "))
}
return nil, nil
}
98 changes: 90 additions & 8 deletions internal/webhook/release_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,97 @@
package webhook

import (
. "github.com/onsi/ginkgo/v2"
"context"
"fmt"
"testing"

. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/Mirantis/hmc/api/v1alpha1"
"github.com/Mirantis/hmc/test/objects/management"
"github.com/Mirantis/hmc/test/objects/release"
"github.com/Mirantis/hmc/test/scheme"
)

var _ = Describe("Release Webhook", func() {
Context("When creating Release under Validating Webhook", func() {
It("Should deny if a required field is empty", func() {
})
func TestReleaseValidateDelete(t *testing.T) {
g := NewWithT(t)

ctx := admission.NewContextWithRequest(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete}})

tests := []struct {
name string
release *v1alpha1.Release
existingObjects []runtime.Object
err string
}{
{
name: "should fail if > 1 Management",
release: release.New(),
existingObjects: []runtime.Object{management.NewManagement(), management.NewManagement(management.WithName("second"))},
err: "expected 1 Management object, got 2",
},
{
name: "should fail if Release is in use",
release: release.New(),
existingObjects: []runtime.Object{management.NewManagement(management.WithRelease(release.DefaultName))},
err: fmt.Sprintf("release %s is still in use", release.DefaultName),
},
{
name: "should fail if some providers are in use",
release: release.New(release.WithProviders(
v1alpha1.NamedProviderTemplate{CoreProviderTemplate: v1alpha1.CoreProviderTemplate{Template: "template-in-use-1"}},
v1alpha1.NamedProviderTemplate{CoreProviderTemplate: v1alpha1.CoreProviderTemplate{Template: "template-in-use-2"}},
v1alpha1.NamedProviderTemplate{CoreProviderTemplate: v1alpha1.CoreProviderTemplate{Template: "template-not-in-use"}}),
release.WithCAPITemplateName("template-capi-in-use"),
release.WithHMCTemplateName("template-hmc-in-use"),
),
existingObjects: []runtime.Object{management.NewManagement(
management.WithRelease("some-release"),
management.WithProviders(
v1alpha1.Provider{Component: v1alpha1.Component{Template: "template-in-use-1"}},
v1alpha1.Provider{Component: v1alpha1.Component{Template: "template-in-use-2"}},
),
management.WithCoreComponents(&v1alpha1.Core{
HMC: v1alpha1.Component{Template: "template-hmc-in-use"},
CAPI: v1alpha1.Component{Template: "template-capi-in-use"},
}),
)},
err: "the following ProviderTemplates associated with the Release are still in use: template-capi-in-use, template-hmc-in-use, template-in-use-1, template-in-use-2",
},
{
name: "should succeed",
release: release.New(release.WithProviders(
v1alpha1.NamedProviderTemplate{CoreProviderTemplate: v1alpha1.CoreProviderTemplate{Template: "template-not-in-use"}},
)),
existingObjects: []runtime.Object{management.NewManagement(
management.WithRelease("some-release"),
management.WithProviders(
v1alpha1.Provider{Component: v1alpha1.Component{Template: "template-in-use"}},
),
)},
},
{
name: "should succeed if Management doesn't exist",
release: release.New(),
},
}

for _, tt := range tests {
t.Run(tt.name, func(_ *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build()
validator := &ReleaseValidator{Client: c}

It("Should admit if all required fields are provided", func() {
_, err := validator.ValidateDelete(ctx, tt.release)
if tt.err != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err).To(MatchError(tt.err))
} else {
g.Expect(err).To(Succeed())
}
})
})
})
}
}
21 changes: 21 additions & 0 deletions templates/provider/hmc/templates/webhooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,25 @@ webhooks:
resources:
- servicetemplatechains
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: {{ include "hmc.webhook.serviceName" . }}
namespace: {{ include "hmc.webhook.serviceNamespace" . }}
path: /validate-hmc-mirantis-com-v1alpha1-release
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.release.hmc.mirantis.com
rules:
- apiGroups:
- hmc.mirantis.com
apiVersions:
- v1alpha1
operations:
- DELETE
resources:
- releases
sideEffects: None
{{- end }}
6 changes: 6 additions & 0 deletions test/objects/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ func WithCAPITemplateName(v string) Opt {
}
}

func WithProviders(v ...v1alpha1.NamedProviderTemplate) Opt {
return func(r *v1alpha1.Release) {
r.Spec.Providers = v
}
}

func WithReadyStatus(ready bool) Opt {
return func(r *v1alpha1.Release) {
r.Status.Ready = ready
Expand Down

0 comments on commit 5ddea6f

Please sign in to comment.