package terminalctl import ( "context" "os" "sync" "testing" "time" "github.com/fluxcd/pkg/ssa" "github.com/google/uuid" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "edge-infra.dev/pkg/k8s/unstructured" "edge-infra.dev/pkg/lib/fog" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" "edge-infra.dev/pkg/sds/k8s/controllers/terminalctl/pkg/plugins" "edge-infra.dev/test/f2" "edge-infra.dev/test/f2/x/ktest" ) var plugin *fakePlugin var f f2.Framework func TestMain(m *testing.M) { ctrl.SetLogger(fog.New()) f = f2.New(context.Background(), f2.WithExtensions( ktest.New( ktest.WithCtrlManager(createMgr), ), ), ). Setup(func(ctx f2.Context) (f2.Context, error) { k, err := ktest.FromContext(ctx) if err != nil { return ctx, err } // Override timeouts if we aren't using a live cluster if !*k.Env.UseExistingCluster { k.Timeout = 5 * time.Second k.Tick = 10 * time.Millisecond } plugin = &fakePlugin{} plugins.Register(plugin) if err := registerController(k.Manager, &Config{}); err != nil { return ctx, err } return ctx, nil }) os.Exit(f.Run(m)) } type fakePlugin struct { plugins.TerminalRegistrationPlugin lock sync.Mutex invDisabled bool } func (fp *fakePlugin) Finalize(_ context.Context, _ client.Client, _ *v1ien.IENode) error { return nil } func (fp *fakePlugin) Reconcile(ctx context.Context, mgr *ssa.ResourceManager, terminal *v1ien.IENode) ([]*unstructured.Unstructured, error) { if terminal.Name == "a-terminal-inventory-creation" { return fp.inventoryCreationReconcile(ctx, mgr, terminal) } return nil, nil } func (fp *fakePlugin) inventoryCreationReconcile(_ context.Context, _ *ssa.ResourceManager, _ *v1ien.IENode) ([]*unstructured.Unstructured, error) { fp.lock.Lock() defer fp.lock.Unlock() if fp.invDisabled { return nil, nil } return unstructured.ToUnstructuredArray(createInventoryCM()) } func TestTerminalctl_Finalizers(t *testing.T) { var ( terminal *v1ien.IENode ) feature := f2.NewFeature("Finalizers"). Setup("Create terminal Resource", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) terminal = createTerminal("a-terminal-finalizers") assert.NilError(t, k.Client.Create(ctx, terminal)) return ctx }). Test("Finalizer added", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) k.WaitOn(t, k.Check(terminal, terminalFinalizer)) return ctx }). Test("Terminal deletion", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) assert.NilError(t, k.Client.Delete(ctx, terminal)) te := v1ien.IENode{ObjectMeta: metav1.ObjectMeta{Name: terminal.Name}} k.WaitOn(t, k.ObjDeleted(&te)) return ctx }). Feature() f.Test(t, feature) } func TestTerminalctlInventory(t *testing.T) { var ( t1 *v1ien.IENode ) createInvFeature := f2.NewFeature("Terminalctl Inventory Creation"). Setup("Create terminal Resource", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) t1 = createTerminal("a-terminal-inventory-creation") assert.NilError(t, k.Client.Create(ctx, t1)) return ctx }). Test("Plugin Executed", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) k.WaitOn(t, k.ObjExists(createInventoryCM())) return ctx }). Test("Inventory added to terminal", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) te := &v1ien.IENode{ObjectMeta: metav1.ObjectMeta{Name: t1.Name}} k.WaitOn(t, k.Check(te, func(o client.Object) cmp.Result { te := o.(*v1ien.IENode) if te.Status.Inventory == nil { return cmp.ResultFailure("nil inventory") } entriesLength := len(te.Status.Inventory.Entries) == 1 // "___" entryID := te.Status.Inventory.Entries[0].ID == "default_inventory-cm__ConfigMap" if entriesLength && entryID { return cmp.ResultSuccess } return cmp.ResultFailure("inventory didnt match expected") })) return ctx }). Test("Old inventory removed", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) // Stop returning inventory to check resources are cleaned up toggleInvPlugin(true) // Update terminal to force reconcilliation updateTerminalDefinition(ctx, t, k.Client, "a-terminal-inventory-creation") k.WaitOn(t, k.Check(t1, func(o client.Object) cmp.Result { te := o.(*v1ien.IENode) if len(te.Status.Inventory.Entries) == 0 { return cmp.ResultSuccess } return cmp.ResultFailure("inventory wasn't cleaned up") })) k.WaitOn(t, k.ObjDeleted(createInventoryCM())) return ctx }). Test("Inventory Recreated", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) toggleInvPlugin(false) // Update terminal to force reconcilliation updateTerminalDefinition(ctx, t, k.Client, "a-terminal-inventory-creation") k.WaitOn(t, k.Check(t1, func(o client.Object) cmp.Result { te := o.(*v1ien.IENode) if te.Status.Inventory == nil { return cmp.ResultFailure("nil inventory") } if len(te.Status.Inventory.Entries) == 1 { // "___" entryID := te.Status.Inventory.Entries[0].ID == "default_inventory-cm__ConfigMap" if entryID { return cmp.ResultSuccess } } return cmp.ResultFailure("inventory didnt match expected") })) k.WaitOn(t, k.ObjExists(createInventoryCM())) return ctx }). Test("Inventory removed on deletion", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) assert.NilError(t, k.Client.Delete(ctx, t1)) k.WaitOn(t, k.ObjsDeleted([]client.Object{t1, createInventoryCM()})) return ctx }). Feature() f.Test(t, createInvFeature) } func createTerminal(name string) *v1ien.IENode { return &v1ien.IENode{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, TypeMeta: metav1.TypeMeta{ Kind: v1ien.IENodeGVK.Kind, APIVersion: v1ien.IENodeGVK.GroupVersion().String(), }, Spec: v1ien.IENodeSpec{ ClusterEdgeID: "clusterEdgeID-1", Role: "worker", Network: []v1ien.Network{ { DHCP4: true, DHCP6: false, MacAddress: "ee:c5:39:b6:47:c3", }, }, NetworkServices: v1ien.NetworkServices{ DNSServers: []string{"8.8.8.8", "8.8.4.4"}, KubeVip: "10.10.10.10", }, PrimaryInterface: &v1ien.PrimaryInterface{ InterfaceID: "b212995a-1af4-4a2a-8479-6b095d788741", MacAddresses: []string{"ee:c5:39:b6:47:c3"}, }, }, } } func createInventoryCM() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "inventory-cm", Namespace: "default", }, Data: map[string]string{}, } } func terminalFinalizer(o client.Object) cmp.Result { if controllerutil.ContainsFinalizer(o, v1ien.Finalizer) { return cmp.ResultSuccess } return cmp.ResultFailure("finalizer not present") } func toggleInvPlugin(disabled bool) { plugin.lock.Lock() plugin.invDisabled = disabled plugin.lock.Unlock() } // helper function to update a terminal to force reconciliation func updateTerminalDefinition(ctx context.Context, t *testing.T, c client.Client, name string) { t.Helper() te := createTerminal(name) assert.NilError(t, c.Get(ctx, client.ObjectKeyFromObject(te), te)) te.Spec.ClusterEdgeID = uuid.NewString() assert.NilError(t, c.Update(ctx, te)) }