Repository: skupperproject/skupper
Pull request #1834 (2024-12-11 23:02:13)

Add support for kube version of site delete all
When all option is selected all resources are deleted in the specified namespace.
    site, links, connectors, listeners, accessTokens, accessGrants, RouterAccess, 
    SecuredAccess, Certificates, AttachedConnectors, AttachedConnectorBindings,
If all is not configured, then just the site is deleted. 

lynnemorrison: need PR #1786  and PR #1778  merged in to finish non-kube site delete -all implementation 

Patch:
diff --git a/cmd/network-observer/internal/api/types_gen.go b/cmd/network-observer/internal/api/types_gen.go
index 0a890a81..2c0197bb 100644
--- a/cmd/network-observer/internal/api/types_gen.go
+++ b/cmd/network-observer/internal/api/types_gen.go
@@ -114,6 +114,8 @@ type ApplicationFlowRecord struct {
 	// Identity The unique identifier for the record.
 	Identity          string `json:"identity"`
 	Method            string `json:"method"`
+	Octets            uint64 `json:"octets"`
+	OctetsReverse     uint64 `json:"octetsReverse"`
 	Protocol          string `json:"protocol"`
 	RoutingKey        string `json:"routingKey"`
 	SourceProcessId   string `json:"sourceProcessId"`
diff --git a/cmd/network-observer/internal/server/views/flows.go b/cmd/network-observer/internal/server/views/flows.go
index f8ce4fef..1098b60f 100644
--- a/cmd/network-observer/internal/server/views/flows.go
+++ b/cmd/network-observer/internal/server/views/flows.go
@@ -26,6 +26,7 @@ func NewConnectionsSliceProvider(stor store.Interface) func([]store.Entry) []api
 		return results
 	}
 }
