Repository: skupperproject/skupper (go)

Issue:
[v2] Listener Bindings invalid with single-stack IPV6 Cluster
**Describe the bug**
Listeners do not work on single-stack IPv6 clusters. I think that is because they are hard coded to bind to `0.0.0.0`.

**How To Reproduce**
Follow kind docs to stand up an ipv6 only cluster. https://kind.sigs.k8s.io/docs/user/configuration/#networking
Try to replicate hello world.

**Expected behavior**
ehrm, it works? Maybe always binding `[::]` is the right answer all of the time? Can't say I've spent much time on this subject.

Pull request description:
Do not use explicit host in tcpListener config
I believe this should fix #1806 

Patch:
diff --git a/Makefile b/Makefile
index 9219fa86..0646041d 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ DOCKER := docker
 SKOPEO := skopeo
 PODMAN := podman
 
-all: build-cmd build-config-sync build-controller build-bootstrap build-manifest build-network-observer update-helm-crd
+all: build-cmd build-config-sync build-controller build-bootstrap build-network-observer update-helm-crd
 
 build-cmd:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o skupper ./cmd/skupper
@@ -42,9 +42,6 @@ build-config-sync:
 build-network-observer:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o network-observer ./cmd/network-observer
 
-build-manifest:
-	go build -ldflags="${LDFLAGS}"  -o manifest ./cmd/manifest
-
 build-doc-generator:
 	GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags="${LDFLAGS}"  -o generate-doc ./internal/cmd/generate-doc
 
@@ -116,8 +113,8 @@ cover:
 		-coverprofile cover.out \
 		./...
 
-generate-manifest: build-manifest
-	./manifest
+generate-manifest: build-cmd
+	./skupper version -o json > manifest.json
 
 generate-doc: build-doc-generator
 	./generate-doc ./doc/cli
@@ -126,6 +123,6 @@ update-helm-crd:
 	./scripts/update-helm-crds.sh
 
 clean:
-	rm -rf skupper controller config-sync manifest \
+	rm -rf skupper controller config-sync \
 		bootstrap network-observer generate-doc \
 		cover.out oci-archives
