Repository: skupperproject/skupper
Pull request #1789 (2024-11-18 11:51:23)

Adapt the Bootstrap feature to the CLI
New commands:
- skupper system ~start~ setup
- skupper system reload (equivalent to skupper system ~start~ setup -f)
- skupper system stop
- skupper system teardown ~(replaces remove.sh script)~

The bootstrap logic was moved to `pkg/nonkube/bootstrap`. I'm open to move those files to new locations or any kind of renaming that makes more sense.

Other updates:
- Removed: ~remove.sh and~ bootstrap.go
- Updated README and bootstrap.sh
- Updated Makefile and config.yaml to replace bootstrap image by ~skupper-~ cli image



Patch:
diff --git a/Dockerfile.bootstrap b/Dockerfile.cli
similarity index 59%
rename from Dockerfile.bootstrap
rename to Dockerfile.cli
index 0887295f..a3b44668 100644
--- a/Dockerfile.bootstrap
+++ b/Dockerfile.cli
@@ -9,12 +9,12 @@ RUN go mod download
 COPY . .
 
 ENV CGO_ENABLED=0
-RUN make build-bootstrap GOARCH=$TARGETARCH
+RUN make build-cli GOARCH=$TARGETARCH
 
 FROM --platform=$TARGETPLATFORM registry.access.redhat.com/ubi9-minimal
 LABEL \
-  org.opencontainers.image.title="Skupper Bootstrap" \
-  org.opencontainers.image.description="Produces and manges static configurations for non-kube installations of Skupper"
+  org.opencontainers.image.title="Skupper CLI" \
+  org.opencontainers.image.description="Provide users with the ability to operate their Skupper resources."
 
 # Create user and group and switch to user's context
 RUN microdnf -y install shadow-utils \
@@ -23,5 +23,5 @@ RUN useradd --uid 10000 runner
 USER 10000
 
 WORKDIR /app
-COPY --from=builder /go/src/app/bootstrap .
-CMD ["/app/bootstrap"]
+COPY --from=builder /go/src/app/skupper .
+ENTRYPOINT ["/app/skupper"]
diff --git a/Makefile b/Makefile
index 0646041d..10b8e89b 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ GOARCH ?= amd64
 REGISTRY := quay.io/skupper
 IMAGE_TAG := v2-latest
 PLATFORMS ?= linux/amd64,linux/arm64
-CONTAINERFILES := Dockerfile.bootstrap Dockerfile.config-sync Dockerfile.controller Dockerfile.network-observer
+CONTAINERFILES := Dockerfile.cli Dockerfile.config-sync Dockerfile.controller Dockerfile.network-observer
 SHARED_IMAGE_LABELS = \
     --label "org.opencontainers.image.created=$(shell TZ=GMT date --iso-8601=seconds)" \
 	--label "org.opencontainers.image.url=https://skupper.io/" \
@@ -25,14 +25,11 @@ DOCKER := docker
 SKOPEO := skopeo
 PODMAN := podman
 
-all: build-cmd build-config-sync build-controller build-bootstrap build-network-observer update-helm-crd
+all: build-cli build-config-sync build-controller build-network-observer update-helm-crd
 
-build-cmd:
+build-cli:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o skupper ./cmd/skupper
 
-build-bootstrap:
-	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o bootstrap ./cmd/bootstrap
-
 build-controller:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o controller ./cmd/controller
 
@@ -124,5 +121,5 @@ update-helm-crd:
 
 clean:
 	rm -rf skupper controller config-sync \
-		bootstrap network-observer generate-doc \
+		network-observer generate-doc \
 		cover.out oci-archives
diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md
index 9c300249..3c242b57 100644
--- a/cmd/bootstrap/README.md
+++ b/cmd/bootstrap/README.md
@@ -1,8 +1,7 @@
 # Bootstrapping non-kubernetes sites
 
 In this current phase of the Skupper V2 implementation, non-kubernetes sites
-can be bootstrapped using a locally built binary, that can be produced by running
-`make build-bootstrap`, using the quay.io/skupper/bootstrap:v2-latest
+can be bootstrapped using the Skupper CLI, using the quay.io/skupper/skupper-cli:v2-latest
 container image by calling `./cmd/bootstrap/bootstrap.sh` with the appropriate
 flags.
 
@@ -120,9 +119,9 @@ Now that you have all your CRs placed on a local directory, just run:
 #### Bootstrapping
 
 ```shell
-# To bootstrap your site using the binary
+# To bootstrap your site using Skupper
 export SKUPPER_PLATFORM=podman
-./bootstrap -p ./
+skupper system setup --path ./
 ```
 
 You can also use `-n=<name>` to override the namespace specified in the CRs.
@@ -179,12 +178,16 @@ The bundle installation script accepts the following flags:
 
 #### Removing
 
-To remove your site, you can run a local script, that takes the namespace
+To remove your site, you can run  the `system teardown` command, providing a namespace as a flag.
+If the namespace is omitted, the "default" namespace is used. 
+
+```shell
+skupper system teardown -n [namespace]
+```
+Or you can run a local script, that takes the namespace
 as an argument. The platform is identified by the script so you don't need
 to export it.
 
-If the namespace is omitted, the "default" namespace is used. 
-
 ```shell
 ./cmd/bootstrap/remove.sh [namespace]
 ```
@@ -299,6 +302,11 @@ Considering all your CRs have been saved to a directory named `west`, use:
 ```shell
 ./bootstrap -p ./west -n west
 ```
+or 
+
+```shell
+skupper system setup --path ./west -n west
+```
 
 The CRs are defined without a namespace, that is why we have the `-n west` flag,
 to indicate we want this site to run under the `west` namespace.
@@ -373,6 +381,12 @@ Considering all your CRs have been saved to a directory named `east`, use:
 ./bootstrap -p ./east -n east
 ```
 
+or 
+
+```shell
+skupper system setup --path ./east -n east
+```
+
 ### Testing the scenario
 
 Once both sites have been initialized, open **http://127.0.0.1:7070**
@@ -389,17 +403,30 @@ To re-initialize the west site, run:
 bootstrap -n west -f
 ```
 
+or 
+
+```shell
+system setup -n west -f
+```
+
+or 
+
+```shell
+system reload -n west 
+```
+
 The command above will reprocess all source CRs from the namespace path and
 restart the related components. The Certificate Authorities (CAs) are preserved,
 therefore eventual incoming links must still work.
 
+
 ### Cleanup
 
 To remove both namespaces, run:
 
 ```shell
-./cmd/bootstrap/remove.sh west
-./cmd/bootstrap/remove.sh east
+skupper system teardown -n west
+skupper system teardown -n east
 ```
 
 ### Producing site bundles
@@ -416,6 +443,16 @@ Installation bundle available at: /home/user/.local/share/skupper/bundles/skuppe
 Default namespace: default
 Default platform: podman
 ```
+or 
+
+```shell
+skupper system setup --path ./west  -b bundle                                       
+2024/11/18 12:17:56 updating listener /backend...
+Site "west" has been created (as a distributable bundle)
+Installation bundle available at: {HOME}/.local/share/skupper/bundles/skupper-install-west.sh
+Default namespace: default
+Default platform: podman
+```
 
 #### Extracting the static links
 
diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go
deleted file mode 100644
index 14cedf44..00000000
--- a/cmd/bootstrap/bootstrap.go
+++ /dev/null
@@ -1,295 +0,0 @@
-package main
-
-import (
-	"flag"
-	"fmt"
-	"os"
-	"os/exec"
-	"path"
-	"path/filepath"
-
-	"github.com/skupperproject/skupper/api/types"
-	"github.com/skupperproject/skupper/internal/config"
-	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
-	internalutils "github.com/skupperproject/skupper/internal/utils"
-	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"github.com/skupperproject/skupper/pkg/nonkube/bundle"
-	"github.com/skupperproject/skupper/pkg/nonkube/common"
-	"github.com/skupperproject/skupper/pkg/nonkube/compat"
-	"github.com/skupperproject/skupper/pkg/nonkube/systemd"
-	"github.com/skupperproject/skupper/pkg/utils"
-	"github.com/skupperproject/skupper/pkg/version"
-)
-
-var (
-	platform       = config.GetPlatform()
-	inputPath      string
-	userNamespace  string
-	bundleStrategy string
-)
-
-const (
-	description = `
-Bootstraps a nonkube Skupper site base on the provided flags.
-
-When the path (-p) flag is provided, it will be used as the source
-directory containing the Skupper custom resources to be processed,
-generating a local Skupper site using the "default" namespace, unless
-a namespace is set in the custom resources, or if the namespace (-n)
-flag is provided.
-
-A namespace is just a directory in the file system where all site specific
-files are stored, like certificates, configurations, the original sources
-(original custom resources used to bootstrap the nonkube site) and
-the runtime files generated during initialization.
-
-Namespaces are stored under ${XDG_DATA_HOME}/skupper/namespaces
-for regular users when XDG_DATA_HOME environment variable is set, or under
-${HOME}/.local/share/skupper/namespaces when it is not set.
-As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
-
-In case the path (-p) flag is omitted, Skupper will try to process
-custom resources stored at the input/resources directory of the default namespace,
-or from the namespace provided through the namespace (-n) flag.
-
-If the respective namespace already exists and you want to bootstrap it
-over, you must provide the force (-f) flag. When you do that, the existing
-Certificate Authorities (CAs) are preserved, so eventual existing incoming
-links should be able to reconnect.
-
-To produce a bundle, instead of rendering a site, the bundle strategy (-b)
-flag must be set to "bundle" or "tarball".
-`
-)
-
-func main() {
-	// if -version used, report and exit
-	flag.Usage = func() {
-		fmt.Println("Skupper bootstrap")
-		fmt.Printf("%s\n", description)
-		fmt.Printf("Usage:\n  %s [options...]\n\n", os.Args[0])
-		fmt.Printf("Flags:\n")
-		flag.PrintDefaults()
-	}
-	flag.StringVar(&inputPath, "p", "", "Custom resources location on the file system")
-	flag.StringVar(&userNamespace, "n", "", "The target namespace used for installation")
-	flag.StringVar(&bundleStrategy, "b", "", "The bundle strategy to be produced: bundle or tarball")
-	force := flag.Bool("f", false, "Forces to overwrite an existing namespace")
-	isVersion := flag.Bool("v", false, "Report the version of the Skupper bootstrap command")
-	flag.Parse()
-	if *isVersion {
-		fmt.Println(version.Version)
-		os.Exit(0)
-	}
-	if inputPath != "" {
-		var err error
-		inputPath, err = filepath.Abs(inputPath)
-		if err != nil {
-			fmt.Printf("Unable to determine absolute path of %s: %v\n", inputPath, err)
-			os.Exit(1)
-		}
-	}
-	if bundleStrategy != "" {
-		if !internalbundle.IsValidBundle(bundleStrategy) {
-			fmt.Printf("Invalid bundle strategy: %s\n", bundleStrategy)
-			os.Exit(1)
-		}
-	}
-	//
-	// NOTE FOR CONTAINERS
-	// When running bootstrap process through a container
-	// the /input path must be mapped to a directory containing a site
-	// definition based on CR files, if an input path has been provided.
-	// The /output path must be mapped to the Host's XDG_DATA_HOME/skupper
-	// or $HOME/.local/share/skupper (non-root)
-	// and /var/lib/skupper (root).
-	//
-	fmt.Printf("Skupper nonkube bootstrap (version: %s)\n", version.Version)
-
-	if platform.IsKubernetes() {
-		platform = types.PlatformPodman
-		// Bootstrap uses podman as the default platform
-		//_ = os.Setenv(types.ENV_PLATFORM, "podman")
-	}
-	isBundle := internalbundle.GetBundleStrategy(bundleStrategy) != ""
-	if api.IsRunningInContainer() {
-		requiredPaths := []string{"/output"}
-		if inputPath != "" {
-			if inputPath != "/input" {
-				fmt.Println("The input path must be set to /input when using a container to bootstrap")
-				os.Exit(1)
-			}
-			requiredPaths = append(requiredPaths, "/input")
-		}
-		for _, directory := range requiredPaths {
-			stat, err := os.Stat(directory)
-			if err != nil {
-				fmt.Printf("Failed to stat %s: %s\n", directory, err)
-				os.Exit(1)
-			}
-			if !stat.IsDir() {
-				fmt.Printf("%s is not a directory\n", directory)
-				os.Exit(1)
-			}
-		}
-	} else if !isBundle {
-		binary := "podman"
-		if platform == types.PlatformSystemd {
-			binary = "skrouterd"
-		} else if platform == types.PlatformDocker {
-			binary = "docker"
-		}
-		_, err := exec.LookPath(binary)
-		if err != nil {
-			fmt.Printf("Platform %q is not available.\n", platform)
-			fmt.Printf("ERROR! Command not found: %s.\n", binary)
-			os.Exit(1)
-		}
-	}
-	namespace := userNamespace
-	if namespace == "" {
-		namespace = "default"
-	}
-	existingPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath)
-	inputSourcesDefined := false
-	if _, err := os.Stat(existingPath); err == nil {
-		dirReader := new(internalutils.DirectoryReader)
-		filesFound, _ := dirReader.ReadDir(existingPath, nil)
-		inputSourcesDefined = len(filesFound) > 0
-	}
-	if inputPath == "" {
-		// when input path is empty, but a namespace is provided, try to reload an existing site definition
-		if inputSourcesDefined {
-			inputPath = existingPath
-			fmt.Printf("Resources will consumed from namespace %q\n", namespace)
-		} else {
-			fmt.Printf("Input path has not been provided and namespace %s does not exist\n", namespace)
-			fmt.Printf("No resources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
-			os.Exit(1)
-		}
-	} else if inputSourcesDefined {
-		fmt.Printf("Input path has been provided, but namespace %s has input resources defined at:\n", namespace)
-		fmt.Printf("%s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
-		os.Exit(1)
-	}
-
-	// if namespace already exists, fail if force is not set
-	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
-	if !isBundle && err == nil && !*force {
-		fmt.Printf("Namespace already exists: %s\n", namespace)
-		os.Exit(1)
-	}
-
-	siteState, err := bootstrap(inputPath, userNamespace, internalbundle.GetBundleStrategy(bundleStrategy))
-	if err != nil {
-		fmt.Println("Failed to bootstrap:", err)
-		os.Exit(1)
-	}
-	var bundleSuffix string
-	if isBundle {
-		bundleSuffix = " (as a distributable bundle)"
-	} else {
-		bundleSuffix = fmt.Sprintf(" on namespace %q", siteState.GetNamespace())
-		// create bootstrap.out file
-		if api.IsRunningInContainer() {
-			outFile, err := os.Stat("/bootstrap.out")
-			if err == nil && !outFile.IsDir() {
-				err = os.WriteFile("/bootstrap.out", []byte(siteState.GetNamespace()), 0644)
-				if err != nil {
-					fmt.Println("Failed to write to bootstrap.out:", err)
-					fmt.Println("The systemd service will not be created.")
-				}
-			}
-		}
-	}
-	fmt.Printf("Site %q has been created%s\n", siteState.Site.Name, bundleSuffix)
-	if !isBundle {
-		fmt.Printf("Platform: %s\n", platform)
-		tokenPath := api.GetInternalOutputPath(siteState.Site.Namespace, api.RuntimeTokenPath)
-		hostTokenPath := api.GetHostSiteInternalPath(siteState.Site, api.RuntimeTokenPath)
-		tokens, _ := os.ReadDir(tokenPath)
-		for _, token := range tokens {
-			if !token.IsDir() {
-				fmt.Println("Static links have been defined at:", hostTokenPath)
-				break
-			}
-		}
-		sourcesPath := api.GetHostSiteInternalPath(siteState.Site, api.InputSiteStatePath)
-		fmt.Printf("Definition is available at: %s\n", sourcesPath)
-	} else {
-		siteHome := api.GetHostBundlesPath()
-		installationFile := path.Join(siteHome, fmt.Sprintf("skupper-install-%s.sh", siteState.Site.Name))
-		if internalbundle.GetBundleStrategy(bundleStrategy) == string(internalbundle.BundleStrategyTarball) {
-			installationFile = path.Join(siteHome, fmt.Sprintf("skupper-install-%s.tar.gz", siteState.Site.Name))
-		}
-		fmt.Println("Installation bundle available at:", installationFile)
-		fmt.Println("Default namespace:", siteState.GetNamespace())
-		fmt.Println("Default platform:", utils.DefaultStr(string(platform), "podman"))
-	}
-}
-
-func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.SiteState, error) {
-	var siteStateLoader api.SiteStateLoader
-	var reloadExisting bool
-	isBundle := bundleStrategy != ""
-	sourcesPath := api.GetInternalOutputPath(namespace, api.InputSiteStatePath)
-	_, err := os.Stat(sourcesPath)
-	if !isBundle && err == nil {
-		reloadExisting = true
-		nsPlatformLoader := &common.NamespacePlatformLoader{}
-		nsPlatform, err := nsPlatformLoader.Load(namespace)
-		if err != nil {
-			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
-			if runtimeStateErr == nil {
-				return nil, fmt.Errorf("unable to determine current platform used in namespace %q", namespace)
-			}
-			// platform.yaml not present, which is ok if a site is not yet rendered
-			nsPlatform = string(platform)
-			reloadExisting = false
-		}
-		currentPlatform := string(platform)
-		if platform.IsKubernetes() {
-			currentPlatform = "podman"
-		}
-		if nsPlatform != currentPlatform {
-			return nil, fmt.Errorf("existing namespace uses %q platform and it cannot change to %q", nsPlatform, currentPlatform)
-		}
-	}
-	siteStateLoader = &common.FileSystemSiteStateLoader{
-		Path:   inputPath,
-		Bundle: isBundle,
-	}
-	siteState, err := siteStateLoader.Load()
-	if err != nil {
-		return nil, fmt.Errorf("failed to load site state: %v", err)
-	}
-	// if sources are being consume from namespace sources, they must be properly set
-	crNamespace := siteState.GetNamespace()
-	targetNamespace := utils.DefaultStr(namespace, "default")
-	if inputPath == sourcesPath {
-		if crNamespace != targetNamespace {
-			return nil, fmt.Errorf("namespace must be %q, but resources are defined using %q", targetNamespace, crNamespace)
-		}
-	} else if namespace != "" {
-		siteState.SetNamespace(namespace)
-	}
-
-	var siteStateRenderer api.StaticSiteStateRenderer
-	if isBundle {
-		siteStateRenderer = &bundle.SiteStateRenderer{
-			Strategy: internalbundle.BundleStrategy(bundleStrategy),
-			Platform: platform,
-		}
-	} else if platform == types.PlatformSystemd {
-		siteStateRenderer = &systemd.SiteStateRenderer{}
-	} else {
-		siteStateRenderer = &compat.SiteStateRenderer{
-			Platform: platform,
-		}
-	}
-	err = siteStateRenderer.Render(siteState, reloadExisting)
-	if err != nil {
-		return nil, fmt.Errorf("failed to render site state: %v", err)
-	}
-	return siteState, nil
-}
diff --git a/cmd/bootstrap/bootstrap.sh b/cmd/bootstrap/bootstrap.sh
index 4f90a11b..dc2f258d 100755
--- a/cmd/bootstrap/bootstrap.sh
+++ b/cmd/bootstrap/bootstrap.sh
@@ -2,7 +2,7 @@
 
 set -Ceu
 
-IMAGE="quay.io/skupper/bootstrap:v2-latest"
+IMAGE="quay.io/skupper/cli:v2-latest"
 export INPUT_PATH=""
 export NAMESPACE=""
 export FORCE_FLAG=""
@@ -221,8 +221,9 @@ main() {
         --network host --security-opt label=disable -u \""${RUNAS}"\" --userns=\""${USERNS}"\" \
         "${MOUNTS}" \
         "${ENV_VARS}" \
+        --entrypoint /app/skupper \
         "${IMAGE}" \
-        /app/bootstrap -p="${INPUT_PATH_ARG}" -n="${NAMESPACE}" ${BUNDLE_STRATEGY} ${FORCE_FLAG} 2>&1
+        system setup --path="${INPUT_PATH_ARG}" -n="${NAMESPACE}" ${BUNDLE_STRATEGY} ${FORCE_FLAG} 2>&1
     create_service
 }
 
diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go
index b7f5d052..10185ed3 100644
--- a/cmd/controller/controller.go
+++ b/cmd/controller/controller.go
@@ -111,25 +111,32 @@ func (c *Controller) Run(stopCh <-chan struct{}) error {
 	//recover existing sites & bindings
 	for _, site := range c.siteWatcher.List() {
 		log.Printf("Recovering site %s/%s", site.ObjectMeta.Namespace, site.ObjectMeta.Name)
-		err := c.getSite(site.ObjectMeta.Namespace).Recover(site)
+		err := c.getSite(site.ObjectMeta.Namespace).StartRecovery(site)
 		if err != nil {
 			log.Printf("Error recovering site for %s/%s: %s", site.ObjectMeta.Namespace, site.ObjectMeta.Name, err)
 		}
 	}
 	for _, connector := range c.connectorWatcher.List() {
 		site := c.getSite(connector.ObjectMeta.Namespace)
-		log.Printf("checking connector %s in %s", connector.ObjectMeta.Name, connector.ObjectMeta.Namespace)
+		log.Printf("Recovering connector %s in %s", connector.ObjectMeta.Name, connector.ObjectMeta.Namespace)
 		site.CheckConnector(connector.ObjectMeta.Name, connector)
 	}
 	for _, listener := range c.listenerWatcher.List() {
 		site := c.getSite(listener.ObjectMeta.Namespace)
-		log.Printf("checking listener %s in %s", listener.ObjectMeta.Name, listener.ObjectMeta.Namespace)
+		log.Printf("Recovering listener %s in %s", listener.ObjectMeta.Name, listener.ObjectMeta.Namespace)
 		site.CheckListener(listener.ObjectMeta.Name, listener)
 	}
 	for _, la := range c.linkAccessWatcher.List() {
 		site := c.getSite(la.ObjectMeta.Namespace)
 		site.CheckRouterAccess(la.ObjectMeta.Name, la)
 	}
+	for _, site := range c.siteWatcher.List() {
+		err := c.getSite(site.ObjectMeta.Namespace).Reconcile(site)
+		if err != nil {
+			log.Printf("Error recovering site for %s/%s: %s", site.ObjectMeta.Namespace, site.ObjectMeta.Name, err)
+		}
+		log.Printf("Recovered site %s/%s", site.ObjectMeta.Namespace, site.ObjectMeta.Name)
+	}
 	c.certMgr.Recover()
 	c.accessRecovery.Recover()
 	if c.startGrantServer != nil {
diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go
index 97a291e2..5c8bcbd0 100644
--- a/internal/cmd/skupper/common/flags.go
+++ b/internal/cmd/skupper/common/flags.go
@@ -72,6 +72,13 @@ for other Kubernetes flavors, loadbalancer is the default.`
 	FlagNameListenerHost = "host"
 	FlagDescListenerHost = "The hostname or IP address of the local listener. Clients at this site use the listener host and port to establish connections to the remote service."
 
