From 5fa645f716b3009ce8f430a4517c98c0ab522c98 Mon Sep 17 00:00:00 2001 From: Oisin Johnston Date: Thu, 6 Aug 2026 16:23:45 +0100 Subject: [PATCH] added inventory check in live destroy/status Signed-off-by: Oisin Johnston --- commands/live/destroy/cmddestroy.go | 8 +++ commands/live/status/cmdstatus.go | 8 +++ pkg/live/inventory-client-factory.go | 31 ++++++++++ pkg/live/inventory-client-factory_test.go | 73 +++++++++++++++++++++++ 4 files changed, 120 insertions(+) diff --git a/commands/live/destroy/cmddestroy.go b/commands/live/destroy/cmddestroy.go index b9c2ecb20e..3f841a3b06 100644 --- a/commands/live/destroy/cmddestroy.go +++ b/commands/live/destroy/cmddestroy.go @@ -149,6 +149,14 @@ func (r *Runner) runE(c *cobra.Command, args []string) error { return err } + invClient, err := inventory.NewClient(r.factory, live.WrapInventoryObj, live.InvToUnstructuredFunc, inventory.StatusPolicyNone, live.ResourceGroupGVK) + if err != nil { + return err + } + if err := live.VerifyInventoryIDMatch(invClient, invInfo); err != nil { + return err + } + dryRunStrategy := common.DryRunNone if r.dryRun { dryRunStrategy = common.DryRunClient diff --git a/commands/live/status/cmdstatus.go b/commands/live/status/cmdstatus.go index ecb61bc4a6..c14bd702ed 100644 --- a/commands/live/status/cmdstatus.go +++ b/commands/live/status/cmdstatus.go @@ -90,5 +90,13 @@ func (rir *RGInventoryLoader) GetInvInfo(cmd *cobra.Command, args []string) (inv return nil, err } + invClient, err := inventory.NewClient(rir.factory, live.WrapInventoryObj, live.InvToUnstructuredFunc, inventory.StatusPolicyNone, live.ResourceGroupGVK) + if err != nil { + return nil, err + } + if err := live.VerifyInventoryIDMatch(invClient, invInfo); err != nil { + return nil, err + } + return invInfo, nil } diff --git a/pkg/live/inventory-client-factory.go b/pkg/live/inventory-client-factory.go index eaf01f9cc0..dcf890e08b 100644 --- a/pkg/live/inventory-client-factory.go +++ b/pkg/live/inventory-client-factory.go @@ -16,8 +16,11 @@ package live import ( "context" + "fmt" + "k8s.io/apimachinery/pkg/api/meta" cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/inventory" ) @@ -65,3 +68,31 @@ func (ccf *ClusterClientFactory) NewClient(factory cmdutil.Factory) (inventory.C } return inventory.NewClient(factory, WrapInventoryObjWithContext(ctx), InvToUnstructuredFunc, ccf.StatusPolicy, ResourceGroupGVK) } + +// VerifyInventoryIDMatch checks that the inventory-id on the cluster matches +// the locally provided id, preventing operations against the wrong inventory. +func VerifyInventoryIDMatch(invClient inventory.Client, invInfo inventory.Info) error { + if invInfo.Strategy() != inventory.NameStrategy || invInfo.ID() == "" { + return nil + } + + prevInvObjs, err := invClient.GetClusterInventoryObjs(invInfo) + if err != nil { + // If the ResourceGroup CRD isn't installed, there's no inventory + // object to mismatch against. + if meta.IsNoMatchError(err) { + return nil + } + return err + } + if len(prevInvObjs) > 1 { + return fmt.Errorf("found %d inventory objects with Name strategy", len(prevInvObjs)) + } + if len(prevInvObjs) == 1 { + val := prevInvObjs[0].GetLabels()[common.InventoryLabel] + if val != invInfo.ID() { + return fmt.Errorf("inventory-id of inventory object in cluster doesn't match provided id %q", invInfo.ID()) + } + } + return nil +} diff --git a/pkg/live/inventory-client-factory_test.go b/pkg/live/inventory-client-factory_test.go index c4c4346cfc..0b13576ee8 100644 --- a/pkg/live/inventory-client-factory_test.go +++ b/pkg/live/inventory-client-factory_test.go @@ -20,6 +20,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/object" ) // TestNewClusterClientFactoryWithContext_NilIsNormalized verifies nil ctx is @@ -58,3 +62,72 @@ func TestNewClusterClientFactory_StructLiteralPathTolerated(t *testing.T) { // the nil Ctx is normalized to Background(). That path is exercised // in the existing apply/destroy tests via the CLI integration tests. } + +// fakeInvClient embeds FakeClient and overrides GetClusterInventoryObjs +// to return configurable inventory objects. +type fakeInvClient struct { + inventory.FakeClient + objs object.UnstructuredSet +} + +func (f *fakeInvClient) GetClusterInventoryObjs(_ inventory.Info) (object.UnstructuredSet, error) { + return f.objs, f.Err +} + +func invObj(id string) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "kpt.dev/v1alpha1", + "kind": "ResourceGroup", + "metadata": map[string]any{ + "name": "test-inv", + "namespace": "test-ns", + "labels": map[string]any{ + common.InventoryLabel: id, + }, + }, + }, + } +} + +func TestVerifyInventoryIDMatch(t *testing.T) { + testCases := map[string]struct { + invInfo inventory.Info + client *fakeInvClient + expectErr string + }{ + "empty ID skips verification": { + invInfo: WrapInventoryInfoObj(invObj("")), + client: &fakeInvClient{}, + }, + "no inventory on cluster passes": { + invInfo: WrapInventoryInfoObj(invObj("my-id")), + client: &fakeInvClient{}, + }, + "matching inventory-id passes": { + invInfo: WrapInventoryInfoObj(invObj("my-id")), + client: &fakeInvClient{ + objs: object.UnstructuredSet{invObj("my-id")}, + }, + }, + "mismatched inventory-id returns error": { + invInfo: WrapInventoryInfoObj(invObj("local-id")), + client: &fakeInvClient{ + objs: object.UnstructuredSet{invObj("cluster-id")}, + }, + expectErr: `inventory-id of inventory object in cluster doesn't match provided id "local-id"`, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + err := VerifyInventoryIDMatch(tc.client, tc.invInfo) + if tc.expectErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectErr) + } else { + assert.NoError(t, err) + } + }) + } +}