diff --git a/cmd/manifest/manifest.go b/cmd/manifest/manifest.go
deleted file mode 100644
index 1dafb566..00000000
--- a/cmd/manifest/manifest.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"github.com/skupperproject/skupper/pkg/utils/configs"
-	"os"
-
-	"github.com/spf13/cobra"
-)
-
-func main() {
-
-	var command = &cobra.Command{
-		Use: "manifest",
-		Long: "This command produces a manifest detailing the images and corresponding SHAs used in Skupper. " +
-			"To create this manifest file, it requires the installation of Docker.",
-		Short: "generates a manifest.json file",
-		Args:  cobra.NoArgs,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			manifestManager := configs.ManifestManager{EnableSHA: true}
-			return manifestManager.CreateFile(manifestManager.GetConfiguredManifest())
-
-		},
-	}
-
-	if err := command.Execute(); err != nil {
-		_, err := fmt.Fprintln(os.Stderr, err)
-		if err != nil {
-			panic(err)
-		}
-		os.Exit(1)
-	}
-}
diff --git a/internal/cmd/skupper/common/flags.go b/internal/cmd/skupper/common/flags.go
index 369da845..97a291e2 100644
--- a/internal/cmd/skupper/common/flags.go
+++ b/internal/cmd/skupper/common/flags.go
@@ -22,6 +22,7 @@ Choices: [route|loadbalancer]. Default: On OpenShift, route is the default;
 for other Kubernetes flavors, loadbalancer is the default.`
 	FlagNameOutput                  = "output"
 	FlagDescOutput                  = "print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml"
+	FlagVerboseOutput               = "print verbose output to the console. Choices: json, yaml"
 	FlagNameServiceAccount          = "service-account"
 	FlagDescServiceAccount          = "the Kubernetes service account under which to run the Skupper controller"
 	FlagNameBindHost                = "bind-host"
diff --git a/internal/cmd/skupper/version/kube/version.go b/internal/cmd/skupper/version/kube/version.go
index f4059276..f120f728 100644
--- a/internal/cmd/skupper/version/kube/version.go
+++ b/internal/cmd/skupper/version/kube/version.go
@@ -2,9 +2,16 @@ package kube
 
 import (
 	"fmt"
+	"os"
+	"text/tabwriter"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/internal/kube/client"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/images"
+	"github.com/skupperproject/skupper/pkg/utils/configs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 	"k8s.io/client-go/kubernetes"
 )
@@ -14,7 +21,9 @@ type CmdVersion struct {
 	KubeClient kubernetes.Interface
 	CobraCmd   *cobra.Command
 	Flags      *common.CommandVersionFlags
-	Namespace  string
+	namespace  string
+	output     string
+	manifest   configs.ManifestManager
 }
 
 func NewCmdVersion() *CmdVersion {
@@ -28,16 +37,53 @@ func (cmd *CmdVersion) NewClient(cobraCommand *cobra.Command, args []string) {
 	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
 
 	if err == nil {
-		cmd.Client = cli.GetSkupperClient().SkupperV2alpha1()
-		cmd.KubeClient = cli.GetKubeClient()
-		cmd.Namespace = cli.Namespace
+		cmd.namespace = cli.Namespace
 	}
 }
 
-func (cmd *CmdVersion) ValidateInput(args []string) []error { return nil }
+func (cmd *CmdVersion) ValidateInput(args []string) []error {
+	var validationErrors []error
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
+
+	if cmd.Flags != nil && cmd.Flags.Output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.Flags.Output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.Flags.Output
+		}
+	}
+
+	return validationErrors
+}
 
-func (cmd *CmdVersion) InputToOptions() {}
+func (cmd *CmdVersion) InputToOptions() {
+	if cmd.output != "" {
+		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: true}
+	} else {
+		cmd.manifest = configs.ManifestManager{Components: images.KubeComponents, EnableSHA: false}
+	}
 
-func (cmd *CmdVersion) Run() error { fmt.Println("not yet implemented"); return nil }
+}
+
+func (cmd *CmdVersion) Run() error {
+	files := cmd.manifest.GetConfiguredManifest()
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, files)
+		if err != nil {
+			return err
+		}
+		fmt.Println(encodedOutput)
+	} else {
+		tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+		_, _ = fmt.Fprintln(tw, fmt.Sprintf("%s\t%s", "COMPONENT", "VERSION"))
+
+		for _, file := range files.Components {
+			fmt.Fprintln(tw, fmt.Sprintf("%s\t%s", file.Component, file.Version))
+		}
+		_ = tw.Flush()
+	}
+	return nil
+}
 
 func (cmd *CmdVersion) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/version/nonkube/version.go b/internal/cmd/skupper/version/nonkube/version.go
index 5811a908..f67d2d78 100644
--- a/internal/cmd/skupper/version/nonkube/version.go
+++ b/internal/cmd/skupper/version/nonkube/version.go
@@ -2,14 +2,28 @@ package nonkube
 
 import (
 	"fmt"
+	"os"
+	"text/tabwriter"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/internal/kube/client"
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/images"
+	"github.com/skupperproject/skupper/pkg/utils/configs"
+	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
+	"k8s.io/client-go/kubernetes"
 )
 
 type CmdVersion struct {
-	CobraCmd  *cobra.Command
-	Flags     *common.CommandVersionFlags
-	Namespace string
+	Client     skupperv2alpha1.SkupperV2alpha1Interface
+	KubeClient kubernetes.Interface
+	CobraCmd   *cobra.Command
+	Flags      *common.CommandVersionFlags
+	namespace  string
+	output     string
+	manifest   configs.ManifestManager
 }
 
 func NewCmdVersion() *CmdVersion {
@@ -20,13 +34,56 @@ func NewCmdVersion() *CmdVersion {
 }
 
 func (cmd *CmdVersion) NewClient(cobraCommand *cobra.Command, args []string) {
+	cli, err := client.NewClient(cobraCommand.Flag("namespace").Value.String(), cobraCommand.Flag("context").Value.String(), cobraCommand.Flag("kubeconfig").Value.String())
+
+	if err == nil {
+		cmd.namespace = cli.Namespace
+	}
+}
+
+func (cmd *CmdVersion) ValidateInput(args []string) []error {
+	var validationErrors []error
+	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 
+	if cmd.Flags != nil && cmd.Flags.Output != "" {
+		ok, err := outputTypeValidator.Evaluate(cmd.Flags.Output)
+		if !ok {
+			validationErrors = append(validationErrors, fmt.Errorf("output type is not valid: %s", err))
+		} else {
+			cmd.output = cmd.Flags.Output
+		}
+	}
+
+	return validationErrors
 }
 
-func (cmd *CmdVersion) ValidateInput(args []string) []error { return nil }
+func (cmd *CmdVersion) InputToOptions() {
+	if cmd.output != "" {
+		cmd.manifest = configs.ManifestManager{Components: images.NonKubeComponents, EnableSHA: true}
+	} else {
+		cmd.manifest = configs.ManifestManager{Components: images.NonKubeComponents, EnableSHA: false}
+	}
+
+}
 
-func (cmd *CmdVersion) InputToOptions() {}
+func (cmd *CmdVersion) Run() error {
+	files := cmd.manifest.GetConfiguredManifest()
+	if cmd.output != "" {
+		encodedOutput, err := utils.Encode(cmd.output, files)
+		if err != nil {
+			return err
+		}
+		fmt.Println(encodedOutput)
+	} else {
+		tw := tabwriter.NewWriter(os.Stdout, 8, 8, 1, '\t', tabwriter.TabIndent)
+		_, _ = fmt.Fprintln(tw, fmt.Sprintf("%s\t%s", "COMPONENT", "VERSION"))
 
-func (cmd *CmdVersion) Run() error { fmt.Println("not yet implemented"); return nil }
+		for _, file := range files.Components {
+			fmt.Fprintln(tw, fmt.Sprintf("%s\t%s", file.Component, file.Version))
+		}
+		_ = tw.Flush()
+	}
+	return nil
+}
 
 func (cmd *CmdVersion) WaitUntil() error { return nil }
diff --git a/internal/cmd/skupper/version/version.go b/internal/cmd/skupper/version/version.go
index 44a5e343..4224a69d 100644
--- a/internal/cmd/skupper/version/version.go
+++ b/internal/cmd/skupper/version/version.go
@@ -24,15 +24,16 @@ func CmdVersionFactory(configuredPlatform types.Platform) *cobra.Command {
 
 	cmdVersionDesc := common.SkupperCmdDescription{
 		Use:   "version",
-		Short: "",
-		Long:  "",
+		Short: "Display versions of Skupper components.",
+		Long:  "Report the version of the Skupper components",
+		Example: `skupper version
+skupper version -o yaml > manifest.yaml`,
 	}
 
 	cmd := common.ConfigureCobraCommand(configuredPlatform, cmdVersionDesc, kubeCommand, nonKubeCommand)
 
 	cmdFlags := common.CommandVersionFlags{}
-
-	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagDescOutput)
+	cmd.Flags().StringVarP(&cmdFlags.Output, common.FlagNameOutput, "o", "", common.FlagVerboseOutput)
 
 	kubeCommand.CobraCmd = cmd
 	kubeCommand.Flags = &cmdFlags
diff --git a/pkg/images/image_utils.go b/pkg/images/image_utils.go
index f8db00f5..df519deb 100644
--- a/pkg/images/image_utils.go
+++ b/pkg/images/image_utils.go
@@ -10,24 +10,25 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
+type SkupperImage struct {
+	Name   string `json:"name,omitempty"`
+	Digest string `json:"digest,omitempty"`
+}
+
 const (
-	RouterImageEnvKey                  string = "SKUPPER_ROUTER_IMAGE"
-	ControllerImageEnvKey              string = "SKUPPER_CONTROLLER_IMAGE"
-	ConfigSyncImageEnvKey              string = "SKUPPER_CONFIG_SYNC_IMAGE"
-	NetworkConsoleCollectorImageEnvKey string = "SKUPPER_FLOW_COLLECTOR_IMAGE"
-	BootstrapImageEnvKey               string = "BOOTSTRAP_IMAGE"
-	PrometheusServerImageEnvKey        string = "PROMETHEUS_SERVER_IMAGE"
-	OauthProxyImageEnvKey              string = "OAUTH_PROXY_IMAGE"
-	RouterPullPolicyEnvKey             string = "SKUPPER_ROUTER_IMAGE_PULL_POLICY"
-	ConfigSyncPullPolicyEnvKey         string = "SKUPPER_CONFIG_SYNC_IMAGE_PULL_POLICY"
-	OauthProxyPullPolicyEnvKey         string = "OAUTH_PROXY_IMAGE_PULL_POLICY"
-	SkupperImageRegistryEnvKey         string = "SKUPPER_IMAGE_REGISTRY"
-	PrometheusImageRegistryEnvKey      string = "PROMETHEUS_IMAGE_REGISTRY"
-	OauthProxyRegistryEnvKey           string = "OAUTH_PROXY_IMAGE_REGISTRY"
-
-	// These constants will be soon deprecated.
-	ServiceControllerImageEnvKey string = "SKUPPER_SERVICE_CONTROLLER_IMAGE"
-	FlowCollectorImageEnvKey     string = "SKUPPER_FLOW_COLLECTOR_IMAGE"
+	RouterImageEnvKey             string = "SKUPPER_ROUTER_IMAGE"
+	ControllerImageEnvKey         string = "SKUPPER_CONTROLLER_IMAGE"
+	ConfigSyncImageEnvKey         string = "SKUPPER_CONFIG_SYNC_IMAGE"
+	NetworkObserverImageEnvKey    string = "SKUPPER_NETWORK_OBSERVER_IMAGE"
+	BootstrapImageEnvKey          string = "BOOTSTRAP_IMAGE"
+	PrometheusServerImageEnvKey   string = "PROMETHEUS_SERVER_IMAGE"
+	OauthProxyImageEnvKey         string = "OAUTH_PROXY_IMAGE"
+	RouterPullPolicyEnvKey        string = "SKUPPER_ROUTER_IMAGE_PULL_POLICY"
+	ConfigSyncPullPolicyEnvKey    string = "SKUPPER_CONFIG_SYNC_IMAGE_PULL_POLICY"
+	OauthProxyPullPolicyEnvKey    string = "OAUTH_PROXY_IMAGE_PULL_POLICY"
+	SkupperImageRegistryEnvKey    string = "SKUPPER_IMAGE_REGISTRY"
+	PrometheusImageRegistryEnvKey string = "PROMETHEUS_IMAGE_REGISTRY"
+	OauthProxyRegistryEnvKey      string = "OAUTH_PROXY_IMAGE_REGISTRY"
 )
 
 func getPullPolicy(key string) string {
@@ -83,11 +84,11 @@ func GetControllerImageName() string {
 	}
 }
 
-func GetNetworkConsoleCollectorImageName() string {
-	image := os.Getenv(NetworkConsoleCollectorImageEnvKey)
+func GetNetworkObserverImageName() string {
+	image := os.Getenv(NetworkObserverImageEnvKey)
 	if image == "" {
 		imageRegistry := GetImageRegistry()
-		return strings.Join([]string{imageRegistry, NetworkConsoleCollectorImageName}, "/")
+		return strings.Join([]string{imageRegistry, NetworkObserverImageName}, "/")
 	} else {
 		return image
 	}
@@ -103,16 +104,6 @@ func GetBootstrapImageName() string {
 	}
 }
 
-func GetServiceControllerImageName() string {
-	image := os.Getenv(ServiceControllerImageEnvKey)
-	if image == "" {
-		imageRegistry := GetImageRegistry()
-		return strings.Join([]string{imageRegistry, ServiceControllerImageName}, "/")
-	} else {
-		return image
-	}
-}
-
 func GetConfigSyncImageDetails() types.ImageDetails {
 	return types.ImageDetails{
 		Name:       GetConfigSyncImageName(),
@@ -134,16 +125,6 @@ func GetConfigSyncImagePullPolicy() string {
 	return getPullPolicy(ConfigSyncPullPolicyEnvKey)
 }
 
-func GetFlowCollectorImageName() string {
-	image := os.Getenv(FlowCollectorImageEnvKey)
-	if image == "" {
-		imageRegistry := GetImageRegistry()
-		return strings.Join([]string{imageRegistry, FlowCollectorImageName}, "/")
-	} else {
-		return image
-	}
-}
-
 func GetPrometheusServerImageName() string {
 	image := os.Getenv(PrometheusServerImageEnvKey)
 	if image == "" {
@@ -170,11 +151,6 @@ func GetPrometheusImageRegistry() string {
 	return imageRegistry
 }
 
-func GetSiteControllerImageName() string {
-	imageRegistry := GetImageRegistry()
-	return strings.Join([]string{imageRegistry, SiteControllerImageName}, "/")
-}
-
 func GetSha(imageName string) string {
 	// Pull the image
 	pullCmd := exec.Command("docker", "pull", imageName)
@@ -228,3 +204,103 @@ func GetOauthProxyImageRegistry() string {
 	}
 	return imageRegistry
 }
+
+func GetImage(imageNames map[string]string, imageRegistry string, enableSHA bool) []SkupperImage {
+	var image SkupperImage
+	var skupperImage []SkupperImage
+
+	for key, name := range imageNames {
+		imageName := os.Getenv(key)
+		if imageName == "" {
+			imageName = strings.Join([]string{imageRegistry, name}, "/")
+		}
+		image.Name = imageName
+
+		if enableSHA {
+			image.Digest = GetSha(imageName)
+		}
+
+		skupperImage = append(skupperImage, image)
+	}
+	return skupperImage
+}
+
+func GetImages(component string, enableSHA bool) []SkupperImage {
+	//var names map[string]string
+	var registry string
+
+	names := make(map[string]string)
+	switch component {
+	case "router":
+		// skupper router has two components
+		names[RouterImageEnvKey] = RouterImageName
+		names[ConfigSyncImageEnvKey] = ConfigSyncImageName
+		registry = GetImageRegistry()
+	case "controller":
+		names[ControllerImageEnvKey] = ControllerImageName
+		registry = GetImageRegistry()
+	case "network-observer":
+		names[NetworkObserverImageEnvKey] = NetworkObserverImageName
+		registry = GetImageRegistry()
+	case "bootstrap":
+		names[BootstrapImageEnvKey] = BootstrapImageName
+		registry = GetImageRegistry()
+	case "prometheus":
+		names[PrometheusServerImageEnvKey] = PrometheusServerImageName
+		registry = GetPrometheusImageRegistry()
+	case "origin-oauth-proxy":
+		names[OauthProxyImageEnvKey] = OauthProxyImageName
+		registry = GetOauthProxyImageRegistry()
+	}
+
+	if names != nil && registry != "" {
+		return GetImage(names, registry, enableSHA)
+	} else {
+		return nil
+	}
+}
+
+func GetImageVersion(component string) string {
+	var image string
+
+	switch component {
+	case "router":
+		image = os.Getenv(RouterImageEnvKey)
+		if image == "" {
+			image = RouterImageName
+		}
+	case "controller":
+		image = os.Getenv(ControllerImageEnvKey)
+		if image == "" {
+			image = ControllerImageName
+		}
+	case "network-observer":
+		image = os.Getenv(ControllerImageEnvKey)
+		if image == "" {
+			image = NetworkObserverImageName
+		}
+	case "bootstrap":
+		image = os.Getenv(BootstrapImageName)
+		if image == "" {
+			image = ControllerImageName
+		}
+	case "prometheus":
+		image = os.Getenv(PrometheusServerImageEnvKey)
+		if image == "" {
+			image = PrometheusServerImageName
+		}
+	case "origin-oauth-proxy":
+		image = os.Getenv(OauthProxyImageEnvKey)
+		if image == "" {
+			image = OauthProxyImageName
+		}
+	}
+	if image != "" {
+		parts := strings.Split(image, ":")
+		if len(parts) == 2 {
+			return parts[1]
+		}
+	}
+
+	return ""
+}
diff --git a/pkg/images/images.go b/pkg/images/images.go
index c54a6791..09261bb3 100644
--- a/pkg/images/images.go
+++ b/pkg/images/images.go
@@ -1,20 +1,21 @@
 package images
 
 const (
-	DefaultImageRegistry             string = "quay.io/skupper"
-	RouterImageName                  string = "skupper-router:main"
-	ControllerImageName              string = "controller:v2-latest"
-	ConfigSyncImageName              string = "config-sync:v2-latest"
-	NetworkConsoleCollectorImageName string = "network-console-collector:v2-latest"
-	BootstrapImageName               string = "bootstrap:v2-latest"
+	DefaultImageRegistry     string = "quay.io/skupper"
+	RouterImageName          string = "skupper-router:main"
+	ControllerImageName      string = "controller:v2-latest"
+	ConfigSyncImageName      string = "config-sync:v2-latest"
+	NetworkObserverImageName string = "network-observer:v2-latest"
+	BootstrapImageName       string = "bootstrap:v2-latest"
 
 	PrometheusImageRegistry   string = "quay.io/prometheus"
 	PrometheusServerImageName string = "prometheus:v2.42.0"
 	OauthProxyImageRegistry   string = "quay.io/openshift"
 	OauthProxyImageName       string = "origin-oauth-proxy:4.14.0"
+)
 
-	// These constants will be soon deprecated.
-	ServiceControllerImageName string = "service-controller:main"
-	FlowCollectorImageName     string = "flow-collector:main"
-	SiteControllerImageName    string = "site-controller:main"
+var (
+	KubeComponents    = []string{"router", "controller", "network-observer", "bootstrap", "prometheus", "origin-oauth-proxy"}
+	NonKubeComponents = []string{"router", "network-observer", "bootstrap", "prometheus", "origin-oauth-proxy"}
+	DefaultComponents = []string{"router", "prometheus", "origin-oauth-proxy"}
 )
diff --git a/pkg/kube/site/extended_bindings.go b/pkg/kube/site/extended_bindings.go
index aca2291a..7d77c463 100644
--- a/pkg/kube/site/extended_bindings.go
+++ b/pkg/kube/site/extended_bindings.go
@@ -146,7 +146,7 @@ func (a *ExtendedBindings) updateBridgeConfigForConnector(siteId string, connect
 
 func (a *ExtendedBindings) updateBridgeConfigForListener(siteId string, listener *skupperv2alpha1.Listener, config *qdr.BridgeConfig) {
 	if port, err := a.mapping.GetPortForKey(listener.Name); err == nil {
-		site.UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, "0.0.0.0", port, config)
+		site.UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, "", port, config)
 	} else {
 		bindings_logger.Error("Could not allocate port for %s/%s: %s",
 			slog.String("namespace", listener.Namespace),
diff --git a/pkg/utils/configs/manifest.go b/pkg/utils/configs/manifest.go
index c0fcd3db..06d5d537 100644
--- a/pkg/utils/configs/manifest.go
+++ b/pkg/utils/configs/manifest.go
@@ -1,38 +1,38 @@
 package configs
 
 import (
-	"encoding/json"
-	"fmt"
-	"github.com/skupperproject/skupper/pkg/images"
 	"os"
-	"strings"
+
+	"github.com/skupperproject/skupper/pkg/images"
 )
 
-type SkupperImage struct {
-	Name       string `json:"name"`
-	SHA        string `json:"sha,omitempty"`
-	Repository string `json:"repository,omitempty"`
+type SkupperManifest struct {
+	Components []SkupperComponent `json:"components"`
+}
+
+type SkupperComponent struct {
+	Component string                `json:"component"`
+	Version   string                `json:"version"`
+	Images    []images.SkupperImage `json:"images"`
 }
 
 type Manifest struct {
-	Images    []SkupperImage     `json:"images"`
+	Images    SkupperManifest
 	Variables *map[string]string `json:"variables,omitempty"`
 }
 
 type ManifestManager struct {
-	EnableSHA bool
+	EnableSHA  bool
+	Components []string
 }
 
 type ManifestGenerator interface {
-	GetConfiguredManifest() Manifest
+	GetConfiguredManifest() SkupperManifest
 	GetDefaultManifestWithEnv() Manifest
-	CreateFile(m Manifest) error
 }
 
-func (manager *ManifestManager) GetConfiguredManifest() Manifest {
-	return Manifest{
-		Images: getSkupperConfiguredImages(manager.EnableSHA),
-	}
+func (manager *ManifestManager) GetConfiguredManifest() SkupperManifest {
+	return getSkupperConfiguredImages(manager.Components, manager.EnableSHA)
 }
 
 func (manager *ManifestManager) GetDefaultManifestWithEnv() Manifest {
@@ -42,87 +42,32 @@ func (manager *ManifestManager) GetDefaultManifestWithEnv() Manifest {
 	}
 }
 
-func (manager *ManifestManager) CreateFile(m Manifest) error {
-	filename := "manifest.json"
-
-	// Encode the manifest image list as JSON.
-	manifestListJSON, err := json.MarshalIndent(m, "", "   ")
-	if err != nil {
-		return fmt.Errorf("Error encoding manifest image list: %v\n", err)
-
-	}
-
-	// Create a new file.
-	file, err := os.Create(filename)
-	if err != nil {
-		return fmt.Errorf("Error creating file: %v\n", err)
-	}
+func getSkupperConfiguredImages(components []string, enableSHA bool) SkupperManifest {
+	var manifest SkupperManifest
 
-	// Write the JSON data to the file.
-	_, err = file.Write(manifestListJSON)
-	if err != nil {
-		return fmt.Errorf("Error writing to file: %v\n", err)
+	for _, component := range components {
+		var image SkupperComponent
+		image.Component = component
+		image.Version = images.GetImageVersion(component)
+		image.Images = images.GetImages(component, enableSHA)
+		manifest.Components = append(manifest.Components, image)
 	}
 
-	fmt.Printf("%s file successfully generated in the current directory.\n", filename)
-	return nil
+	return manifest
 }
 
-func getSkupperConfiguredImages(enableSHA bool) []SkupperImage {
-	return []SkupperImage{
-		{
-			Name:       images.GetRouterImageName(),
-			SHA:        getSHAIfEnabled(enableSHA, images.GetRouterImageName()),
-			Repository: "https://github.com/skupperproject/skupper-router",
-		},
-		{
-			Name:       images.GetControllerImageName(),
-			SHA:        getSHAIfEnabled(enableSHA, images.GetControllerImageName()),
-			Repository: "https://github.com/skupperproject/skupper",
-		},
-		{
-			Name:       images.GetConfigSyncImageName(),
-			SHA:        getSHAIfEnabled(enableSHA, images.GetConfigSyncImageName()),
-			Repository: "https://github.com/skupperproject/skupper",
-		},
-		{
-			Name:       images.GetNetworkConsoleCollectorImageName(),
-			SHA:        getSHAIfEnabled(enableSHA, images.GetNetworkConsoleCollectorImageName()),
-			Repository: "https://github.com/skupperproject/skupper",
-		},
-		{
-			Name:       images.GetBootstrapImageName(),
-			SHA:        getSHAIfEnabled(enableSHA, images.GetBootstrapImageName()),
-			Repository: "https://github.com/skupperproject/skupper",
-		},
-		{
-			Name: images.GetPrometheusServerImageName(),
-			SHA:  getSHAIfEnabled(enableSHA, images.GetPrometheusServerImageName()),
-		},
-		{
-			Name: images.GetOauthProxyImageName(),
-			SHA:  getSHAIfEnabled(enableSHA, images.GetOauthProxyImageName()),
-		},
-	}
-}
+func getSkupperDefaultImages() SkupperManifest {
+	var manifest SkupperManifest
 
-func getSkupperDefaultImages() []SkupperImage {
-	return []SkupperImage{
-		{
-			Name:       strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
-			Repository: "https://github.com/skupperproject/skupper-router",
-		},
-		{
-			Name:       strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
-			Repository: "https://github.com/skupperproject/skupper",
-		},
-		{
-			Name: strings.Join([]string{images.PrometheusImageRegistry, images.PrometheusServerImageName}, "/"),
-		},
-		{
-			Name: strings.Join([]string{images.OauthProxyImageRegistry, images.OauthProxyImageName}, "/"),
-		},
+	for _, component := range images.DefaultComponents {
+		var image SkupperComponent
+		image.Component = component
+		image.Version = images.GetImageVersion(component)
+		image.Images = images.GetImages(component, false)
+		manifest.Components = append(manifest.Components, image)
 	}
+
+	return manifest
 }
 
 func getEnvironmentVariableMap() *map[string]string {
@@ -159,9 +104,9 @@ func getEnvironmentVariableMap() *map[string]string {
 		envVariables[images.ConfigSyncImageEnvKey] = configSyncImage
 	}
 
-	flowNetworkConsoleCollectorImage := os.Getenv(images.NetworkConsoleCollectorImageEnvKey)
-	if flowNetworkConsoleCollectorImage != "" {
-		envVariables[images.NetworkConsoleCollectorImageEnvKey] = flowNetworkConsoleCollectorImage
+	networkObserverImage := os.Getenv(images.NetworkObserverImageEnvKey)
+	if networkObserverImage != "" {
+		envVariables[images.NetworkObserverImageEnvKey] = networkObserverImage
 	}
 
 	bootstrapImage := os.Getenv(images.BootstrapImageEnvKey)
@@ -181,10 +126,3 @@ func getEnvironmentVariableMap() *map[string]string {
 
 	return &envVariables
 }
-
-func getSHAIfEnabled(enableSHA bool, imageName string) string {
-	if !enableSHA {
-		return ""
-	}
-	return images.GetSha(imageName)
-}


Test patch:
diff --git a/internal/cmd/skupper/version/kube/version_test.go b/internal/cmd/skupper/version/kube/version_test.go
index f84e1e4e..88c7c96d 100644
--- a/internal/cmd/skupper/version/kube/version_test.go
+++ b/internal/cmd/skupper/version/kube/version_test.go
@@ -1,1 +1,154 @@
 package kube
+
+import (
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/pkg/utils/configs"
+	"gotest.tools/v3/assert"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+var (
+	kubeComponents = []string{"router"}
+)
+
+func TestCmdVersion_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		flags          common.CommandVersionFlags
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "bad output",
+			args:           []string{""},
+			flags:          common.CommandVersionFlags{Output: "not-supported"},
+			expectedErrors: []string{"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name:           "good output",
+			flags:          common.CommandVersionFlags{Output: "json"},
+			expectedErrors: []string{},
+		},
+		{
+			name:           "ok no output",
+			flags:          common.CommandVersionFlags{},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			cmd.Flags = &test.flags
+
+			actualErrors := cmd.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+		})
+	}
+}
+
+func TestCmdVersion_InputToOptions(t *testing.T) {
+	type test struct {
+		name           string
+		output         string
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		expected       bool
+	}
+
+	testTable := []test{
+		{
+			name:     "good json",
+			output:   "json",
+			expected: true,
+		},
+		{
+			name:     "good defaul",
+			expected: false,
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			cmd.output = test.output
+			cmd.InputToOptions()
+
+			assert.DeepEqual(t, test.expected, test.expected)
+		})
+	}
+}
+
+func TestCmdVersion_Run(t *testing.T) {
+	type test struct {
+		name                string
+		VersionName         string
+		flags               common.CommandVersionFlags
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	testTable := []test{
+		{
+			name:  "default",
+			flags: common.CommandVersionFlags{},
+		},
+		{
+			name:  "json",
+			flags: common.CommandVersionFlags{Output: "json"},
+		},
+		{
+			name:         "no valid",
+			flags:        common.CommandVersionFlags{Output: "not-valid"},
+			errorMessage: "format not-valid not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, test.skupperErrorMessage)
+		assert.Assert(t, err)
+		cmd.Flags = &test.flags
+		cmd.output = cmd.Flags.Output
+		cmd.namespace = "test"
+		cmd.manifest = configs.ManifestManager{Components: kubeComponents, EnableSHA: false}
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdVersionWithMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*CmdVersion, error) {
+
+	cmdVersion := &CmdVersion{
+		namespace: namespace,
+	}
+
+	return cmdVersion, nil
+}
diff --git a/internal/cmd/skupper/version/nonkube/version_test.go b/internal/cmd/skupper/version/nonkube/version_test.go
index 6072c5b0..dc9f08ef 100644
--- a/internal/cmd/skupper/version/nonkube/version_test.go
+++ b/internal/cmd/skupper/version/nonkube/version_test.go
@@ -1,1 +1,154 @@
 package nonkube
+
+import (
+	"testing"
+
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
+	"github.com/skupperproject/skupper/pkg/utils/configs"
+	"gotest.tools/v3/assert"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+var (
+	nonkubeComponents = []string{"router"}
+)
+
+func TestCmdVersion_ValidateInput(t *testing.T) {
+	type test struct {
+		name           string
+		args           []string
+		flags          common.CommandVersionFlags
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		expectedErrors []string
+	}
+
+	testTable := []test{
+		{
+			name:           "bad output",
+			args:           []string{""},
+			flags:          common.CommandVersionFlags{Output: "not-supported"},
+			expectedErrors: []string{"output type is not valid: value not-supported not allowed. It should be one of this options: [json yaml]"},
+		},
+		{
+			name:           "good output",
+			flags:          common.CommandVersionFlags{Output: "json"},
+			expectedErrors: []string{},
+		},
+		{
+			name:           "ok no output",
+			flags:          common.CommandVersionFlags{},
+			expectedErrors: []string{},
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			cmd.Flags = &test.flags
+
+			actualErrors := cmd.ValidateInput(test.args)
+
+			actualErrorsMessages := utils.ErrorsToMessages(actualErrors)
+
+			assert.DeepEqual(t, actualErrorsMessages, test.expectedErrors)
+		})
+	}
+}
+
+func TestCmdVersion_InputToOptions(t *testing.T) {
+	type test struct {
+		name           string
+		output         string
+		k8sObjects     []runtime.Object
+		skupperObjects []runtime.Object
+		expected       bool
+	}
+
+	testTable := []test{
+		{
+			name:     "good json",
+			output:   "json",
+			expected: true,
+		},
+		{
+			name:     "good defaul",
+			expected: false,
+		},
+	}
+
+	for _, test := range testTable {
+		t.Run(test.name, func(t *testing.T) {
+
+			cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, "")
+			assert.Assert(t, err)
+
+			cmd.output = test.output
+			cmd.InputToOptions()
+
+			assert.DeepEqual(t, test.expected, test.expected)
+		})
+	}
+}
+
+func TestCmdVersion_Run(t *testing.T) {
+	type test struct {
+		name                string
+		VersionName         string
+		flags               common.CommandVersionFlags
+		k8sObjects          []runtime.Object
+		skupperObjects      []runtime.Object
+		skupperErrorMessage string
+		errorMessage        string
+	}
+
+	testTable := []test{
+		{
+			name:  "default",
+			flags: common.CommandVersionFlags{},
+		},
+		{
+			name:  "json",
+			flags: common.CommandVersionFlags{Output: "json"},
+		},
+		{
+			name:         "no valid",
+			flags:        common.CommandVersionFlags{Output: "not-valid"},
+			errorMessage: "format not-valid not supported",
+		},
+	}
+
+	for _, test := range testTable {
+		cmd, err := newCmdVersionWithMocks("test", test.k8sObjects, test.skupperObjects, test.skupperErrorMessage)
+		assert.Assert(t, err)
+		cmd.Flags = &test.flags
+		cmd.output = cmd.Flags.Output
+		cmd.namespace = "test"
+		cmd.manifest = configs.ManifestManager{Components: nonkubeComponents, EnableSHA: false}
+
+		t.Run(test.name, func(t *testing.T) {
+
+			err := cmd.Run()
+			if err != nil {
+				assert.Check(t, test.errorMessage == err.Error())
+			} else {
+				assert.Check(t, err == nil)
+			}
+		})
+	}
+}
+
+// --- helper methods
+
+func newCmdVersionWithMocks(namespace string, k8sObjects []runtime.Object, skupperObjects []runtime.Object, fakeSkupperError string) (*CmdVersion, error) {
+
+	cmdVersion := &CmdVersion{
+		namespace: namespace,
+	}
+
+	return cmdVersion, nil
+}
diff --git a/internal/cmd/skupper/version/version_test.go b/internal/cmd/skupper/version/version_test.go
index f37d99d0..fdac89c7 100644
--- a/internal/cmd/skupper/version/version_test.go
+++ b/internal/cmd/skupper/version/version_test.go
@@ -1,1 +1,53 @@
 package version
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+	"gotest.tools/v3/assert"
+)
+
+func TestCmdVersionFactory(t *testing.T) {
+
+	type test struct {
+		name                          string
+		expectedFlagsWithDefaultValue map[string]interface{}
+		command                       *cobra.Command
+	}
+
+	testTable := []test{
+		{
+			name: "CmdVersionFactory",
+			expectedFlagsWithDefaultValue: map[string]interface{}{
+				common.FlagNameOutput: "",
+			},
+			command: CmdVersionFactory(types.PlatformKubernetes),
+		},
+	}
+
+	for _, test := range testTable {
+
+		var flagList []string
+		t.Run(test.name, func(t *testing.T) {
+
+			test.command.Flags().VisitAll(func(flag *pflag.Flag) {
+				flagList = append(flagList, flag.Name)
+				assert.Check(t, test.expectedFlagsWithDefaultValue[flag.Name] != nil, fmt.Sprintf("flag %q not expected", flag.Name))
+				assert.Check(t, test.expectedFlagsWithDefaultValue[flag.Name] == flag.DefValue, fmt.Sprintf("default value %q for flag %q not expected", flag.DefValue, flag.Name))
+			})
+
+			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/nonkube/client/compat/container_test.go b/internal/nonkube/client/compat/container_test.go
index a3cc2e0b..b2cd00d0 100644
--- a/internal/nonkube/client/compat/container_test.go
+++ b/internal/nonkube/client/compat/container_test.go
@@ -39,7 +39,7 @@ func TestContainer(t *testing.T) {
 
 	name := RandomName("skupper-test")
 
-	image := images.GetServiceControllerImageName()
+	image := images.GetRouterImageName()
 	env := map[string]string{
 		"VAR1": "VAL1",
 		"VAR2": "VAL2",
@@ -116,7 +116,7 @@ func TestContainer(t *testing.T) {
 	// Pulling image
 	t.Run("image-pull", func(t *testing.T) {
 		assert.Assert(t, cli.ImagePull(ctx, image))
-		invalidImage := strings.Replace(images.GetSiteControllerImageName(), ":main", ":invalid", 1)
+		invalidImage := strings.Replace(images.GetRouterImageName(), ":main", ":invalid", 1)
 		invalidImageErr := cli.ImagePull(ctx, invalidImage)
 		assert.Assert(t, invalidImageErr != nil)
 		assert.Assert(t, strings.Contains(invalidImageErr.Error(), "Recommendation:"))
@@ -314,7 +314,7 @@ func TestContainerCreateMock(t *testing.T) {
 }
 
 func TestContainerUpdateMock(t *testing.T) {
-	image := images.GetServiceControllerImageName()
+	image := images.GetRouterImageName()
 	cli := NewCompatClientMock(mockContainers(image))
 
 	// starting the container
@@ -345,7 +345,7 @@ func TestContainerUpdateMock(t *testing.T) {
 }
 
 func TestContainerUpdateErrorMock(t *testing.T) {
-	image := images.GetServiceControllerImageName()
+	image := images.GetRouterImageName()
 	newImage := strings.Replace(image, ":main", ":updated", -1)
 	cli := NewCompatClientMock(mockContainers(image))
 	mock := cli.RestClient.(*RestClientMock)
diff --git a/pkg/kube/site/bindings_test.go b/pkg/kube/site/bindings_test.go
index 3c8a75d6..3b874525 100644
--- a/pkg/kube/site/bindings_test.go
+++ b/pkg/kube/site/bindings_test.go
@@ -594,7 +594,6 @@ func TestBindingAdaptor_updateBridgeConfigForListener(t *testing.T) {
 						"backend": qdr.TcpEndpoint{
 
 							Name:    "backend",
-							Host:    "0.0.0.0",
 							Port:    "1024",
 							Address: "backend",
 							SiteId:  "00000000-0000-0000-0000-000000000001",
diff --git a/pkg/utils/configs/manifest_test.go b/pkg/utils/configs/manifest_test.go
index 858b72ec..e619aaea 100644
--- a/pkg/utils/configs/manifest_test.go
+++ b/pkg/utils/configs/manifest_test.go
@@ -2,11 +2,16 @@ package configs
 
 import (
 	"fmt"
-	"github.com/skupperproject/skupper/pkg/images"
-	"gotest.tools/v3/assert"
 	"os"
 	"strings"
 	"testing"
+
+	"github.com/skupperproject/skupper/pkg/images"
+	"gotest.tools/v3/assert"
+)
+
+var (
+	manifestComponents = []string{"router", "controller"}
 )
 
 func TestManifestManager(t *testing.T) {
@@ -21,29 +26,58 @@ func TestManifestManager(t *testing.T) {
 			envVariablesWithValue: []string{
 				images.SkupperImageRegistryEnvKey,
 				images.ConfigSyncImageEnvKey,
-				images.RouterImageEnvKey},
+				images.RouterImageEnvKey,
+				images.ControllerImageEnvKey,
+			},
 			expectedConfiguredManifest: Manifest{
-				Images: []SkupperImage{
-					{
-						Name: "SKUPPER_CONFIG_SYNC_IMAGE_TESTING",
+				Images: SkupperManifest{
+					Components: []SkupperComponent{
+						{
+							Component: "router",
+							Version:   "main",
+							Images: []images.SkupperImage{
+								{
+									Name: "SKUPPER_CONFIG_SYNC_IMAGE_TESTING",
+								},
+								{
+									Name: "SKUPPER_ROUTER_IMAGE_TESTING",
+								},
+							},
+						},
+						{
+							Component: "controller",
+							Version:   strings.Split(images.ControllerImageName, ":")[1],
+							Images: []images.SkupperImage{
+								{
+									Name: "SKUPPER_CONTROLLER_IMAGE_TESTING",
+								},
+							},
+						},
 					},
-					{
-						Name: "SKUPPER_ROUTER_IMAGE_TESTING"},
 				},
 			},
 			expectedDefaultManifestWithEnv: Manifest{
-				Images: []SkupperImage{
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
-					},
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+				Images: SkupperManifest{
+					Components: []SkupperComponent{
+						{
+							Component: "router",
+							Version:   "",
+							Images: []images.SkupperImage{
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
+								},
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+								},
+							},
+						},
 					},
 				},
 				Variables: &map[string]string{
 					images.SkupperImageRegistryEnvKey: "SKUPPER_IMAGE_REGISTRY_TESTING",
 					images.ConfigSyncImageEnvKey:      "SKUPPER_CONFIG_SYNC_IMAGE_TESTING",
 					images.RouterImageEnvKey:          "SKUPPER_ROUTER_IMAGE_TESTING",
+					images.ControllerImageEnvKey:      "SKUPPER_CONTROLLER_IMAGE_TESTING",
 				},
 			},
 		},
@@ -51,22 +85,38 @@ func TestManifestManager(t *testing.T) {
 			title:                 "configured manifest the same images that the default manifest",
 			envVariablesWithValue: []string{},
 			expectedConfiguredManifest: Manifest{
-				Images: []SkupperImage{
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
-					},
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+				Images: SkupperManifest{
+					Components: []SkupperComponent{
+						{
+							Component: "router",
+							Version:   "main",
+							Images: []images.SkupperImage{
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
+								},
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+								},
+							},
+						},
 					},
 				},
 			},
 			expectedDefaultManifestWithEnv: Manifest{
-				Images: []SkupperImage{
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
-					},
-					{
-						Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+				Images: SkupperManifest{
+					Components: []SkupperComponent{
+						{
+							Component: "router",
+							Version:   "main",
+							Images: []images.SkupperImage{
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.ConfigSyncImageName}, "/"),
+								},
+								{
+									Name: strings.Join([]string{images.DefaultImageRegistry, images.RouterImageName}, "/"),
+								},
+							},
+						},
 					},
 				},
 				Variables: &map[string]string{},
@@ -78,22 +128,24 @@ func TestManifestManager(t *testing.T) {
 
 		setUpEnvVariables(c.envVariablesWithValue)
 
-		manifestManager := ManifestManager{EnableSHA: false}
+		manifestManager := ManifestManager{Components: manifestComponents, EnableSHA: false}
 		configuredManifest := manifestManager.GetConfiguredManifest()
 		defaultManifest := manifestManager.GetDefaultManifestWithEnv()
 
-		for _, expectedImage := range c.expectedConfiguredManifest.Images {
+		for _, expectedImage := range c.expectedConfiguredManifest.Images.Components {
 
-			configuredImage := getSkupperImageFromManifest(&configuredManifest, expectedImage.Name)
+			configuredImage := getSkupperImageFromManifest(&configuredManifest, expectedImage.Component)
 
 			assert.Check(t, configuredImage != nil)
 		}
 
-		assert.Check(t, configuredManifest.Variables == nil)
+		if c.expectedDefaultManifestWithEnv.Variables == nil {
+			assert.Check(t, defaultManifest.Variables == nil)
+		}
 
-		for _, expectedImage := range c.expectedDefaultManifestWithEnv.Images {
+		for _, expectedImage := range c.expectedDefaultManifestWithEnv.Images.Components {
 
-			defaultImage := getSkupperImageFromManifest(&defaultManifest, expectedImage.Name)
+			defaultImage := getSkupperImageFromManifest(&defaultManifest.Images, expectedImage.Component)
 
 			assert.Check(t, defaultImage != nil)
 		}
@@ -124,11 +176,11 @@ func clearEnvVariables(variables []string) {
 	}
 }
 
-func getSkupperImageFromManifest(m *Manifest, name string) *SkupperImage {
+func getSkupperImageFromManifest(m *SkupperManifest, name string) *SkupperComponent {
 
-	for _, skImage := range m.Images {
+	for _, skImage := range m.Components {
 
-		if skImage.Name == name {
+		if skImage.Component == name {
 			return &skImage
 		}
 	}