+	FlagNamePath     = "path"
+	FlagDescPath     = "Custom resources location on the file system"
+	FlagNameStrategy = "strategy"
+	FlagDescStrategy = "The bundle strategy to be produced. Choices: bundle, tarball"
+	FlagNameForce    = "force"
+	FlagDescForce    = "Forces to overwrite an existing namespace"
+
 	FlagNameWait       = "wait"
 	FlagDescWait       = "Wait for the given status before exiting. Choices: configured, ready, none"
 	FlagDescDeleteWait = "Wait for deletion to complete before exiting"
@@ -118,10 +125,12 @@ type CommandLinkUpdateFlags struct {
 	Cost           string
 	Output         string
 	Timeout        time.Duration
+	Wait           string
 }
 
 type CommandLinkDeleteFlags struct {
 	Timeout time.Duration
+	Wait    bool
 }
 type CommandLinkStatusFlags struct {
 	Output string
@@ -148,6 +157,7 @@ type CommandConnectorCreateFlags struct {
 	Workload            string
 	Timeout             time.Duration
 	Output              string
+	Wait                string
 }
 
 type CommandConnectorUpdateFlags struct {
@@ -161,10 +171,12 @@ type CommandConnectorUpdateFlags struct {
 	IncludeNotReadyPods bool
 	Timeout             time.Duration
 	Output              string
+	Wait                string
 }
 
 type CommandConnectorDeleteFlags struct {
 	Timeout time.Duration
+	Wait    bool
 }
 
 type CommandConnectorStatusFlags struct {
@@ -178,6 +190,7 @@ type CommandListenerCreateFlags struct {
 	ListenerType   string
 	Timeout        time.Duration
 	Output         string
+	Wait           string
 }
 
 type CommandListenerUpdateFlags struct {
@@ -188,6 +201,7 @@ type CommandListenerUpdateFlags struct {
 	Timeout        time.Duration
 	Port           int
 	Output         string
+	Wait           string
 }
 
 type CommandListenerStatusFlags struct {
@@ -196,6 +210,13 @@ type CommandListenerStatusFlags struct {
 
 type CommandListenerDeleteFlags struct {
 	Timeout time.Duration
+	Wait    bool
+}
+
+type CommandSystemSetupFlags struct {
+	Path     string
+	Strategy string
+	Force    bool
 }
 
 type CommandVersionFlags struct {
diff --git a/internal/cmd/skupper/connector/connector.go b/internal/cmd/skupper/connector/connector.go
index 703dd34f..a526b110 100644
--- a/internal/cmd/skupper/connector/connector.go
+++ b/internal/cmd/skupper/connector/connector.go
@@ -54,6 +54,7 @@ skupper connector create backend 8080 --workload deployment/backend`,
 	cmd.Flags().StringVarP(&cmdFlags.Workload, common.FlagNameWorkload, "w", "", common.FlagDescWorkload)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "configured", common.FlagDescWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
@@ -64,6 +65,8 @@ skupper connector create backend 8080 --workload deployment/backend`,
 		cmd.Flags().MarkHidden(common.FlagNameIncludeNotReadyPods)
 		cmd.Flags().MarkHidden(common.FlagNameSelector)
 		cmd.Flags().MarkHidden(common.FlagNameWorkload)
+		cmd.Flags().MarkHidden(common.FlagNameTimeout)
+		cmd.Flags().MarkHidden(common.FlagNameWait)
 	}
 
 	return cmd
@@ -119,6 +122,7 @@ func CmdConnectorUpdateFactory(configuredPlatform types.Platform) *cobra.Command
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
 	cmd.Flags().IntVar(&cmdFlags.Port, common.FlagNameConnectorPort, 0, common.FlagDescConnectorPort)
+	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "configured", common.FlagDescWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
@@ -129,6 +133,8 @@ func CmdConnectorUpdateFactory(configuredPlatform types.Platform) *cobra.Command
 		cmd.Flags().MarkHidden(common.FlagNameIncludeNotReadyPods)
 		cmd.Flags().MarkHidden(common.FlagNameSelector)
 		cmd.Flags().MarkHidden(common.FlagNameWorkload)
+		cmd.Flags().MarkHidden(common.FlagNameTimeout)
+		cmd.Flags().MarkHidden(common.FlagNameWait)
 	}
 
 	return cmd
@@ -149,6 +155,7 @@ func CmdConnectorDeleteFactory(configuredPlatform types.Platform) *cobra.Command
 
 	cmdFlags := common.CommandConnectorDeleteFlags{}
 	cmd.Flags().DurationVarP(&cmdFlags.Timeout, common.FlagNameTimeout, "t", 60*time.Second, common.FlagDescTimeout)
+	cmd.Flags().BoolVar(&cmdFlags.Wait, common.FlagNameWait, true, common.FlagDescDeleteWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
diff --git a/internal/cmd/skupper/connector/kube/connector_create.go b/internal/cmd/skupper/connector/kube/connector_create.go
index 411b9644..c2d3c5e7 100644
--- a/internal/cmd/skupper/connector/kube/connector_create.go
+++ b/internal/cmd/skupper/connector/kube/connector_create.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"strconv"
 	"time"
 
@@ -36,6 +37,7 @@ type CmdConnectorCreate struct {
 	includeNotReadyPods bool
 	timeout             time.Duration
 	KubeClient          kubernetes.Interface
+	status              string
 }
 
 func NewCmdConnectorCreate() *CmdConnectorCreate {
@@ -61,6 +63,7 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
 	workloadStringValidator := validator.NewWorkloadStringValidator(common.WorkloadTypes)
 	selectorStringValidator := validator.NewSelectorStringValidator()
+	statusValidator := validator.NewOptionValidator(common.WaitStatusTypes)
 
 	// Validate arguments name and port
 	if len(args) < 2 {
@@ -203,6 +206,14 @@ func (cmd *CmdConnectorCreate) ValidateInput(args []string) []error {
 			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
 		}
 	}
+
+	if cmd.Flags != nil && cmd.Flags.Wait != "" {
+		ok, err := statusValidator.Evaluate(cmd.Flags.Wait)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("status is not valid: %s", err))
+		}
+	}
+
 	return validationErrors
 }
 
@@ -231,6 +242,7 @@ func (cmd *CmdConnectorCreate) InputToOptions() {
 	cmd.connectorType = cmd.Flags.ConnectorType
 	cmd.output = cmd.Flags.Output
 	cmd.includeNotReadyPods = cmd.Flags.IncludeNotReadyPods
+	cmd.status = cmd.Flags.Wait
 }
 
 func (cmd *CmdConnectorCreate) Run() error {
@@ -271,7 +283,14 @@ func (cmd *CmdConnectorCreate) WaitUntil() error {
 		return nil
 	}
 
+	if cmd.status == "none" {
+		return nil
+	}
+
 	waitTime := int(cmd.timeout.Seconds())
+
+	var connectorCondition *metav1.Condition
+
 	err := utils.NewSpinnerWithTimeout("Waiting for create to complete...", waitTime, func() error {
 
 		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
@@ -279,17 +298,39 @@ func (cmd *CmdConnectorCreate) WaitUntil() error {
 			return err
 		}
 
-		if resource != nil && resource.IsConfigured() {
+		isConditionFound := false
+		isConditionTrue := false
+
+		switch cmd.status {
+		case "ready":
+			connectorCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_READY)
+		default:
+			connectorCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_CONFIGURED)
+
+		}
+
+		if connectorCondition != nil {
+			isConditionFound = true
+			isConditionTrue = connectorCondition.Status == metav1.ConditionTrue
+		}
+
+		if resource != nil && isConditionFound && isConditionTrue {
 			return nil
 		}
 
+		if resource != nil && isConditionFound && !isConditionTrue {
+			return fmt.Errorf("error in the condition")
+		}
+
 		return fmt.Errorf("error getting the resource")
 	})
 
-	if err != nil {
-		return fmt.Errorf("Connector %q not ready yet, check the status for more information\n", cmd.name)
+	if err != nil && connectorCondition == nil {
+		return fmt.Errorf("Connector %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+	} else if err != nil && connectorCondition.Status == metav1.ConditionFalse {
+		return fmt.Errorf("Connector %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
 	}
 
-	fmt.Printf("Connector %q is ready\n", cmd.name)
+	fmt.Printf("Connector %q is %s.\n", cmd.name, cmd.status)
 	return nil
 }
diff --git a/internal/cmd/skupper/connector/kube/connector_delete.go b/internal/cmd/skupper/connector/kube/connector_delete.go
index 013d242d..527cd943 100644
--- a/internal/cmd/skupper/connector/kube/connector_delete.go
+++ b/internal/cmd/skupper/connector/kube/connector_delete.go
@@ -20,6 +20,7 @@ type CmdConnectorDelete struct {
 	Flags     *common.CommandConnectorDeleteFlags
 	namespace string
 	name      string
+	wait      bool
 }
 
 func NewCmdConnectorDelete() *CmdConnectorDelete {
@@ -79,23 +80,29 @@ func (cmd *CmdConnectorDelete) Run() error {
 }
 
 func (cmd *CmdConnectorDelete) WaitUntil() error {
-	waitTime := int(cmd.Flags.Timeout.Seconds())
-	err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
 
-		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
-		if err == nil && resource != nil {
-			return fmt.Errorf("error deleting the resource")
-		} else {
-			return nil
+	if cmd.wait {
+		waitTime := int(cmd.Flags.Timeout.Seconds())
+		err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
+
+			resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+			if err == nil && resource != nil {
+				return fmt.Errorf("error deleting the resource")
+			} else {
+				return nil
+			}
+		})
+
+		if err != nil {
+			return fmt.Errorf("Connector %q not deleted yet, check the status for more information %s\n", cmd.name, err)
 		}
-	})
 
-	if err != nil {
-		return fmt.Errorf("Connector %q not deleted yet, check the status for more information %s\n", cmd.name, err)
-	}
+		fmt.Printf("Connector %q deleted\n", cmd.name)
 
-	fmt.Printf("Connector %q deleted\n", cmd.name)
+	}
 	return nil
 }
 
-func (cmd *CmdConnectorDelete) InputToOptions() {}
+func (cmd *CmdConnectorDelete) InputToOptions() {
+	cmd.wait = cmd.Flags.Wait
+}
diff --git a/internal/cmd/skupper/connector/kube/connector_update.go b/internal/cmd/skupper/connector/kube/connector_update.go
index 3551c166..b15a2360 100644
--- a/internal/cmd/skupper/connector/kube/connector_update.go
+++ b/internal/cmd/skupper/connector/kube/connector_update.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"time"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
@@ -43,6 +44,7 @@ type CmdConnectorUpdate struct {
 	existingHost     string
 	existingSelector string
 	KubeClient       kubernetes.Interface
+	status           string
 }
 
 func NewCmdConnectorUpdate() *CmdConnectorUpdate {
@@ -69,6 +71,7 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
 	workloadStringValidator := validator.NewWorkloadStringValidator(common.WorkloadTypes)
 	selectorStringValidator := validator.NewSelectorStringValidator()
+	statusValidator := validator.NewOptionValidator(common.WaitStatusTypes)
 
 	// Validate arguments name
 	if len(args) < 1 {
@@ -235,6 +238,14 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 			cmd.newSettings.output = cmd.Flags.Output
 		}
 	}
+
+	if cmd.Flags != nil && cmd.Flags.Wait != "" {
+		ok, err := statusValidator.Evaluate(cmd.Flags.Wait)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("status is not valid: %s", err))
+		}
+	}
+
 	return validationErrors
 }
 
@@ -278,7 +289,12 @@ func (cmd *CmdConnectorUpdate) WaitUntil() error {
 		return nil
 	}
 
+	if cmd.status == "none" {
+		return nil
+	}
+
 	waitTime := int(cmd.Flags.Timeout.Seconds())
+	var connectorCondition *metav1.Condition
 	err := utils.NewSpinnerWithTimeout("Waiting for update to complete...", waitTime, func() error {
 
 		resource, err := cmd.client.Connectors(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
@@ -286,19 +302,43 @@ func (cmd *CmdConnectorUpdate) WaitUntil() error {
 			return err
 		}
 
-		if resource != nil && resource.IsConfigured() {
+		isConditionFound := false
+		isConditionTrue := false
+
+		switch cmd.status {
+		case "ready":
+			connectorCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_READY)
+		default:
+			connectorCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_CONFIGURED)
+
+		}
+
+		if connectorCondition != nil {
+			isConditionFound = true
+			isConditionTrue = connectorCondition.Status == metav1.ConditionTrue
+		}
+
+		if resource != nil && isConditionFound && isConditionTrue {
 			return nil
 		}
 
+		if resource != nil && isConditionFound && !isConditionTrue {
+			return fmt.Errorf("error in the condition")
+		}
+
 		return fmt.Errorf("error getting the resource")
 	})
 
-	if err != nil {
-		return fmt.Errorf("Connector %q not ready yet, check the status for more information\n", cmd.name)
+	if err != nil && connectorCondition == nil {
+		return fmt.Errorf("Connector %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+	} else if err != nil && connectorCondition.Status == metav1.ConditionFalse {
+		return fmt.Errorf("Connector %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
 	}
 
-	fmt.Printf("Connector %q is ready\n", cmd.name)
+	fmt.Printf("Connector %q is %s.\n", cmd.name, cmd.status)
 	return nil
 }
 
-func (cmd *CmdConnectorUpdate) InputToOptions() {}
+func (cmd *CmdConnectorUpdate) InputToOptions() {
+	cmd.status = cmd.Flags.Wait
+}
diff --git a/internal/cmd/skupper/link/kube/link_delete.go b/internal/cmd/skupper/link/kube/link_delete.go
index 5d8a2060..f722c676 100644
--- a/internal/cmd/skupper/link/kube/link_delete.go
+++ b/internal/cmd/skupper/link/kube/link_delete.go
@@ -20,6 +20,7 @@ type CmdLinkDelete struct {
 	Flags     *common.CommandLinkDeleteFlags
 	linkName  string
 	timeout   time.Duration
+	wait      bool
 }
 
 func NewCmdLinkDelete() *CmdLinkDelete {
@@ -66,6 +67,7 @@ func (cmd *CmdLinkDelete) ValidateInput(args []string) []error {
 }
 func (cmd *CmdLinkDelete) InputToOptions() {
 	cmd.timeout = cmd.Flags.Timeout
+	cmd.wait = cmd.Flags.Wait
 }
 
 func (cmd *CmdLinkDelete) Run() error {
@@ -73,24 +75,27 @@ func (cmd *CmdLinkDelete) Run() error {
 	return err
 }
 func (cmd *CmdLinkDelete) WaitUntil() error {
-	waitTime := int(cmd.timeout.Seconds())
-	err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
 
-		resource, err := cmd.Client.Links(cmd.Namespace).Get(context.TODO(), cmd.linkName, metav1.GetOptions{})
+	if cmd.wait {
+		waitTime := int(cmd.timeout.Seconds())
+		err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
 
-		if err == nil && resource != nil {
-			return fmt.Errorf("error deleting the resource")
-		} else {
-			return nil
-		}
+			resource, err := cmd.Client.Links(cmd.Namespace).Get(context.TODO(), cmd.linkName, metav1.GetOptions{})
 
-	})
+			if err == nil && resource != nil {
+				return fmt.Errorf("error deleting the resource")
+			} else {
+				return nil
+			}
 
-	if err != nil {
-		return fmt.Errorf("Link %q not deleted yet, check the status for more information\n", cmd.linkName)
-	}
+		})
 
-	fmt.Printf("Link %q is deleted\n", cmd.linkName)
+		if err != nil {
+			return fmt.Errorf("Link %q not deleted yet, check the status for more information\n", cmd.linkName)
+		}
+
+		fmt.Printf("Link %q is deleted\n", cmd.linkName)
+	}
 
 	return nil
 }
diff --git a/internal/cmd/skupper/link/kube/link_update.go b/internal/cmd/skupper/link/kube/link_update.go
index 7378fcc9..2ba3d753 100644
--- a/internal/cmd/skupper/link/kube/link_update.go
+++ b/internal/cmd/skupper/link/kube/link_update.go
@@ -13,6 +13,7 @@ import (
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 	"strconv"
@@ -30,6 +31,7 @@ type CmdLinkUpdate struct {
 	cost           int
 	output         string
 	timeout        time.Duration
+	status         string
 }
 
 func NewCmdLinkUpdate() *CmdLinkUpdate {
@@ -51,6 +53,7 @@ func (cmd *CmdLinkUpdate) ValidateInput(args []string) []error {
 	numberValidator := validator.NewNumberValidator()
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+	statusValidator := validator.NewOptionValidator(common.WaitStatusTypes)
 
 	//Validate if there is already a site defined in the namespace
 	siteList, _ := cmd.Client.Sites(cmd.Namespace).List(context.TODO(), metav1.ListOptions{})
@@ -98,6 +101,13 @@ func (cmd *CmdLinkUpdate) ValidateInput(args []string) []error {
 		validationErrors = append(validationErrors, fmt.Errorf("timeout is not valid: %s", err))
 	}
 
+	if cmd.Flags != nil && cmd.Flags.Wait != "" {
+		ok, err := statusValidator.Evaluate(cmd.Flags.Wait)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("status is not valid: %s", err))
+		}
+	}
+
 	return validationErrors
 }
 
@@ -107,6 +117,7 @@ func (cmd *CmdLinkUpdate) InputToOptions() {
 	cmd.tlsCredentials = cmd.Flags.TlsCredentials
 	cmd.output = cmd.Flags.Output
 	cmd.timeout = cmd.Flags.Timeout
+	cmd.status = cmd.Flags.Wait
 
 }
 
@@ -165,7 +176,12 @@ func (cmd *CmdLinkUpdate) WaitUntil() error {
 		return nil
 	}
 
+	if cmd.status == "none" {
+		return nil
+	}
+
 	waitTime := int(cmd.timeout.Seconds())
+	var linkCondition *metav1.Condition
 	err := utils.NewSpinnerWithTimeout("Waiting for update to complete...", waitTime, func() error {
 
 		resource, err := cmd.Client.Links(cmd.Namespace).Get(context.TODO(), cmd.linkName, metav1.GetOptions{})
@@ -173,15 +189,36 @@ func (cmd *CmdLinkUpdate) WaitUntil() error {
 			return err
 		}
 
-		if resource != nil && resource.IsConfigured() {
+		isConditionFound := false
+		isConditionTrue := false
+
+		switch cmd.status {
+		case "configured":
+			linkCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_CONFIGURED)
+		default:
+			linkCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_READY)
+		}
+
+		if linkCondition != nil {
+			isConditionFound = true
+			isConditionTrue = linkCondition.Status == metav1.ConditionTrue
+		}
+
+		if resource != nil && isConditionFound && isConditionTrue {
 			return nil
 		}
 
+		if resource != nil && isConditionFound && !isConditionTrue {
+			return fmt.Errorf("error in the condition")
+		}
+
 		return fmt.Errorf("error getting the resource")
 	})
 
