diff --git a/docs/user/gen-docs/kyma_dashboard_start.md b/docs/user/gen-docs/kyma_dashboard_start.md index 122484dba..7f3dd5dd7 100644 --- a/docs/user/gen-docs/kyma_dashboard_start.md +++ b/docs/user/gen-docs/kyma_dashboard_start.md @@ -14,7 +14,9 @@ kyma dashboard start [flags] ```text --container-name string Specify the name of the local container. (default "kyma-dashboard") + -o, --open Specify if the browser should open after executing the command. -p, --port string Specify the port on which the local dashboard will be exposed. (default "3001") + -v, --verbose Enable verbose output with detailed logs. --context string The name of the kubeconfig context to use -h, --help Help for the command --kubeconfig string Path to the Kyma kubeconfig file diff --git a/go.mod b/go.mod index cdf8c644f..75a117df2 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/kyma-project/api-gateway v0.0.0-20250814120053-7d617def4106 github.com/moby/go-archive v0.1.0 github.com/moby/term v0.5.2 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index fcb75afaf..5287aacbf 100644 --- a/go.sum +++ b/go.sum @@ -463,6 +463,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -679,6 +681,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/busola/container.go b/internal/busola/container.go new file mode 100644 index 000000000..5039c199e --- /dev/null +++ b/internal/busola/container.go @@ -0,0 +1,87 @@ +package busola + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/container" + "github.com/kyma-project/cli.v3/internal/docker" + "github.com/kyma-project/cli.v3/internal/out" + "github.com/pkg/browser" +) + +const ( + dashboardImage = "europe-docker.pkg.dev/kyma-project/prod/kyma-dashboard-local-prod:latest" +) + +// Container is a wrapper around the kyma dashboard docker container, providing an easy to use API to manage the kyam dashboard. +type Container struct { + name string + id string + port string + docker *docker.Client + verbose bool +} + +// New creates a new dashboard container with the given configuration +func New(name, port string, verbose bool) (*Container, error) { + dockerClient, err := docker.NewClient() + if err != nil { + return nil, fmt.Errorf("could not create docker client: %w", err) + } + + return &Container{ + name: name, + port: port, + docker: dockerClient, + verbose: verbose, + }, nil +} + +// Start runs the dashboard container. +func (c *Container) Start() error { + var envs []string + + opts := c.containerOpts(envs) + out.Msg("\n") + + var err error + if c.id, err = c.docker.PullImageAndStartContainer(context.Background(), opts); err != nil { + return fmt.Errorf("unable to start container: %w", err) + } + return nil +} + +// Opens the kyma dashboard in a browser. +func (c *Container) Open(path string) error { + url := fmt.Sprintf("http://localhost:%s%s/clusters", c.port, path) + + err := browser.OpenURL(url) + if err != nil { + return fmt.Errorf("dashboard at %q could not be opened: %w", url, err) + } + return nil +} + +// Watch attaches to the running docker container and forwards its output. +func (c *Container) Watch(ctx context.Context) error { + return c.docker.ContainerFollowRun(ctx, c.id, c.verbose) +} + +// Stop stops the dashboard container. +func (c *Container) Stop(ctx context.Context) error { + return c.docker.ContainerStop(ctx, c.id, container.StopOptions{}) +} + +func (c *Container) containerOpts(envs []string) docker.ContainerRunOpts { + containerRunOpts := docker.ContainerRunOpts{ + Envs: envs, + ContainerName: c.name, + Image: dashboardImage, + Ports: map[string]string{ + "3001": c.port, + }, + } + + return containerRunOpts +} diff --git a/internal/cmd/dashboard/start.go b/internal/cmd/dashboard/start.go index 2694a550d..ffbe9fae2 100644 --- a/internal/cmd/dashboard/start.go +++ b/internal/cmd/dashboard/start.go @@ -1,6 +1,7 @@ package dashboard import ( + "github.com/kyma-project/cli.v3/internal/busola" "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" "github.com/spf13/cobra" @@ -10,8 +11,8 @@ type dashboardStartConfig struct { *cmdcommon.KymaConfig port string containerName string - //kubeconfigPath string <- to be used in a future PR - //verbose bool <- to be used in a future PR + verbose bool + open bool } func NewDashboardStartCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { @@ -24,16 +25,38 @@ func NewDashboardStartCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { Short: `Runs Kyma dashboard locally.`, Long: `Use this command to run Kyma dashboard locally in a Docker container and open it directly in a web browser.`, Run: func(_ *cobra.Command, _ []string) { - clierror.Check(runDashboardStart(&dashboardStartConfig{})) + clierror.Check(runDashboardStart(&cfg)) }} cmd.Flags().StringVarP(&cfg.port, "port", "p", "3001", `Specify the port on which the local dashboard will be exposed.`) cmd.Flags().StringVar(&cfg.containerName, "container-name", "kyma-dashboard", `Specify the name of the local container.`) + cmd.Flags().BoolVarP(&cfg.verbose, "verbose", "v", true, `Enable verbose output with detailed logs.`) + cmd.Flags().BoolVarP(&cfg.open, "open", "o", false, `Specify if the browser should open after executing the command.`) return cmd } func runDashboardStart(cfg *dashboardStartConfig) clierror.Error { - //To be implemented in a future PR + dash, err := busola.New( + cfg.containerName, + cfg.port, + cfg.verbose, + ) + + if err != nil { + return clierror.Wrap(err, clierror.New("failed to initialize docker client")) + } + + if err = dash.Start(); err != nil { + return clierror.Wrap(err, clierror.New("failed to start kyma dashboard")) + } + + if cfg.open { + err = dash.Open("") + } + if err != nil { + return clierror.Wrap(err, clierror.New("failed to open kyma dashboard")) + } + return nil } diff --git a/internal/docker/build.go b/internal/docker/build.go index 7b6f420f1..c9b3291d9 100644 --- a/internal/docker/build.go +++ b/internal/docker/build.go @@ -57,7 +57,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { progressOutput := streamformatter.NewProgressOutput(out.Default.MsgWriter()) bodyProgressReader := progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") - response, err := c.client.ImageBuild( + response, err := c.ImageBuild( ctx, bodyProgressReader, dockerbuild.ImageBuildOptions{ diff --git a/internal/docker/client.go b/internal/docker/client.go index 54a770693..e5fd2a9af 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -5,7 +5,7 @@ import ( ) type Client struct { - client client.APIClient + client.APIClient } func NewClient() (*Client, error) { @@ -13,9 +13,9 @@ func NewClient() (*Client, error) { if err != nil { return nil, err } - return &Client{client: cli}, nil + return &Client{APIClient: cli}, nil } func NewTestClient(mock client.APIClient) *Client { - return &Client{client: mock} + return &Client{APIClient: mock} } diff --git a/internal/docker/containerfollowrun.go b/internal/docker/containerfollowrun.go new file mode 100644 index 000000000..f2e04e119 --- /dev/null +++ b/internal/docker/containerfollowrun.go @@ -0,0 +1,32 @@ +package docker + +import ( + "context" + "io" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" + "github.com/kyma-project/cli.v3/internal/out" +) + +func (c *Client) ContainerFollowRun(ctx context.Context, containerID string, forwardOutput bool) error { + buf, err := c.ContainerAttach(ctx, containerID, container.AttachOptions{ + Stdout: true, + Stderr: true, + Stream: true, + }) + if err != nil { + return err + } + defer buf.Close() + + dstout, dsterr := io.Discard, io.Discard + if forwardOutput { + dstout = out.Default.MsgWriter() + dsterr = out.Default.ErrWriter() + } + + _, err = stdcopy.StdCopy(dstout, dsterr, buf.Reader) + + return err +} diff --git a/internal/docker/pull.go b/internal/docker/pull.go index e2f07460c..ccfc2c559 100644 --- a/internal/docker/pull.go +++ b/internal/docker/pull.go @@ -42,8 +42,7 @@ func (c *Client) PullImageAndStartContainer(ctx context.Context, opts ContainerR } var r io.ReadCloser - //mozliwosc dlugiego pullowania, wskazanie writera do streams out - r, err := c.client.ImagePull(ctx, config.Image, image.PullOptions{}) + r, err := c.ImagePull(ctx, config.Image, image.PullOptions{}) if err != nil { return "", err } @@ -54,12 +53,12 @@ func (c *Client) PullImageAndStartContainer(ctx context.Context, opts ContainerR return "", err } - body, err := c.client.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.ContainerName) + body, err := c.ContainerCreate(ctx, config, hostConfig, nil, nil, opts.ContainerName) if err != nil { return "", err } - err = c.client.ContainerStart(ctx, body.ID, container.StartOptions{}) + err = c.ContainerStart(ctx, body.ID, container.StartOptions{}) if err != nil { return "", err }