argo-cd/pkg/sync/sync_context_test.go

647 lines
20 KiB
Go
Raw Normal View History

2020-05-15 20:01:24 +00:00
package sync
import (
"fmt"
"reflect"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
fakedisco "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
testcore "k8s.io/client-go/testing"
2020-05-29 00:18:31 +00:00
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
2020-05-15 20:01:24 +00:00
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
testingutils "github.com/argoproj/gitops-engine/pkg/utils/testing"
)
func newTestSyncCtx(opts ...SyncOpt) *syncContext {
fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
fakeDisco.Resources = append(make([]*v1.APIResourceList, 0),
&v1.APIResourceList{
GroupVersion: "v1",
APIResources: []v1.APIResource{
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true},
{Kind: "Service", Group: "", Version: "v1", Namespaced: true},
},
},
&v1.APIResourceList{
GroupVersion: "apps/v1",
APIResources: []v1.APIResource{
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true},
},
})
sc := syncContext{
config: &rest.Config{},
rawConfig: &rest.Config{},
namespace: FakeArgoCDNamespace,
revision: "FooBarBaz",
disco: fakeDisco,
log: log.WithFields(log.Fields{"application": "fake-app"}),
resources: map[kube.ResourceKey]reconciledResource{},
syncRes: map[string]synccommon.ResourceSyncResult{},
validate: true,
}
sc.permissionValidator = func(un *unstructured.Unstructured, res *v1.APIResource) error {
return nil
}
sc.kubectl = &kubetest.MockKubectlCmd{}
for _, opt := range opts {
opt(&sc)
}
return &sc
}
// make sure Validate means we don't validate
func TestSyncValidate(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
pod.SetNamespace("fake-argocd-ns")
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{pod},
Target: []*unstructured.Unstructured{pod},
})
syncCtx.validate = false
syncCtx.Sync()
kubectl := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
assert.False(t, kubectl.GetLastValidate())
2020-05-15 20:01:24 +00:00
}
func TestSyncNotPermittedNamespace(t *testing.T) {
syncCtx := newTestSyncCtx(WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error {
return fmt.Errorf("not permitted in project")
}))
targetPod := NewPod()
targetPod.SetNamespace("kube-system")
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil, nil},
Target: []*unstructured.Unstructured{targetPod, NewService()},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationFailed, phase)
assert.Contains(t, resources[0].Message, "not permitted in project")
}
func TestSyncCreateInSortedOrder(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil, nil},
Target: []*unstructured.Unstructured{NewPod(), NewService()},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
assert.Len(t, resources, 2)
for i := range resources {
result := resources[i]
if result.ResourceKey.Kind == "Pod" {
assert.Equal(t, synccommon.ResultCodeSynced, result.Status)
assert.Equal(t, "", result.Message)
} else if result.ResourceKey.Kind == "Service" {
assert.Equal(t, "", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncCustomResources(t *testing.T) {
type fields struct {
skipDryRunAnnotationPresent bool
crdAlreadyPresent bool
crdInSameSync bool
}
tests := []struct {
name string
fields fields
wantDryRun bool
wantSuccess bool
}{
{"unknown crd", fields{
skipDryRunAnnotationPresent: false, crdAlreadyPresent: false, crdInSameSync: false,
}, true, false},
{"crd present in same sync", fields{
skipDryRunAnnotationPresent: false, crdAlreadyPresent: false, crdInSameSync: true,
}, false, true},
{"crd is already present in cluster", fields{
skipDryRunAnnotationPresent: false, crdAlreadyPresent: true, crdInSameSync: false,
}, true, true},
{"crd is already present in cluster, skip dry run annotated", fields{
skipDryRunAnnotationPresent: true, crdAlreadyPresent: true, crdInSameSync: false,
}, true, true},
{"unknown crd, skip dry run annotated", fields{
skipDryRunAnnotationPresent: true, crdAlreadyPresent: false, crdInSameSync: false,
}, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
knownCustomResourceTypes := []v1.APIResource{}
if tt.fields.crdAlreadyPresent {
knownCustomResourceTypes = append(knownCustomResourceTypes, v1.APIResource{Kind: "TestCrd", Group: "argoproj.io", Version: "v1", Namespaced: true})
}
syncCtx := newTestSyncCtx()
fakeDisco := syncCtx.disco.(*fakedisco.FakeDiscovery)
fakeDisco.Resources = []*v1.APIResourceList{{
GroupVersion: "argoproj.io/v1",
APIResources: knownCustomResourceTypes,
},
{
GroupVersion: "apiextensions.k8s.io/v1beta1",
APIResources: []v1.APIResource{
{Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io", Version: "v1beta1", Namespaced: true},
},
},
}
cr := testingutils.Unstructured(`
{
"apiVersion": "argoproj.io/v1",
"kind": "TestCrd",
"metadata": {
"name": "my-resource"
}
}
`)
if tt.fields.skipDryRunAnnotationPresent {
2020-05-29 00:18:31 +00:00
cr.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "SkipDryRunOnMissingResource=true"})
2020-05-15 20:01:24 +00:00
}
resources := []*unstructured.Unstructured{cr}
if tt.fields.crdInSameSync {
resources = append(resources, NewCRD())
}
syncCtx.resources = groupResources(ReconciliationResult{
Live: make([]*unstructured.Unstructured, len(resources)),
Target: resources,
})
tasks, successful := syncCtx.getSyncTasks()
if successful != tt.wantSuccess {
t.Errorf("successful = %v, want: %v", successful, tt.wantSuccess)
return
}
skipDryRun := false
for _, task := range tasks {
if task.targetObj.GetKind() == cr.GetKind() {
skipDryRun = task.skipDryRun
break
}
}
if tt.wantDryRun != !skipDryRun {
t.Errorf("dryRun = %v, want: %v", !skipDryRun, tt.wantDryRun)
}
})
}
}
func TestSyncSuccessfully(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, true, false, false))
pod := NewPod()
pod.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil, pod},
Target: []*unstructured.Unstructured{NewService(), nil},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
assert.Len(t, resources, 2)
for i := range resources {
result := resources[i]
if result.ResourceKey.Kind == "Pod" {
assert.Equal(t, synccommon.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else if result.ResourceKey.Kind == "Service" {
assert.Equal(t, synccommon.ResultCodeSynced, result.Status)
assert.Equal(t, "", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncDeleteSuccessfully(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, true, false, false))
svc := NewService()
svc.SetNamespace(FakeArgoCDNamespace)
pod := NewPod()
pod.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{svc, pod},
Target: []*unstructured.Unstructured{nil, nil},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
for i := range resources {
result := resources[i]
if result.ResourceKey.Kind == "Pod" {
assert.Equal(t, synccommon.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else if result.ResourceKey.Kind == "Service" {
assert.Equal(t, synccommon.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncCreateFailure(t *testing.T) {
syncCtx := newTestSyncCtx()
testSvc := NewService()
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
testSvc.GetName(): {
Output: "",
Err: fmt.Errorf("foo"),
},
},
}
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{testSvc},
})
syncCtx.Sync()
_, _, resources := syncCtx.GetState()
assert.Len(t, resources, 1)
result := resources[0]
assert.Equal(t, synccommon.ResultCodeSyncFailed, result.Status)
assert.Equal(t, "foo", result.Message)
}
func TestSyncPruneFailure(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, true, false, false))
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
"test-service": {
Output: "",
Err: fmt.Errorf("foo"),
},
},
}
testSvc := NewService()
testSvc.SetName("test-service")
testSvc.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{testSvc},
Target: []*unstructured.Unstructured{testSvc},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationFailed, phase)
assert.Len(t, resources, 1)
result := resources[0]
assert.Equal(t, synccommon.ResultCodeSyncFailed, result.Status)
assert.Equal(t, "foo", result.Message)
}
func TestDontSyncOrPruneHooks(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, false, false, true))
targetPod := NewPod()
targetPod.SetName("dont-create-me")
2020-05-29 00:18:31 +00:00
targetPod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
2020-05-15 20:01:24 +00:00
liveSvc := NewService()
liveSvc.SetName("dont-prune-me")
liveSvc.SetNamespace(FakeArgoCDNamespace)
2020-05-29 00:18:31 +00:00
liveSvc.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
2020-05-15 20:01:24 +00:00
syncCtx.hooks = []*unstructured.Unstructured{targetPod, liveSvc}
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Len(t, resources, 0)
assert.Equal(t, synccommon.OperationSucceeded, phase)
}
// make sure that we do not prune resources with Prune=false
func TestDontPrunePruneFalse(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, true, false, false))
pod := NewPod()
2020-05-29 00:18:31 +00:00
pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Prune=false"})
2020-05-15 20:01:24 +00:00
pod.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{pod},
Target: []*unstructured.Unstructured{nil},
})
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
assert.Len(t, resources, 1)
assert.Equal(t, synccommon.ResultCodePruneSkipped, resources[0].Status)
assert.Equal(t, "ignored (no prune)", resources[0].Message)
syncCtx.Sync()
phase, _, _ = syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
}
//// make sure Validate=false means we don't validate
func TestSyncOptionValidate(t *testing.T) {
tests := []struct {
name string
annotationVal string
want bool
}{
{"Empty", "", true},
{"True", "Validate=true", true},
{"False", "Validate=false", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
2020-05-29 00:18:31 +00:00
pod.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: tt.annotationVal})
2020-05-15 20:01:24 +00:00
pod.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{pod},
Target: []*unstructured.Unstructured{pod},
})
syncCtx.Sync()
kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
assert.Equal(t, tt.want, kubectl.GetLastValidate())
2020-05-15 20:01:24 +00:00
})
}
}
func TestSelectiveSyncOnly(t *testing.T) {
pod1 := NewPod()
pod1.SetName("pod-1")
pod2 := NewPod()
pod2.SetName("pod-2")
syncCtx := newTestSyncCtx(WithResourcesFilter(func(key kube.ResourceKey, _ *unstructured.Unstructured, _ *unstructured.Unstructured) bool {
return key.Kind == pod1.GetKind() && key.Name == pod1.GetName()
}))
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod1},
})
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, "pod-1", tasks[0].name())
}
func TestUnnamedHooksGetUniqueNames(t *testing.T) {
t.Run("Truncated revision", func(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
pod.SetName("")
2020-05-29 00:18:31 +00:00
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync,PostSync"})
2020-05-15 20:01:24 +00:00
syncCtx.hooks = []*unstructured.Unstructured{pod}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 2)
assert.Contains(t, tasks[0].name(), "foobarb-presync-")
assert.Contains(t, tasks[1].name(), "foobarb-postsync-")
assert.Equal(t, "", pod.GetName())
})
t.Run("Short revision", func(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
pod.SetName("")
2020-05-29 00:18:31 +00:00
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync,PostSync"})
2020-05-15 20:01:24 +00:00
syncCtx.hooks = []*unstructured.Unstructured{pod}
syncCtx.revision = "foobar"
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 2)
assert.Contains(t, tasks[0].name(), "foobar-presync-")
assert.Contains(t, tasks[1].name(), "foobar-postsync-")
assert.Equal(t, "", pod.GetName())
})
}
func TestManagedResourceAreNotNamed(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
pod.SetName("")
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod},
})
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, "", tasks[0].name())
assert.Equal(t, "", pod.GetName())
}
func TestDeDupingTasks(t *testing.T) {
syncCtx := newTestSyncCtx(WithOperationSettings(false, true, false, false))
pod := NewPod()
2020-05-29 00:18:31 +00:00
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "Sync"})
2020-05-15 20:01:24 +00:00
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod},
})
syncCtx.hooks = []*unstructured.Unstructured{pod}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
}
func TestObjectsGetANamespace(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod},
})
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, FakeArgoCDNamespace, tasks[0].namespace())
assert.Equal(t, "", pod.GetNamespace())
}
func TestSyncFailureHookWithSuccessfulSync(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{NewPod()},
})
syncCtx.hooks = []*unstructured.Unstructured{newHook(synccommon.HookTypeSyncFail)}
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationSucceeded, phase)
// only one result, we did not run the failure failureHook
assert.Len(t, resources, 1)
}
func TestSyncFailureHookWithFailedSync(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := NewPod()
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod},
})
syncCtx.hooks = []*unstructured.Unstructured{newHook(synccommon.HookTypeSyncFail)}
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{pod.GetName(): {Err: fmt.Errorf("")}},
}
syncCtx.Sync()
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
assert.Equal(t, synccommon.OperationFailed, phase)
assert.Len(t, resources, 2)
}
func TestBeforeHookCreation(t *testing.T) {
syncCtx := newTestSyncCtx()
2020-05-29 00:18:31 +00:00
hook := Annotate(Annotate(NewPod(), synccommon.AnnotationKeyHook, "Sync"), synccommon.AnnotationKeyHookDeletePolicy, "BeforeHookCreation")
2020-05-15 20:01:24 +00:00
hook.SetNamespace(FakeArgoCDNamespace)
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{hook},
Target: []*unstructured.Unstructured{nil},
})
syncCtx.hooks = []*unstructured.Unstructured{hook}
syncCtx.dynamicIf = fake.NewSimpleDynamicClient(runtime.NewScheme())
syncCtx.Sync()
_, _, resources := syncCtx.GetState()
assert.Len(t, resources, 1)
assert.Empty(t, resources[0].Message)
}
func TestRunSyncFailHooksFailed(t *testing.T) {
// Tests that other SyncFail Hooks run even if one of them fail.
syncCtx := newTestSyncCtx()
pod := NewPod()
successfulSyncFailHook := newHook(synccommon.HookTypeSyncFail)
successfulSyncFailHook.SetName("successful-sync-fail-hook")
failedSyncFailHook := newHook(synccommon.HookTypeSyncFail)
failedSyncFailHook.SetName("failed-sync-fail-hook")
syncCtx.resources = groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{pod},
})
syncCtx.hooks = []*unstructured.Unstructured{successfulSyncFailHook, failedSyncFailHook}
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
// Fail operation
pod.GetName(): {Err: fmt.Errorf("")},
// Fail a single SyncFail hook
failedSyncFailHook.GetName(): {Err: fmt.Errorf("")}},
}
syncCtx.Sync()
syncCtx.Sync()
phase, _, resources := syncCtx.GetState()
// Operation as a whole should fail
assert.Equal(t, synccommon.OperationFailed, phase)
// failedSyncFailHook should fail
assert.Equal(t, synccommon.OperationFailed, resources[1].HookPhase)
assert.Equal(t, synccommon.ResultCodeSyncFailed, resources[1].Status)
// successfulSyncFailHook should be synced running (it is an nginx pod)
assert.Equal(t, synccommon.OperationRunning, resources[2].HookPhase)
assert.Equal(t, synccommon.ResultCodeSynced, resources[2].Status)
}
func Test_syncContext_liveObj(t *testing.T) {
type fields struct {
compareResult ReconciliationResult
}
type args struct {
obj *unstructured.Unstructured
}
obj := NewPod()
obj.SetNamespace("my-ns")
found := NewPod()
foundNoNamespace := NewPod()
foundNoNamespace.SetNamespace("")
tests := []struct {
name string
fields fields
args args
want *unstructured.Unstructured
}{
{"None", fields{compareResult: ReconciliationResult{}}, args{obj: &unstructured.Unstructured{}}, nil},
{"Found", fields{compareResult: ReconciliationResult{Target: []*unstructured.Unstructured{nil}, Live: []*unstructured.Unstructured{found}}}, args{obj: obj}, found},
{"EmptyNamespace", fields{compareResult: ReconciliationResult{Target: []*unstructured.Unstructured{nil}, Live: []*unstructured.Unstructured{foundNoNamespace}}}, args{obj: obj}, found},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &syncContext{
resources: groupResources(tt.fields.compareResult),
hooks: tt.fields.compareResult.Hooks,
}
if got := sc.liveObj(tt.args.obj); !reflect.DeepEqual(got, tt.want) {
t.Errorf("syncContext.liveObj() = %v, want %v", got, tt.want)
}
})
}
}
func Test_syncContext_hasCRDOfGroupKind(t *testing.T) {
// target
assert.False(t, (&syncContext{resources: groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{NewCRD()},
})}).hasCRDOfGroupKind("", ""))
assert.True(t, (&syncContext{resources: groupResources(ReconciliationResult{
Live: []*unstructured.Unstructured{nil},
Target: []*unstructured.Unstructured{NewCRD()},
})}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
// hook
assert.False(t, (&syncContext{hooks: []*unstructured.Unstructured{NewCRD()}}).hasCRDOfGroupKind("", ""))
assert.True(t, (&syncContext{hooks: []*unstructured.Unstructured{NewCRD()}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
}