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

Add credential admission checks for managedcluster #481

Merged
merged 1 commit into from
Oct 18, 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
2 changes: 0 additions & 2 deletions internal/controller/managedcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,6 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, managedCluster *h
Reason: hmc.FailedReason,
Message: "Credential is not in Ready state",
})
return ctrl.Result{},
fmt.Errorf("credential is not in Ready state")
}

apimeta.SetStatusCondition(managedCluster.GetConditions(), metav1.Condition{
Expand Down
47 changes: 45 additions & 2 deletions internal/controller/managedcluster_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ var _ = Describe("ManagedCluster Controller", func() {
managedClusterName = "test-managed-cluster"
managedClusterNamespace = "test"

templateName = "test-template"
templateName = "test-template"
credentialName = "test-credential"
)

ctx := context.Background()
Expand All @@ -50,6 +51,7 @@ var _ = Describe("ManagedCluster Controller", func() {
managedCluster := &hmc.ManagedCluster{}
template := &hmc.ClusterTemplate{}
management := &hmc.Management{}
credential := &hmc.Credential{}
namespace := &corev1.Namespace{}

BeforeEach(func() {
Expand Down Expand Up @@ -92,6 +94,13 @@ var _ = Describe("ManagedCluster Controller", func() {
Raw: []byte(`{"foo":"bar"}`),
},
},
Providers: hmc.ProvidersTupled{
InfrastructureProviders: []hmc.ProviderTuple{
{
Name: "aws",
},
},
},
}
Expect(k8sClient.Status().Update(ctx, template)).To(Succeed())
}
Expand All @@ -106,7 +115,40 @@ var _ = Describe("ManagedCluster Controller", func() {
Spec: hmc.ManagementSpec{},
}
Expect(k8sClient.Create(ctx, management)).To(Succeed())
management.Status = hmc.ManagementStatus{
AvailableProviders: hmc.ProvidersTupled{
InfrastructureProviders: []hmc.ProviderTuple{
{
Name: "aws",
},
},
},
}
Expect(k8sClient.Status().Update(ctx, management)).To(Succeed())
}
By("creating the custom resource for the Kind Credential")
err = k8sClient.Get(ctx, typeNamespacedName, credential)
a13x5 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil && errors.IsNotFound(err) {
credential = &hmc.Credential{
ObjectMeta: metav1.ObjectMeta{
Name: credentialName,
Namespace: managedClusterNamespace,
},
Spec: hmc.CredentialSpec{
IdentityRef: &corev1.ObjectReference{
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2",
Kind: "AWSClusterStaticIdentity",
Name: "foo",
},
},
}
Expect(k8sClient.Create(ctx, credential)).To(Succeed())
credential.Status = hmc.CredentialStatus{
State: hmc.CredentialReady,
}
Expect(k8sClient.Status().Update(ctx, credential)).To(Succeed())
}

By("creating the custom resource for the Kind ManagedCluster")
err = k8sClient.Get(ctx, typeNamespacedName, managedCluster)
if err != nil && errors.IsNotFound(err) {
Expand All @@ -116,7 +158,8 @@ var _ = Describe("ManagedCluster Controller", func() {
Namespace: managedClusterNamespace,
},
Spec: hmc.ManagedClusterSpec{
Template: templateName,
Template: templateName,
Credential: credentialName,
},
}
Expect(k8sClient.Create(ctx, managedCluster)).To(Succeed())
Expand Down
70 changes: 70 additions & 0 deletions internal/webhook/managedcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim
return admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"}, fmt.Errorf("failed to validate k8s compatibility: %v", err)
}

if err := v.validateCredential(ctx, managedCluster, template); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

return nil, nil
}

Expand All @@ -93,6 +97,10 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, _ runtime.
return admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"}, fmt.Errorf("failed to validate k8s compatibility: %v", err)
}

if err := v.validateCredential(ctx, newManagedCluster, template); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

return nil, nil
}

Expand Down Expand Up @@ -178,10 +186,72 @@ func (v *ManagedClusterValidator) getManagedClusterTemplate(ctx context.Context,
return tpl, v.Get(ctx, client.ObjectKey{Namespace: templateNamespace, Name: templateName}, tpl)
}

func (v *ManagedClusterValidator) getManagedClusterCredential(ctx context.Context, credNamespace, credName string) (*hmcv1alpha1.Credential, error) {
cred := &hmcv1alpha1.Credential{}
credRef := client.ObjectKey{
Name: credName,
Namespace: credNamespace,
}
if err := v.Get(ctx, credRef, cred); err != nil {
return nil, err
}
return cred, nil
}

func isTemplateValid(template *hmcv1alpha1.ClusterTemplate) error {
if !template.Status.Valid {
return fmt.Errorf("the template is not valid: %s", template.Status.ValidationError)
}

return nil
}

func (v *ManagedClusterValidator) validateCredential(ctx context.Context, managedCluster *hmcv1alpha1.ManagedCluster, template *hmcv1alpha1.ClusterTemplate) error {
infraProviders := template.Status.Providers.InfrastructureProviders

if len(infraProviders) == 0 {
return fmt.Errorf("template %q has no infrastructure providers defined", template.Name)
}

cred, err := v.getManagedClusterCredential(ctx, managedCluster.Namespace, managedCluster.Spec.Credential)
if err != nil {
return err
}

if cred.Status.State != hmcv1alpha1.CredentialReady {
return fmt.Errorf("credential is not Ready")
}

return isCredMatchTemplate(cred, template)
}

func isCredMatchTemplate(cred *hmcv1alpha1.Credential, template *hmcv1alpha1.ClusterTemplate) error {
idtyKind := cred.Spec.IdentityRef.Kind
infraProviders := template.Status.Providers.InfrastructureProviders

errMsg := func(idtyKind string, provider string) error {
return fmt.Errorf("wrong kind of the ClusterIdentity %q for provider %q", idtyKind, provider)
}

for _, provider := range infraProviders {
switch provider.Name {
case "aws":
if idtyKind != "AWSClusterStaticIdentity" &&
idtyKind != "AWSClusterRoleIdentity" &&
idtyKind != "AWSClusterControllerIdentity" {
return errMsg(idtyKind, provider.Name)
}
case "azure":
if idtyKind != "AzureClusterIdentity" {
zerospiel marked this conversation as resolved.
Show resolved Hide resolved
return errMsg(idtyKind, provider.Name)
}
case "vsphere":
if idtyKind != "VSphereClusterIdentity" {
return errMsg(idtyKind, provider.Name)
}
default:
return fmt.Errorf("unsupported infrastructure provider %s", provider.Name)
}
}
return nil
}
120 changes: 112 additions & 8 deletions internal/webhook/managedcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ import (

. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/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/credential"
"github.com/Mirantis/hmc/test/objects/managedcluster"
"github.com/Mirantis/hmc/test/objects/management"
"github.com/Mirantis/hmc/test/objects/template"
"github.com/Mirantis/hmc/test/scheme"
)

var (
testTemplateName = "template-test"
testNamespace = "test"
testTemplateName = "template-test"
testCredentialName = "cred-test"
testNamespace = "test"

mgmt = management.NewManagement(
management.WithAvailableProviders(v1alpha1.ProvidersTupled{
Expand All @@ -44,6 +47,16 @@ var (
}),
)

cred = credential.NewCredential(
credential.WithName(testCredentialName),
credential.WithState(v1alpha1.CredentialReady),
credential.WithIdentityRef(
&corev1.ObjectReference{
Kind: "AWSClusterStaticIdentity",
Name: "awsclid",
}),
)

createAndUpdateTests = []struct {
name string
managedCluster *v1alpha1.ManagedCluster
Expand All @@ -57,10 +70,14 @@ var (
err: "the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"\" not found",
},
{
name: "should fail if the ClusterTemplate is not found in the ManagedCluster's namespace",
managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)),
name: "should fail if the ClusterTemplate is not found in the ManagedCluster's namespace",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt,
cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithNamespace(testNamespace),
Expand All @@ -69,10 +86,14 @@ var (
err: fmt.Sprintf("the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"%s\" not found", testTemplateName),
},
{
name: "should fail if the cluster template was found but is invalid (some validation error)",
managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)),
name: "should fail if the cluster template was found but is invalid (some validation error)",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt,
cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{
Expand All @@ -84,12 +105,21 @@ var (
err: "the ManagedCluster is invalid: the template is not valid: validation error example",
},
{
name: "should succeed",
managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)),
name: "should succeed",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt,
cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
},
Expand All @@ -101,6 +131,7 @@ var (
managedcluster.WithServiceTemplate(testTemplateName),
),
existingObjects: []runtime.Object{
cred,
management.NewManagement(management.WithAvailableProviders(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws", VersionOrConstraint: "v1.0.0"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s", VersionOrConstraint: "v1.0.0"}},
Expand All @@ -120,6 +151,79 @@ var (
err: fmt.Sprintf(`failed to validate k8s compatibility: k8s version v1.30.0 of the ManagedCluster default/%s does not satisfy constrained version <1.30 from the ServiceTemplate default/%s`, managedcluster.DefaultName, testTemplateName),
warnings: admission.Warnings{"Failed to validate k8s version compatibility with ServiceTemplates"},
},
{
name: "should fail if the credential is unset",
managedCluster: managedcluster.NewManagedCluster(managedcluster.WithClusterTemplate(testTemplateName)),
existingObjects: []runtime.Object{
mgmt,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
},
err: "the ManagedCluster is invalid: credentials.hmc.mirantis.com \"\" not found",
},
{
name: "should fail if credential is not Ready",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
mgmt,
credential.NewCredential(
credential.WithName(testCredentialName),
credential.WithState(v1alpha1.CredentialNotFound),
credential.WithIdentityRef(
&corev1.ObjectReference{
Kind: "AWSClusterStaticIdentity",
Name: "awsclid",
}),
),
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
},
err: "the ManagedCluster is invalid: credential is not Ready",
},
{
name: "should fail if credential and template providers doesn't match",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
),
existingObjects: []runtime.Object{
cred,
management.NewManagement(
management.WithAvailableProviders(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "azure"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
}),
),
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.ProvidersTupled{
InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "azure"}},
BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
ControlPlaneProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}},
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
},
err: "the ManagedCluster is invalid: wrong kind of the ClusterIdentity \"AWSClusterStaticIdentity\" for provider \"azure\"",
},
}
)

Expand Down
Loading