mirror of
https://github.com/storytold/irsa-manager.git
synced 2026-10-09 00:09:43 +00:00
feature: cleanup serviceAccount
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/kkb0318/irsa-manager/internal/issuer"
|
||||
"github.com/kkb0318/irsa-manager/internal/kubernetes"
|
||||
"github.com/kkb0318/irsa-manager/internal/manifests"
|
||||
"github.com/kkb0318/irsa-manager/internal/utils"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -118,8 +119,6 @@ func (r *IRSAReconciler) reconcileDelete(ctx context.Context, obj *irsav1alpha1.
|
||||
if !obj.Spec.Cleanup {
|
||||
return nil
|
||||
}
|
||||
serviceAccount := obj.Spec.ServiceAccount
|
||||
kubeHandler := handler.NewKubernetesHandler(kubeClient)
|
||||
roleManager := awsclient.RoleManager{
|
||||
RoleName: obj.Spec.IamRole.Name,
|
||||
Policies: obj.Spec.IamPolicies,
|
||||
@@ -131,15 +130,8 @@ func (r *IRSAReconciler) reconcileDelete(ctx context.Context, obj *irsav1alpha1.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ns := range serviceAccount.Namespaces {
|
||||
sa := manifests.NewServiceAccountBuilder().Build(types.NamespacedName{
|
||||
Name: serviceAccount.Name,
|
||||
Namespace: ns,
|
||||
})
|
||||
kubeHandler.Append(sa)
|
||||
|
||||
}
|
||||
err = kubeHandler.DeleteAll(ctx)
|
||||
deleted, err := cleanupKubernetesResources(ctx, kubeClient, obj.Spec.ServiceAccount.NamespacedNameList())
|
||||
*obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, deleted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,24 +188,46 @@ func (r *IRSAReconciler) reconcile(ctx context.Context, obj *irsav1alpha1.IRSA,
|
||||
}
|
||||
|
||||
kubeHandler := handler.NewKubernetesHandler(kubeClient)
|
||||
|
||||
for _, ns := range serviceAccount.Namespaces {
|
||||
sa := manifests.NewServiceAccountBuilder().WithIRSAAnnotation(roleManager).Build(types.NamespacedName{
|
||||
Name: serviceAccount.Name,
|
||||
Namespace: ns,
|
||||
})
|
||||
for _, namespacedName := range serviceAccount.NamespacedNameList() {
|
||||
sa := manifests.NewServiceAccountBuilder().WithIRSAAnnotation(roleManager).Build(namespacedName)
|
||||
kubeHandler.Append(sa)
|
||||
}
|
||||
err = kubeHandler.ApplyAll(ctx)
|
||||
applied, err := kubeHandler.ApplyAll(ctx)
|
||||
*obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, applied)
|
||||
if err != nil {
|
||||
e = err
|
||||
reason = irsav1alpha1.IRSAReasonFailedK8sApply
|
||||
return err
|
||||
}
|
||||
|
||||
deleted, err := cleanupKubernetesResources(
|
||||
ctx,
|
||||
kubeClient,
|
||||
utils.DiffNamespacedNames(obj.Status.ServiceNamespacedNameList(), serviceAccount.NamespacedNameList()),
|
||||
)
|
||||
*obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, deleted)
|
||||
if err != nil {
|
||||
e = err
|
||||
reason = irsav1alpha1.IRSAReasonFailedK8sCleanUp
|
||||
return err
|
||||
}
|
||||
*obj = irsav1alpha1.IRSAStatusReady(*obj, string(irsav1alpha1.IRSAReasonReady), "successfully setup resources")
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupKubernetesResources(ctx context.Context, client *kubernetes.KubernetesClient, nsNames []types.NamespacedName) ([]types.NamespacedName, error) {
|
||||
kubeHandler := handler.NewKubernetesHandler(client)
|
||||
for _, namespacedName := range nsNames {
|
||||
sa := manifests.NewServiceAccountBuilder().Build(namespacedName)
|
||||
kubeHandler.Append(sa)
|
||||
}
|
||||
deleted, err := kubeHandler.DeleteAll(ctx)
|
||||
if err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *IRSAReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -80,10 +81,6 @@ var _ = Describe("IRSA Controller", func() {
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, err = r.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
for _, expect := range expected {
|
||||
checkExist(expect)
|
||||
}
|
||||
@@ -101,7 +98,7 @@ var _ = Describe("IRSA Controller", func() {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
name: "AWS API Error Case",
|
||||
obj: &irsav1alpha1.IRSA{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-resource2",
|
||||
@@ -169,6 +166,75 @@ var _ = Describe("IRSA Controller", func() {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should update serviceaccount successfully",
|
||||
obj: &irsav1alpha1.IRSA{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-resource3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: irsav1alpha1.IRSASpec{
|
||||
Cleanup: true,
|
||||
ServiceAccount: irsav1alpha1.IRSAServiceAccount{
|
||||
Name: "sa-3",
|
||||
Namespaces: []string{
|
||||
"kube-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
irsaSetupObj: newMockIRSASetup(),
|
||||
f: func(r *IRSAReconciler, obj *irsav1alpha1.IRSA) {
|
||||
expected := []expectedResource{
|
||||
{
|
||||
NamespacedName: types.NamespacedName{Name: "sa-3", Namespace: "kube-system"},
|
||||
f: newServiceAccount,
|
||||
},
|
||||
{
|
||||
NamespacedName: types.NamespacedName{Name: "sa-3", Namespace: "default"},
|
||||
f: newServiceAccount,
|
||||
},
|
||||
}
|
||||
|
||||
By("Reconciling the created resource")
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: obj.Name,
|
||||
Namespace: obj.Namespace,
|
||||
}
|
||||
_, err := r.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
checkExist(
|
||||
expectedResource{
|
||||
NamespacedName: types.NamespacedName{Name: "sa-3", Namespace: "kube-system"},
|
||||
f: newServiceAccount,
|
||||
},
|
||||
)
|
||||
f := createCallBack(ctx, r, typeNamespacedName, obj)
|
||||
|
||||
By("Add Namespace 'default'")
|
||||
f(obj.Spec.ServiceAccount.Name, []string{"default", "kube-system"})
|
||||
By("Remove Namespace 'kube-system'")
|
||||
f(obj.Spec.ServiceAccount.Name, []string{"default"})
|
||||
checkNoExist(expectedResource{
|
||||
NamespacedName: types.NamespacedName{Name: "sa-3", Namespace: "kube-system"},
|
||||
f: newServiceAccount,
|
||||
})
|
||||
|
||||
By("removing the custom resource for the Kind")
|
||||
Eventually(func() error {
|
||||
return k8sClient.Delete(ctx, obj)
|
||||
}, timeout).Should(Succeed())
|
||||
_, err = r.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).To(Not(HaveOccurred()))
|
||||
for _, expect := range expected {
|
||||
checkNoExist(expect)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
It(tt.name, func() {
|
||||
@@ -231,3 +297,31 @@ func newMockIRSASetup() *irsav1alpha1.IRSASetup {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createCallBack(ctx context.Context, r *IRSAReconciler, typeNamespacedName types.NamespacedName, obj *irsav1alpha1.IRSA) func(name string, namespaces []string) {
|
||||
return func(name string, namespaces []string) {
|
||||
fixNamespacesAndReconcile(ctx, r, typeNamespacedName, obj, name, namespaces)
|
||||
}
|
||||
}
|
||||
|
||||
func fixNamespacesAndReconcile(ctx context.Context, r *IRSAReconciler, typeNamespacedName types.NamespacedName, obj *irsav1alpha1.IRSA, name string, namespaces []string) {
|
||||
Expect(k8sClient.Get(ctx, typeNamespacedName, obj)).NotTo(HaveOccurred())
|
||||
Eventually(func() error {
|
||||
obj.Spec.ServiceAccount.Namespaces = namespaces
|
||||
return k8sClient.Update(ctx, obj)
|
||||
}, timeout).Should(Succeed())
|
||||
_, err := r.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expected := []expectedResource{}
|
||||
for _, ns := range namespaces {
|
||||
expected = append(expected, expectedResource{
|
||||
NamespacedName: types.NamespacedName{Name: name, Namespace: ns},
|
||||
f: newServiceAccount,
|
||||
})
|
||||
}
|
||||
for _, e := range expected {
|
||||
checkExist(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ func (r *IRSASetupReconciler) reconcileDelete(ctx context.Context, obj *irsav1al
|
||||
for _, r := range webhookSetup.Resources() {
|
||||
kubeHandler.Append(r)
|
||||
}
|
||||
err = kubeHandler.DeleteAll(ctx)
|
||||
_, err = kubeHandler.DeleteAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsCl
|
||||
return err
|
||||
}
|
||||
if forceUpdate {
|
||||
err = kubeHandlerForOidc.ApplyAll(ctx)
|
||||
_, err = kubeHandlerForOidc.ApplyAll(ctx)
|
||||
} else {
|
||||
err = kubeHandlerForOidc.CreateAll(ctx)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsCl
|
||||
for _, r := range webhookSetup.Resources() {
|
||||
kubeHandlerForWebhook.Append(r)
|
||||
}
|
||||
err = kubeHandlerForWebhook.ApplyAll(ctx)
|
||||
_, err = kubeHandlerForWebhook.ApplyAll(ctx)
|
||||
if err != nil {
|
||||
e = err
|
||||
reason = irsav1alpha1.SelfHostedReasonFailedWebhook
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
@@ -39,24 +40,28 @@ func (k *KubernetesHandler) CreateAll(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubernetesHandler) ApplyAll(ctx context.Context) error {
|
||||
func (k *KubernetesHandler) ApplyAll(ctx context.Context) ([]types.NamespacedName, error) {
|
||||
applied := []types.NamespacedName{}
|
||||
for _, obj := range k.objs {
|
||||
err := k.client.Apply(ctx, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
return applied, err
|
||||
}
|
||||
applied = append(applied, client.ObjectKeyFromObject(obj))
|
||||
}
|
||||
return nil
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
func (k *KubernetesHandler) DeleteAll(ctx context.Context) error {
|
||||
func (k *KubernetesHandler) DeleteAll(ctx context.Context) ([]types.NamespacedName, error) {
|
||||
deleted := []types.NamespacedName{}
|
||||
for _, obj := range k.objs {
|
||||
err := k.client.Delete(ctx, obj, DeleteOptions{
|
||||
DeletionPropagation: metav1.DeletePropagationBackground,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return deleted, err
|
||||
}
|
||||
deleted = append(deleted, client.ObjectKeyFromObject(obj))
|
||||
}
|
||||
return nil
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// DiffNamespacedNames returns the namespaced names that are in target but not in reference.
|
||||
func DiffNamespacedNames(target, reference []types.NamespacedName) []types.NamespacedName {
|
||||
referenceSet := make(map[types.NamespacedName]struct{})
|
||||
|
||||
for _, item := range reference {
|
||||
referenceSet[item] = struct{}{}
|
||||
}
|
||||
|
||||
diff := []types.NamespacedName{}
|
||||
for _, item := range target {
|
||||
if _, exists := referenceSet[item]; !exists {
|
||||
diff = append(diff, item)
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
func TestDiffNamespacedNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target []types.NamespacedName
|
||||
reference []types.NamespacedName
|
||||
expected []types.NamespacedName
|
||||
}{
|
||||
{
|
||||
"NoDifference",
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{},
|
||||
},
|
||||
{
|
||||
"SomeDifference",
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource3"},
|
||||
},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"AllDifferent",
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "other", Name: "resource3"},
|
||||
{Namespace: "other", Name: "resource4"},
|
||||
},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"EmptyTarget",
|
||||
[]types.NamespacedName{},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{},
|
||||
},
|
||||
{
|
||||
"EmptyReference",
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
[]types.NamespacedName{},
|
||||
[]types.NamespacedName{
|
||||
{Namespace: "default", Name: "resource1"},
|
||||
{Namespace: "default", Name: "resource2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := DiffNamespacedNames(tt.target, tt.reference)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user