-	if err != nil {
-		return fmt.Errorf("Link %q not updated yet, check the status for more information\n", cmd.linkName)
+	if err != nil && linkCondition == nil {
+		return fmt.Errorf("Link %q is not %s yet, check the status for more information\n", cmd.linkName, cmd.status)
+	} else if err != nil && linkCondition.Status == metav1.ConditionFalse {
+		return fmt.Errorf("Link %q is %s with errors, check the status for more information\n", cmd.linkName, cmd.status)
 	}
 
 	fmt.Printf("Link %q is updated\n", cmd.linkName)
diff --git a/internal/cmd/skupper/link/link.go b/internal/cmd/skupper/link/link.go
index 5b698067..6661daf6 100644
--- a/internal/cmd/skupper/link/link.go
+++ b/internal/cmd/skupper/link/link.go
@@ -72,12 +72,17 @@ func CmdLinkUpdateFactory(configuredPlatform types.Platform) *cobra.Command {
 	cmd.Flags().StringVar(&cmdFlags.Cost, common.FlagNameCost, "1", common.FlagDescCost)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "ready", common.FlagDescWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
 	nonKubeCommand.Flags = &cmdFlags
 
+	if configuredPlatform != types.PlatformKubernetes {
+		cmd.Flags().MarkHidden(common.FlagNameWait)
+	}
+
 	return cmd
 }
 
@@ -118,11 +123,16 @@ func CmdLinkDeleteFactory(configuredPlatform types.Platform) *cobra.Command {
 	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdLinkDeleteDesc, kubeCommand, nonKubeCommand)
 	cmdFlags := common.CommandLinkDeleteFlags{}
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
+	cmd.Flags().BoolVar(&cmdFlags.Wait, common.FlagNameWait, true, common.FlagDescDeleteWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
 	nonKubeCommand.Flags = &cmdFlags
 
+	if configuredPlatform != types.PlatformKubernetes {
+		cmd.Flags().MarkHidden(common.FlagNameWait)
+	}
+
 	return cmd
 }
diff --git a/internal/cmd/skupper/listener/kube/listener_create.go b/internal/cmd/skupper/listener/kube/listener_create.go
index 07b947c9..6574f923 100644
--- a/internal/cmd/skupper/listener/kube/listener_create.go
+++ b/internal/cmd/skupper/listener/kube/listener_create.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"strconv"
 	"time"
 
@@ -33,6 +34,7 @@ type CmdListenerCreate struct {
 	timeout        time.Duration
 	output         string
 	KubeClient     kubernetes.Interface
+	status         string
 }
 
 func NewCmdListenerCreate() *CmdListenerCreate {
@@ -57,6 +59,7 @@ func (cmd *CmdListenerCreate) ValidateInput(args []string) []error {
 	listenerTypeValidator := validator.NewOptionValidator(common.ListenerTypes)
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
+	statusValidator := validator.NewOptionValidator(common.WaitStatusTypes)
 
 	// Validate arguments name and port
 	if len(args) < 2 {
@@ -129,6 +132,14 @@ func (cmd *CmdListenerCreate) ValidateInput(args []string) []error {
 			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
 		}
 	}
+
+	if cmd.Flags != nil && cmd.Flags.Wait != "" {
+		ok, err := statusValidator.Evaluate(cmd.Flags.Wait)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("status is not valid: %s", err))
+		}
+	}
+
 	return validationErrors
 }
 
@@ -148,6 +159,7 @@ func (cmd *CmdListenerCreate) InputToOptions() {
 	cmd.tlsCredentials = cmd.Flags.TlsCredentials
 	cmd.listenerType = cmd.Flags.ListenerType
 	cmd.output = cmd.Flags.Output
+	cmd.status = cmd.Flags.Wait
 }
 
 func (cmd *CmdListenerCreate) Run() error {
@@ -186,7 +198,13 @@ func (cmd *CmdListenerCreate) WaitUntil() error {
 		return nil
 	}
 
+	if cmd.status == "none" {
+		return nil
+	}
+
 	waitTime := int(cmd.timeout.Seconds())
+	var listenerCondition *metav1.Condition
+
 	err := utils.NewSpinnerWithTimeout("Waiting for create to complete...", waitTime, func() error {
 
 		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
@@ -194,17 +212,38 @@ func (cmd *CmdListenerCreate) WaitUntil() error {
 			return err
 		}
 
-		if resource != nil && resource.IsConfigured() {
+		isConditionFound := false
+		isConditionTrue := false
+
+		switch cmd.status {
+		case "ready":
+			listenerCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_READY)
+		default:
+			listenerCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_CONFIGURED)
+		}
+
+		if listenerCondition != nil {
+			isConditionFound = true
+			isConditionTrue = listenerCondition.Status == metav1.ConditionTrue
+		}
+
+		if resource != nil && isConditionFound && isConditionTrue {
 			return nil
 		}
 
+		if resource != nil && isConditionFound && !isConditionTrue {
+			return fmt.Errorf("error in the condition")
+		}
+
 		return fmt.Errorf("error getting the resource")
 	})
 
-	if err != nil {
-		return fmt.Errorf("Listener %q not ready yet, check the status for more information\n", cmd.name)
+	if err != nil && listenerCondition == nil {
+		return fmt.Errorf("Listener %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+	} else if err != nil && listenerCondition.Status == metav1.ConditionFalse {
+		return fmt.Errorf("Listener %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
 	}
 
-	fmt.Printf("Listener %q is ready\n", cmd.name)
+	fmt.Printf("Listener %q is %s.\n", cmd.name, cmd.status)
 	return nil
 }
diff --git a/internal/cmd/skupper/listener/kube/listener_delete.go b/internal/cmd/skupper/listener/kube/listener_delete.go
index e35df2fb..38e8bfa1 100644
--- a/internal/cmd/skupper/listener/kube/listener_delete.go
+++ b/internal/cmd/skupper/listener/kube/listener_delete.go
@@ -20,6 +20,7 @@ type CmdListenerDelete struct {
 	Flags     *common.CommandListenerDeleteFlags
 	namespace string
 	name      string
+	wait      bool
 }
 
 func NewCmdListenerDelete() *CmdListenerDelete {
@@ -80,23 +81,28 @@ func (cmd *CmdListenerDelete) Run() error {
 }
 
 func (cmd *CmdListenerDelete) WaitUntil() error {
-	waitTime := int(cmd.Flags.Timeout.Seconds())
-	err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
 
-		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
-		if err == nil && resource != nil {
-			return fmt.Errorf("error deleting the resource")
-		} else {
-			return nil
+	if cmd.wait {
+		waitTime := int(cmd.Flags.Timeout.Seconds())
+		err := utils.NewSpinnerWithTimeout("Waiting for deletion to complete...", waitTime, func() error {
+
+			resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
+			if err == nil && resource != nil {
+				return fmt.Errorf("error deleting the resource")
+			} else {
+				return nil
+			}
+		})
+
+		if err != nil {
+			return fmt.Errorf("Listener %q not deleted yet, check the status for more information %s\n", cmd.name, err)
 		}
-	})
 
-	if err != nil {
-		return fmt.Errorf("Listener %q not deleted yet, check the status for more information %s\n", cmd.name, err)
+		fmt.Printf("Listener %q deleted\n", cmd.name)
 	}
-
-	fmt.Printf("Listener %q deleted\n", cmd.name)
 	return nil
 }
 
-func (cmd *CmdListenerDelete) InputToOptions() {}
+func (cmd *CmdListenerDelete) InputToOptions() {
+	cmd.wait = cmd.Flags.Wait
+}
diff --git a/internal/cmd/skupper/listener/kube/listener_update.go b/internal/cmd/skupper/listener/kube/listener_update.go
index abb576df..ca829296 100644
--- a/internal/cmd/skupper/listener/kube/listener_update.go
+++ b/internal/cmd/skupper/listener/kube/listener_update.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/api/meta"
 	"time"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
@@ -36,6 +37,7 @@ type CmdListenerUpdate struct {
 	resourceVersion string
 	newSettings     ListenerUpdates
 	KubeClient      kubernetes.Interface
+	status          string
 }
 
 func NewCmdListenerUpdate() *CmdListenerUpdate {
@@ -59,6 +61,7 @@ func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error {
 	listenerTypeValidator := validator.NewOptionValidator(common.ListenerTypes)
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 	timeoutValidator := validator.NewTimeoutInSecondsValidator()
+	statusValidator := validator.NewOptionValidator(common.WaitStatusTypes)
 
 	// Validate arguments name
 	if len(args) < 1 {
@@ -143,6 +146,13 @@ func (cmd *CmdListenerUpdate) ValidateInput(args []string) []error {
 		}
 	}
 
+	if cmd.Flags != nil && cmd.Flags.Wait != "" {
+		ok, err := statusValidator.Evaluate(cmd.Flags.Wait)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("status is not valid: %s", err))
+		}
+	}
+
 	return validationErrors
 }
 
@@ -183,27 +193,54 @@ func (cmd *CmdListenerUpdate) WaitUntil() error {
 		return nil
 	}
 
+	if cmd.status == "none" {
+		return nil
+	}
+
 	waitTime := int(cmd.Flags.Timeout.Seconds())
+	var listenerCondition *metav1.Condition
 	err := utils.NewSpinnerWithTimeout("Waiting for update to complete...", waitTime, func() error {
 
 		resource, err := cmd.client.Listeners(cmd.namespace).Get(context.TODO(), cmd.name, metav1.GetOptions{})
 		if err != nil {
 			return err
 		}
+		isConditionFound := false
+		isConditionTrue := false
+
+		switch cmd.status {
+		case "ready":
+			listenerCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_READY)
+		default:
+			listenerCondition = meta.FindStatusCondition(resource.Status.Conditions, v2alpha1.CONDITION_TYPE_CONFIGURED)
+		}
+
+		if listenerCondition != nil {
+			isConditionFound = true
+			isConditionTrue = listenerCondition.Status == metav1.ConditionTrue
+		}
 
-		if resource != nil && resource.IsConfigured() {
+		if resource != nil && isConditionFound && isConditionTrue {
 			return nil
 		}
 
+		if resource != nil && isConditionFound && !isConditionTrue {
+			return fmt.Errorf("error in the condition")
+		}
+
 		return fmt.Errorf("error getting the resource")
 	})
 
-	if err != nil {
-		return fmt.Errorf("Listener %q not ready yet, check the status for more information\n", cmd.name)
+	if err != nil && listenerCondition == nil {
+		return fmt.Errorf("Listener %q is not %s yet, check the status for more information\n", cmd.name, cmd.status)
+	} else if err != nil && listenerCondition.Status == metav1.ConditionFalse {
+		return fmt.Errorf("Listener %q is %s with errors, check the status for more information\n", cmd.name, cmd.status)
 	}
 
-	fmt.Printf("Listener %q is ready\n", cmd.name)
+	fmt.Printf("Listener %q is updated\n", cmd.name)
 	return nil
 }
 
-func (cmd *CmdListenerUpdate) InputToOptions() {}
+func (cmd *CmdListenerUpdate) InputToOptions() {
+	cmd.status = cmd.Flags.Wait
+}
diff --git a/internal/cmd/skupper/listener/listener.go b/internal/cmd/skupper/listener/listener.go
index 6675ee60..3215f561 100644
--- a/internal/cmd/skupper/listener/listener.go
+++ b/internal/cmd/skupper/listener/listener.go
@@ -50,12 +50,18 @@ func CmdListenerCreateFactory(configuredPlatform types.Platform) *cobra.Command
 	cmd.Flags().StringVar(&cmdFlags.ListenerType, common.FlagNameListenerType, "tcp", common.FlagDescListenerType)
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "configured", common.FlagDescWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
 	nonKubeCommand.Flags = &cmdFlags
 
+	if configuredPlatform != types.PlatformKubernetes {
+		cmd.Flags().MarkHidden(common.FlagNameTimeout)
+		cmd.Flags().MarkHidden(common.FlagNameWait)
+	}
+
 	return cmd
 }
 
@@ -82,12 +88,18 @@ func CmdListenerUpdateFactory(configuredPlatform types.Platform) *cobra.Command
 	cmd.Flags().DurationVar(&cmdFlags.Timeout, common.FlagNameTimeout, 60*time.Second, common.FlagDescTimeout)
 	cmd.Flags().IntVar(&cmdFlags.Port, common.FlagNameListenerPort, 0, common.FlagDescListenerPort)
 	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+	cmd.Flags().StringVar(&cmdFlags.Wait, common.FlagNameWait, "configured", common.FlagDescWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
 	nonKubeCommand.Flags = &cmdFlags
 
+	if configuredPlatform != types.PlatformKubernetes {
+		cmd.Flags().MarkHidden(common.FlagNameTimeout)
+		cmd.Flags().MarkHidden(common.FlagNameWait)
+	}
+
 	return cmd
 }
 
@@ -132,11 +144,17 @@ func CmdListenerDeleteFactory(configuredPlatform types.Platform) *cobra.Command
 	cmdFlags := common.CommandListenerDeleteFlags{}
 
 	cmd.Flags().DurationVarP(&cmdFlags.Timeout, common.FlagNameTimeout, "t", 60*time.Second, common.FlagDescTimeout)
+	cmd.Flags().BoolVar(&cmdFlags.Wait, common.FlagNameWait, true, common.FlagDescDeleteWait)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
 	nonKubeCommand.CobraCmd = cmd
 	nonKubeCommand.Flags = &cmdFlags
 
+	if configuredPlatform != types.PlatformKubernetes {
+		cmd.Flags().MarkHidden(common.FlagNameTimeout)
+		cmd.Flags().MarkHidden(common.FlagNameWait)
+	}
+
 	return cmd
 }
diff --git a/internal/cmd/skupper/root/root.go b/internal/cmd/skupper/root/root.go
index 098efb22..8c87d7d3 100644
--- a/internal/cmd/skupper/root/root.go
+++ b/internal/cmd/skupper/root/root.go
@@ -8,6 +8,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/link"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/listener"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/system"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/token"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/version"
 	"github.com/skupperproject/skupper/internal/config"
@@ -34,6 +35,7 @@ func NewSkupperRootCommand() *cobra.Command {
 	rootCmd.AddCommand(connector.NewCmdConnector())
 	rootCmd.AddCommand(version.NewCmdVersion())
 	rootCmd.AddCommand(debug.NewCmdDebug())
+	rootCmd.AddCommand(system.NewCmdSystem())
 
 	return rootCmd
 }