+
 func NewConnectionsProvider(stor store.Interface) func(collector.ConnectionRecord) (api.ConnectionRecord, bool) {
 	traceProvider := newTraceProvider(stor)
 	return func(conn collector.ConnectionRecord) (api.ConnectionRecord, bool) {
@@ -199,6 +200,8 @@ func NewRequestProvider(stor store.Interface) func(collector.RequestRecord) (api
 		out.StartTime, out.EndTime = vanflowTimes(record.BaseRecord)
 		setOpt(&out.Method, record.Method)
 		setOpt(&out.Status, record.Result)
+		setOpt(&out.Octets, record.Octets)
+		setOpt(&out.OctetsReverse, record.OctetsReverse)
 
 		if record.EndTime != nil && record.StartTime != nil && record.EndTime.After(record.StartTime.Time) {
 			if record.EndTime != nil && record.StartTime != nil {
diff --git a/cmd/network-observer/spec/openapi.yaml b/cmd/network-observer/spec/openapi.yaml
index 4f657497..dfb35d1b 100644
--- a/cmd/network-observer/spec/openapi.yaml
+++ b/cmd/network-observer/spec/openapi.yaml
@@ -1378,6 +1378,8 @@ components:
             - status
             - traceRouters
             - traceSites
+            - octets
+            - octetsReverse
           properties:
             connectionId:
               type: string
@@ -1412,6 +1414,12 @@ components:
             status:
               type: string
               example: 200
+            octets:
+              type: integer
+              format: uint64
+            octetsReverse:
+              type: integer
+              format: uint64
             traceRouters:
               type: array
               description: Ordered array of the names of routers involved in proxying the connection
diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go
index 5c8bcbd0..383ed2b4 100644
--- a/internal/cmd/skupper/common/flags.go
+++ b/internal/cmd/skupper/common/flags.go
@@ -82,6 +82,10 @@ for other Kubernetes flavors, loadbalancer is the default.`
 	FlagNameWait       = "wait"
 	FlagDescWait       = "Wait for the given status before exiting. Choices: configured, ready, none"
 	FlagDescDeleteWait = "Wait for deletion to complete before exiting"
+
+	FlagNameAll       = "all"
+	FlagDescAll       = "delete all skupper resources in current namespace"
+	FlagDescDeleteAll = "delete all skupper resources associated with site in current namespace"
 )
 
 type CommandSiteCreateFlags struct {
@@ -109,6 +113,7 @@ type CommandSiteUpdateFlags struct {
 }
 
 type CommandSiteDeleteFlags struct {
+	All     bool
 	Timeout time.Duration
 	Wait    bool
 }
@@ -141,6 +146,7 @@ type CommandTokenIssueFlags struct {
 	Timeout            time.Duration
 	ExpirationWindow   time.Duration
 	RedemptionsAllowed int
+	Cost               string
 }
 
 type CommandTokenRedeemFlags struct {
diff --git a/internal/cmd/skupper/site/kube/site_delete.go b/internal/cmd/skupper/site/kube/site_delete.go
index 1779161d..6d731770 100644
--- a/internal/cmd/skupper/site/kube/site_delete.go
+++ b/internal/cmd/skupper/site/kube/site_delete.go
@@ -3,10 +3,11 @@ package kube
 import (
 	"context"
 	"fmt"
+	"time"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
-	"time"
 
 	"github.com/skupperproject/skupper/internal/kube/client"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
@@ -22,6 +23,7 @@ type CmdSiteDelete struct {
 	siteName  string
 	timeout   time.Duration
 	wait      bool
+	all       bool
 }
 
 func NewCmdSiteDelete() *CmdSiteDelete {
@@ -87,11 +89,120 @@ func (cmd *CmdSiteDelete) ValidateInput(args []string) []error {
 func (cmd *CmdSiteDelete) InputToOptions() {
 	cmd.timeout = cmd.Flags.Timeout
 	cmd.wait = cmd.Flags.Wait
+	cmd.all = cmd.Flags.All
 }
 
 func (cmd *CmdSiteDelete) Run() error {
+
 	err := cmd.Client.Sites(cmd.Namespace).Delete(context.TODO(), cmd.siteName, metav1.DeleteOptions{})
-	return err
+	if err != nil {
+		return err
+	}
+
+	// if delete all, also remove all the other resources
+	if cmd.all {
+		connectors, err := cmd.Client.Connectors(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && connectors != nil && len(connectors.Items) != 0 {
+			for _, connector := range connectors.Items {
+				err = cmd.Client.Connectors(cmd.Namespace).Delete(context.TODO(), connector.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		listeners, err := cmd.Client.Listeners(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && listeners != nil && len(listeners.Items) != 0 {
+			for _, listener := range listeners.Items {
+				err = cmd.Client.Listeners(cmd.Namespace).Delete(context.TODO(), listener.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		links, err := cmd.Client.Links(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && links != nil && len(links.Items) != 0 {
+			for _, link := range links.Items {
+				err = cmd.Client.Links(cmd.Namespace).Delete(context.TODO(), link.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		accessTokens, err := cmd.Client.AccessTokens(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && accessTokens != nil && len(accessTokens.Items) != 0 {
+			for _, accessToken := range accessTokens.Items {
+				err = cmd.Client.AccessTokens(cmd.Namespace).Delete(context.TODO(), accessToken.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		accessGrants, err := cmd.Client.AccessGrants(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && accessGrants != nil && len(accessGrants.Items) != 0 {
+			for _, accessGrant := range accessGrants.Items {
+				err = cmd.Client.AccessGrants(cmd.Namespace).Delete(context.TODO(), accessGrant.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		routerAccesses, err := cmd.Client.RouterAccesses(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && routerAccesses != nil && len(routerAccesses.Items) != 0 {
+			for _, routerAccess := range routerAccesses.Items {
+				err = cmd.Client.RouterAccesses(cmd.Namespace).Delete(context.TODO(), routerAccess.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		securedAccesses, err := cmd.Client.SecuredAccesses(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && securedAccesses != nil && len(securedAccesses.Items) != 0 {
+			for _, securedAccess := range securedAccesses.Items {
+				err = cmd.Client.SecuredAccesses(cmd.Namespace).Delete(context.TODO(), securedAccess.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		certificates, err := cmd.Client.Certificates(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && certificates != nil && len(certificates.Items) != 0 {
+			for _, certificate := range certificates.Items {
+				err = cmd.Client.Certificates(cmd.Namespace).Delete(context.TODO(), certificate.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		attachedConnectors, err := cmd.Client.AttachedConnectors(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && attachedConnectors != nil && len(attachedConnectors.Items) != 0 {
+			for _, attachedConnector := range attachedConnectors.Items {
+				err = cmd.Client.AttachedConnectors(cmd.Namespace).Delete(context.TODO(), attachedConnector.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+
+		attachedConnectorBindings, err := cmd.Client.AttachedConnectorBindings(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
+		if err == nil && attachedConnectorBindings != nil && len(attachedConnectorBindings.Items) != 0 {
+			for _, attachedConnectorBinding := range attachedConnectorBindings.Items {
+				err = cmd.Client.AttachedConnectorBindings(cmd.Namespace).Delete(context.TODO(), attachedConnectorBinding.Name, metav1.DeleteOptions{})
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
 }
 func (cmd *CmdSiteDelete) WaitUntil() error {
 
diff --git a/internal/cmd/skupper/site/site.go b/internal/cmd/skupper/site/site.go
index 8b5c2549..5c565219 100644
--- a/internal/cmd/skupper/site/site.go
+++ b/internal/cmd/skupper/site/site.go
@@ -148,6 +148,7 @@ skupper site delete --wait=false`,
 	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSiteDeleteDesc, kubeCommand, nonKubeCommand)
 	cmdFlags := common.CommandSiteDeleteFlags{}
 
+	cmd.Flags().BoolVar(&cmdFlags.All, common.FlagNameAll, false, common.FlagDescDeleteAll)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().BoolVar(&cmdFlags.Wait, common.FlagNameWait, true, common.FlagDescDeleteWait)
 
diff --git a/internal/cmd/skupper/token/kube/token_issue.go b/internal/cmd/skupper/token/kube/token_issue.go
index d6a0873e..9283cfa1 100644
--- a/internal/cmd/skupper/token/kube/token_issue.go
+++ b/internal/cmd/skupper/token/kube/token_issue.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strconv"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
@@ -25,6 +26,7 @@ type CmdTokenIssue struct {
 	namespace string
 	grantName string
 	fileName  string
+	cost      int
 }
 
 func NewCmdTokenIssue() *CmdTokenIssue {
@@ -47,6 +49,7 @@ func (cmd *CmdTokenIssue) ValidateInput(args []string) []error {
 	tokenStringValidator := validator.NewFilePathStringValidator()
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
 	expirationValidator := validator.NewExpirationInSecondsValidator()
+	numberValidator := validator.NewNumberValidator()
 
 	// Validate token file name
 	if len(args) < 1 {
@@ -114,6 +117,17 @@ func (cmd *CmdTokenIssue) ValidateInput(args []string) []error {
 		}
 	}
 
+	selectedCost, err := strconv.Atoi(cmd.Flags.Cost)
+	if err != nil {
+		validationErrors = append(validationErrors, fmt.Errorf("link cost is not valid: %s", err))
+	}
+	ok, err := numberValidator.Evaluate(selectedCost)
+	if !ok {
+		validationErrors = append(validationErrors, fmt.Errorf("link cost is not valid: %s", err))
+	} else {
+		cmd.cost = selectedCost
+	}
+
 	return validationErrors
 }
 
@@ -157,9 +171,10 @@ func (cmd *CmdTokenIssue) WaitUntil() error {
 					Name: accessGrant.Name,
 				},
 				Spec: v2alpha1.AccessTokenSpec{
-					Url:  accessGrant.Status.Url,
-					Code: accessGrant.Status.Code,
-					Ca:   accessGrant.Status.Ca,
+					Url:      accessGrant.Status.Url,
+					Code:     accessGrant.Status.Code,
+					Ca:       accessGrant.Status.Ca,
+					LinkCost: cmd.cost,
 				},
 			}
 
diff --git a/internal/cmd/skupper/token/token.go b/internal/cmd/skupper/token/token.go
index 274c3dc3..35956b2c 100644
--- a/internal/cmd/skupper/token/token.go
+++ b/internal/cmd/skupper/token/token.go
@@ -49,6 +49,7 @@ func CmdTokenIssueFactory(configuredPlatform types.Platform) *cobra.Command {
 	cmd.Flags().DurationVarP(&cmdFlags.ExpirationWindow, common.FlagNameExpirationWindow, "e", 15*time.Minute, common.FlagDescExpirationWindow)
 	cmd.Flags().DurationVarP(&cmdFlags.Timeout, common.FlagNameTimeout, "t", 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVar(&cmdFlags.Name, common.FlagNameToken, "", common.FlagDescToken)
+	cmd.Flags().StringVar(&cmdFlags.Cost, common.FlagNameCost, "1", common.FlagDescCost)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
diff --git a/pkg/vanflow/records.go b/pkg/vanflow/records.go
index 3fc56696..e0f33188 100644
--- a/pkg/vanflow/records.go
+++ b/pkg/vanflow/records.go
@@ -292,11 +292,13 @@ func (r TransportBiflowRecord) GetTypeMeta() TypeMeta {
 
 type AppBiflowRecord struct {
 	BaseRecord
-	Parent   *string `vflow:"2"`
-	Protocol *string `vflow:"16"`
-	Latency  *uint64 `vflow:"24"`
-	Method   *string `vflow:"27"`
-	Result   *string `vflow:"28"`
+	Parent        *string `vflow:"2"`
+	Protocol      *string `vflow:"16"`
+	Latency       *uint64 `vflow:"24"`
+	Method        *string `vflow:"27"`
+	Result        *string `vflow:"28"`
+	Octets        *uint64 `vflow:"23"`
+	OctetsReverse *uint64 `vflow:"58"`
 }
 
 func (r AppBiflowRecord) GetTypeMeta() TypeMeta {


Test patch:
diff --git a/internal/cmd/skupper/site/kube/site_delete_test.go b/internal/cmd/skupper/site/kube/site_delete_test.go
index 6006e550..6b87e055 100644
--- a/internal/cmd/skupper/site/kube/site_delete_test.go
+++ b/internal/cmd/skupper/site/kube/site_delete_test.go
@@ -1,11 +1,12 @@
 package kube
 
 import (
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
-	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"testing"
 	"time"
 
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
@@ -251,6 +252,7 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 		flags           *common.CommandSiteDeleteFlags
 		expectedTimeout time.Duration
 		expectedWait    bool
+		expectedAll     bool
 	}
 
 	testTable := []test{
@@ -264,6 +266,11 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 			flags:        &common.CommandSiteDeleteFlags{Wait: true},
 			expectedWait: true,
 		},
+		{
+			name:        "all site resources",
+			flags:       &common.CommandSiteDeleteFlags{All: true},
+			expectedAll: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -276,6 +283,7 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 
 			assert.Check(t, command.timeout == test.expectedTimeout)
 			assert.Check(t, command.wait == test.expectedWait)
+			assert.Check(t, command.all == test.expectedAll)
 		})
 	}
 }
@@ -288,6 +296,7 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 		skupperError   string
 		siteName       string
 		errorMessage   string
+		all            bool
 	}
 
 	testTable := []test{
@@ -327,6 +336,101 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 			siteName:       "my-site",
 			errorMessage:   "sites.skupper.io \"my-site\" not found",
 		},
+		{
+			name:       "deletes all",
+			all:        true,
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-site",
+						Namespace: "test",
+					},
+					Status: v2alpha1.SiteStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+				&v2alpha1.AccessGrant{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-token",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AccessToken{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-token",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Site{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "the-site",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Certificate{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link-test",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-link",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AttachedConnectorBinding{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-attachedConnectorBinding",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.AttachedConnector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-attachedConnector",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.RouterAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-routerAccess",
+						Namespace: "test",
+					},
+				},
+				&v2alpha1.SecuredAccess{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-securedAccess",
+						Namespace: "test",
+					},
+				},
+			},
+			skupperError: "",
+			siteName:     "my-site",
+		},
 	}
 
 	for _, test := range testTable {
@@ -337,7 +441,7 @@ func TestCmdSiteDelete_Run(t *testing.T) {
 		fakeSkupperClient, err := fakeclient.NewFakeClient(command.Namespace, test.k8sObjects, test.skupperObjects, test.skupperError)
 		assert.Assert(t, err)
 		command.Client = fakeSkupperClient.GetSkupperClient().SkupperV2alpha1()
-
+		command.all = test.all
 		command.siteName = test.siteName
 
 		t.Run(test.name, func(t *testing.T) {
diff --git a/internal/cmd/skupper/site/site_test.go b/internal/cmd/skupper/site/site_test.go
index 53339e1a..9cbe32d2 100644
--- a/internal/cmd/skupper/site/site_test.go
+++ b/internal/cmd/skupper/site/site_test.go
@@ -1,12 +1,13 @@
 package site
 
 import (
+	"testing"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"gotest.tools/v3/assert"
-	"testing"
 )
 
 func TestCmdSiteFactory(t *testing.T) {
@@ -51,6 +52,7 @@ func TestCmdSiteFactory(t *testing.T) {
 			expectedFlagsWithDefaultValue: map[string]interface{}{
 				common.FlagNameTimeout: "1m0s",
 				common.FlagNameWait:    "true",
+				common.FlagNameAll:     "false",
 			},
 			command: CmdSiteDeleteFactory(types.PlatformKubernetes),
 		},
diff --git a/internal/cmd/skupper/token/kube/token_issue_test.go b/internal/cmd/skupper/token/kube/token_issue_test.go
index f9a538b2..0502d760 100644
--- a/internal/cmd/skupper/token/kube/token_issue_test.go
+++ b/internal/cmd/skupper/token/kube/token_issue_test.go
@@ -34,6 +34,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
 				Name:               "my-token",
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -94,6 +95,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.AccessGrant{
@@ -122,6 +124,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -153,6 +156,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -193,6 +197,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -233,6 +238,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -274,6 +280,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
 				Name:               "my new token",
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -314,6 +321,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -354,6 +362,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 0,
 				Timeout:            60 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -395,6 +404,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   10 * time.Second,
 				RedemptionsAllowed: 1,
 				Timeout:            10 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -435,6 +445,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            0 * time.Second,
+				Cost:               "1",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -468,6 +479,48 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 			},
 			expectedErrors: []string{"timeout is not valid: duration must not be less than 10s; got 0s"},
 		},
+		{
+			name: "cost is not valid",
+			args: []string{"~/token.yaml"},
+			flags: common.CommandTokenIssueFlags{
+				ExpirationWindow:   15 * time.Minute,
+				RedemptionsAllowed: 1,
+				Timeout:            60 * time.Second,
+				Cost:               "Not-valid",
+			},
+			skupperObjects: []runtime.Object{
+				&v2alpha1.SiteList{
+					Items: []v2alpha1.Site{
+						{
+							ObjectMeta: v1.ObjectMeta{
+								Name:      "site1",
+								Namespace: "test",
+							},
+							Status: v2alpha1.SiteStatus{
+								Status: v2alpha1.Status{
+									Conditions: []v1.Condition{
+										{
+											Type:   "Configured",
+											Status: "True",
+										},
+										{
+											Type:   "Running",
+											Status: "True",
+										},
+										{
+											Type:   "Ready",
+											Status: "True",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				`link cost is not valid: strconv.Atoi: parsing "Not-valid": invalid syntax`},
+		},
 		{
 			name: "flags all valid",
 			args: []string{"~/token.yaml"},
@@ -475,6 +528,7 @@ func TestCmdTokenIssue_ValidateInput(t *testing.T) {
 				ExpirationWindow:   15 * time.Minute,
 				RedemptionsAllowed: 1,
 				Timeout:            60 * time.Second,
+				Cost:               "5",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.SiteList{
@@ -569,6 +623,7 @@ func TestCmdTokenIssue_Run(t *testing.T) {
 				ExpirationWindow:   20 * time.Minute,
 				RedemptionsAllowed: 2,
 				Timeout:            60 * time.Second,
+				Cost:               "5",
 			},
 			skupperObjects: []runtime.Object{
 				&v2alpha1.AccessGrant{
diff --git a/internal/cmd/skupper/token/token_test.go b/internal/cmd/skupper/token/token_test.go
index c3608f6c..8adbd880 100644
--- a/internal/cmd/skupper/token/token_test.go
+++ b/internal/cmd/skupper/token/token_test.go
@@ -27,6 +27,7 @@ func TestCmdTokenFactory(t *testing.T) {
 				common.FlagNameExpirationWindow:   "15m0s",
 				common.FlagNameRedemptionsAllowed: "1",
 				common.FlagNameToken:              "",
+				common.FlagNameCost:               "1",
 			},
 			command: CmdTokenIssueFactory(types.PlatformKubernetes),
 		},