From 7aa1cc37a69539e9968ab69369062cff45580c17 Mon Sep 17 00:00:00 2001 From: fgschwan Date: Thu, 19 Oct 2023 14:22:24 +0200 Subject: [PATCH] moved vpp-probe handling to swctl dependency command and also added agentctl handling in similar way (#145) moved vpp-probe handling to swctl dependency command and also added agentctl handling in similar way Also: -replaced function for printing into swctl console to remove warnings about generally ignored edge case error of writing -renamed vpp config getter command in swctl to prevent confusion with stonework startup config Signed-off-by: Filip Gschwandtner --- cmd/swctl/app/cli.go | 32 +- cmd/swctl/app/cli_options.go | 8 + cmd/swctl/app/cmd_config.go | 7 +- cmd/swctl/app/cmd_dependency.go | 481 +++++++++++++++++------ cmd/swctl/app/cmd_deploy.go | 18 +- cmd/swctl/app/cmd_manage.go | 6 +- cmd/swctl/app/cmd_status.go | 4 +- cmd/swctl/app/cmd_support.go | 17 +- cmd/swctl/app/cmd_trace.go | 5 +- cmd/swctl/app/docker/agentctl.Dockerfile | 43 ++ cmd/swctl/app/exec.go | 54 ++- cmd/swctl/app/options.go | 5 +- cmd/swctl/app/util.go | 159 +++----- go.mod | 2 +- 14 files changed, 534 insertions(+), 307 deletions(-) create mode 100644 cmd/swctl/app/docker/agentctl.Dockerfile diff --git a/cmd/swctl/app/cli.go b/cmd/swctl/app/cli.go index fa1f9f1b..cf6d7db8 100644 --- a/cmd/swctl/app/cli.go +++ b/cmd/swctl/app/cli.go @@ -4,19 +4,13 @@ import ( "errors" "fmt" "io" - "os" "strings" "github.com/docker/cli/cli/streams" "github.com/moby/term" - "github.com/sirupsen/logrus" - "go.pantheon.tech/stonework/client" ) -// TODO: to be refactored: -// - refactor the usage of external apps: agentctl, vpp-probe - // Cli is a client API for CLI application. type Cli interface { Initialize(opts *GlobalOptions) error @@ -37,7 +31,6 @@ type CLI struct { client client.API entities []Entity - vppProbePath string globalOptions *GlobalOptions out *streams.Out @@ -45,6 +38,9 @@ type CLI struct { in *streams.In appName string + // customizations is the generic way how to pass CLI customizations without extending the API. It should + // be used for small modifications or changes that are not worthy to change the CLI API. + customizations map[string]interface{} } // NewCli returns a new CLI instance. It accepts CliOption for customization. @@ -90,14 +86,6 @@ func (cli *CLI) Initialize(opts *GlobalOptions) (err error) { return fmt.Errorf("loading embedded entity files failed: %w", err) } - // get vpp-probe - vppProbePath, err := initVppProbe() - if err != nil { - logrus.Errorf("vpp-probe error: %v", err) - } else { - cli.vppProbePath = vppProbePath - } - return nil } @@ -110,20 +98,6 @@ func initClient(opts ...client.Option) (*client.Client, error) { return c, nil } -func initVppProbe() (string, error) { - if os.Getenv(EnvVarVppProbeNoDownload) != "" { - logrus.Debugf("vpp-probe download disabled by user") - return "", fmt.Errorf("downloading disabled by user") - } - - vppProbePath, err := downloadVppProbe() - if err != nil { - return "", fmt.Errorf("downloading vpp-probe failed: %w", err) - } - - return vppProbePath, nil -} - func (cli *CLI) Client() client.API { return cli.client } diff --git a/cmd/swctl/app/cli_options.go b/cmd/swctl/app/cli_options.go index 9a0ba2cd..03a470a1 100644 --- a/cmd/swctl/app/cli_options.go +++ b/cmd/swctl/app/cli_options.go @@ -64,3 +64,11 @@ func WithClient(c client.API) CliOption { return nil } } + +// WithCustomizations sets the generic customizations of the CLI. +func WithCustomizations(customizations map[string]interface{}) CliOption { + return func(cli *CLI) error { + cli.customizations = customizations + return nil + } +} diff --git a/cmd/swctl/app/cmd_config.go b/cmd/swctl/app/cmd_config.go index 027f1d4a..28651089 100644 --- a/cmd/swctl/app/cmd_config.go +++ b/cmd/swctl/app/cmd_config.go @@ -1,8 +1,7 @@ package app import ( - "fmt" - + "github.com/gookit/color" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/exp/slices" @@ -53,7 +52,7 @@ func runConfigCmd(cli Cli, opts ConfigCmdOptions) error { return err } - fmt.Fprintln(cli.Err(), stderr) - fmt.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) return nil } diff --git a/cmd/swctl/app/cmd_dependency.go b/cmd/swctl/app/cmd_dependency.go index aa095f5d..f361ef15 100644 --- a/cmd/swctl/app/cmd_dependency.go +++ b/cmd/swctl/app/cmd_dependency.go @@ -1,8 +1,12 @@ package app import ( + _ "embed" "errors" "fmt" + "io" + "os" + "path/filepath" "strconv" "strings" "text/template" @@ -12,12 +16,23 @@ import ( "github.com/spf13/cobra" ) +//go:embed docker/agentctl.Dockerfile +var embeddedAgentctlDockerfile []byte + +const ( + dockerVersion = "default" + vppProbeTagVersion = "v0.2.0" + agentctlCommitVersion = "723f8db0bf7a67908e2dda1d860444a4747a99d8" +) + +var binaryToolsInstallDir = filepath.Join(os.Getenv("HOME"), ".cache", "stonework", "bin") + func exampleDependencyCmd(appName string) string { return ` # Status of all dependencies $ ` + appName + ` dependency status - # Install external tools (docker, docker compose) + # Install external tools (docker, docker compose, vpp-probe, agentctl) $ ` + appName + ` dependency install-tools # Set quantity of runtime 2MB HugePages manually @@ -26,11 +41,11 @@ func exampleDependencyCmd(appName string) string { # Assign(up) or Unassign(down) interfaces to/from kernel $ ` + appName + ` dependency link up | down - # Print out startup config with dpdk interfaces - $ ` + appName + ` dependency get-startup [] + # Print out VPP startup config with dpdk interfaces + $ ` + appName + ` dependency get-vpp-startup [] - # Print out startup config with dpdk plugin disable - $ ` + appName + ` dependency get-startup + # Print out VPP startup config with dpdk plugin disable + $ ` + appName + ` dependency get-vpp-startup ` } @@ -55,7 +70,6 @@ func NewDependencyCmd(cli Cli) *cobra.Command { // overriding Root's PersistentPreRunE because in any dependency // commands is not needed docker client connection PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return nil }, } @@ -69,12 +83,74 @@ func NewDependencyCmd(cli Cli) *cobra.Command { } cli.Initialize(&glob) - cmd.AddCommand(installExternalTools(cli), dependencyStatus(cli), installHugePages(cli), linkSetUpDown(cli), startupConf(cli)) + cmd.AddCommand(installExternalToolsCmd(cli), + dependencyStatusCmd(cli), + installHugePagesCmd(cli), + linkSetUpDownCmd(cli), + vppStartupConfCmd(cli)) + + return cmd +} + +func installExternalToolsCmd(cli Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "install-tools", + Short: "Install external tools", + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + color.Fprintln(cli.Out(), "checking docker availability...") + docker, err := IsDockerAvailable(cli) + if err != nil { + return errors.New(fmt.Sprintf("Unable to check docker availability: %v", err)) + } + if docker { + color.Fprintln(cli.Out(), "Docker is already installed") + } else { + color.Fprintln(cli.Out(), "Installing docker...") + err = InstallDocker(cli, dockerVersion) + if err != nil { + return err + } + color.Fprintln(cli.Out(), "Installation of docker was successful") + } + + vppProbeAvailable, err := IsVPPProbeAvailable(vppProbeTagVersion, cli.Out()) + if err != nil { + return errors.New(fmt.Sprintf("Unable to check vpp-probe availability: %v", err)) + } + if vppProbeAvailable { + color.Fprintln(cli.Out(), "VPP-probe is already installed") + } else { + color.Fprintln(cli.Out(), "Installing vpp-probe...") + err = InstallVPPProbe(cli, vppProbeTagVersion) + if err != nil { + return err + } + color.Fprintln(cli.Out(), "Installation of the vpp-probe tool was successful") + } + + agentctlAvailable, err := IsAgentctlAvailable(agentctlCommitVersion, cli.Out()) + if err != nil { + return errors.New(fmt.Sprintf("Unable to check agentctl availability: %v", err)) + } + if agentctlAvailable { + color.Fprintln(cli.Out(), "Agentctl is already installed") + } else { + color.Fprintln(cli.Out(), "Installing agentctl...") + err = InstallAgentCtl(cli, agentctlCommitVersion) + if err != nil { + return err + } + color.Fprintln(cli.Out(), "Installation of the agentctl tool was successful") + } + return nil + }, + } return cmd } -func dependencyStatus(cli Cli) *cobra.Command { +func dependencyStatusCmd(cli Cli) *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "status", @@ -82,38 +158,58 @@ func dependencyStatus(cli Cli) *cobra.Command { SilenceErrors: true, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - docker, err := IsDockerAvailable(cli) + // docker + dockerAvailable, err := IsDockerAvailable(cli) + status := "Not installed" if err != nil { - return err + status = fmt.Sprintf("", err) + } else if dockerAvailable { + status = "OK" } - hugePages, err := AllocatedHugePages(cli) - if err != nil { - return err + color.Fprintf(cli.Out(), "Docker: %s\n", status) - } - physicalInterfaces, err := DumpDevices(cli) + // vpp-probe + vppProbeAvailable, err := IsVPPProbeAvailable(vppProbeTagVersion, nil) + status = fmt.Sprintf("Not installed/installed incorrect version "+ + "(needed version is %s)", vppProbeTagVersion) if err != nil { - return err + status = fmt.Sprintf("", err) + } else if vppProbeAvailable { + status = "OK" } - var status string - if docker { + color.Fprintf(cli.Out(), "VPP-Probe: %s\n", status) + + // agentctl + agentctlAvailable, err := IsAgentctlAvailable(agentctlCommitVersion, nil) + status = fmt.Sprintf("Not installed/installed incorrect version "+ + "(needed version from commit %s in ligato/vpp-agent repository)", agentctlCommitVersion) + if err != nil { + status = fmt.Sprintf("", err) + } else if agentctlAvailable { status = "OK" - } else { - status = "Not installed" } - fmt.Fprintf(cli.Out(), "Docker: %s\n", status) + color.Fprintf(cli.Out(), "Agentctl: %s\n", status) - if hugePages == 0 { - status = "Disabled" - } else { - status = strconv.Itoa(hugePages) + // hugepages + hugePagesCount, err := AllocatedHugePages(cli) + status = "Disabled" + if err != nil { + status = fmt.Sprintf("", err) + } else if hugePagesCount != 0 { + status = strconv.Itoa(hugePagesCount) } - fmt.Fprintf(cli.Out(), "Hugepages: %s\n", status) + color.Fprintf(cli.Out(), "Hugepages: %s\n", status) + // physical interfaces + physicalInterfaces, err := DumpDevices(cli) + if err != nil { + color.Fprintf(cli.Out(), "Physical interfaces: \n", err) + return err + } if physicalInterfaces == nil { - status = "No available interfaces\n" - fmt.Fprintf(cli.Out(), status) + color.Fprint(cli.Out(), "No available interfaces\n") } else { + color.Fprint(cli.Out(), "Physical interfaces:\n") table := tablewriter.NewWriter(cli.Out()) table.SetHeader([]string{"Name", "Pci", "Mode", "Driver"}) @@ -130,41 +226,15 @@ func dependencyStatus(cli Cli) *cobra.Command { } table.Render() } - return nil - }, - } - return cmd -} - -func installExternalTools(cli Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "install-tools", - Short: "Install external tools", - Args: cobra.ArbitraryArgs, - RunE: func(cmd *cobra.Command, args []string) error { - - docker, err := IsDockerAvailable(cli) - if err != nil { - return errors.New(fmt.Sprintf("Unable to check docker availability: %v", err)) - } - - if docker { - fmt.Fprintln(cli.Out(), "Docker is already installed") - return nil - } - - err = InstallDocker(cli, "default") - if err != nil { - return err - } - - return nil + // errors are already logged to console, so returning only last error to indicate + // partial/full failure in cmd status/return code + return err }, } return cmd } -func installHugePages(cli Cli) *cobra.Command { +func installHugePagesCmd(cli Cli) *cobra.Command { cmd := &cobra.Command{ Use: "set-hugepages", Short: "set-hugepages ", @@ -184,14 +254,12 @@ func installHugePages(cli Cli) *cobra.Command { return cmd } -/* -linkSetUpDown changes the link state and binds/unbinds the pci driver -(up=kernel driver usable for kernel network stack, down=no kernel driver). -The kernel driver unbinding is helpfull in case of DPDK interfaces that -can't have kernel network stack usable driver when VPP should use them -as DPDK interfaces. -*/ -func linkSetUpDown(cli Cli) *cobra.Command { +// linkSetUpDownCmd changes the link state and binds/unbinds the pci driver +// (up=kernel driver usable for kernel network stack, down=no kernel driver). +// The kernel driver unbinding is helpfull in case of DPDK interfaces that +// can't have kernel network stack usable driver when VPP should use them +// as DPDK interfaces. +func linkSetUpDownCmd(cli Cli) *cobra.Command { cmd := &cobra.Command{ Use: "link ", Short: "link < pci ...> up | down", @@ -224,7 +292,7 @@ func linkSetUpDown(cli Cli) *cobra.Command { if args[len(args)-1] == "up" { //returning interface back to kernel driver if physicalInterfaces[matchId].Driver == "" { - fmt.Fprintln(cli.Out(), fmt.Sprintf("don't need to unbind the already unbinded pci %s", physicalInterfaces[matchId].Name)) + color.Fprintln(cli.Out(), fmt.Sprintf("don't need to unbind the already unbinded pci %s", physicalInterfaces[matchId].Name)) } else { err = unbindDevice(cli, physicalInterfaces[matchId].Pci, physicalInterfaces[matchId].Driver) if err != nil { @@ -268,6 +336,78 @@ func linkSetUpDown(cli Cli) *cobra.Command { return cmd } +func vppStartupConfCmd(cli Cli) *cobra.Command { + const vppStartupconfig = `unix { +cli-no-pager +cli-listen /run/vpp/cli.sock +log /tmp/vpp.log +coredump-size unlimited +full-coredump +poll-sleep-usec 50 +} +{{if .}} +dpdk { +{{range .}} dev {{.Pci}} { + name {{.SwName}} +} +{{end}} +} +{{else}} +plugins { + plugin dpdk_plugin.so { disable } +} +{{end}} +api-trace { + on +} + +socksvr { + default +} + +statseg { + default + per-node-counters on +} + +punt { + socket /run/stonework/vpp/punt-to-vpp.sock +} +` + cmd := &cobra.Command{ + Use: "get-vpp-startup", + Short: "Print out VPP startup config", + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + + var desiredInterfaces []NetworkInterface + for _, arg := range args { + + var netInterface NetworkInterface + trimIndex := strings.LastIndex(arg, ":") + names := []string{arg[:trimIndex], arg[trimIndex+1:]} + if len(names) != 2 { + return errors.New("bad format of argument. Every argument in this command" + + " must have \"word:word\" pattern") + } + netInterface.Pci = names[0] + netInterface.SwName = names[1] + + desiredInterfaces = append(desiredInterfaces, netInterface) + + } + + t := template.Must(template.New("vppStartupConf").Parse(vppStartupconfig)) + err := t.Execute(cli.Out(), desiredInterfaces) + if err != nil { + return err + } + return nil + }, + } + return cmd +} + func IsDockerAvailable(cli Cli) (bool, error) { out, _, err := cli.Exec("whereis docker", nil, false) if err != nil { @@ -297,7 +437,7 @@ func ResizeHugePages(cli Cli, size uint) error { //TODO: Make persistent hugepages //TODO: Handle numa case, Big (1GB)hugepages(are immutable and can be setted only during booting) if size == 0 { - fmt.Fprintln(cli.Out(), "Skipping hugepages") + color.Fprintln(cli.Out(), "Skipping hugepages") return nil } _, _, err := cli.Exec(fmt.Sprintf("sudo sysctl -w vm.nr_hugepages=%d", size), nil, false) @@ -318,7 +458,6 @@ func ResizeHugePages(cli Cli, size uint) error { } func InstallDocker(cli Cli, dockerVersion string) error { - commands := []string{"sudo apt-get update -y", "sudo apt-get install ca-certificates curl gnupg -y", "sudo install -m 0755 -d /etc/apt/keyrings", @@ -348,83 +487,171 @@ func InstallDocker(cli Cli, dockerVersion string) error { if err != nil { return errors.New(err.Error() + "(" + command + ")") } - fmt.Fprintln(cli.Out(), out) + color.Fprintln(cli.Out(), out) } return nil } -func startupConf(cli Cli) *cobra.Command { - const startupconfig = `unix { -cli-no-pager -cli-listen /run/vpp/cli.sock -log /tmp/vpp.log -coredump-size unlimited -full-coredump -poll-sleep-usec 50 -} -{{if .}} -dpdk { -{{range .}} dev {{.Pci}} { - name {{.SwName}} -} -{{end}} -} -{{else}} -plugins { - plugin dpdk_plugin.so { disable } +func InstallVPPProbe(cli Cli, vppProbeTagVersion string) error { + const ( + repoOwner = "ligato" + repoName = "vpp-probe" + ) + + // Construct the path to the installation file + installPath := cmdVppProbe.installPath() // absolute path to vpp-probe executable + versionPath := installPath + ".version" + + // Get release info from vpp-probe github repo + assetUrl, err := retrieveReleaseAssetUrl(repoOwner, repoName, vppProbeTagVersion) + if err != nil { + return err + } + + // Create the installation directory if it doesn't exist + if err := os.MkdirAll(binaryToolsInstallDir, 0755); err != nil { + return err + } + + // Get the vpp-probe binary and copy it to install + err = downloadAndExtractSubAsset(assetUrl, "vpp-probe", installPath) + if err != nil { + return err + } + + // Store the release version info + if err := os.WriteFile(versionPath, []byte(vppProbeTagVersion), 0755); err != nil { + return fmt.Errorf("writing version to file failed: %w", err) + } + + return nil } -{{end}} -api-trace { - on +func IsVPPProbeAvailable(vppProbeTagVersion string, logger io.Writer) (bool, error) { + return isExternalToolAvailable(cmdVppProbe, vppProbeTagVersion, logger) } -socksvr { - default +func IsAgentctlAvailable(agentctlCommitVersion string, logger io.Writer) (bool, error) { + return isExternalToolAvailable(cmdAgentCtl, agentctlCommitVersion, logger) } -statseg { - default - per-node-counters on -} +func isExternalToolAvailable(tool externalExe, targetVersion string, logger io.Writer) (bool, error) { + // Construct the path to the installation file + installPath := tool.installPath() // absolute path to tool executable + versionPath := installPath + ".version" -punt { - socket /run/stonework/vpp/punt-to-vpp.sock + // Check current state of tool installation and tool version file in the system + if logger != nil { + color.Fprintf(logger, "checking availability of external tool %s\n", string(tool)) + } + var installedVersion string + if _, err := os.Stat(installPath); err == nil { + version, err := os.ReadFile(versionPath) + if err == nil { + installedVersion = string(version) + } else if os.IsNotExist(err) { + if logger != nil { + color.Fprintf(logger, "%s version file not found, proceed to download\n", string(tool)) + } + return false, nil + } else if err != nil { + return false, err + } + } else if os.IsNotExist(err) { + if logger != nil { + color.Fprintf(logger, "%s installation not found, proceed to download\n", string(tool)) + } + return false, nil + } else if err != nil { + return false, err + } + + // Check whether desired version is already in place + if installedVersion != "" { + if logger != nil { + color.Fprintf(logger, "installed version of %s: %v\n", string(tool), installedVersion) + } + if installedVersion == targetVersion { + if logger != nil { + color.Fprintf(logger, "installed version of %s is the correct version to be used\n", string(tool)) + } + return true, nil + } + if logger != nil { + color.Fprintf(logger, "required version of %s is %s, proceed to download\n", + string(tool), targetVersion) + } + } + return false, nil } -` - cmd := &cobra.Command{ - Use: "get-startup", - Short: "Print out startup config", - Args: cobra.ArbitraryArgs, - RunE: func(cmd *cobra.Command, args []string) error { - var desiredInterfaces []NetworkInterface - for _, arg := range args { +func InstallAgentCtl(cli Cli, agentctlCommitVersion string) error { + const builderImage = "agentctl.builder:latest" - var netInterface NetworkInterface - trimIndex := strings.LastIndex(arg, ":") - names := []string{arg[:trimIndex], arg[trimIndex+1:]} - if len(names) != 2 { - return errors.New("bad format of argument. Every argument in this command" + - " must have \"word:word\" pattern") - } - netInterface.Pci = names[0] - netInterface.SwName = names[1] + // write embedded agentctl builder dockerfile to tmp folder + dir, err := os.MkdirTemp("", "agentctl-builder-*") + if err != nil { + return fmt.Errorf("can't create tmp folder for agentctl building due to %w", err) + } + defer os.RemoveAll(dir) // cleanup of files needed for docker build of agentctl + dockerFile := filepath.Join(dir, "agentctl.Dockerfile") + if err = os.WriteFile(dockerFile, embeddedAgentctlDockerfile, 0644); err != nil { + return fmt.Errorf("can't write agenctl builder dockerfile to tmp folder %s due to %w", dir, err) + } - desiredInterfaces = append(desiredInterfaces, netInterface) + // run agentctl build in docker + color.Fprintln(cli.Out(), "building agentctl in docker container...") + _, _, err = cli.Exec("docker build", []string{ + "-f", dockerFile, + "--build-arg", fmt.Sprintf("COMMIT=%s", agentctlCommitVersion), + "-t", builderImage, + "--rm=true", + dir}, + true) + if err != nil { + return fmt.Errorf("can't build agentctl due to builder docker build failure: %w", err) + } + defer func() { // cleanup of docker image + _, _, err = cli.Exec("docker rmi", []string{"-f", builderImage}, false) + if err != nil { + color.Fprintf(cli.Out(), "clean up of agentctl builder image failed (%v), continuing... ", err) + } + }() - } + // extract agentctl into external tools binary folder + stdout, _, err := cli.Exec("docker create", []string{builderImage}, false) + if err != nil { + return fmt.Errorf("can't extract agentctl from builder docker image due "+ + "to container creation failure: %w", err) + } + containerId := fmt.Sprint(stdout) + defer func() { // cleanup of docker container + _, _, err = cli.Exec("docker rm", []string{containerId}, false) + if err != nil { + color.Fprintf(cli.Out(), "clean up of agentctl builder container failed (%v), continuing... ", err) + } + }() + _, _, err = cli.Exec("docker cp", []string{ + fmt.Sprintf("%s:/go/bin/agentctl", containerId), cmdAgentCtl.installPath(), + }, false) + if err != nil { + return fmt.Errorf("can't extract agentctl from builder docker image due "+ + "to docker cp failure: %w", err) + } - t := template.Must(template.New("startupConf").Parse(startupconfig)) - err := t.Execute(cli.Out(), desiredInterfaces) - if err != nil { - return err - } - return nil - }, + err = os.Chmod(cmdAgentCtl.installPath(), 0755) + if err != nil { + return fmt.Errorf("can't set for agentctl proper file permissions: %w", err) } - return cmd + + // store the release version info + versionPath := cmdAgentCtl.installPath() + ".version" + if err := os.WriteFile(versionPath, []byte(agentctlCommitVersion), 0755); err != nil { + return fmt.Errorf("writing version to file failed: %w", err) + } + + return nil } func unbindDevice(cli Cli, pci string, driver string) error { diff --git a/cmd/swctl/app/cmd_deploy.go b/cmd/swctl/app/cmd_deploy.go index 5a5e2670..f42d26b2 100644 --- a/cmd/swctl/app/cmd_deploy.go +++ b/cmd/swctl/app/cmd_deploy.go @@ -1,8 +1,6 @@ package app import ( - "fmt" - "github.com/gookit/color" "github.com/spf13/cobra" ) @@ -97,8 +95,8 @@ func newDeploymentConfig(cli Cli) *cobra.Command { if err != nil { return err } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) return nil }, } @@ -116,8 +114,8 @@ func newDeploymentInfo(cli Cli) *cobra.Command { if err != nil { return err } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) return nil }, } @@ -135,8 +133,8 @@ func newDeploymentImages(cli Cli) *cobra.Command { if err != nil { return err } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) return nil }, } @@ -154,8 +152,8 @@ func newDeploymentServices(cli Cli) *cobra.Command { if err != nil { return err } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) return nil }, } diff --git a/cmd/swctl/app/cmd_manage.go b/cmd/swctl/app/cmd_manage.go index d8429566..d48524d2 100644 --- a/cmd/swctl/app/cmd_manage.go +++ b/cmd/swctl/app/cmd_manage.go @@ -627,8 +627,8 @@ func prepareVarValuesInteractive(w io.Writer, e Entity, evars map[string]string) desc := prefixTmpl(v.Description, " ") color.Fprintln(w, color.Gray.Sprint(desc)) } - fmt.Fprintln(w) - fmt.Fprintf(w, " ") + color.Fprintln(w) + color.Fprintf(w, " ") cval, err := promptUserValue(v.Name, vv) if err != nil { return nil, err @@ -648,7 +648,7 @@ func prepareVarValuesInteractive(w io.Writer, e Entity, evars map[string]string) evars[v.Name] = val color.Fprintf(w, "%s=%s\n", color.Cyan.Sprint(v.Name), color.LightGreen.Sprint(val)) - fmt.Fprintln(w) + color.Fprintln(w) } return evars, nil } diff --git a/cmd/swctl/app/cmd_status.go b/cmd/swctl/app/cmd_status.go index 0582b356..f956a5aa 100644 --- a/cmd/swctl/app/cmd_status.go +++ b/cmd/swctl/app/cmd_status.go @@ -94,8 +94,8 @@ func runStatusCmd(cli Cli, opts StatusOptions) error { continue } } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) } } return nil diff --git a/cmd/swctl/app/cmd_support.go b/cmd/swctl/app/cmd_support.go index f21f22d1..5df4297e 100644 --- a/cmd/swctl/app/cmd_support.go +++ b/cmd/swctl/app/cmd_support.go @@ -1,7 +1,6 @@ package app import ( - "archive/zip" "fmt" "io" "os" @@ -11,7 +10,9 @@ import ( "strings" "time" + "archive/zip" compose "github.com/docker/compose/v2/pkg/api" + "github.com/gookit/color" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -227,7 +228,7 @@ func writeInterfaces(cli Cli, w io.Writer, components []client.Component, otherA continue } } - fmt.Fprintln(w, stdout) + color.Fprintln(w, stdout) } } return nil @@ -265,7 +266,7 @@ func writeDockerComposeConfig(cli Cli, w io.Writer, components []client.Componen if stderr != "" { return fmt.Errorf("%s: %s", cmd, stderr) } - fmt.Fprintln(w, stdout) + color.Fprintln(w, stdout) return nil } @@ -278,7 +279,7 @@ func writeDockerContainers(cli Cli, w io.Writer, components []client.Component, if stderr != "" { return fmt.Errorf("%s: %s", cmd, stderr) } - fmt.Fprintln(w, stdout) + color.Fprintln(w, stdout) return nil } @@ -288,7 +289,7 @@ func writeDockerInspect(cli Cli, w io.Writer, components []client.Component, oth if err != nil { return err } - fmt.Fprintln(w, stdout) + color.Fprintln(w, stdout) return nil } @@ -348,7 +349,7 @@ func writeDockerLogs(cli Cli, w io.Writer, components []client.Component, args . if stderr != "" { return fmt.Errorf("%s: %s", cmd, stderr) } - fmt.Fprintln(w, stdout) + color.Fprintln(w, stdout) return nil } @@ -419,8 +420,8 @@ func writeErrors(cli Cli, w io.Writer, components []client.Component, otherArgs for _, error := range errors { if error != nil { - fmt.Fprintln(w, "###########################") - fmt.Fprintln(w, error) + color.Fprintln(w, "###########################") + color.Fprintln(w, error) } } return nil diff --git a/cmd/swctl/app/cmd_trace.go b/cmd/swctl/app/cmd_trace.go index 431b9c99..007e5602 100644 --- a/cmd/swctl/app/cmd_trace.go +++ b/cmd/swctl/app/cmd_trace.go @@ -3,6 +3,7 @@ package app import ( "fmt" + "github.com/gookit/color" "github.com/spf13/cobra" ) @@ -37,8 +38,8 @@ func runTraceCmd(cli Cli, opts TraceCmdOptions) error { if err != nil { return err } - fmt.Fprintln(cli.Out(), stdout) - fmt.Fprintln(cli.Err(), stderr) + color.Fprintln(cli.Out(), stdout) + color.Fprintln(cli.Err(), stderr) return nil } diff --git a/cmd/swctl/app/docker/agentctl.Dockerfile b/cmd/swctl/app/docker/agentctl.Dockerfile new file mode 100644 index 00000000..948b2a7a --- /dev/null +++ b/cmd/swctl/app/docker/agentctl.Dockerfile @@ -0,0 +1,43 @@ +FROM ubuntu:20.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + git \ + make \ + nano \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install Go +ENV GOLANG_VERSION 1.20.7 +RUN set -eux; \ + dpkgArch="$(dpkg --print-architecture)"; \ + case "${dpkgArch##*-}" in \ + amd64) goRelArch='linux-amd64'; ;; \ + armhf) goRelArch='linux-armv6l'; ;; \ + arm64) goRelArch='linux-arm64'; ;; \ + esac; \ + wget -nv -O go.tgz "https://golang.org/dl/go${GOLANG_VERSION}.${goRelArch}.tar.gz"; \ + tar -C /usr/local -xzf go.tgz; \ + rm go.tgz; + +ENV GOPATH /go +ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH +RUN mkdir -p "$GOPATH/bin" && chmod -R 777 "$GOPATH" + + +# Install agentctl +RUN mkdir -p "/src/ligato" && \ + cd "/src/ligato" && \ + git clone https://github.com/ligato/vpp-agent.git +WORKDIR /src/ligato/vpp-agent +ARG COMMIT +RUN git checkout $COMMIT + +ARG VERSION +ARG BRANCH +ARG BUILD_DATE +RUN make agentctl + +CMD exec agentctl diff --git a/cmd/swctl/app/exec.go b/cmd/swctl/app/exec.go index 64d35668..f5198948 100644 --- a/cmd/swctl/app/exec.go +++ b/cmd/swctl/app/exec.go @@ -3,8 +3,11 @@ package app import ( "bytes" "fmt" + "golang.org/x/sys/unix" "io" + "os" "os/exec" + "path/filepath" "strings" "syscall" "time" @@ -14,6 +17,10 @@ import ( "golang.org/x/exp/slices" ) +const ( + MissingExternalToolMessageKey = "MissingExternalTool" +) + type externalExe string // known external programs that swctl can execute @@ -23,6 +30,36 @@ const ( cmdAgentCtl externalExe = "agentctl" ) +func (ee externalExe) installPath() string { + return filepath.Join(binaryToolsInstallDir, string(ee)) +} + +func (ee externalExe) validateUsability(cli *CLI) error { + // check for file existence + fileInfo, err := os.Stat(ee.installPath()) + if err != nil { + if os.IsNotExist(err) { + if messageOverride, found := cli.customizations[MissingExternalToolMessageKey]; found { + return fmt.Errorf(messageOverride.(string), ee.installPath()) + } + return fmt.Errorf("%s is not installed, try running "+ + "`swctl dependency install-tools`", ee.installPath()) + } + return fmt.Errorf("existence of %s could not be determined due to %w", ee.installPath(), err) + } + + // check for execution permissions + if uint32(fileInfo.Mode()&0111) != 0111 { + // someone can execute the file (file owner or owner's group or someone else) + // -> need to check if this process can execute it with more expensive OS call + if unix.Access(ee.installPath(), unix.X_OK) != nil { + return fmt.Errorf("%s can't be executed by this process/user", ee.installPath()) + } + } + + return nil +} + type externalCmd struct { cli *CLI exe externalExe @@ -84,9 +121,6 @@ func (ec *externalCmd) setMiscFlags() { if ec.color { ec.prependUniqueArg("--color", "always") } - if ec.cli.vppProbePath != "" { - ec.name = ec.cli.vppProbePath - } case cmdAgentCtl: ec.prependUniqueArg("--host", ec.cli.client.GetHost(), "-H") case cmdDocker: @@ -109,7 +143,19 @@ type ExecResult struct { func (ec *externalCmd) exec(liveOutput bool) (*ExecResult, error) { var stdout, stderr bytes.Buffer - cmd := exec.Command(ec.name, ec.args...) + + // compute executable name that will define used filepath to executable file + executable := ec.name // just file name -> using PATH to resolve to absolute path + if ec.exe == cmdVppProbe || ec.exe == cmdAgentCtl { + // this is override of executed command to take the properly installed version of external command + // instead of whetever is in the linux PATH + executable = filepath.Join(binaryToolsInstallDir, string(ec.exe)) // absolute path to external tool + if err := ec.exe.validateUsability(ec.cli); err != nil { + return nil, fmt.Errorf("can't use external tool %s due to: %w", string(ec.exe), err) + } + } + + cmd := exec.Command(executable, ec.args...) if liveOutput { stdoutMultiWriter := io.MultiWriter(&stdout, ec.cli.out) stderrMultiWriter := io.MultiWriter(&stderr, ec.cli.err) diff --git a/cmd/swctl/app/options.go b/cmd/swctl/app/options.go index feb4bdc7..f146896b 100644 --- a/cmd/swctl/app/options.go +++ b/cmd/swctl/app/options.go @@ -14,9 +14,8 @@ import ( ) const ( - EnvVarDebug = "SWCTL_DEBUG" - EnvVarLogLevel = "SWCTL_LOGLEVEL" - EnvVarVppProbeNoDownload = "SWCTL_VPP_PROBE_NO_DOWNLOAD" + EnvVarDebug = "SWCTL_DEBUG" + EnvVarLogLevel = "SWCTL_LOGLEVEL" ) type GlobalOptions struct { diff --git a/cmd/swctl/app/util.go b/cmd/swctl/app/util.go index 480f9bdc..4bf0724e 100644 --- a/cmd/swctl/app/util.go +++ b/cmd/swctl/app/util.go @@ -5,23 +5,16 @@ import ( "compress/gzip" "encoding/json" "fmt" + "github.com/sirupsen/logrus" "io" "net/http" "os" "path" - "path/filepath" "runtime" "strings" - "time" - - "github.com/sirupsen/logrus" ) -var ( - installDir = filepath.Join(os.Getenv("HOME"), ".cache", "stonework", "bin") - - defaultVppProbeEnv = "docker" -) +var defaultVppProbeEnv = "docker" type GitHubRelease struct { TagName string `json:"tag_name"` @@ -31,52 +24,52 @@ type GitHubRelease struct { } `json:"assets"` } -func downloadVppProbe() (string, error) { - const ( - repoOwner = "ligato" - repoName = "vpp-probe" +func downloadAndExtractSubAsset(assetUrl string, subAssetName string, extractionFilePath string) error { + logrus.Debugf("downloading release asset: %v", assetUrl) - latestVersionCheckPeriod = time.Hour - ) + // Make a GET request to the asset URL to download the binary archive + resp, err := http.Get(assetUrl) + if err != nil { + return err + } + defer resp.Body.Close() - // Construct the path to the installation file - installPath := filepath.Join(installDir, repoName) - versionPath := installPath + ".version" + // Create a new gzip reader for the response body + gzipReader, err := gzip.NewReader(resp.Body) + if err != nil { + return err + } + defer gzipReader.Close() - var installedVersion string - var lastCheck time.Time + // Create a new tar reader for the gzip reader + tarReader := tar.NewReader(gzipReader) - if _, err := os.Stat(installPath); err == nil { - info, err := os.Stat(versionPath) - if err == nil { - lastCheck = info.ModTime() + // Iterate over the files in the tar archive + for { + header, err := tarReader.Next() + if err == io.EOF { + break } - version, err := os.ReadFile(versionPath) - if err == nil { - installedVersion = string(version) - } else if os.IsNotExist(err) { - logrus.Debugf("version file not found, proceed to download") - } else if err != nil { - return "", err + if err != nil { + return err } - } else if os.IsNotExist(err) { - logrus.Debugf("vpp-probe install directory not found, proceed to download") - } else if err != nil { - return "", err - } - if installedVersion != "" { - logrus.Debugf("installed version of vpp-probe: %v", installedVersion) - if d := time.Since(lastCheck); d < latestVersionCheckPeriod { - logrus.Debugf("last check or download occurred recently %v ago (less than %v), skipping check for the latest release", d.Round(time.Minute), latestVersionCheckPeriod) - return installPath, nil + // Extract the file if it is a regular file + if header.Typeflag == tar.TypeReg && path.Base(header.Name) == subAssetName { + if err := extractFromReader(tarReader, extractionFilePath); err != nil { + return fmt.Errorf("failed to extract %s binary: %w", subAssetName, err) + } + return nil } } - // Construct the URL for the latest release of the repository - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName) + return fmt.Errorf("couldn't find the subasset %s in downloaded asset (url: %s)", subAssetName, assetUrl) +} - logrus.Debugf("checking latest release: %v", url) +func retrieveReleaseAssetUrl(repoOwner string, repoName string, releaseCommitTag string) (string, error) { + // Construct the URL for the given release of the repository + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", repoOwner, repoName, releaseCommitTag) + logrus.Debugf("checking %s release: %v", releaseCommitTag, url) // Make a GET request to the release URL to get the latest release data resp, err := http.Get(url) @@ -99,8 +92,8 @@ func downloadVppProbe() (string, error) { return "", fmt.Errorf("failed to decode data for latest release: %w", err) } - latestVersion := release.TagName - logrus.Debugf("latest release version on GitHub: %v", latestVersion) + releaseVersion := release.TagName + logrus.Debugf("target release version on GitHub: %v", releaseVersion) nameOsArch := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) @@ -114,88 +107,26 @@ func downloadVppProbe() (string, error) { } // Return an error if no matching asset was found if assetUrl == "" { - return "", fmt.Errorf("no binary asset containing %q found in release %s", nameOsArch, latestVersion) - } - - // Compare installed version with the latest release - if installedVersion == latestVersion { - logrus.Debugf("vpp-probe is already at latest version %s, skipping download", installedVersion) - if err := os.WriteFile(versionPath, []byte(latestVersion), 0755); err != nil { - return "", fmt.Errorf("writing version to file failed: %w", err) - } - return installPath, nil - } else { - logrus.Debugf("vpp-probe version (%s) differs from latest release (%s), proceed to download", installedVersion, latestVersion) - } - - // Create the installation directory if it doesn't exist - if err := os.MkdirAll(installDir, 0755); err != nil { - return "", err - } - - logrus.Debugf("downloading release asset: %v", assetUrl) - - // Make a GET request to the asset URL to download the binary archive - resp, err = http.Get(assetUrl) - if err != nil { - return "", err - } - defer resp.Body.Close() - - // Create a new gzip reader for the response body - gzipReader, err := gzip.NewReader(resp.Body) - if err != nil { - return "", err - } - defer gzipReader.Close() - - // Create a new tar reader for the gzip reader - tarReader := tar.NewReader(gzipReader) - - // Iterate over the files in the tar archive - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return "", err - } - - // Extract the file if it is a regular file - if header.Typeflag == tar.TypeReg && path.Base(header.Name) == "vpp-probe" { - - if err := extractVppProbe(tarReader, installPath); err != nil { - return "", fmt.Errorf("failed to extract vpp-probe binary: %w", err) - } - - // Store the release version info - if err := os.WriteFile(versionPath, []byte(latestVersion), 0755); err != nil { - return "", fmt.Errorf("writing version to file failed: %w", err) - } - - break - } + return "", fmt.Errorf("no binary asset containing %q found in release %s", nameOsArch, releaseVersion) } - - return installPath, nil + return assetUrl, nil } -func extractVppProbe(tarReader io.Reader, installPath string) error { +func extractFromReader(reader io.Reader, extractionFilePath string) error { // Create the installation file - installFile, err := os.Create(installPath) + installFile, err := os.Create(extractionFilePath) if err != nil { return fmt.Errorf("creating file failed: %w", err) } defer installFile.Close() // Copy the contents of the file from the tar archive to the installation file - if _, err := io.Copy(installFile, tarReader); err != nil { + if _, err := io.Copy(installFile, reader); err != nil { return fmt.Errorf("copying file data failed: %w", err) } // Make the installation file executable - if err := os.Chmod(installPath, 0755); err != nil { + if err := os.Chmod(extractionFilePath, 0755); err != nil { return fmt.Errorf("setting file mode failed: %w", err) } diff --git a/go.mod b/go.mod index 787a9c5d..17490f23 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( go.ligato.io/cn-infra/v2 v2.5.0-alpha.0.20230824082901-356dce1f1754 go.ligato.io/vpp-agent/v3 v3.5.0-alpha.0.20231009134600-723f8db0bf7a golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/sys v0.13.0 google.golang.org/grpc v1.56.2 google.golang.org/protobuf v1.30.0 gopkg.in/yaml.v3 v3.0.1 @@ -121,7 +122,6 @@ require ( golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.7.0 // indirect