diff --git a/internal/cmd/skupper/site/kube/site_create.go b/internal/cmd/skupper/site/kube/site_create.go
index a65d8c08..6e8f4571 100644
--- a/internal/cmd/skupper/site/kube/site_create.go
+++ b/internal/cmd/skupper/site/kube/site_create.go
@@ -226,7 +226,7 @@ func (cmd *CmdSiteCreate) WaitUntil() error {
 
 	if err != nil && siteCondition == nil {
 		return fmt.Errorf("Site %q is not %s yet, check the status for more information\n", cmd.siteName, cmd.status)
-	} else if err != nil {
+	} else if err != nil && siteCondition.Status == metav1.ConditionFalse {
 		return fmt.Errorf("Site %q is %s with errors, check the status for more information\n", cmd.siteName, cmd.status)
 	}
 
diff --git a/internal/cmd/skupper/site/kube/site_delete.go b/internal/cmd/skupper/site/kube/site_delete.go
index 584b694c..1779161d 100644
--- a/internal/cmd/skupper/site/kube/site_delete.go
+++ b/internal/cmd/skupper/site/kube/site_delete.go
@@ -86,6 +86,7 @@ func (cmd *CmdSiteDelete) ValidateInput(args []string) []error {
 }
 func (cmd *CmdSiteDelete) InputToOptions() {
 	cmd.timeout = cmd.Flags.Timeout
+	cmd.wait = cmd.Flags.Wait
 }
 
 func (cmd *CmdSiteDelete) Run() error {
diff --git a/internal/cmd/skupper/site/kube/site_update.go b/internal/cmd/skupper/site/kube/site_update.go
index f8ef43f3..c5a22a2e 100644
--- a/internal/cmd/skupper/site/kube/site_update.go
+++ b/internal/cmd/skupper/site/kube/site_update.go
@@ -245,7 +245,7 @@ func (cmd *CmdSiteUpdate) WaitUntil() error {
 
 	if err != nil && siteCondition == nil {
 		return fmt.Errorf("Site %q is not %s yet, check the status for more information\n", cmd.siteName, cmd.status)
-	} else if err != nil {
+	} else if err != nil && siteCondition.Status == metav1.ConditionFalse {
 		return fmt.Errorf("Site %q is %s with errors, check the status for more information\n", cmd.siteName, cmd.status)
 	}
 
diff --git a/internal/cmd/skupper/system/kube/system_reload.go b/internal/cmd/skupper/system/kube/system_reload.go
new file mode 100644
index 00000000..7499a2a4
--- /dev/null
+++ b/internal/cmd/skupper/system/kube/system_reload.go
@@ -0,0 +1,35 @@
+package kube
+
+import (
+	"fmt"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdSystemReload struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Namespace  string
+}
+
+func NewCmdSystemReload() *CmdSystemReload {
+
+	skupperCmd := CmdSystemReload{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemReload) NewClient(cobraCommand *cobra.Command, args []string) {}
+
+func (cmd *CmdSystemReload) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdSystemReload) InputToOptions() {}
+
+func (cmd *CmdSystemReload) Run() error {
+	fmt.Println("This command does not support kubernetes platforms.")
+	return nil
+}
+
+func (cmd *CmdSystemReload) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/kube/system_setup.go b/internal/cmd/skupper/system/kube/system_setup.go
new file mode 100644
index 00000000..9c9c8cd2
--- /dev/null
+++ b/internal/cmd/skupper/system/kube/system_setup.go
@@ -0,0 +1,37 @@
+package kube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdSystemSetup struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Flags      *common.CommandSystemSetupFlags
+	Namespace  string
+}
+
+func NewCmdSystemSetup() *CmdSystemSetup {
+
+	skupperCmd := CmdSystemSetup{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemSetup) NewClient(cobraCommand *cobra.Command, args []string) {}
+
+func (cmd *CmdSystemSetup) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdSystemSetup) InputToOptions() {}
+
+func (cmd *CmdSystemSetup) Run() error {
+	fmt.Println("This command does not support kubernetes platforms.")
+	return nil
+}
+
+func (cmd *CmdSystemSetup) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/kube/system_start.go b/internal/cmd/skupper/system/kube/system_start.go
new file mode 100644
index 00000000..971cf184
--- /dev/null
+++ b/internal/cmd/skupper/system/kube/system_start.go
@@ -0,0 +1,35 @@
+package kube
+
+import (
+	"fmt"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdSystemStart struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Namespace  string
+}
+
+func NewCmdCmdSystemStart() *CmdSystemStart {
+
+	skupperCmd := CmdSystemStart{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemStart) NewClient(cobraCommand *cobra.Command, args []string) {}
+
+func (cmd *CmdSystemStart) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdSystemStart) InputToOptions() {}
+
+func (cmd *CmdSystemStart) Run() error {
+	fmt.Println("This command does not support kubernetes platforms.")
+	return nil
+}
+
+func (cmd *CmdSystemStart) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/kube/system_stop.go b/internal/cmd/skupper/system/kube/system_stop.go
new file mode 100644
index 00000000..d4afe8c1
--- /dev/null
+++ b/internal/cmd/skupper/system/kube/system_stop.go
@@ -0,0 +1,35 @@
+package kube
+
+import (
+	"fmt"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdSystemStop struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Namespace  string
+}
+
+func NewCmdSystemStop() *CmdSystemStop {
+
+	skupperCmd := CmdSystemStop{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemStop) NewClient(cobraCommand *cobra.Command, args []string) {}
+
+func (cmd *CmdSystemStop) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdSystemStop) InputToOptions() {}
+
+func (cmd *CmdSystemStop) Run() error {
+	fmt.Println("This command does not support kubernetes platforms.")
+	return nil
+}
+
+func (cmd *CmdSystemStop) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/kube/system_teardown.go b/internal/cmd/skupper/system/kube/system_teardown.go
new file mode 100644
index 00000000..73d7c68d
--- /dev/null
+++ b/internal/cmd/skupper/system/kube/system_teardown.go
@@ -0,0 +1,35 @@
+package kube
+
+import (
+	"fmt"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
+)
+
+type CmdSystemTeardown struct {
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Namespace  string
+}
+
+func NewCmdSystemTeardown() *CmdSystemTeardown {
+
+	skupperCmd := CmdSystemTeardown{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemTeardown) NewClient(cobraCommand *cobra.Command, args []string) {}
+
+func (cmd *CmdSystemTeardown) ValidateInput(args []string) []error { return nil }
+
+func (cmd *CmdSystemTeardown) InputToOptions() {}
+
+func (cmd *CmdSystemTeardown) Run() error {
+	fmt.Println("This command does not support kubernetes platforms.")
+	return nil
+}
+
+func (cmd *CmdSystemTeardown) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/nonkube/system_reload.go b/internal/cmd/skupper/system/nonkube/system_reload.go
new file mode 100644
index 00000000..9a2a731e
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_reload.go
@@ -0,0 +1,89 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/config"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"github.com/spf13/cobra"
+)
+
+type CmdSystemReload struct {
+	CobraCmd        *cobra.Command
+	Namespace       string
+	PreCheck        func(config *bootstrap.Config) error
+	Bootstrap       func(config *bootstrap.Config) (*api.SiteState, error)
+	PostExec        func(config *bootstrap.Config, siteState *api.SiteState)
+	Platform        string
+	ConfigBootstrap bootstrap.Config
+}
+
+func NewCmdSystemReload() *CmdSystemReload {
+
+	skupperCmd := CmdSystemReload{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemReload) NewClient(cobraCommand *cobra.Command, args []string) {
+	cmd.Bootstrap = bootstrap.Bootstrap
+	cmd.PreCheck = bootstrap.PreBootstrap
+	cmd.PostExec = bootstrap.PostBootstrap
+	cmd.Namespace = cobraCommand.Flag("namespace").Value.String()
+	cmd.Platform = cobraCommand.Flag("platform").Value.String()
+}
+
+func (cmd *CmdSystemReload) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if args != nil && len(args) > 0 {
+		validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments"))
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSystemReload) InputToOptions() {
+	cmd.ConfigBootstrap.Namespace = "default"
+	if cmd.Namespace != "" {
+		cmd.ConfigBootstrap.Namespace = cmd.Namespace
+	}
+	var binary string
+
+	selectedPlatform := config.GetPlatform()
+
+	if cmd.Platform != "" {
+		selectedPlatform = types.Platform(cmd.Platform)
+	}
+
+	switch selectedPlatform {
+	case types.PlatformSystemd:
+		binary = "skrouterd"
+	case types.PlatformDocker:
+		binary = "docker"
+	default:
+		binary = "podman"
+	}
+
+	cmd.ConfigBootstrap.Platform = selectedPlatform
+	cmd.ConfigBootstrap.Binary = binary
+}
+
+func (cmd *CmdSystemReload) Run() error {
+	err := cmd.PreCheck(&cmd.ConfigBootstrap)
+	if err != nil {
+		return err
+	}
+
+	siteState, err := cmd.Bootstrap(&cmd.ConfigBootstrap)
+	if err != nil {
+		return fmt.Errorf("Failed to bootstrap: %s", err)
+	}
+
+	cmd.PostExec(&cmd.ConfigBootstrap, siteState)
+
+	return nil
+}
+
+func (cmd *CmdSystemReload) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/nonkube/system_setup.go b/internal/cmd/skupper/system/nonkube/system_setup.go
new file mode 100644
index 00000000..1194ab35
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_setup.go
@@ -0,0 +1,145 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/config"
+	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"github.com/spf13/cobra"
+	"os"
+	"path/filepath"
+)
+
+type CmdSystemSetup struct {
+	PreCheck        func(config *bootstrap.Config) error
+	Bootstrap       func(config *bootstrap.Config) (*api.SiteState, error)
+	PostExec        func(config *bootstrap.Config, siteState *api.SiteState)
+	CobraCmd        *cobra.Command
+	Flags           *common.CommandSystemSetupFlags
+	Namespace       string
+	Platform        string
+	ConfigBootstrap bootstrap.Config
+}
+
+func NewCmdSystemSetup() *CmdSystemSetup {
+
+	skupperCmd := CmdSystemSetup{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemSetup) NewClient(cobraCommand *cobra.Command, args []string) {
+	cmd.PreCheck = bootstrap.PreBootstrap
+	cmd.Bootstrap = bootstrap.Bootstrap
+	cmd.PostExec = bootstrap.PostBootstrap
+	cmd.Namespace = cobraCommand.Flag("namespace").Value.String()
+	cmd.Platform = cobraCommand.Flag("platform").Value.String()
+}
+
+func (cmd *CmdSystemSetup) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if args != nil && len(args) > 0 {
+		validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments"))
+	}
+
+	if cmd.Flags != nil && cmd.Flags.Path != "" {
+
+		inputPath, err := filepath.Abs(cmd.Flags.Path)
+		if err != nil {
+			validationErrors = append(validationErrors, fmt.Errorf("unable to determine absolute path of %s: %v", inputPath, err))
+
+		}
+
+		if api.IsRunningInContainer() {
+			if inputPath != "/input" {
+				validationErrors = append(validationErrors, fmt.Errorf("the input path must be set to /input when using a container to bootstrap"))
+			}
+		}
+	}
+	if cmd.Flags != nil && cmd.Flags.Strategy != "" {
+		if !internalbundle.IsValidBundle(cmd.Flags.Strategy) {
+			validationErrors = append(validationErrors, fmt.Errorf("invalid bundle strategy: %s", cmd.Flags.Strategy))
+		}
+	}
+
+	if cmd.Flags != nil && !cmd.Flags.Force && cmd.Flags.Strategy == "" {
+		selectedNamespace := "default"
+		if cmd.Namespace != "" {
+			selectedNamespace = cmd.Namespace
+		}
+
+		_, err := os.Stat(api.GetInternalOutputPath(selectedNamespace, api.RuntimeSiteStatePath))
+		if err == nil {
+			validationErrors = append(validationErrors, fmt.Errorf("namespace already exists: %s", selectedNamespace))
+		}
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSystemSetup) InputToOptions() {
+
+	var inputPath string
+	if cmd.Flags.Path != "" {
+		inputPath, _ = filepath.Abs(cmd.Flags.Path)
+	}
+
+	namespace := "default"
+	if cmd.Namespace != "" {
+		namespace = cmd.Namespace
+	}
+
+	isBundle := internalbundle.GetBundleStrategy(cmd.Flags.Strategy) != ""
+
+	var binary string
+	selectedPlatform := config.GetPlatform()
+
+	if cmd.Platform != "" {
+		selectedPlatform = types.Platform(cmd.Platform)
+	}
+
+	if !isBundle {
+		switch selectedPlatform {
+		case types.PlatformSystemd:
+			binary = "skrouterd"
+		case types.PlatformDocker:
+			binary = "docker"
+		default:
+			binary = "podman"
+		}
+	}
+
+	configBootStrap := bootstrap.Config{
+		InputPath:      inputPath,
+		Namespace:      namespace,
+		BundleStrategy: internalbundle.GetBundleStrategy(cmd.Flags.Strategy),
+		IsBundle:       internalbundle.GetBundleStrategy(cmd.Flags.Strategy) != "",
+		Platform:       selectedPlatform,
+		Binary:         binary,
+	}
+
+	cmd.ConfigBootstrap = configBootStrap
+}
+
+func (cmd *CmdSystemSetup) Run() error {
+
+	err := cmd.PreCheck(&cmd.ConfigBootstrap)
+	if err != nil {
+		return err
+	}
+
+	siteState, err := cmd.Bootstrap(&cmd.ConfigBootstrap)
+	if err != nil {
+		return fmt.Errorf("Failed to bootstrap: %s", err)
+	}
+
+	cmd.PostExec(&cmd.ConfigBootstrap, siteState)
+
+	return nil
+}
+
+func (cmd *CmdSystemSetup) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/nonkube/system_start.go b/internal/cmd/skupper/system/nonkube/system_start.go
new file mode 100644
index 00000000..dba0adf9
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_start.go
@@ -0,0 +1,58 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"github.com/spf13/cobra"
+)
+
+type CmdSystemStart struct {
+	CobraCmd    *cobra.Command
+	Namespace   string
+	Platform    string
+	SystemStart func(service string) error
+}
+
+func NewCmdCmdSystemStart() *CmdSystemStart {
+
+	skupperCmd := CmdSystemStart{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemStart) NewClient(cobraCommand *cobra.Command, args []string) {
+	cmd.SystemStart = bootstrap.Start
+	cmd.Namespace = cobraCommand.Flag("namespace").Value.String()
+	cmd.Platform = cobraCommand.Flag("platform").Value.String()
+}
+
+func (cmd *CmdSystemStart) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if args != nil && len(args) > 0 {
+		validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments"))
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSystemStart) InputToOptions() {
+
+	if cmd.Namespace == "" {
+		cmd.Namespace = "default"
+	}
+}
+
+func (cmd *CmdSystemStart) Run() error {
+	err := cmd.SystemStart(cmd.Namespace)
+
+	if err != nil {
+		return fmt.Errorf("failed to start router: %s", err)
+	}
+
+	fmt.Printf("%s-skupper-router is now started\n", cmd.Namespace)
+
+	return nil
+}
+
+func (cmd *CmdSystemStart) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/nonkube/system_stop.go b/internal/cmd/skupper/system/nonkube/system_stop.go
new file mode 100644
index 00000000..b894a8c9
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_stop.go
@@ -0,0 +1,60 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"github.com/spf13/cobra"
+)
+
+type CmdSystemStop struct {
+	CobraCmd   *cobra.Command
+	Namespace  string
+	Platform   string
+	SystemStop func(service string) error
+}
+
+func NewCmdSystemStop() *CmdSystemStop {
+
+	skupperCmd := CmdSystemStop{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemStop) NewClient(cobraCommand *cobra.Command, args []string) {
+	cmd.SystemStop = bootstrap.Stop
+	cmd.Namespace = cobraCommand.Flag("namespace").Value.String()
+	cmd.Platform = cobraCommand.Flag("platform").Value.String()
+}
+
+func (cmd *CmdSystemStop) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if args != nil && len(args) > 0 {
+		validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments"))
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSystemStop) InputToOptions() {
+
+	if cmd.Namespace == "" {
+		cmd.Namespace = "default"
+	}
+
+}
+
+func (cmd *CmdSystemStop) Run() error {
+
+	err := cmd.SystemStop(cmd.Namespace)
+
+	if err != nil {
+		return fmt.Errorf("failed to stop router: %s", err)
+	}
+
+	fmt.Printf("%s-skupper-router is now stopped \n", cmd.Namespace)
+
+	return nil
+}
+
+func (cmd *CmdSystemStop) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/nonkube/system_teardown.go b/internal/cmd/skupper/system/nonkube/system_teardown.go
new file mode 100644
index 00000000..7bf60a3b
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_teardown.go
@@ -0,0 +1,56 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"github.com/spf13/cobra"
+)
+
+type CmdSystemTeardown struct {
+	CobraCmd  *cobra.Command
+	TearDown  func(namespace string, platform string) error
+	Namespace string
+	Platform  string
+}
+
+func NewCmdSystemTeardown() *CmdSystemTeardown {
+
+	skupperCmd := CmdSystemTeardown{}
+
+	return &skupperCmd
+}
+
+func (cmd *CmdSystemTeardown) NewClient(cobraCommand *cobra.Command, args []string) {
+	cmd.TearDown = bootstrap.Teardown
+	cmd.Namespace = cobraCommand.Flag("namespace").Value.String()
+	cmd.Platform = cobraCommand.Flag("platform").Value.String()
+}
+
+func (cmd *CmdSystemTeardown) ValidateInput(args []string) []error {
+	var validationErrors []error
+
+	if args != nil && len(args) > 0 {
+		validationErrors = append(validationErrors, fmt.Errorf("this command does not accept arguments"))
+	}
+
+	return validationErrors
+}
+
+func (cmd *CmdSystemTeardown) InputToOptions() {
+	if cmd.Namespace == "" {
+		cmd.Namespace = "default"
+	}
+}
+
+func (cmd *CmdSystemTeardown) Run() error {
+
+	err := cmd.TearDown(cmd.Namespace, cmd.Platform)
+
+	if err != nil {
+		return fmt.Errorf("System teardown has failed: %s", err)
+	}
+
+	return nil
+}
+
+func (cmd *CmdSystemTeardown) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/system/system.go b/internal/cmd/skupper/system/system.go
new file mode 100644
index 00000000..9ea0e8ef
--- /dev/null
+++ b/internal/cmd/skupper/system/system.go
@@ -0,0 +1,166 @@
+package system
+
+import (
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/system/kube"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/system/nonkube"
+	"github.com/skupperproject/skupper/internal/config"
+
+	"github.com/spf13/cobra"
+)
+
+var (
+	systemSetupDescription = `
+Bootstraps a nonkube Skupper site base on the provided flags.
+
+When the path (--path) flag is provided, it will be used as the source
+directory containing the Skupper custom resources to be processed,
+generating a local Skupper site using the "default" namespace, unless
+a namespace is set in the custom resources, or if the namespace (-n)
+flag is provided.
+
+A namespace is just a directory in the file system where all site specific
+files are stored, like certificates, configurations, the original sources
+(original custom resources used to bootstrap the nonkube site) and
+the runtime files generated during initialization.
+
+Namespaces are stored under ${XDG_DATA_HOME}/skupper/namespaces
+for regular users when XDG_DATA_HOME environment variable is set, or under
+${HOME}/.local/share/skupper/namespaces when it is not set.
+
+As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
+In case the path (--path) flag is omitted, Skupper will try to process
+custom resources stored at the input/resources directory of the default namespace,
+or from the namespace provided through the namespace (-n) flag.
+
+If the respective namespace already exists and you want to bootstrap it
+over, you must provide the force (-f) flag. When you do that, the existing
+Certificate Authorities (CAs) are preserved, so eventual existing incoming
+links should be able to reconnect.
+
+To produce a bundle, instead of rendering a site, the bundle strategy (-b)
+flag must be set to "bundle" or "tarball".
+`
+)
+
+func NewCmdSystem() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "system",
+		Short: "non-kubernetes sites are static and Custom Resources need to be provided.",
+		Long: `Non-kubernetes sites can be created using the standard V2 site declaration 
+approach, which is based on the new set of Custom Resource Definitions (CRDs).`,
+		Example: "system setup --path ./my-config-path -n my-namespace",
+	}
+
+	cmd.AddCommand(CmdSystemSetupFactory(config.GetPlatform()))
+	cmd.AddCommand(CmdSystemReloadFactory(config.GetPlatform()))
+	cmd.AddCommand(CmdSystemStartFactory(config.GetPlatform()))
+	cmd.AddCommand(CmdSystemStopFactory(config.GetPlatform()))
+	cmd.AddCommand(CmdSystemTeardownFactory(config.GetPlatform()))
+
+	return cmd
+}
+
+func CmdSystemSetupFactory(configuredPlatform types.Platform) *cobra.Command {
+
+	//This implementation will warn the user that the command is not available for Kubernetes environments.
+	kubeCommand := kube.NewCmdSystemSetup()
+	nonKubeCommand := nonkube.NewCmdSystemSetup()
+
+	cmdSystemStartDesc := common.SkupperCmdDescription{
+		Use:   "setup",
+		Short: "Create a non-kube site based on provided Skupper Custom Resources",
+		Long:  systemSetupDescription,
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSystemStartDesc, kubeCommand, nonKubeCommand)
+
+	cmdFlags := common.CommandSystemSetupFlags{}
+
+	cmd.Flags().StringVar(&cmdFlags.Path, common.FlagNamePath, "", common.FlagDescPath)
+	cmd.Flags().StringVarP(&cmdFlags.Strategy, common.FlagNameStrategy, "b", "", common.FlagDescStrategy)
+	cmd.Flags().BoolVarP(&cmdFlags.Force, common.FlagNameForce, "f", false, common.FlagDescForce)
+
+	kubeCommand.CobraCmd = cmd
+	kubeCommand.Flags = &cmdFlags
+	nonKubeCommand.CobraCmd = cmd
+	nonKubeCommand.Flags = &cmdFlags
+
+	return cmd
+}
+
+func CmdSystemReloadFactory(configuredPlatform types.Platform) *cobra.Command {
+
+	//This implementation will warn the user that the command is not available for Kubernetes environments.
+	kubeCommand := kube.NewCmdSystemReload()
+	nonKubeCommand := nonkube.NewCmdSystemReload()
+
+	cmdSystemReloadDesc := common.SkupperCmdDescription{
+		Use:     "reload",
+		Short:   "Forces to overwrite an existing namespace based on input/resources",
+		Long:    "Forces to overwrite an existing namespace based on input/resources, if the namespace is not provided, the default one is going to be reloaded",
+		Example: "skupper system reload -n my-namespace",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSystemReloadDesc, kubeCommand, nonKubeCommand)
+
+	kubeCommand.CobraCmd = cmd
+	nonKubeCommand.CobraCmd = cmd
+
+	return cmd
+}
+
+func CmdSystemStartFactory(configuredPlatform types.Platform) *cobra.Command {
+
+	//This implementation will warn the user that the command is not available for Kubernetes environments.
+	kubeCommand := kube.NewCmdCmdSystemStart()
+	nonKubeCommand := nonkube.NewCmdCmdSystemStart()
+
+	cmdSystemStartDesc := common.SkupperCmdDescription{
+		Use:     "start",
+		Short:   "Start the Skupper components for the current site",
+		Long:    "Start down the Skupper components for the current site",
+		Example: "skupper system start -n my-namespace",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSystemStartDesc, kubeCommand, nonKubeCommand)
+
+	return cmd
+}
+
+func CmdSystemStopFactory(configuredPlatform types.Platform) *cobra.Command {
+
+	//This implementation will warn the user that the command is not available for Kubernetes environments.
+	kubeCommand := kube.NewCmdSystemStop()
+	nonKubeCommand := nonkube.NewCmdSystemStop()
+
+	cmdSystemStopDesc := common.SkupperCmdDescription{
+		Use:     "stop",
+		Short:   "Shut down the Skupper components for the current site",
+		Long:    "Shut down the Skupper components for the current site",
+		Example: "skupper system stop -n my-namespace",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSystemStopDesc, kubeCommand, nonKubeCommand)
+
+	return cmd
+}
+
+func CmdSystemTeardownFactory(configuredPlatform types.Platform) *cobra.Command {
+
+	//This implementation will warn the user that the command is not available for Kubernetes environments.
+	kubeCommand := kube.NewCmdSystemTeardown()
+	nonKubeCommand := nonkube.NewCmdSystemTeardown()
+
+	cmdSystemTeardownDesc := common.SkupperCmdDescription{
+		Use:     "teardown",
+		Short:   "Remove the Skupper components and resources from the from the current namespace",
+		Long:    "Remove the Skupper components and resources from the current namespace",
+		Example: "skupper system teardown -n my-namespace",
+	}
+
+	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdSystemTeardownDesc, kubeCommand, nonKubeCommand)
+
+	return cmd
+}
diff --git a/pkg/apis/skupper/v2alpha1/types.go b/pkg/apis/skupper/v2alpha1/types.go
index 5394298a..785390f2 100644
--- a/pkg/apis/skupper/v2alpha1/types.go
+++ b/pkg/apis/skupper/v2alpha1/types.go
@@ -596,13 +596,14 @@ type LinkStatus struct {
 type AccessToken struct {
 	v1.TypeMeta   `json:",inline"`
 	v1.ObjectMeta `json:"metadata,omitempty"`
-	Spec          AccessTokenSpec `json:"spec,omitempty"`
-	Status        Status          `json:"status,omitempty"`
+	Spec          AccessTokenSpec   `json:"spec,omitempty"`
+	Status        AccessTokenStatus `json:"status,omitempty"`
 }
 
 func (t *AccessToken) SetRedeemed(err error) bool {
 	state := ErrorOrReadyCondition(err)
 	if t.Status.SetCondition(CONDITION_TYPE_REDEEMED, ErrorOrReadyCondition(err), t.ObjectMeta.Generation) {
+		t.Status.Redeemed = t.IsRedeemed()
 		t.Status.StatusType = state.Reason
 		t.Status.Message = state.Message
 		return true
diff --git a/pkg/images/image_utils.go b/pkg/images/image_utils.go
index df519deb..faffd3c2 100644
--- a/pkg/images/image_utils.go
+++ b/pkg/images/image_utils.go
@@ -20,7 +20,7 @@ const (
 	ControllerImageEnvKey         string = "SKUPPER_CONTROLLER_IMAGE"
 	ConfigSyncImageEnvKey         string = "SKUPPER_CONFIG_SYNC_IMAGE"
 	NetworkObserverImageEnvKey    string = "SKUPPER_NETWORK_OBSERVER_IMAGE"
-	BootstrapImageEnvKey          string = "BOOTSTRAP_IMAGE"
+	CliImageEnvKey                string = "SKUPPER_CLI_IMAGE"
 	PrometheusServerImageEnvKey   string = "PROMETHEUS_SERVER_IMAGE"
 	OauthProxyImageEnvKey         string = "OAUTH_PROXY_IMAGE"
 	RouterPullPolicyEnvKey        string = "SKUPPER_ROUTER_IMAGE_PULL_POLICY"
@@ -94,11 +94,11 @@ func GetNetworkObserverImageName() string {
 	}
 }
 
-func GetBootstrapImageName() string {
-	image := os.Getenv(BootstrapImageEnvKey)
+func GetCliImageName() string {
+	image := os.Getenv(CliImageEnvKey)
 	if image == "" {
 		imageRegistry := GetImageRegistry()
-		return strings.Join([]string{imageRegistry, BootstrapImageName}, "/")
+		return strings.Join([]string{imageRegistry, CliImageName}, "/")
 	} else {
 		return image
 	}
@@ -242,8 +242,8 @@ func GetImages(component string, enableSHA bool) []SkupperImage {
 	case "network-observer":
 		names[NetworkObserverImageEnvKey] = NetworkObserverImageName
 		registry = GetImageRegistry()
-	case "bootstrap":
-		names[BootstrapImageEnvKey] = BootstrapImageName
+	case "cli":
+		names[CliImageEnvKey] = CliImageName
 		registry = GetImageRegistry()
 	case "prometheus":
 		names[PrometheusServerImageEnvKey] = PrometheusServerImageName
@@ -279,8 +279,8 @@ func GetImageVersion(component string) string {
 		if image == "" {
 			image = NetworkObserverImageName
 		}
-	case "bootstrap":
-		image = os.Getenv(BootstrapImageName)
+	case "cli":
+		image = os.Getenv(CliImageName)
 		if image == "" {
 			image = ControllerImageName
 		}
diff --git a/pkg/images/images.go b/pkg/images/images.go
index 09261bb3..0e7c7560 100644
--- a/pkg/images/images.go
+++ b/pkg/images/images.go
@@ -6,7 +6,7 @@ const (
 	ControllerImageName      string = "controller:v2-latest"
 	ConfigSyncImageName      string = "config-sync:v2-latest"
 	NetworkObserverImageName string = "network-observer:v2-latest"
-	BootstrapImageName       string = "bootstrap:v2-latest"
+	CliImageName             string = "cli:v2-latest"
 
 	PrometheusImageRegistry   string = "quay.io/prometheus"
 	PrometheusServerImageName string = "prometheus:v2.42.0"
@@ -15,7 +15,7 @@ const (
 )
 
 var (
-	KubeComponents    = []string{"router", "controller", "network-observer", "bootstrap", "prometheus", "origin-oauth-proxy"}
-	NonKubeComponents = []string{"router", "network-observer", "bootstrap", "prometheus", "origin-oauth-proxy"}
+	KubeComponents    = []string{"router", "controller", "network-observer", "cli", "prometheus", "origin-oauth-proxy"}
+	NonKubeComponents = []string{"router", "network-observer", "cli", "prometheus", "origin-oauth-proxy"}
 	DefaultComponents = []string{"router", "prometheus", "origin-oauth-proxy"}
 )
diff --git a/pkg/kube/grants/redeem.go b/pkg/kube/grants/redeem.go
index 3f8397db..202f4a36 100644
--- a/pkg/kube/grants/redeem.go
+++ b/pkg/kube/grants/redeem.go
@@ -49,17 +49,17 @@ func postTokenRequest(token *skupperv2alpha1.AccessToken, site *skupperv2alpha1.
 	}
 	request, err := http.NewRequest(http.MethodPost, token.Spec.Url, bytes.NewReader([]byte(token.Spec.Code)))
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Controller got error: %s", err)
 	}
 	request.Header.Add("name", token.Name)
 	request.Header.Add("subject", string(site.ObjectMeta.UID))
 	response, err := client.Do(request)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Controller got error: %s", err)
 	}
 	if response.StatusCode != http.StatusOK {
 		body, _ := io.ReadAll(response.Body)
-		return nil, fmt.Errorf("%d (%s) %s", response.StatusCode, http.StatusText(response.StatusCode), strings.TrimSpace(string(body)))
+		return nil, fmt.Errorf("Controller got failed response: %d (%s) %s", response.StatusCode, http.StatusText(response.StatusCode), strings.TrimSpace(string(body)))
 	}
 	return response.Body, nil
 }
@@ -68,7 +68,7 @@ func handleTokenResponse(body io.Reader, token *skupperv2alpha1.AccessToken, sit
 	decoder := newLinkDecoder(body)
 	if err := decoder.decodeAll(); err != nil {
 		log.Printf("Could not decode response for AccessToken %s/%s: %s", token.Namespace, token.Name, err)
-		return updateAccessTokenStatus(token, errors.New("Could not decode response"), clients)
+		return updateAccessTokenStatus(token, errors.New("Controller could not decode response"), clients)
 	}
 	refs := []metav1.OwnerReference{
 		{
@@ -80,7 +80,7 @@ func handleTokenResponse(body io.Reader, token *skupperv2alpha1.AccessToken, sit
 	}
 	decoder.secret.ObjectMeta.OwnerReferences = refs
 	if _, err := clients.GetKubeClient().CoreV1().Secrets(token.ObjectMeta.Namespace).Create(context.TODO(), &decoder.secret, metav1.CreateOptions{}); err != nil {
-		return updateAccessTokenStatus(token, fmt.Errorf("Could not create received secret: %s", err), clients)
+		return updateAccessTokenStatus(token, fmt.Errorf("Controller could not create received secret: %s", err), clients)
 	}
 	for _, link := range decoder.links {
 		link.ObjectMeta.OwnerReferences = refs
@@ -88,7 +88,7 @@ func handleTokenResponse(body io.Reader, token *skupperv2alpha1.AccessToken, sit
 			link.Spec.Cost = token.Spec.LinkCost
 		}
 		if _, err := clients.GetSkupperClient().SkupperV2alpha1().Links(token.ObjectMeta.Namespace).Create(context.TODO(), &link, metav1.CreateOptions{}); err != nil {
-			return updateAccessTokenStatus(token, fmt.Errorf("Could not create received link: %s", err), clients)
+			return updateAccessTokenStatus(token, fmt.Errorf("Controller could not create received link: %s", err), clients)
 		}
 	}
 
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 79a1b688..8b83a18b 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -61,9 +61,9 @@ func NewSite(namespace string, controller *kube.Controller, certs certificates.C
 	}
 }
 
-func (s *Site) Recover(site *skupperv2alpha1.Site) error {
+func (s *Site) StartRecovery(site *skupperv2alpha1.Site) error {
 	//TODO: check version and perform any necessary update tasks
-	return s.Reconcile(site)
+	return s.reconcile(site, true)
 }
 
 func (s *Site) isEdge() bool {
@@ -81,11 +81,11 @@ func (s *Site) routerMode() qdr.Mode {
 const SSL_PROFILE_PATH = "/etc/skupper-router-certs"
 
 func (s *Site) Reconcile(siteDef *skupperv2alpha1.Site) error {
-	err := s.reconcile(siteDef)
+	err := s.reconcile(siteDef, false)
 	return s.updateConfigured(err)
 }
 
-func (s *Site) reconcile(siteDef *skupperv2alpha1.Site) error {
+func (s *Site) reconcile(siteDef *skupperv2alpha1.Site, inRecovery bool) error {
 	if s.site != nil && s.site.Name != siteDef.Name {
 		s.logger.Error("Rejecting sitedef as active site already exists in the namespace",
 			slog.String("sitedef_namespace", siteDef.Namespace),
@@ -146,12 +146,13 @@ func (s *Site) reconcile(siteDef *skupperv2alpha1.Site) error {
 			s.logger.Info("Router config created for site",
 				slog.String("namespace", siteDef.Namespace),
 				slog.String("name", siteDef.Name))
-		} else {
+		} else if !inRecovery {
 			//TODO: include any RouterAccess configuration
 			if err := s.updateRouterConfigForGroups(ConfigUpdateList{s.bindings, s}); err != nil {
 				return err
 			}
 		}
+		s.setBindingsConfiguredStatus(nil)
 		s.checkSecuredAccess()
 	} else {
 		if err := s.updateRouterConfigForGroups(s); err != nil {
@@ -678,7 +679,10 @@ func (s *Site) updateConnectorConfiguredStatusWithSelectedPods(connector *skuppe
 
 func (s *Site) CheckConnector(name string, connector *skupperv2alpha1.Connector) error {
 	update := s.bindings.UpdateConnector(name, connector)
-	if update == nil || s.site == nil {
+	if s.site == nil {
+		return s.updateConnectorConfiguredStatus(connector, stderrors.New("No active site in namespace"))
+	}
+	if update == nil {
 		return nil
 	}
 	err := s.updateRouterConfigForGroups(update)
@@ -696,12 +700,15 @@ func (s *Site) updateListenerStatus(listener *skupperv2alpha1.Listener, err erro
 		}
 		s.bindings.UpdateListener(updated.Name, updated)
 	}
-	return err
+	return nil
 }
 
 func (s *Site) CheckListener(name string, listener *skupperv2alpha1.Listener) error {
 	update, err1 := s.bindings.UpdateListener(name, listener)
-	if update == nil || s.site == nil {
+	if s.site == nil {
+		return s.updateListenerStatus(listener, stderrors.New("No active site in namespace"))
+	}
+	if update == nil {
 		return nil
 	}
 	err2 := s.updateRouterConfigForGroups(update)
@@ -711,6 +718,38 @@ func (s *Site) CheckListener(name string, listener *skupperv2alpha1.Listener) er
 	return s.updateListenerStatus(listener, stderrors.Join(err1, err2))
 }
 
+func (s *Site) setBindingsConfiguredStatus(err error) {
+	lf := func(listener *skupperv2alpha1.Listener) *skupperv2alpha1.Listener {
+		if listener.SetConfigured(nil) {
+			updated, err := s.controller.GetSkupperClient().SkupperV2alpha1().Listeners(listener.ObjectMeta.Namespace).UpdateStatus(context.TODO(), listener, metav1.UpdateOptions{})
+			if err == nil {
+				return updated
+			} else {
+				s.logger.Error("Could not update listener status",
+					slog.String("namespace", listener.ObjectMeta.Namespace),
+					slog.String("listener", listener.ObjectMeta.Name),
+					slog.Any("error", err))
+			}
+		}
+		return nil
+	}
+	cf := func(connector *skupperv2alpha1.Connector) *skupperv2alpha1.Connector {
+		if connector.SetConfigured(nil) {
+			updated, err := s.controller.GetSkupperClient().SkupperV2alpha1().Connectors(connector.ObjectMeta.Namespace).UpdateStatus(context.TODO(), connector, metav1.UpdateOptions{})
+			if err == nil {
+				return updated
+			} else {
+				s.logger.Error("Could not update connector status",
+					slog.String("namespace", connector.ObjectMeta.Namespace),
+					slog.String("connector", connector.ObjectMeta.Name),
+					slog.Any("error", err))
+			}
+		}
+		return nil
+	}
+	s.bindings.Map(cf, lf)
+}
+
 func (s *Site) newLink(linkconfig *skupperv2alpha1.Link) *site.Link {
 	config := site.NewLink(linkconfig.ObjectMeta.Name, SSL_PROFILE_PATH)
 	config.Update(linkconfig)
@@ -752,6 +791,7 @@ func (s *Site) link(linkconfig *skupperv2alpha1.Link) error {
 		s.logger.Info("Site is not yet initialised, cannot configure router for link",
 			slog.String("namespace", linkconfig.ObjectMeta.Namespace),
 			slog.String("token", linkconfig.ObjectMeta.Name))
+		return s.updateLinkConfiguredCondition(linkconfig, stderrors.New("No active site in namespace"))
 	}
 	return nil
 }
@@ -793,6 +833,7 @@ func (s *Site) Deleted() {
 		slog.String("namespace", s.namespace),
 		slog.String("name", s.name))
 	s.bindings.cleanup()
+	s.setBindingsConfiguredStatus(stderrors.New("No active site"))
 }
 
 func (s *Site) setDefaultIssuerInStatus() bool {
diff --git a/pkg/nonkube/bootstrap/bootstrap.go b/pkg/nonkube/bootstrap/bootstrap.go
new file mode 100644
index 00000000..074b6b71
--- /dev/null
+++ b/pkg/nonkube/bootstrap/bootstrap.go
@@ -0,0 +1,184 @@
+package bootstrap
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
+	internalutils "github.com/skupperproject/skupper/internal/utils"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/bundle"
+	"github.com/skupperproject/skupper/pkg/nonkube/common"
+	"github.com/skupperproject/skupper/pkg/nonkube/compat"
+	"github.com/skupperproject/skupper/pkg/nonkube/systemd"
+	"github.com/skupperproject/skupper/pkg/utils"
+	"os"
+	"os/exec"
+	"path"
+)
+
+type Config struct {
+	InputPath      string
+	Namespace      string
+	BundleStrategy string
+	IsBundle       bool
+	Platform       types.Platform
+	Binary         string
+}
+
+func PreBootstrap(config *Config) error {
+
+	existingPath := api.GetInternalOutputPath(config.Namespace, api.InputSiteStatePath)
+	inputSourcesDefined := false
+	if _, err := os.Stat(existingPath); err == nil {
+		dirReader := new(internalutils.DirectoryReader)
+		filesFound, _ := dirReader.ReadDir(existingPath, nil)
+		inputSourcesDefined = len(filesFound) > 0
+	}
+	if config.InputPath == "" {
+		// when input path is empty, but a namespace is provided, try to reload an existing site definition
+		if inputSourcesDefined {
+			config.InputPath = existingPath
+			fmt.Printf("Sources will be consumed from namespace %q\n", config.Namespace)
+		} else {
+			fmt.Printf("Input path has not been provided and namespace %s does not exist\n", config.Namespace)
+			return fmt.Errorf("No sources found at: %s\n", path.Join(api.GetHostNamespaceHome(config.Namespace), string(api.InputSiteStatePath)))
+		}
+	} else if inputSourcesDefined {
+		return fmt.Errorf("Input path has been provided, but namespace %s has input sources defined at:\n %s\n", config.Namespace, path.Join(api.GetHostNamespaceHome(config.Namespace), string(api.InputSiteStatePath)))
+	}
+
+	if api.IsRunningInContainer() {
+		requiredPaths := []string{"/output"}
+		if config.InputPath != "" {
+			requiredPaths = append(requiredPaths, "/input")
+		}
+		for _, directory := range requiredPaths {
+			stat, err := os.Stat(directory)
+			if err != nil {
+				return fmt.Errorf("Failed to stat %s: %s\n", directory, err)
+			}
+			if !stat.IsDir() {
+				return fmt.Errorf("%s is not a directory\n", directory)
+			}
+		}
+	} else if !config.IsBundle {
+
+		_, err := exec.LookPath(config.Binary)
+		if err != nil {
+			return fmt.Errorf("Platform %q is not available:\n ERROR! Command not found: %s", config.Platform, config.Binary)
+		}
+	}
+
+	return nil
+}
+
+func Bootstrap(config *Config) (*api.SiteState, error) {
+	var siteStateLoader api.SiteStateLoader
+	var reloadExisting bool
+
+	sourcesPath := api.GetInternalOutputPath(config.Namespace, api.InputSiteStatePath)
+	_, err := os.Stat(sourcesPath)
+	if !config.IsBundle && err == nil {
+		reloadExisting = true
+		nsPlatformLoader := &common.NamespacePlatformLoader{}
+		nsPlatform, err := nsPlatformLoader.Load(config.Namespace)
+		if err != nil {
+			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(config.Namespace, api.RuntimeSiteStatePath))
+			if runtimeStateErr == nil {
+				return nil, fmt.Errorf("unable to determine current platform used in namespace %q", config.Namespace)
+			}
+			// platform.yaml not present, which is ok if a site is not yet rendered
+			nsPlatform = string(config.Platform)
+			reloadExisting = false
+		}
+		currentPlatform := string(config.Platform)
+		if config.Platform.IsKubernetes() {
+			currentPlatform = "podman"
+		}
+		if nsPlatform != currentPlatform {
+			return nil, fmt.Errorf("existing namespace uses %q platform and it cannot change to %q", nsPlatform, currentPlatform)
+		}
+	}
+	siteStateLoader = &common.FileSystemSiteStateLoader{
+		Path:   config.InputPath,
+		Bundle: config.IsBundle,
+	}
+
+	siteState, err := siteStateLoader.Load()
+	if err != nil {
+		return nil, fmt.Errorf("failed to load site state: %v", err)
+	}
+	// if sources are being consume from namespace sources, they must be properly set
+	crNamespace := siteState.GetNamespace()
+	targetNamespace := utils.DefaultStr(config.Namespace, "default")
+	if config.InputPath == sourcesPath {
+		if crNamespace != targetNamespace {
+			return nil, fmt.Errorf("namespace must be %q, but sources are defined using %q", targetNamespace, crNamespace)
+		}
+	} else if config.Namespace != "" {
+		siteState.SetNamespace(config.Namespace)
+	}
+
+	var siteStateRenderer api.StaticSiteStateRenderer
+	if config.IsBundle {
+		siteStateRenderer = &bundle.SiteStateRenderer{
+			Strategy: internalbundle.BundleStrategy(config.BundleStrategy),
+			Platform: config.Platform,
+		}
+	} else if config.Platform == types.PlatformSystemd {
+		siteStateRenderer = &systemd.SiteStateRenderer{}
+	} else {
+		siteStateRenderer = &compat.SiteStateRenderer{
+			Platform: config.Platform,
+		}
+	}
+	err = siteStateRenderer.Render(siteState, reloadExisting)
+	if err != nil {
+		return nil, fmt.Errorf("failed to render site state: %v", err)
+	}
+	return siteState, nil
+}
+
+func PostBootstrap(config *Config, siteState *api.SiteState) {
+	var bundleSuffix string
+	if config.IsBundle {
+		bundleSuffix = " (as a distributable bundle)"
+	} else {
+		bundleSuffix = fmt.Sprintf(" on namespace %q", siteState.GetNamespace())
+		// create bootstrap.out file
+		if api.IsRunningInContainer() {
+			outFile, err := os.Stat("/bootstrap.out")
+			if err == nil && !outFile.IsDir() {
+				err = os.WriteFile("/bootstrap.out", []byte(siteState.GetNamespace()), 0644)
+				if err != nil {
+					fmt.Println("Failed to write to bootstrap.out:", err)
+					fmt.Println("The systemd service will not be created.")
+				}
+			}
+		}
+	}
+	fmt.Printf("Site %q has been created%s\n", siteState.Site.Name, bundleSuffix)
+	if !config.IsBundle {
+		fmt.Printf("Platform: %s\n", config.Platform)
+		tokenPath := api.GetInternalOutputPath(siteState.Site.Namespace, api.RuntimeTokenPath)
+		hostTokenPath := api.GetHostSiteInternalPath(siteState.Site, api.RuntimeTokenPath)
+		tokens, _ := os.ReadDir(tokenPath)
+		for _, token := range tokens {
+			if !token.IsDir() {
+				fmt.Println("Static links have been defined at:", hostTokenPath)
+				break
+			}
+		}
+		sourcesPath := api.GetHostSiteInternalPath(siteState.Site, api.InputSiteStatePath)
+		fmt.Printf("Definition is available at: %s\n", sourcesPath)
+	} else {
+		siteHome := api.GetHostBundlesPath()
+		installationFile := path.Join(siteHome, fmt.Sprintf("skupper-install-%s.sh", siteState.Site.Name))
+		if internalbundle.GetBundleStrategy(config.BundleStrategy) == string(internalbundle.BundleStrategyTarball) {
+			installationFile = path.Join(siteHome, fmt.Sprintf("skupper-install-%s.tar.gz", siteState.Site.Name))
+		}
+		fmt.Println("Installation bundle available at:", installationFile)
+		fmt.Println("Default namespace:", siteState.GetNamespace())
+		fmt.Println("Default platform:", string(config.Platform))
+	}
+}
diff --git a/pkg/nonkube/bootstrap/start.go b/pkg/nonkube/bootstrap/start.go
new file mode 100644
index 00000000..775d1926
--- /dev/null
+++ b/pkg/nonkube/bootstrap/start.go
@@ -0,0 +1,30 @@
+package bootstrap
+
+import (
+	"fmt"
+	internalclient "github.com/skupperproject/skupper/internal/nonkube/client/compat"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"os"
+)
+
+func Start(namespace string) error {
+	_, err := os.Stat(api.GetHostNamespaceHome(namespace))
+	if err != nil {
+		return fmt.Errorf("there is no definition for namespace %s: %s", namespace, err)
+	}
+
+	cli, err := internalclient.NewCompatClient(os.Getenv("CONTAINER_ENDPOINT"), "")
+	if err != nil {
+		return fmt.Errorf("failed to create container client: %v", err)
+	}
+
+	containerName := namespace + "-skupper-router"
+	if _, err := cli.ContainerInspect(containerName); err == nil {
+		err = cli.ContainerStart(containerName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/nonkube/bootstrap/stop.go b/pkg/nonkube/bootstrap/stop.go
new file mode 100644
index 00000000..33cab926
--- /dev/null
+++ b/pkg/nonkube/bootstrap/stop.go
@@ -0,0 +1,31 @@
+package bootstrap
+
+import (
+	"fmt"
+	internalclient "github.com/skupperproject/skupper/internal/nonkube/client/compat"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"os"
+)
+
+func Stop(namespace string) error {
+
+	_, err := os.Stat(api.GetHostNamespaceHome(namespace))
+	if err != nil {
+		return fmt.Errorf("there is no definition for namespace %s: %s", namespace, err)
+	}
+
+	cli, err := internalclient.NewCompatClient(os.Getenv("CONTAINER_ENDPOINT"), "")
+	if err != nil {
+		return fmt.Errorf("failed to create container client: %v", err)
+	}
+
+	containerName := namespace + "-skupper-router"
+	if _, err := cli.ContainerInspect(containerName); err == nil {
+		err = cli.ContainerStop(containerName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/nonkube/bootstrap/teardown.go b/pkg/nonkube/bootstrap/teardown.go
new file mode 100644
index 00000000..d33467f5
--- /dev/null
+++ b/pkg/nonkube/bootstrap/teardown.go
@@ -0,0 +1,90 @@
+package bootstrap
+
+import (
+	"fmt"
+	internalclient "github.com/skupperproject/skupper/internal/nonkube/client/compat"
+	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/common"
+	"os"
+)
+
+type LocalData struct {
+	servicePath string
+	service     string
+}
+
+func Teardown(namespace string, platform string) error {
+
+	if err := removeRouter(namespace); err != nil {
+		return err
+	}
+
+	if err := removeService(namespace, platform); err != nil {
+		return err
+	}
+
+	if err := removeDefinition(namespace); err != nil {
+		return err
+	}
+
+	fmt.Printf("Namespace \"%s\" has been removed\n", namespace)
+	return nil
+
+}
+
+func removeDefinition(namespace string) error {
+
+	_, err := os.Stat(api.GetHostNamespaceHome(namespace))
+	if err != nil {
+		return err
+	}
+
+	return os.RemoveAll(api.GetHostNamespaceHome(namespace))
+}
+
+func removeRouter(namespace string) error {
+
+	cli, err := internalclient.NewCompatClient(os.Getenv("CONTAINER_ENDPOINT"), "")
+	if err != nil {
+		return fmt.Errorf("failed to create container client: %v", err)
+	}
+
+	containerName := namespace + "-skupper-router"
+	if _, err := cli.ContainerInspect(containerName); err == nil {
+		err = cli.ContainerRemove(containerName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func removeService(namespace string, platform string) error {
+
+	pathProvider := fs.PathProvider{Namespace: namespace}
+
+	siteStateLoader := &common.FileSystemSiteStateLoader{
+		Path: pathProvider.GetRuntimeNamespace(),
+	}
+
+	siteState, err := siteStateLoader.Load()
+	if err != nil {
+		return err
+	}
+
+	systemdService, err := common.NewSystemdServiceInfo(siteState, platform)
+	if err != nil {
+		return err
+	}
+
+	if systemdService.GetServiceFile() != "" {
+		err = systemdService.Remove()
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/nonkube/common/systemd.go b/pkg/nonkube/common/systemd.go
index b50647c8..18f2cddb 100644
--- a/pkg/nonkube/common/systemd.go
+++ b/pkg/nonkube/common/systemd.go
@@ -31,6 +31,7 @@ type SystemdService interface {
 	GetServiceName() string
 	Create() error
 	Remove() error
+	GetServiceFile() string
 }
 
 type CommandExecutor func(name string, arg ...string) *exec.Cmd
@@ -100,7 +101,7 @@ func (s *systemdServiceInfo) Create() error {
 	}
 
 	// Creating the base dir
-	baseDir := filepath.Dir(s.getServiceFile())
+	baseDir := filepath.Dir(s.GetServiceFile())
 	if _, err := os.Stat(baseDir); err != nil {
 		if err = os.MkdirAll(baseDir, 0755); err != nil {
 			return fmt.Errorf("unable to create base directory %s - %q", baseDir, err)
@@ -109,10 +110,10 @@ func (s *systemdServiceInfo) Create() error {
 
 	// Saving systemd user service
 	serviceName := s.GetServiceName()
-	logger.Debug("writing service file", slog.String("path", s.getServiceFile()))
-	err = os.WriteFile(s.getServiceFile(), buf.Bytes(), 0644)
+	logger.Debug("writing service file", slog.String("path", s.GetServiceFile()))
+	err = os.WriteFile(s.GetServiceFile(), buf.Bytes(), 0644)
 	if err != nil {
-		return fmt.Errorf("unable to write unit file (%s): %w", s.getServiceFile(), err)
+		return fmt.Errorf("unable to write unit file (%s): %w", s.GetServiceFile(), err)
 	}
 
 	// Only enable when running locally
@@ -124,7 +125,7 @@ func (s *systemdServiceInfo) Create() error {
 	return nil
 }
 
-func (s *systemdServiceInfo) getServiceFile() string {
+func (s *systemdServiceInfo) GetServiceFile() string {
 	if api.IsRunningInContainer() {
 		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.ScriptsPath), s.GetServiceName())
 	}
@@ -154,8 +155,8 @@ func (s *systemdServiceInfo) Remove() error {
 	}
 
 	// Removing the .service file
-	logger.Debug("removing service", slog.String("path", s.getServiceFile()))
-	_ = os.Remove(s.getServiceFile())
+	logger.Debug("removing service", slog.String("path", s.GetServiceFile()))
+	_ = os.Remove(s.GetServiceFile())
 
 	// Reloading systemd user daemon
 	if !api.IsRunningInContainer() {
@@ -177,7 +178,7 @@ func (s *systemdServiceInfo) enableService(serviceName string) error {
 	cmd := s.getCmdEnableSystemdService(serviceName)
 	err := cmd.Run()
 	if err != nil {
-		return fmt.Errorf("unable to enable service (%s): %w", s.getServiceFile(), err)
+		return fmt.Errorf("unable to enable service (%s): %w", s.GetServiceFile(), err)
 	}
 
 	// Reloading systemd user daemon
diff --git a/pkg/utils/configs/manifest.go b/pkg/utils/configs/manifest.go
index 06d5d537..e39a1990 100644
--- a/pkg/utils/configs/manifest.go
+++ b/pkg/utils/configs/manifest.go
@@ -109,9 +109,9 @@ func getEnvironmentVariableMap() *map[string]string {
 		envVariables[images.NetworkObserverImageEnvKey] = networkObserverImage
 	}
 
-	bootstrapImage := os.Getenv(images.BootstrapImageEnvKey)
-	if bootstrapImage != "" {
-		envVariables[images.BootstrapImageEnvKey] = bootstrapImage
+	cliImage := os.Getenv(images.CliImageEnvKey)
+	if cliImage != "" {
+		envVariables[images.CliImageEnvKey] = cliImage
 	}
 
 	prometheusImage := os.Getenv(images.PrometheusServerImageEnvKey)


Test patch:
diff --git a/internal/cmd/skupper/connector/connector_test.go b/internal/cmd/skupper/connector/connector_test.go
index c88ddb1e..bd79a342 100644
--- a/internal/cmd/skupper/connector/connector_test.go
+++ b/internal/cmd/skupper/connector/connector_test.go
@@ -32,6 +32,7 @@ func TestCmdConnectorFactory(t *testing.T) {
 				common.FlagNameWorkload:            "",
 				common.FlagNameOutput:              "",
 				common.FlagNameTimeout:             "1m0s",
+				common.FlagNameWait:                "configured",
 			},
 			command: CmdConnectorCreateFactory(types.PlatformKubernetes),
 		},
@@ -48,6 +49,7 @@ func TestCmdConnectorFactory(t *testing.T) {
 				common.FlagNameOutput:              "",
 				common.FlagNameTimeout:             "1m0s",
 				common.FlagNameConnectorPort:       "0",
+				common.FlagNameWait:                "configured",
 			},
 			command: CmdConnectorUpdateFactory(types.PlatformKubernetes),
 		},
@@ -62,6 +64,7 @@ func TestCmdConnectorFactory(t *testing.T) {
 			name: "CmdConnectorDeleteFactory",
 			expectedFlagsWithDefaultValue: map[string]interface{}{
 				common.FlagNameTimeout: "1m0s",
+				common.FlagNameWait:    "true",
 			},
 			command: CmdConnectorDeleteFactory(types.PlatformKubernetes),
 		},
diff --git a/internal/cmd/skupper/connector/kube/connector_create_test.go b/internal/cmd/skupper/connector/kube/connector_create_test.go
index 6904347b..0d60839c 100644
--- a/internal/cmd/skupper/connector/kube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_create_test.go
@@ -540,6 +540,14 @@ func TestCmdConnectorCreate_ValidateWorkload(t *testing.T) {
 			expectedErrors:   []string{},
 			expectedSelector: "app=backend",
 		},
+		{
+			name:  "wait status is not valid",
+			args:  []string{"workload-deployment", "1234"},
+			flags: common.CommandConnectorCreateFlags{Timeout: time.Minute, Wait: "created"},
+			expectedErrors: []string{
+				"status is not valid: value created not allowed. It should be one of this options: [ready configured none]",
+			},
+		},
 	}
 
 	for _, test := range testTable {
@@ -575,12 +583,13 @@ func TestCmdConnectorCreate_InputToOptions(t *testing.T) {
 		expectedConnectorType  string
 		expectedOutput         string
 		expectedTimeout        time.Duration
+		expectedStatus         string
 	}
 
 	testTable := []test{
 		{
 			name:                   "test1",
-			flags:                  common.CommandConnectorCreateFlags{"backend", "", "app=backend", "secret", "tcp", true, "", 20 * time.Second, "json"},
+			flags:                  common.CommandConnectorCreateFlags{"backend", "", "app=backend", "secret", "tcp", true, "", 20 * time.Second, "json", "ready"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "",
 			expectedRoutingKey:     "backend",
@@ -588,10 +597,11 @@ func TestCmdConnectorCreate_InputToOptions(t *testing.T) {
 			expectedConnectorType:  "tcp",
 			expectedOutput:         "json",
 			expectedSelector:       "app=backend",
+			expectedStatus:         "ready",
 		},
 		{
 			name:                   "test2",
-			flags:                  common.CommandConnectorCreateFlags{"backend", "backend", "", "secret", "tcp", true, "", 20 * time.Second, "json"},
+			flags:                  common.CommandConnectorCreateFlags{"backend", "backend", "", "secret", "tcp", true, "", 20 * time.Second, "json", "configured"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "backend",
 			expectedRoutingKey:     "backend",
@@ -599,10 +609,11 @@ func TestCmdConnectorCreate_InputToOptions(t *testing.T) {
 			expectedConnectorType:  "tcp",
 			expectedOutput:         "json",
 			expectedSelector:       "",
+			expectedStatus:         "configured",
 		},
 		{
 			name:                   "test3",
-			flags:                  common.CommandConnectorCreateFlags{"", "", "", "secret", "tcp", false, "", 30 * time.Second, "yaml"},
+			flags:                  common.CommandConnectorCreateFlags{"", "", "", "secret", "tcp", false, "", 30 * time.Second, "yaml", "none"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "",
 			expectedRoutingKey:     "test3",
@@ -610,6 +621,7 @@ func TestCmdConnectorCreate_InputToOptions(t *testing.T) {
 			expectedConnectorType:  "tcp",
 			expectedOutput:         "yaml",
 			expectedSelector:       "app=test3",
+			expectedStatus:         "none",
 		},
 	}
 
@@ -631,6 +643,7 @@ func TestCmdConnectorCreate_InputToOptions(t *testing.T) {
 			assert.Check(t, cmd.timeout == test.expectedTimeout)
 			assert.Check(t, cmd.selector == test.expectedSelector)
 			assert.Check(t, cmd.connectorType == test.expectedConnectorType)
+			assert.Check(t, cmd.status == test.expectedStatus)
 		})
 	}
 }
@@ -703,6 +716,7 @@ func TestCmdConnectorCreate_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
 		output              string
+		status              string
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
@@ -728,7 +742,8 @@ func TestCmdConnectorCreate_WaitUntil(t *testing.T) {
 			expectError: true,
 		},
 		{
-			name: "connector is ready",
+			name:   "connector is ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Connector{
 					ObjectMeta: v1.ObjectMeta{
@@ -739,7 +754,7 @@ func TestCmdConnectorCreate_WaitUntil(t *testing.T) {
 						Status: v2alpha1.Status{
 							Conditions: []v1.Condition{
 								{
-									Type:   "Configured",
+									Type:   "Ready",
 									Status: "True",
 								},
 							},
@@ -772,6 +787,87 @@ func TestCmdConnectorCreate_WaitUntil(t *testing.T) {
 			},
 			expectError: false,
 		},
+		{
+			name:       "connector is not ready yet, but user waits for configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user does not wait",
+			status:     "none",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user waits for configured, but site had some errors while being configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "Error",
+									ObservedGeneration: 1,
+									Reason:             "Error",
+									Status:             "False",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -782,6 +878,7 @@ func TestCmdConnectorCreate_WaitUntil(t *testing.T) {
 		cmd.output = test.output
 		cmd.timeout = 1 * time.Second
 		cmd.namespace = "test"
+		cmd.status = test.status
 
 		t.Run(test.name, func(t *testing.T) {
 
diff --git a/internal/cmd/skupper/connector/kube/connector_delete_test.go b/internal/cmd/skupper/connector/kube/connector_delete_test.go
index e21ea838..c54a0b32 100644
--- a/internal/cmd/skupper/connector/kube/connector_delete_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_delete_test.go
@@ -140,6 +140,7 @@ func TestCmdConnectorDelete_Run(t *testing.T) {
 func TestCmdConnectorDelete_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
+		wait                bool
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
@@ -149,6 +150,7 @@ func TestCmdConnectorDelete_WaitUntil(t *testing.T) {
 	testTable := []test{
 		{
 			name: "error deleting connector",
+			wait: true,
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Connector{
 					ObjectMeta: v1.ObjectMeta{
@@ -171,6 +173,30 @@ func TestCmdConnectorDelete_WaitUntil(t *testing.T) {
 		},
 		{
 			name:        "connector is deleted",
+			wait:        true,
+			expectError: false,
+		},
+		{
+			name: "connector is not deleted but user does not want to wait",
+			wait: false,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
 			expectError: false,
 		},
 	}
@@ -182,6 +208,7 @@ func TestCmdConnectorDelete_WaitUntil(t *testing.T) {
 		cmd.name = "my-connector"
 		cmd.Flags = &common.CommandConnectorDeleteFlags{Timeout: 1 * time.Second}
 		cmd.namespace = "test"
+		cmd.wait = test.wait
 
 		t.Run(test.name, func(t *testing.T) {
 
@@ -195,6 +222,33 @@ func TestCmdConnectorDelete_WaitUntil(t *testing.T) {
 	}
 }
 
+func TestCmdConnectorDelete_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name         string
+		flags        *common.CommandConnectorDeleteFlags
+		expectedWait bool
+	}
+
+	testTable := []test{
+		{
+			name:         "wait for connector to be deleted",
+			flags:        &common.CommandConnectorDeleteFlags{Wait: true},
+			expectedWait: true,
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdConnectorDelete{}
+			command.Flags = test.flags
+			command.InputToOptions()
+
+			assert.Check(t, command.wait == test.expectedWait)
+		})
+	}
+}
+
 // --- helper methods
 
 func newCmdConnectorDeleteWithMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*CmdConnectorDelete, error) {
diff --git a/internal/cmd/skupper/connector/kube/connector_update_test.go b/internal/cmd/skupper/connector/kube/connector_update_test.go
index 80960bd5..130df872 100644
--- a/internal/cmd/skupper/connector/kube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_update_test.go
@@ -400,6 +400,32 @@ func TestCmdConnectorUpdate_ValidateInput(t *testing.T) {
 			},
 			expectedErrors: []string{},
 		},
+		{
+			name:  "wait status is not valid",
+			args:  []string{"backend-connector"},
+			flags: common.CommandConnectorUpdateFlags{Timeout: time.Minute, Wait: "created"},
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"status is not valid: value created not allowed. It should be one of this options: [ready configured none]",
+			},
+		},
 	}
 
 	for _, test := range testTable {
@@ -429,6 +455,7 @@ func TestCmdConnectorUpdate_ValidateWorkload(t *testing.T) {
 		skupperObjects   []runtime.Object
 		expectedErrors   []string
 		expectedSelector string
+		expectedStatus   string
 	}
 
 	testTable := []test{
@@ -1027,6 +1054,7 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
 		output              string
+		status              string
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
@@ -1052,7 +1080,8 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 			expectError: true,
 		},
 		{
-			name: "connector is ready",
+			name:   "connector is ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Connector{
 					ObjectMeta: v1.ObjectMeta{
@@ -1063,7 +1092,7 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 						Status: v2alpha1.Status{
 							Conditions: []v1.Condition{
 								{
-									Type:   "Configured",
+									Type:   "Ready",
 									Status: "True",
 								},
 							},
@@ -1096,6 +1125,87 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 			},
 			expectError: false,
 		},
+		{
+			name:       "connector is not ready yet, but user waits for configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user does not wait",
+			status:     "none",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Connector",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user waits for configured, but site had some errors while being configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-connector",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ConnectorStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "Error",
+									ObservedGeneration: 1,
+									Reason:             "Error",
+									Status:             "False",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -1109,6 +1219,7 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 		}
 		cmd.namespace = "test"
 		cmd.newSettings.output = cmd.Flags.Output
+		cmd.status = test.status
 
 		t.Run(test.name, func(t *testing.T) {
 
@@ -1122,6 +1233,36 @@ func TestCmdConnectorUpdate_WaitUntil(t *testing.T) {
 	}
 }
 
+func TestCmdConnectorUpdate_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name           string
+		args           []string
+		flags          common.CommandConnectorUpdateFlags
+		expectedStatus string
+	}
+
+	testTable := []test{
+		{
+			name:           "options with waiting status",
+			args:           []string{"backend-listener"},
+			flags:          common.CommandConnectorUpdateFlags{Wait: "configured"},
+			expectedStatus: "configured",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdConnectorUpdate{}
+			command.Flags = &test.flags
+
+			command.InputToOptions()
+
+			assert.Check(t, command.status == test.expectedStatus)
+		})
+	}
+}
+
 // --- helper methods
 
 func newCmdConnectorUpdateWithMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*CmdConnectorUpdate, error) {
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create_test.go b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
index 08472dd2..bb095a87 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
@@ -164,7 +164,7 @@ func TestNonKubeCmdConnectorCreate_InputToOptions(t *testing.T) {
 	testTable := []test{
 		{
 			name:                   "test1",
-			flags:                  common.CommandConnectorCreateFlags{"backend", "", "", "secret", "tcp", false, "", 0, "json"},
+			flags:                  common.CommandConnectorCreateFlags{"backend", "", "", "secret", "tcp", false, "", 0, "json", "none"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "",
 			expectedRoutingKey:     "backend",
@@ -175,7 +175,7 @@ func TestNonKubeCmdConnectorCreate_InputToOptions(t *testing.T) {
 		{
 			name:                   "test2",
 			namespace:              "test",
-			flags:                  common.CommandConnectorCreateFlags{"backend", "1.2.3.4", "", "secret", "tcp", false, "", 0, "json"},
+			flags:                  common.CommandConnectorCreateFlags{"backend", "1.2.3.4", "", "secret", "tcp", false, "", 0, "json", "configured"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "1.2.3.4",
 			expectedRoutingKey:     "backend",
@@ -186,7 +186,7 @@ func TestNonKubeCmdConnectorCreate_InputToOptions(t *testing.T) {
 		{
 			name:                   "test3",
 			namespace:              "test",
-			flags:                  common.CommandConnectorCreateFlags{"", "", "", "secret", "tcp", false, "", 0, "yaml"},
+			flags:                  common.CommandConnectorCreateFlags{"", "", "", "secret", "tcp", false, "", 0, "yaml", "ready"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "",
 			expectedRoutingKey:     "my-Connector",
diff --git a/internal/cmd/skupper/link/kube/link_delete_test.go b/internal/cmd/skupper/link/kube/link_delete_test.go
index 029a243a..918287b7 100644
--- a/internal/cmd/skupper/link/kube/link_delete_test.go
+++ b/internal/cmd/skupper/link/kube/link_delete_test.go
@@ -156,13 +156,15 @@ func TestCmdLinkDelete_InputToOptions(t *testing.T) {
 		name            string
 		flags           common.CommandLinkDeleteFlags
 		expectedTimeout time.Duration
+		expectedWait    bool
 	}
 
 	testTable := []test{
 		{
 			name:            "check options",
-			flags:           common.CommandLinkDeleteFlags{time.Minute},
+			flags:           common.CommandLinkDeleteFlags{Timeout: time.Minute, Wait: false},
 			expectedTimeout: time.Minute,
+			expectedWait:    false,
 		},
 	}
 
@@ -176,6 +178,7 @@ func TestCmdLinkDelete_InputToOptions(t *testing.T) {
 			cmd.InputToOptions()
 
 			assert.Check(t, cmd.timeout == test.expectedTimeout)
+			assert.Check(t, cmd.wait == test.expectedWait)
 		})
 	}
 }
@@ -230,6 +233,7 @@ func TestCmdLinkDelete_WaitUntil(t *testing.T) {
 	type test struct {
 		name           string
 		timeout        time.Duration
+		wait           bool
 		skupperObjects []runtime.Object
 		expectError    bool
 	}
@@ -267,6 +271,34 @@ func TestCmdLinkDelete_WaitUntil(t *testing.T) {
 			timeout:     time.Second,
 			expectError: false,
 		},
+		{
+			name: "link is not deleted but user does not want to wait",
+			wait: false,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-link",
+						Namespace: "test",
+					},
+					Status: v2alpha1.LinkStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			timeout:     time.Second,
+			expectError: false,
+		},
 	}
 
 	for _, test := range testTable {
@@ -274,6 +306,7 @@ func TestCmdLinkDelete_WaitUntil(t *testing.T) {
 		assert.Assert(t, err)
 		cmd.linkName = "my-link"
 		cmd.timeout = test.timeout
+		cmd.wait = test.wait
 		t.Run(test.name, func(t *testing.T) {
 
 			err := cmd.WaitUntil()
diff --git a/internal/cmd/skupper/link/kube/link_update_test.go b/internal/cmd/skupper/link/kube/link_update_test.go
index 6717eaff..9ff6fcb9 100644
--- a/internal/cmd/skupper/link/kube/link_update_test.go
+++ b/internal/cmd/skupper/link/kube/link_update_test.go
@@ -264,6 +264,33 @@ func TestCmdLinkUpdate_ValidateInput(t *testing.T) {
 				"timeout is not valid: duration must not be less than 10s; got 0s",
 			},
 		},
+		{
+			name:       "wait status is not valid",
+			args:       []string{"east-link"},
+			flags:      common.CommandLinkUpdateFlags{Cost: "1", Timeout: time.Minute, Wait: "created"},
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.SiteList{
+					Items: []v2alpha1.Site{
+						{
+							ObjectMeta: v1.ObjectMeta{
+								Name:      "the-site",
+								Namespace: "test",
+							},
+						},
+					},
+				},
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "east-link",
+						Namespace: "test",
+					},
+				},
+			},
+			expectedErrors: []string{
+				"status is not valid: value created not allowed. It should be one of this options: [ready configured none]",
+			},
+		},
 	}
 
 	for _, test := range testTable {
@@ -294,17 +321,19 @@ func TestCmdLinkUpdate_InputToOptions(t *testing.T) {
 		expectedCost           int
 		expectedOutput         string
 		expectedTimeout        time.Duration
+		expectedStatus         string
 	}
 
 	testTable := []test{
 		{
 			name:                   "check options",
 			args:                   []string{"my-link"},
-			flags:                  common.CommandLinkUpdateFlags{"secret", "1", "json", time.Minute},
+			flags:                  common.CommandLinkUpdateFlags{TlsCredentials: "secret", Cost: "1", Output: "json", Timeout: time.Minute, Wait: "ready"},
 			expectedCost:           1,
 			expectedTlsCredentials: "secret",
 			expectedOutput:         "json",
 			expectedTimeout:        time.Minute,
+			expectedStatus:         "ready",
 		},
 	}
 
@@ -321,6 +350,7 @@ func TestCmdLinkUpdate_InputToOptions(t *testing.T) {
 			assert.Check(t, cmd.tlsCredentials == test.expectedTlsCredentials)
 			assert.Check(t, cmd.cost == test.expectedCost)
 			assert.Check(t, cmd.timeout == test.expectedTimeout)
+			assert.Check(t, cmd.status == test.expectedStatus)
 		})
 	}
 }
@@ -417,6 +447,7 @@ func TestCmdLinkUpdate_Run(t *testing.T) {
 func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
+		status              string
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
@@ -428,7 +459,8 @@ func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 
 	testTable := []test{
 		{
-			name: "link is not configured",
+			name:   "link is not configured",
+			status: "configured",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Link{
 					ObjectMeta: v1.ObjectMeta{
@@ -456,7 +488,8 @@ func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 			expectError: false,
 		},
 		{
-			name: "link is configured",
+			name:   "link is ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Link{
 					ObjectMeta: v1.ObjectMeta{
@@ -472,7 +505,7 @@ func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 									ObservedGeneration: 1,
 									Reason:             "OK",
 									Status:             "True",
-									Type:               "Configured",
+									Type:               "Ready",
 								},
 							},
 						},
@@ -483,6 +516,96 @@ func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 			timeout:     time.Second,
 			expectError: false,
 		},
+		{
+			name:       "link is not ready yet, but user waits for configured",
+			status:     "configured",
+			timeout:    time.Second,
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-link",
+						Namespace: "test",
+					},
+					Status: v2alpha1.LinkStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			linkName:    "my-link",
+			expectError: false,
+		},
+		{
+			name:       "user does not wait",
+			status:     "none",
+			timeout:    time.Second,
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-link",
+						Namespace: "test",
+					},
+					Status: v2alpha1.LinkStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "OK",
+									ObservedGeneration: 1,
+									Reason:             "OK",
+									Status:             "True",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			linkName:    "my-link",
+			expectError: false,
+		},
+		{
+			name:       "user waits for configured, but link had some errors while being configured",
+			status:     "configured",
+			timeout:    time.Second,
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-link",
+						Namespace: "test",
+					},
+					Status: v2alpha1.LinkStatus{
+						Status: v2alpha1.Status{
+							Message: "OK",
+							Conditions: []v1.Condition{
+								{
+									Message:            "Error",
+									ObservedGeneration: 1,
+									Reason:             "Error",
+									Status:             "False",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			linkName:    "my-link",
+			expectError: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -491,6 +614,7 @@ func TestCmdLinkUpdate_WaitUntil(t *testing.T) {
 		cmd.linkName = test.linkName
 		cmd.output = test.output
 		cmd.timeout = test.timeout
+		cmd.status = test.status
 
 		t.Run(test.name, func(t *testing.T) {
 
diff --git a/internal/cmd/skupper/link/link_test.go b/internal/cmd/skupper/link/link_test.go
index 5b92e516..b0889d6b 100644
--- a/internal/cmd/skupper/link/link_test.go
+++ b/internal/cmd/skupper/link/link_test.go
@@ -37,6 +37,7 @@ func TestCmdLinkFactory(t *testing.T) {
 				common.FlagNameCost:           "1",
 				common.FlagNameOutput:         "",
 				common.FlagNameTimeout:        "1m0s",
+				common.FlagNameWait:           "ready",
 			},
 			command: CmdLinkUpdateFactory(types.PlatformKubernetes),
 		},
@@ -51,6 +52,7 @@ func TestCmdLinkFactory(t *testing.T) {
 			name: "CmdLinkDeleteFactory",
 			expectedFlagsWithDefaultValue: map[string]interface{}{
 				common.FlagNameTimeout: "1m0s",
+				common.FlagNameWait:    "true",
 			},
 			command: CmdLinkDeleteFactory(types.PlatformKubernetes),
 		},
diff --git a/internal/cmd/skupper/listener/kube/listener_create_test.go b/internal/cmd/skupper/listener/kube/listener_create_test.go
index 8d4f5f69..38e2c445 100644
--- a/internal/cmd/skupper/listener/kube/listener_create_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_create_test.go
@@ -193,6 +193,14 @@ func TestCmdListenerCreate_ValidateInput(t *testing.T) {
 			},
 			expectedErrors: []string{},
 		},
+		{
+			name:  "wait status is not valid",
+			args:  []string{"my-listener-tls", "8080"},
+			flags: common.CommandListenerCreateFlags{Timeout: time.Minute, Wait: "created"},
+			expectedErrors: []string{
+				"status is not valid: value created not allowed. It should be one of this options: [ready configured none]",
+			},
+		},
 	}
 
 	for _, test := range testTable {
@@ -225,28 +233,31 @@ func TestCmdListenerCreate_InputToOptions(t *testing.T) {
 		expectedListenerType   string
 		expectedOutput         string
 		expectedTimeout        time.Duration
+		expectedStatus         string
 	}
 
 	testTable := []test{
 		{
 			name:                   "test1",
-			flags:                  common.CommandListenerCreateFlags{"backend", "backend", "secret", "tcp", 20 * time.Second, "json"},
+			flags:                  common.CommandListenerCreateFlags{"backend", "backend", "secret", "tcp", 20 * time.Second, "json", "configured"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "backend",
 			expectedRoutingKey:     "backend",
 			expectedTimeout:        20 * time.Second,
 			expectedListenerType:   "tcp",
 			expectedOutput:         "json",
+			expectedStatus:         "configured",
 		},
 		{
 			name:                   "test2",
-			flags:                  common.CommandListenerCreateFlags{"", "", "secret", "tcp", 30 * time.Second, "yaml"},
+			flags:                  common.CommandListenerCreateFlags{"", "", "secret", "tcp", 30 * time.Second, "yaml", "configured"},
 			expectedTlsCredentials: "secret",
 			expectedHost:           "test2",
 			expectedRoutingKey:     "test2",
 			expectedTimeout:        30 * time.Second,
 			expectedListenerType:   "tcp",
 			expectedOutput:         "yaml",
+			expectedStatus:         "configured",
 		},
 	}
 
@@ -268,6 +279,7 @@ func TestCmdListenerCreate_InputToOptions(t *testing.T) {
 			assert.Check(t, cmd.timeout == test.expectedTimeout)
 			assert.Check(t, cmd.output == test.expectedOutput)
 			assert.Check(t, cmd.listenerType == test.expectedListenerType)
+			assert.Check(t, cmd.status == test.expectedStatus)
 		})
 	}
 }
@@ -334,10 +346,12 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
 		output              string
+		status              string
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
 		expectError         bool
+		expectedStatus      string
 	}
 
 	testTable := []test{
@@ -361,7 +375,8 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 			expectError: true,
 		},
 		{
-			name: "listener is ready",
+			name:   "listener is ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Listener{
 					ObjectMeta: v1.ObjectMeta{
@@ -372,7 +387,7 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 						Status: v2alpha1.Status{
 							Conditions: []v1.Condition{
 								{
-									Type:   "Configured",
+									Type:   "Ready",
 									Status: "True",
 								},
 							},
@@ -383,7 +398,7 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 			expectError: false,
 		},
 		{
-			name:   "listener is ready yaml output",
+			name:   "listener is configured yaml output",
 			output: "yaml",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Listener{
@@ -405,6 +420,57 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 			},
 			expectError: false,
 		},
+		{
+			name:       "user does not wait",
+			status:     "none",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user waits for configured, but listener had some errors while being configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "Error",
+									ObservedGeneration: 1,
+									Reason:             "Error",
+									Status:             "False",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: true,
+		},
 	}
 
 	for _, test := range testTable {
@@ -416,6 +482,7 @@ func TestCmdListenerCreate_WaitUntil(t *testing.T) {
 		cmd.output = test.output
 		cmd.timeout = 1 * time.Second
 		cmd.namespace = "test"
+		cmd.status = test.status
 
 		t.Run(test.name, func(t *testing.T) {
 
diff --git a/internal/cmd/skupper/listener/kube/listener_delete_test.go b/internal/cmd/skupper/listener/kube/listener_delete_test.go
index 8849e705..495ace8e 100644
--- a/internal/cmd/skupper/listener/kube/listener_delete_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_delete_test.go
@@ -100,6 +100,33 @@ func TestCmdListenerDelete_ValidateInput(t *testing.T) {
 	}
 }
 
+func TestCmdListenerDelete_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name         string
+		flags        *common.CommandListenerDeleteFlags
+		expectedWait bool
+	}
+
+	testTable := []test{
+		{
+			name:         "wait for site enabled",
+			flags:        &common.CommandListenerDeleteFlags{Wait: true},
+			expectedWait: true,
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdListenerDelete{}
+			command.Flags = test.flags
+			command.InputToOptions()
+
+			assert.Check(t, command.wait == test.expectedWait)
+		})
+	}
+}
+
 func TestCmdListenerDelete_Run(t *testing.T) {
 	type test struct {
 		name                string
@@ -163,6 +190,7 @@ func TestCmdListenerDelete_Run(t *testing.T) {
 func TestCmdListenerDelete_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
+		wait                bool
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
@@ -172,6 +200,7 @@ func TestCmdListenerDelete_WaitUntil(t *testing.T) {
 	testTable := []test{
 		{
 			name: "error deleting listener",
+			wait: true,
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Listener{
 					ObjectMeta: v1.ObjectMeta{
@@ -194,6 +223,31 @@ func TestCmdListenerDelete_WaitUntil(t *testing.T) {
 		},
 		{
 			name:        "listener is deleted",
+			wait:        true,
+			expectError: false,
+		},
+		{
+			name:       "listener is not deleted but user does not want to wait",
+			k8sObjects: nil,
+			wait:       false,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
 			expectError: false,
 		},
 	}
@@ -205,6 +259,7 @@ func TestCmdListenerDelete_WaitUntil(t *testing.T) {
 		cmd.name = "my-listener"
 		cmd.Flags = &common.CommandListenerDeleteFlags{Timeout: 1 * time.Second}
 		cmd.namespace = "test"
+		cmd.wait = test.wait
 
 		t.Run(test.name, func(t *testing.T) {
 
diff --git a/internal/cmd/skupper/listener/kube/listener_update_test.go b/internal/cmd/skupper/listener/kube/listener_update_test.go
index cbd3b4c1..2fe680be 100644
--- a/internal/cmd/skupper/listener/kube/listener_update_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_update_test.go
@@ -257,6 +257,33 @@ func TestCmdListenerUpdate_ValidateInput(t *testing.T) {
 			},
 			expectedErrors: []string{},
 		},
+		{
+			name:       "wait status is not valid",
+			args:       []string{"backend-listener"},
+			flags:      common.CommandListenerUpdateFlags{Timeout: time.Second * 30, Wait: "created"},
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "backend-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedErrors: []string{
+				"status is not valid: value created not allowed. It should be one of this options: [ready configured none]",
+			},
+		},
 	}
 
 	for _, test := range testTable {
@@ -277,6 +304,36 @@ func TestCmdListenerUpdate_ValidateInput(t *testing.T) {
 	}
 }
 
+func TestCmdListenerUpdate_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name           string
+		args           []string
+		flags          common.CommandListenerUpdateFlags
+		expectedStatus string
+	}
+
+	testTable := []test{
+		{
+			name:           "options with waiting status",
+			args:           []string{"backend-listener"},
+			flags:          common.CommandListenerUpdateFlags{Wait: "configured"},
+			expectedStatus: "configured",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			command := &CmdListenerUpdate{}
+			command.Flags = &test.flags
+
+			command.InputToOptions()
+
+			assert.Check(t, command.status == test.expectedStatus)
+		})
+	}
+}
+
 func TestCmdListenerUpdate_Run(t *testing.T) {
 	type test struct {
 		name                string
@@ -379,15 +436,18 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 	type test struct {
 		name                string
 		output              string
+		status              string
 		k8sObjects          []runtime.Object
 		skupperObjects      []runtime.Object
 		skupperErrorMessage string
 		expectError         bool
+		errorMessage        string
 	}
 
 	testTable := []test{
 		{
-			name: "listener is not ready",
+			name:   "listener is not ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Listener{
 					ObjectMeta: v1.ObjectMeta{
@@ -399,14 +459,18 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 					},
 				},
 			},
-			expectError: true,
+			expectError:  true,
+			errorMessage: "Listener \"my-listener\" is not ready yet, check the status for more information\n",
 		},
 		{
-			name:        "listener is not returned",
-			expectError: true,
+			name:         "listener is not returned",
+			status:       "ready",
+			expectError:  true,
+			errorMessage: "Listener \"my-listener\" is not ready yet, check the status for more information\n",
 		},
 		{
-			name: "listener is ready",
+			name:   "listener is ready",
+			status: "ready",
 			skupperObjects: []runtime.Object{
 				&v2alpha1.Listener{
 					ObjectMeta: v1.ObjectMeta{
@@ -417,7 +481,7 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 						Status: v2alpha1.Status{
 							Conditions: []v1.Condition{
 								{
-									Type:   "Configured",
+									Type:   "Ready",
 									Status: "True",
 								},
 							},
@@ -450,6 +514,82 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 			},
 			expectError: false,
 		},
+		{
+			name:       "listener is not ready yet, but user waits for configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Configured",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user does not wait",
+			status:     "none",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Ready",
+									Status: "True",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name:       "user waits for configured, but site had some errors while being configured",
+			status:     "configured",
+			k8sObjects: nil,
+			skupperObjects: []runtime.Object{
+				&v2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "my-listener",
+						Namespace: "test",
+					},
+					Status: v2alpha1.ListenerStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Message:            "Error",
+									ObservedGeneration: 1,
+									Reason:             "Error",
+									Status:             "False",
+									Type:               "Configured",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectError:  true,
+			errorMessage: "Listener \"my-listener\" is configured with errors, check the status for more information\n",
+		},
 	}
 
 	for _, test := range testTable {
@@ -463,11 +603,13 @@ func TestCmdListenerUpdate_WaitUntil(t *testing.T) {
 		}
 		cmd.namespace = "test"
 		cmd.newSettings.output = cmd.Flags.Output
+		cmd.status = test.status
 
 		t.Run(test.name, func(t *testing.T) {
 			err := cmd.WaitUntil()
 			if test.expectError {
 				assert.Check(t, err != nil)
+				assert.Equal(t, test.errorMessage, err.Error())
 			} else {
 				assert.Assert(t, err)
 			}
diff --git a/internal/cmd/skupper/listener/listener_test.go b/internal/cmd/skupper/listener/listener_test.go
index dbf0c0eb..79a276c7 100644
--- a/internal/cmd/skupper/listener/listener_test.go
+++ b/internal/cmd/skupper/listener/listener_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 )
 
-func TestCmdLinkFactory(t *testing.T) {
+func TestCmdListenerFactory(t *testing.T) {
 
 	type test struct {
 		name                          string
@@ -28,6 +28,7 @@ func TestCmdLinkFactory(t *testing.T) {
 				common.FlagNameListenerType:   "tcp",
 				common.FlagNameOutput:         "",
 				common.FlagNameTimeout:        "1m0s",
+				common.FlagNameWait:           "configured",
 			},
 			command: CmdListenerCreateFactory(types.PlatformKubernetes),
 		},
@@ -41,6 +42,7 @@ func TestCmdLinkFactory(t *testing.T) {
 				common.FlagNameOutput:         "",
 				common.FlagNameTimeout:        "1m0s",
 				common.FlagNameListenerPort:   "0",
+				common.FlagNameWait:           "configured",
 			},
 			command: CmdListenerUpdateFactory(types.PlatformKubernetes),
 		},
@@ -55,6 +57,7 @@ func TestCmdLinkFactory(t *testing.T) {
 			name: "CmdListenerDeleteFactory",
 			expectedFlagsWithDefaultValue: map[string]interface{}{
 				common.FlagNameTimeout: "1m0s",
+				common.FlagNameWait:    "true",
 			},
 			command: CmdListenerDeleteFactory(types.PlatformKubernetes),
 		},
diff --git a/internal/cmd/skupper/site/kube/site_delete_test.go b/internal/cmd/skupper/site/kube/site_delete_test.go
index 76ad588b..6006e550 100644
--- a/internal/cmd/skupper/site/kube/site_delete_test.go
+++ b/internal/cmd/skupper/site/kube/site_delete_test.go
@@ -275,6 +275,7 @@ func TestCmdSiteDelete_InputToOptions(t *testing.T) {
 			command.InputToOptions()
 
 			assert.Check(t, command.timeout == test.expectedTimeout)
+			assert.Check(t, command.wait == test.expectedWait)
 		})
 	}
 }
diff --git a/internal/cmd/skupper/system/nonkube/system_reload_test.go b/internal/cmd/skupper/system/nonkube/system_reload_test.go
new file mode 100644
index 00000000..f4f6ea51
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_reload_test.go
@@ -0,0 +1,159 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"gotest.tools/v3/assert"
+	"os"
+	"testing"
+)
+
+func TestCmdSystemReload_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "args-are-not-accepted",
+			args:           []string{"something"},
+			expectedErrors: []string{"this command does not accept arguments"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command := &CmdSystemReload{}
+			command.CobraCmd = common.ConfigureCobraCommand(types.PlatformSystemd, common.SkupperCmdDescription{}, command, nil)
+
+			actualErrors := command.ValidateInput(test.args)
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSystemReload_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name              string
+		args              []string
+		platform          string
+		namespace         string
+		expectedBinary    string
+		expectedNamespace string
+	}
+
+	testTable := []test{
+		{
+			name:              "options-by-default",
+			expectedBinary:    "podman",
+			expectedNamespace: "default",
+		},
+		{
+			name:              "systemd",
+			namespace:         "east",
+			platform:          "systemd",
+			expectedBinary:    "skrouterd",
+			expectedNamespace: "east",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			os.Setenv(types.ENV_PLATFORM, test.platform)
+
+			cmd := newCmdSystemReloadWithMocks(false, false)
+			cmd.Namespace = test.namespace
+
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.ConfigBootstrap.Binary == test.expectedBinary)
+			assert.Check(t, cmd.ConfigBootstrap.Namespace == test.expectedNamespace)
+		})
+	}
+}
+
+func TestCmdSystemReload_Run(t *testing.T) {
+	type test struct {
+		name           string
+		preCheckFails  bool
+		bootstrapFails bool
+		errorMessage   string
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			preCheckFails:  false,
+			bootstrapFails: false,
+			errorMessage:   "",
+		},
+		{
+			name:           "pre check fails",
+			preCheckFails:  true,
+			bootstrapFails: false,
+			errorMessage:   "precheck fails",
+		},
+		{
+			name:           "bootstrap fails",
+			preCheckFails:  false,
+			bootstrapFails: true,
+			errorMessage:   "Failed to bootstrap: bootstrap fails",
+		},
+	}
+
+	for _, test := range testTable {
+		command := newCmdSystemReloadWithMocks(test.preCheckFails, test.bootstrapFails)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdSystemReloadWithMocks(precheckFails bool, bootstrapFails bool) *CmdSystemReload {
+
+	cmdMock := &CmdSystemReload{
+		PreCheck:  mockCmdSystemReloadPreCheck,
+		Bootstrap: mockCmdSystemReloadBootStrap,
+		PostExec:  mockCmdSystemReloadPostExec,
+	}
+	if precheckFails {
+		cmdMock.PreCheck = mockCmdSystemReloadPreCheckFails
+	}
+
+	if bootstrapFails {
+		cmdMock.Bootstrap = mockCmdSystemReloadBootStrapFails
+	}
+
+	return cmdMock
+}
+
+func mockCmdSystemReloadPreCheck(config *bootstrap.Config) error { return nil }
+func mockCmdSystemReloadPreCheckFails(config *bootstrap.Config) error {
+	return fmt.Errorf("precheck fails")
+}
+func mockCmdSystemReloadBootStrap(config *bootstrap.Config) (*api.SiteState, error) {
+	return &api.SiteState{}, nil
+}
+func mockCmdSystemReloadBootStrapFails(config *bootstrap.Config) (*api.SiteState, error) {
+	return nil, fmt.Errorf("bootstrap fails")
+}
+func mockCmdSystemReloadPostExec(config *bootstrap.Config, siteState *api.SiteState) {}
diff --git a/internal/cmd/skupper/system/nonkube/system_setup_test.go b/internal/cmd/skupper/system/nonkube/system_setup_test.go
new file mode 100644
index 00000000..c911a0a7
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_setup_test.go
@@ -0,0 +1,206 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
+	"github.com/skupperproject/skupper/pkg/nonkube/bootstrap"
+	"gotest.tools/v3/assert"
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestCmdSystemSetup_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		flags          *common.CommandSystemSetupFlags
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "args-are-not-accepted",
+			args:           []string{"something"},
+			expectedErrors: []string{"this command does not accept arguments"},
+		},
+		{
+			name: "invalid-bundle-strategy",
+			flags: &common.CommandSystemSetupFlags{
+				Strategy: "not-valid",
+			},
+			expectedErrors: []string{"invalid bundle strategy: not-valid"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command := &CmdSystemSetup{}
+			command.CobraCmd = common.ConfigureCobraCommand(types.PlatformSystemd, common.SkupperCmdDescription{}, command, nil)
+
+			if test.flags != nil {
+				command.Flags = test.flags
+			}
+
+			actualErrors := command.ValidateInput(test.args)
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSystemSetup_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name                   string
+		args                   []string
+		flags                  common.CommandSystemSetupFlags
+		platform               string
+		namespace              string
+		expectedBinary         string
+		expectedNamespace      string
+		expectedIsBundle       bool
+		expectedBundleStrategy string
+	}
+
+	testTable := []test{
+		{
+			name:              "options-by-default",
+			flags:             common.CommandSystemSetupFlags{},
+			expectedBinary:    "podman",
+			expectedNamespace: "default",
+		},
+		{
+			name: "systemd",
+			flags: common.CommandSystemSetupFlags{
+				Path: "input-path",
+			},
+			namespace:         "east",
+			platform:          "systemd",
+			expectedBinary:    "skrouterd",
+			expectedNamespace: "east",
+		},
+		{
+			name: "docker",
+			flags: common.CommandSystemSetupFlags{
+				Path: "input-path",
+			},
+			namespace:         "east",
+			platform:          "docker",
+			expectedBinary:    "docker",
+			expectedNamespace: "east",
+		},
+		{
+			name: "bundle-default",
+			flags: common.CommandSystemSetupFlags{
+				Path:     "input-path",
+				Strategy: "bundle",
+			},
+			namespace:              "east",
+			platform:               "podman",
+			expectedBinary:         "",
+			expectedNamespace:      "east",
+			expectedIsBundle:       true,
+			expectedBundleStrategy: "bundle",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+			os.Setenv(types.ENV_PLATFORM, test.platform)
+
+			cmd := newCmdSystemSetupWithMocks(false, false)
+			cmd.Flags = &test.flags
+			cmd.Namespace = test.namespace
+
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.ConfigBootstrap.Binary == test.expectedBinary)
+			assert.Check(t, cmd.ConfigBootstrap.BundleStrategy == test.expectedBundleStrategy)
+			assert.Check(t, cmd.ConfigBootstrap.Namespace == test.expectedNamespace)
+			assert.Check(t, cmd.ConfigBootstrap.IsBundle == test.expectedIsBundle)
+			assert.Check(t, strings.Contains(cmd.ConfigBootstrap.InputPath, cmd.Flags.Path))
+		})
+	}
+}
+
+func TestCmdSystemSetup_Run(t *testing.T) {
+	type test struct {
+		name           string
+		preCheckFails  bool
+		bootstrapFails bool
+		errorMessage   string
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			preCheckFails:  false,
+			bootstrapFails: false,
+			errorMessage:   "",
+		},
+		{
+			name:           "pre check fails",
+			preCheckFails:  true,
+			bootstrapFails: false,
+			errorMessage:   "precheck fails",
+		},
+		{
+			name:           "bootstrap fails",
+			preCheckFails:  false,
+			bootstrapFails: true,
+			errorMessage:   "Failed to bootstrap: bootstrap fails",
+		},
+	}
+
+	for _, test := range testTable {
+		command := newCmdSystemSetupWithMocks(test.preCheckFails, test.bootstrapFails)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdSystemSetupWithMocks(precheckFails bool, bootstrapFails bool) *CmdSystemSetup {
+
+	cmdMock := &CmdSystemSetup{
+		PreCheck:  mockCmdSystemSetupPreCheck,
+		Bootstrap: mockCmdSystemSetupBootStrap,
+		PostExec:  mockCmdSystemSetupPostExec,
+	}
+	if precheckFails {
+		cmdMock.PreCheck = mockCmdSystemSetupPreCheckFails
+	}
+
+	if bootstrapFails {
+		cmdMock.Bootstrap = mockCmdSystemSetupBootStrapFails
+	}
+
+	return cmdMock
+}
+
+func mockCmdSystemSetupPreCheck(config *bootstrap.Config) error { return nil }
+func mockCmdSystemSetupPreCheckFails(config *bootstrap.Config) error {
+	return fmt.Errorf("precheck fails")
+}
+func mockCmdSystemSetupBootStrap(config *bootstrap.Config) (*api.SiteState, error) {
+	return &api.SiteState{}, nil
+}
+func mockCmdSystemSetupBootStrapFails(config *bootstrap.Config) (*api.SiteState, error) {
+	return nil, fmt.Errorf("bootstrap fails")
+}
+func mockCmdSystemSetupPostExec(config *bootstrap.Config, siteState *api.SiteState) {}
diff --git a/internal/cmd/skupper/system/nonkube/system_start_test.go b/internal/cmd/skupper/system/nonkube/system_start_test.go
new file mode 100644
index 00000000..9f8db1c4
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_start_test.go
@@ -0,0 +1,130 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"gotest.tools/v3/assert"
+	"testing"
+)
+
+func TestCmdSystemStart_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "arg-not-accepted",
+			args:           []string{"namespace"},
+			expectedErrors: []string{"this command does not accept arguments"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command := &CmdSystemStart{}
+			command.CobraCmd = common.ConfigureCobraCommand(types.PlatformSystemd, common.SkupperCmdDescription{}, command, nil)
+
+			actualErrors := command.ValidateInput(test.args)
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSystemStart_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name              string
+		args              []string
+		namespace         string
+		expectedNamespace string
+	}
+
+	testTable := []test{
+		{
+			name:              "options-by-default",
+			expectedNamespace: "default",
+		},
+		{
+			name:              "namespace-provided",
+			args:              []string{"east"},
+			namespace:         "east",
+			expectedNamespace: "east",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd := newCmdSystemStopWithMocks(false)
+			cmd.Namespace = test.namespace
+
+			cmd.ValidateInput(test.args)
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.Namespace == test.expectedNamespace)
+
+		})
+	}
+}
+
+func TestCmdSystemStart_Run(t *testing.T) {
+	type test struct {
+		name           string
+		systemCtlFails bool
+		errorMessage   string
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			systemCtlFails: false,
+			errorMessage:   "",
+		},
+		{
+			name:           "router start fails",
+			systemCtlFails: true,
+			errorMessage:   "failed to start router: fail",
+		},
+	}
+
+	for _, test := range testTable {
+		command := newCmdSystemStartWithMocks(test.systemCtlFails)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error(), err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdSystemStartWithMocks(systemCtlStartFails bool) *CmdSystemStart {
+
+	cmdMock := &CmdSystemStart{
+		SystemStart: mockCmdSystemStartSystemCtl,
+	}
+	if systemCtlStartFails {
+		cmdMock.SystemStart = mockCmdSystemStartSystemCtlFails
+	}
+
+	return cmdMock
+}
+
+func mockCmdSystemStartSystemCtl(namespace string) error { return nil }
+func mockCmdSystemStartSystemCtlFails(namespace string) error {
+	return fmt.Errorf("fail")
+}
diff --git a/internal/cmd/skupper/system/nonkube/system_stop_test.go b/internal/cmd/skupper/system/nonkube/system_stop_test.go
new file mode 100644
index 00000000..6a133b03
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_stop_test.go
@@ -0,0 +1,131 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"gotest.tools/v3/assert"
+
+	"testing"
+)
+
+func TestCmdSystemStop_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "arg-not-accepted",
+			args:           []string{"namespace"},
+			expectedErrors: []string{"this command does not accept arguments"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command := &CmdSystemStop{}
+			command.CobraCmd = common.ConfigureCobraCommand(types.PlatformSystemd, common.SkupperCmdDescription{}, command, nil)
+
+			actualErrors := command.ValidateInput(test.args)
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSystemStop_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name              string
+		args              []string
+		namespace         string
+		expectedNamespace string
+	}
+
+	testTable := []test{
+		{
+			name:              "options-by-default",
+			expectedNamespace: "default",
+		},
+		{
+			name:              "namespace-provided",
+			args:              []string{"east"},
+			namespace:         "east",
+			expectedNamespace: "east",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd := newCmdSystemStopWithMocks(false)
+			cmd.Namespace = test.namespace
+
+			cmd.ValidateInput(test.args)
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.Namespace == test.expectedNamespace)
+
+		})
+	}
+}
+
+func TestCmdSystemStop_Run(t *testing.T) {
+	type test struct {
+		name           string
+		systemCtlFails bool
+		errorMessage   string
+	}
+
+	testTable := []test{
+		{
+			name:           "runs ok",
+			systemCtlFails: false,
+			errorMessage:   "",
+		},
+		{
+			name:           "stop router fails",
+			systemCtlFails: true,
+			errorMessage:   "failed to stop router: fail",
+		},
+	}
+
+	for _, test := range testTable {
+		command := newCmdSystemStopWithMocks(test.systemCtlFails)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error(), err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdSystemStopWithMocks(systemCtlStopFails bool) *CmdSystemStop {
+
+	cmdMock := &CmdSystemStop{
+		SystemStop: mockCmdSystemStopSystemCtl,
+	}
+	if systemCtlStopFails {
+		cmdMock.SystemStop = mockCmdSystemStopSystemCtlFails
+	}
+
+	return cmdMock
+}
+
+func mockCmdSystemStopSystemCtl(namespace string) error { return nil }
+func mockCmdSystemStopSystemCtlFails(namespace string) error {
+	return fmt.Errorf("fail")
+}
diff --git a/internal/cmd/skupper/system/nonkube/system_teardown_test.go b/internal/cmd/skupper/system/nonkube/system_teardown_test.go
new file mode 100644
index 00000000..029e90ee
--- /dev/null
+++ b/internal/cmd/skupper/system/nonkube/system_teardown_test.go
@@ -0,0 +1,128 @@
+package nonkube
+
+import (
+	"fmt"
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"gotest.tools/v3/assert"
+	"testing"
+)
+
+func TestCmdSystemTearDown_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "arg-not-accepted",
+			args:           []string{"namespace"},
+			expectedErrors: []string{"this command does not accept arguments"},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			command := &CmdSystemTeardown{}
+			command.CobraCmd = common.ConfigureCobraCommand(types.PlatformSystemd, common.SkupperCmdDescription{}, command, nil)
+
+			actualErrors := command.ValidateInput(test.args)
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+
+		})
+	}
+}
+
+func TestCmdSystemTearDown_InputToOptions(t *testing.T) {
+
+	type test struct {
+		name              string
+		args              []string
+		namespace         string
+		expectedNamespace string
+	}
+
+	testTable := []test{
+		{
+			name:              "options-by-default",
+			expectedNamespace: "default",
+		},
+		{
+			name:              "namespace-provided",
+			args:              []string{"east"},
+			namespace:         "east",
+			expectedNamespace: "east",
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd := newCmdSystemTeardownWithMocks(false)
+			cmd.Namespace = test.namespace
+			cmd.InputToOptions()
+
+			assert.Check(t, cmd.Namespace == test.expectedNamespace)
+
+		})
+	}
+}
+
+func TestCmdSystemTeardown_Run(t *testing.T) {
+	type test struct {
+		name          string
+		teardownFails bool
+		errorMessage  string
+	}
+
+	testTable := []test{
+		{
+			name:          "runs ok",
+			teardownFails: false,
+			errorMessage:  "",
+		},
+		{
+			name:          "teardown fails",
+			teardownFails: true,
+			errorMessage:  "System teardown has failed: fail",
+		},
+	}
+
+	for _, test := range testTable {
+		command := newCmdSystemTeardownWithMocks(test.teardownFails)
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := command.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdSystemTeardownWithMocks(systemTeardDownFails bool) *CmdSystemTeardown {
+
+	cmdMock := &CmdSystemTeardown{
+		TearDown: mockCmdSystemTeardown,
+	}
+	if systemTeardDownFails {
+		cmdMock.TearDown = mockCmdSystemTeardownFails
+	}
+
+	return cmdMock
+}
+
+func mockCmdSystemTeardown(namespace string, platform string) error { return nil }
+func mockCmdSystemTeardownFails(namespace string, platform string) error {
+	return fmt.Errorf("fail")
+}
diff --git a/internal/cmd/skupper/system/system_test.go b/internal/cmd/skupper/system/system_test.go
new file mode 100644
index 00000000..416696b3
--- /dev/null
+++ b/internal/cmd/skupper/system/system_test.go
@@ -0,0 +1,71 @@
+package system
+
+import (
+	"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 TestCmdSystemFactory(t *testing.T) {
+
+	type test struct {
+		name                          string
+		expectedFlagsWithDefaultValue map[string]interface{}
+		command                       *cobra.Command
+	}
+
+	testTable := []test{
+		{
+			name: "CmdSystemSetupFactory",
+			expectedFlagsWithDefaultValue: map[string]interface{}{
+				common.FlagNamePath:     "",
+				common.FlagNameStrategy: "",
+				common.FlagNameForce:    "false",
+			},
+			command: CmdSystemSetupFactory(types.PlatformKubernetes),
+		},
+		{
+			name:                          "CmdSystemReloadFactory",
+			expectedFlagsWithDefaultValue: map[string]interface{}{},
+			command:                       CmdSystemReloadFactory(types.PlatformKubernetes),
+		},
+		{
+			name:                          "CmdSystemStopFactory",
+			expectedFlagsWithDefaultValue: map[string]interface{}{},
+			command:                       CmdSystemStopFactory(types.PlatformKubernetes),
+		},
+	}
+
+	for _, test := range testTable {
+
+		var flagList []interface{}
+		t.Run(test.name, func(t *testing.T) {
+
+			test.command.Flags().VisitAll(func(flag *pflag.Flag) {
+				flagList = append(flagList, flag.Name)
+
+				// Check if the flag name exists in the expectedFlagsWithDefaultValue map
+				expectedValue, exists := test.expectedFlagsWithDefaultValue[flag.Name]
+				if !exists {
+					t.Errorf("flag %q not expected", flag.Name)
+					return
+				}
+
+				// Check if the default value matches the expected default value
+				assert.Equal(t, expectedValue, flag.DefValue)
+			})
+
+			assert.Check(t, len(flagList) == len(test.expectedFlagsWithDefaultValue))
+
+			assert.Assert(t, test.command.PreRunE != nil)
+			assert.Assert(t, test.command.Run != nil)
+			assert.Assert(t, test.command.PostRun != nil)
+			assert.Assert(t, test.command.Use != "")
+			assert.Assert(t, test.command.Short != "")
+			assert.Assert(t, test.command.Long != "")
+		})
+	}
+}
diff --git a/internal/cmd/skupper/token/kube/token_redeem_test.go b/internal/cmd/skupper/token/kube/token_redeem_test.go
index 59de2fb1..c0ec2cb3 100644
--- a/internal/cmd/skupper/token/kube/token_redeem_test.go
+++ b/internal/cmd/skupper/token/kube/token_redeem_test.go
@@ -321,11 +321,13 @@ func TestCmdTokenRedeem_Run(t *testing.T) {
 						Name:      "my-token",
 						Namespace: "test",
 					},
-					Status: v2alpha1.Status{
-						Conditions: []v1.Condition{
-							{
-								Type:   "Redeemed",
-								Status: "True",
+					Status: v2alpha1.AccessTokenStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Redeemed",
+									Status: "True",
+								},
 							},
 						},
 					},
@@ -377,7 +379,7 @@ func TestCmdTokenRedeem_WaitUntil(t *testing.T) {
 						Name:      "my-grant",
 						Namespace: "test",
 					},
-					Status: v2alpha1.Status{},
+					Status: v2alpha1.AccessTokenStatus{},
 				},
 			},
 			expectError: true,
@@ -395,11 +397,13 @@ func TestCmdTokenRedeem_WaitUntil(t *testing.T) {
 						Name:      "my-grant",
 						Namespace: "test",
 					},
-					Status: v2alpha1.Status{
-						Conditions: []v1.Condition{
-							{
-								Type:   "Redeemed",
-								Status: "True",
+					Status: v2alpha1.AccessTokenStatus{
+						Status: v2alpha1.Status{
+							Conditions: []v1.Condition{
+								{
+									Type:   "Redeemed",
+									Status: "True",
+								},
 							},
 						},
 					},
diff --git a/pkg/kube/grants/redeem_test.go b/pkg/kube/grants/redeem_test.go
index 6ffb3ed8..b16db041 100644
--- a/pkg/kube/grants/redeem_test.go
+++ b/pkg/kube/grants/redeem_test.go
@@ -138,7 +138,7 @@ func Test_handleTokenResponse(t *testing.T) {
 			name:           "no data",
 			token:          tf.token("my-token", "test", "http://foo/xyz", "mycode", ""),
 			site:           tf.site("my-site", "test"),
-			expectedStatus: "Could not decode response",
+			expectedStatus: "Controller could not decode response",
 		},
 		{
 			name:  "failed read",
@@ -156,7 +156,7 @@ func Test_handleTokenResponse(t *testing.T) {
 				},
 			},
 			failReadAt:     1025,
-			expectedStatus: "Could not decode response",
+			expectedStatus: "Controller could not decode response",
 		},
 		{
 			name:  "secret collision",
@@ -173,7 +173,7 @@ func Test_handleTokenResponse(t *testing.T) {
 					}, "my-token"),
 				},
 			},
-			expectedStatus:  "Could not create received secret: secrets \"my-token\" already exists",
+			expectedStatus:  "Controller could not create received secret: secrets \"my-token\" already exists",
 			extraK8sObjects: []runtime.Object{tf.secret("my-token", "test", "Another Subject", nil)},
 		},
 		{
@@ -191,7 +191,7 @@ func Test_handleTokenResponse(t *testing.T) {
 					}, "my-token"),
 				},
 			},
-			expectedStatus:      "Could not create received link: links.skupper.io \"my-token\" already exists",
+			expectedStatus:      "Controller could not create received link: links.skupper.io \"my-token\" already exists",
 			extraSkupperObjects: []runtime.Object{tf.link("my-token", "test", nil, "")},
 		},
 		{
@@ -365,21 +365,21 @@ func Test_RedeemAccessToken(t *testing.T) {
 			scheme:         "https",
 			tokenName:      "my-token",
 			grantUID:       "a40fbe84-f276-4755-bf22-5ba980ab1661",
-			expectedStatus: "404 (Not Found) No such claim",
+			expectedStatus: "Controller got failed response: 404 (Not Found) No such claim",
 			expectRedeemed: false,
 		},
 		{
 			name:           "no resolved endpoints",
 			scheme:         "https",
 			tokenName:      "my-token",
-			expectedStatus: "500 (Internal Server Error) Could not resolve any endpoints for requested link",
+			expectedStatus: "Controller got failed response: 500 (Internal Server Error) Could not resolve any endpoints for requested link",
 		},
 		{
 			name:           "bad default issuer",
 			scheme:         "https",
 			tokenName:      "my-token",
 			defaultIssuer:  "i-dont-exist",
-			expectedStatus: "500 (Internal Server Error) Could not get issuer for requested certficate",
+			expectedStatus: "Controller got failed response: 500 (Internal Server Error) Could not get issuer for requested certficate",
 		},
 	}
 	for _, tt := range tests {
diff --git a/pkg/kube/site/site_test.go b/pkg/kube/site/site_test.go
index 410c1695..ddca7a68 100644
--- a/pkg/kube/site/site_test.go
+++ b/pkg/kube/site/site_test.go
@@ -83,7 +83,7 @@ func TestSite_Recover(t *testing.T) {
 			s, err := newSiteMocks("test", tt.k8sObjects, tt.skupperObjects, tt.skupperErrorMessage, false)
 			assert.Assert(t, err)
 
-			if err := s.Recover(tt.args.site); (err != nil) != tt.wantErr {
+			if err := s.StartRecovery(tt.args.site); (err != nil) != tt.wantErr {
 				t.Errorf("Site.Reconcile() error = %v", err)
 			}
 		})
@@ -539,6 +539,14 @@ func TestSite_CheckLink(t *testing.T) {
 			},
 			wantErr:   false,
 			wantLinks: 1,
+			skupperObjects: []runtime.Object{
+				&skupperv2alpha1.Link{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "link1",
+						Namespace: "test",
+					},
+				},
+			},
 		},
 		{
 			name: "link - not found",
diff --git a/pkg/nonkube/common/systemd_test.go b/pkg/nonkube/common/systemd_test.go
index 91155dd8..e3d0345f 100644
--- a/pkg/nonkube/common/systemd_test.go
+++ b/pkg/nonkube/common/systemd_test.go
@@ -49,7 +49,7 @@ func TestSystemdService(t *testing.T) {
 			systemdServiceImpl.rootSystemdBasePath = outputPath
 			t.Run(fmt.Sprintf("create-systemd-%s-as-uid-%d", platform, uid), func(t *testing.T) {
 				assert.Assert(t, systemdService.Create())
-				serviceFile, err := os.ReadFile(systemdServiceImpl.getServiceFile())
+				serviceFile, err := os.ReadFile(systemdServiceImpl.GetServiceFile())
 				assert.Assert(t, err)
 				var startCmd string
 				var stopCmd string
@@ -66,7 +66,7 @@ func TestSystemdService(t *testing.T) {
 				assert.Assert(t, strings.Contains(string(serviceFile), stopCmd))
 			})
 			assert.Assert(t, systemdService.Remove())
-			_, err = os.ReadFile(systemdServiceImpl.getServiceFile())
+			_, err = os.ReadFile(systemdServiceImpl.GetServiceFile())
 			assert.Assert(t, err != nil)
 		}
 	}
diff --git a/pkg/utils/retry_test.go b/pkg/utils/retry_test.go
index 80183392..38c7ca43 100644
--- a/pkg/utils/retry_test.go
+++ b/pkg/utils/retry_test.go
@@ -346,7 +346,7 @@ func TestRetryErrorWithContext(t *testing.T) {
 			expectSuccess: true,
 		}, {
 			doc:           "The execution should time out after one retry due the context",
-			timeout:       time.Millisecond * 100,
+			timeout:       time.Millisecond * 110,
 			workOnTry:     5,
 			expectedTries: 1,
 			expectSuccess: false,
@@ -381,8 +381,17 @@ func TestRetryErrorWithContext(t *testing.T) {
 
 			elapsed := time.Since(start)
 
-			if item.expectSuccess && elapsed > item.timeout {
-				t.Errorf("The execution should have timed out, but it has not.")
+			if item.expectSuccess {
+				// Expecting success: check that the function completed successfully
+				if resp != nil {
+					t.Errorf("Expected success, but got error: %v", resp)
+				}
+				if elapsed > item.timeout {
+					t.Errorf("Expected to complete before timeout, but took %v", elapsed)
+				}
+			}
+			if !item.expectSuccess && (elapsed <= item.timeout || elapsed > item.timeout+20*time.Millisecond) {
+				t.Errorf("The execution should have timed out, but it did not or took too long.")
 			}
 
 			if item.expectSuccess != (resp == nil) {