Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions commands/live/destroy/cmddestroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions commands/live/status/cmdstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 31 additions & 0 deletions pkg/live/inventory-client-factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
73 changes: 73 additions & 0 deletions pkg/live/inventory-client-factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
})
}
}