feature: cleanup serviceAccount

This commit is contained in:
kkb0318
2024-07-12 22:36:26 +09:00
parent 907e58e94a
commit 13b8450ba2
12 changed files with 507 additions and 32 deletions
+96
View File
@@ -17,8 +17,11 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"slices"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
) )
const ( const (
@@ -55,6 +58,18 @@ type IRSAServiceAccount struct {
Namespaces []string `json:"namespaces,omitempty"` Namespaces []string `json:"namespaces,omitempty"`
} }
// NamespacedNameList returns a slice of types.NamespacedName constructed from the Name and Namespace settings.
func (sa *IRSAServiceAccount) NamespacedNameList() []types.NamespacedName {
namespacedName := make([]types.NamespacedName, len(sa.Namespaces))
for i, ns := range sa.Namespaces {
namespacedName[i] = types.NamespacedName{
Name: sa.Name,
Namespace: ns,
}
}
return namespacedName
}
// IamRole represents the IAM role configuration // IamRole represents the IAM role configuration
type IamRole struct { type IamRole struct {
// Name represents the name of the IAM role. // Name represents the name of the IAM role.
@@ -64,6 +79,60 @@ type IamRole struct {
// IRSAStatus defines the observed state of IRSA. // IRSAStatus defines the observed state of IRSA.
type IRSAStatus struct { type IRSAStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"`
// Inventory of applied service resources
ServiceAccounts StatusServiceAccountList `json:"serviceAccounts,omitempty"`
}
type StatusServiceAccountList []IRSANamespacedNameWithTags
func (s *StatusServiceAccountList) IsExist(nsNames types.NamespacedName) bool {
return slices.ContainsFunc(*s, func(sa IRSANamespacedNameWithTags) bool {
return sa.Name == nsNames.Name && sa.Name == nsNames.Namespace
})
}
// Append adds a new IRSANamespacedNameWithTags to the StatusServiceAccountList.
// If the provided NamespacedName already exists in the list, it will be ignored.
func (s *StatusServiceAccountList) Append(nsNames types.NamespacedName) {
*s = append(*s, IRSANamespacedNameWithTags{
Name: nsNames.Name,
Namespace: nsNames.Namespace,
},
)
}
// Delete removes an IRSANamespacedNameWithTags from the StatusServiceAccountList
// that matches the provided NamespacedName. If the provided NamespacedName does
// not exist in the list, the method does nothing.
func (s *StatusServiceAccountList) Delete(nsNames types.NamespacedName) {
index := slices.IndexFunc(*s, func(sa IRSANamespacedNameWithTags) bool {
return sa.Name == nsNames.Name && sa.Namespace == nsNames.Namespace
})
if index != -1 {
*s = slices.Delete(*s, index, index+1)
}
}
// IRSANamespacedNameWithTags is like a types.NamespacedName with JSON tags
type IRSANamespacedNameWithTags struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
func (s *IRSAStatus) ServiceNamespacedNameList() []types.NamespacedName {
namespacedNameList := make([]types.NamespacedName, len(s.ServiceAccounts))
for i, n := range s.ServiceAccounts {
namespacedNameList[i] = types.NamespacedName{
Name: n.Name,
Namespace: n.Namespace,
}
}
return namespacedNameList
}
// GetIRSAStatusServiceAccounts returns a pointer to the ServiceAccount slice
func (in *IRSA) GetIRSAStatusServiceAccounts() *StatusServiceAccountList {
return &in.Status.ServiceAccounts
} }
// GetIRSAStatusConditions returns a pointer to the Conditions slice // GetIRSAStatusConditions returns a pointer to the Conditions slice
@@ -93,11 +162,38 @@ func IRSAStatusNotReady(irsa IRSA, reason, message string) IRSA {
return irsa return irsa
} }
func IRSAStatusSetServiceAccount(irsa IRSA, namespacedNames []types.NamespacedName) IRSA {
for _, namespacedName := range namespacedNames {
setStatusServiceAccounts(irsa.GetIRSAStatusServiceAccounts(), namespacedName)
}
return irsa
}
func IRSAStatusRemoveServiceAccount(irsa IRSA, namespacedNames []types.NamespacedName) IRSA {
for _, namespacedName := range namespacedNames {
removeStatusServiceAccounts(irsa.GetIRSAStatusServiceAccounts(), namespacedName)
}
return irsa
}
func setStatusServiceAccounts(s *StatusServiceAccountList, namespacedName types.NamespacedName) {
if !s.IsExist(namespacedName) {
s.Append(namespacedName)
}
}
func removeStatusServiceAccounts(s *StatusServiceAccountList, namespacedName types.NamespacedName) {
if s.IsExist(namespacedName) {
s.Append(namespacedName)
}
}
type IRSAReason string type IRSAReason string
const ( const (
IRSAReasonFailedRoleUpdate IRSAReason = "IRSAFailedRoleUpdate" IRSAReasonFailedRoleUpdate IRSAReason = "IRSAFailedRoleUpdate"
IRSAReasonFailedK8sApply IRSAReason = "IRSAFailedApplyingResources" IRSAReasonFailedK8sApply IRSAReason = "IRSAFailedApplyingResources"
IRSAReasonFailedK8sCleanUp IRSAReason = "IRSAFailedDeletingResources"
IRSAReasonReady IRSAReason = "IRSAReady" IRSAReasonReady IRSAReason = "IRSAReady"
) )
+85
View File
@@ -0,0 +1,85 @@
package v1alpha1
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/types"
)
func TestStatusServiceAccountList_Append(t *testing.T) {
tests := []struct {
name string
initial StatusServiceAccountList
toAppend types.NamespacedName
expected StatusServiceAccountList
}{
{
name: "Append new item",
initial: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
},
toAppend: types.NamespacedName{Name: "new", Namespace: "default"},
expected: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
{Name: "new", Namespace: "default"},
},
},
{
name: "Append existing item",
initial: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
},
toAppend: types.NamespacedName{Name: "existing", Namespace: "default"},
expected: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
{Name: "existing", Namespace: "default"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.initial.Append(tt.toAppend)
assert.Equal(t, tt.expected, tt.initial)
})
}
}
func TestStatusServiceAccountList_Delete(t *testing.T) {
tests := []struct {
name string
initial StatusServiceAccountList
toDelete types.NamespacedName
expected StatusServiceAccountList
}{
{
name: "Delete existing item",
initial: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
{Name: "todelete", Namespace: "default"},
},
toDelete: types.NamespacedName{Name: "todelete", Namespace: "default"},
expected: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
},
},
{
name: "Delete non-existing item",
initial: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
},
toDelete: types.NamespacedName{Name: "nonexisting", Namespace: "default"},
expected: StatusServiceAccountList{
{Name: "existing", Namespace: "default"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.initial.Delete(tt.toDelete)
assert.Equal(t, tt.expected, tt.initial)
})
}
}
+39
View File
@@ -100,6 +100,21 @@ func (in *IRSAList) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IRSANamespacedNameWithTags) DeepCopyInto(out *IRSANamespacedNameWithTags) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IRSANamespacedNameWithTags.
func (in *IRSANamespacedNameWithTags) DeepCopy() *IRSANamespacedNameWithTags {
if in == nil {
return nil
}
out := new(IRSANamespacedNameWithTags)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IRSAServiceAccount) DeepCopyInto(out *IRSAServiceAccount) { func (in *IRSAServiceAccount) DeepCopyInto(out *IRSAServiceAccount) {
*out = *in *out = *in
@@ -249,6 +264,11 @@ func (in *IRSAStatus) DeepCopyInto(out *IRSAStatus) {
(*in)[i].DeepCopyInto(&(*out)[i]) (*in)[i].DeepCopyInto(&(*out)[i])
} }
} }
if in.ServiceAccounts != nil {
in, out := &in.ServiceAccounts, &out.ServiceAccounts
*out = make(StatusServiceAccountList, len(*in))
copy(*out, *in)
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IRSAStatus. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IRSAStatus.
@@ -290,3 +310,22 @@ func (in *S3Discovery) DeepCopy() *S3Discovery {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in StatusServiceAccountList) DeepCopyInto(out *StatusServiceAccountList) {
{
in := &in
*out = make(StatusServiceAccountList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusServiceAccountList.
func (in StatusServiceAccountList) DeepCopy() StatusServiceAccountList {
if in == nil {
return nil
}
out := new(StatusServiceAccountList)
in.DeepCopyInto(out)
return *out
}
+15
View File
@@ -152,6 +152,21 @@ spec:
- type - type
type: object type: object
type: array type: array
serviceAccounts:
description: Inventory of applied service resources
items:
description: IRSANamespacedNameWithTags is like a types.NamespacedName
with JSON tags
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
type: array
type: object type: object
type: object type: object
served: true served: true
@@ -153,6 +153,21 @@ spec:
- type - type
type: object type: object
type: array type: array
serviceAccounts:
description: Inventory of applied service resources
items:
description: IRSANamespacedNameWithTags is like a types.NamespacedName
with JSON tags
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
type: array
type: object type: object
type: object type: object
served: true served: true
+2
View File
@@ -51,6 +51,8 @@ IRSA is the Schema for the irsas API
#### IRSAServiceAccount #### IRSAServiceAccount
+32 -18
View File
@@ -25,6 +25,7 @@ import (
"github.com/kkb0318/irsa-manager/internal/issuer" "github.com/kkb0318/irsa-manager/internal/issuer"
"github.com/kkb0318/irsa-manager/internal/kubernetes" "github.com/kkb0318/irsa-manager/internal/kubernetes"
"github.com/kkb0318/irsa-manager/internal/manifests" "github.com/kkb0318/irsa-manager/internal/manifests"
"github.com/kkb0318/irsa-manager/internal/utils"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
@@ -118,8 +119,6 @@ func (r *IRSAReconciler) reconcileDelete(ctx context.Context, obj *irsav1alpha1.
if !obj.Spec.Cleanup { if !obj.Spec.Cleanup {
return nil return nil
} }
serviceAccount := obj.Spec.ServiceAccount
kubeHandler := handler.NewKubernetesHandler(kubeClient)
roleManager := awsclient.RoleManager{ roleManager := awsclient.RoleManager{
RoleName: obj.Spec.IamRole.Name, RoleName: obj.Spec.IamRole.Name,
Policies: obj.Spec.IamPolicies, Policies: obj.Spec.IamPolicies,
@@ -131,15 +130,8 @@ func (r *IRSAReconciler) reconcileDelete(ctx context.Context, obj *irsav1alpha1.
if err != nil { if err != nil {
return err return err
} }
for _, ns := range serviceAccount.Namespaces { deleted, err := cleanupKubernetesResources(ctx, kubeClient, obj.Spec.ServiceAccount.NamespacedNameList())
sa := manifests.NewServiceAccountBuilder().Build(types.NamespacedName{ *obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, deleted)
Name: serviceAccount.Name,
Namespace: ns,
})
kubeHandler.Append(sa)
}
err = kubeHandler.DeleteAll(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -196,24 +188,46 @@ func (r *IRSAReconciler) reconcile(ctx context.Context, obj *irsav1alpha1.IRSA,
} }
kubeHandler := handler.NewKubernetesHandler(kubeClient) kubeHandler := handler.NewKubernetesHandler(kubeClient)
for _, namespacedName := range serviceAccount.NamespacedNameList() {
for _, ns := range serviceAccount.Namespaces { sa := manifests.NewServiceAccountBuilder().WithIRSAAnnotation(roleManager).Build(namespacedName)
sa := manifests.NewServiceAccountBuilder().WithIRSAAnnotation(roleManager).Build(types.NamespacedName{
Name: serviceAccount.Name,
Namespace: ns,
})
kubeHandler.Append(sa) kubeHandler.Append(sa)
} }
err = kubeHandler.ApplyAll(ctx) applied, err := kubeHandler.ApplyAll(ctx)
*obj = irsav1alpha1.IRSAStatusSetServiceAccount(*obj, applied)
if err != nil { if err != nil {
e = err e = err
reason = irsav1alpha1.IRSAReasonFailedK8sApply reason = irsav1alpha1.IRSAReasonFailedK8sApply
return err 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") *obj = irsav1alpha1.IRSAStatusReady(*obj, string(irsav1alpha1.IRSAReasonReady), "successfully setup resources")
return nil 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. // SetupWithManager sets up the controller with the Manager.
func (r *IRSAReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *IRSAReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
+99 -5
View File
@@ -17,6 +17,7 @@ limitations under the License.
package controller package controller
import ( import (
"context"
"fmt" "fmt"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@@ -80,10 +81,6 @@ var _ = Describe("IRSA Controller", func() {
NamespacedName: typeNamespacedName, NamespacedName: typeNamespacedName,
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
_, err = r.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
for _, expect := range expected { for _, expect := range expected {
checkExist(expect) checkExist(expect)
} }
@@ -101,7 +98,7 @@ var _ = Describe("IRSA Controller", func() {
}, },
}, },
{ {
name: "error", name: "AWS API Error Case",
obj: &irsav1alpha1.IRSA{ obj: &irsav1alpha1.IRSA{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "test-resource2", 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 { for _, tt := range tests {
It(tt.name, func() { 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)
}
}
+3 -3
View File
@@ -150,7 +150,7 @@ func (r *IRSASetupReconciler) reconcileDelete(ctx context.Context, obj *irsav1al
for _, r := range webhookSetup.Resources() { for _, r := range webhookSetup.Resources() {
kubeHandler.Append(r) kubeHandler.Append(r)
} }
err = kubeHandler.DeleteAll(ctx) _, err = kubeHandler.DeleteAll(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -233,7 +233,7 @@ func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsCl
return err return err
} }
if forceUpdate { if forceUpdate {
err = kubeHandlerForOidc.ApplyAll(ctx) _, err = kubeHandlerForOidc.ApplyAll(ctx)
} else { } else {
err = kubeHandlerForOidc.CreateAll(ctx) err = kubeHandlerForOidc.CreateAll(ctx)
} }
@@ -247,7 +247,7 @@ func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsCl
for _, r := range webhookSetup.Resources() { for _, r := range webhookSetup.Resources() {
kubeHandlerForWebhook.Append(r) kubeHandlerForWebhook.Append(r)
} }
err = kubeHandlerForWebhook.ApplyAll(ctx) _, err = kubeHandlerForWebhook.ApplyAll(ctx)
if err != nil { if err != nil {
e = err e = err
reason = irsav1alpha1.SelfHostedReasonFailedWebhook reason = irsav1alpha1.SelfHostedReasonFailedWebhook
+11 -6
View File
@@ -6,6 +6,7 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
) )
@@ -39,24 +40,28 @@ func (k *KubernetesHandler) CreateAll(ctx context.Context) error {
return nil 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 { for _, obj := range k.objs {
err := k.client.Apply(ctx, obj) err := k.client.Apply(ctx, obj)
if err != nil { 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 { for _, obj := range k.objs {
err := k.client.Delete(ctx, obj, DeleteOptions{ err := k.client.Delete(ctx, obj, DeleteOptions{
DeletionPropagation: metav1.DeletePropagationBackground, DeletionPropagation: metav1.DeletePropagationBackground,
}) })
if err != nil { if err != nil {
return err return deleted, err
} }
deleted = append(deleted, client.ObjectKeyFromObject(obj))
} }
return nil return deleted, nil
} }
+23
View File
@@ -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
}
+87
View File
@@ -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)
})
}
}