File mgrctl.obscpio of Package uyuni-tools

07070100000000000081a400000000000000000000000168ed21dd00000ad8000000000000000000000000000000000000001600000000mgrctl/cmd/api/api.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type apiFlags struct {
	api.ConnectionDetails `mapstructure:"api"`
	ForceLogin            bool `mapstructure:"force"`
}

// NewCommand generates a JSON over HTTP API helper tool command.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	var flags apiFlags

	apiCmd := &cobra.Command{
		Use:   "api",
		Short: L("JSON over HTTP API helper tool"),
	}

	apiGet := &cobra.Command{
		Use:   "get path [parameters]...",
		Short: L("Call API GET request"),
		Long: L(`Takes an API path and optional parameters and then issues GET request with them.

Example:
# mgrctl api get user/getDetails login=test`),
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runGet)
		},
	}

	apiPost := &cobra.Command{
		Use:   "post path parameters...",
		Short: L("Call API POST request"),
		Long: L(`Takes an API path and parameters and then issues POST request with them.

Parameters can be either JSON encoded string or one or more key=value pairs.

Key=Value pairs example:
# mgrctl api post user/create login=test password=testXX firstName=F lastName=L email=test@localhost

JSON example:
# mgrctl api post user/create \
   '{"login":"test", "password":"testXX", "firstName":"F", "lastName":"L", "email":"test@localhost"}'
`),
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runPost)
		},
	}

	apiLogin := &cobra.Command{
		Use:   "login",
		Short: L("Store login information for future API usage"),
		Long: L(`Login stores login information for next API calls.

User name, password and remote host can be provided using flags or will be asked interactively.
Environment variables are also supported.`),
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runLogin)
		},
	}
	apiLogin.Flags().BoolP("force", "f", false, L("Overwrite existing login if exists"))

	apiLogout := &cobra.Command{
		Use:   "logout",
		Short: L("Remove stored login information"),
		Long:  L("Logout removes stored login information."),
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runLogout)
		},
	}

	apiCmd.AddCommand(apiGet)
	apiCmd.AddCommand(apiPost)
	apiCmd.AddCommand(apiLogin)
	apiCmd.AddCommand(apiLogout)
	api.AddAPIFlags(apiCmd)

	return apiCmd
}
07070100000001000081a400000000000000000000000168ed21dd00000150000000000000000000000000000000000000001b00000000mgrctl/cmd/api/api_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"testing"

	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func TestNewCommand(t *testing.T) {
	var globalflags types.GlobalFlags
	cmd := NewCommand(&globalflags)
	if cmd == nil {
		t.Error("Unexpected nil command")
	}
}
07070100000002000081a400000000000000000000000168ed21dd000004ee000000000000000000000000000000000000001600000000mgrctl/cmd/api/get.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func runGet(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
	log.Debug().Msgf("Running GET command %s", args[0])
	client, err := api.Init(&flags.ConnectionDetails)
	if err == nil && (client.Details.User != "" || client.Details.InSession) {
		err = client.Login()
	}
	if err != nil {
		return utils.Errorf(err, L("unable to login to the server"))
	}
	path := args[0]
	options := args[1:]

	res, err := api.Get[interface{}](client, fmt.Sprintf("%s?%s", path, strings.Join(options, "&")))
	if err != nil {
		return utils.Errorf(err, L("error in query '%s'"), path)
	}

	// TODO do this only when result is JSON or TEXT. Watchout for binary data
	// Decode JSON to the string and pretty print it
	out, err := json.MarshalIndent(res.Result, "", "  ")
	if err != nil {
		return err
	}
	fmt.Print(string(out))

	return nil
}
07070100000003000081a400000000000000000000000168ed21dd000005ea000000000000000000000000000000000000001800000000mgrctl/cmd/api/login.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"errors"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func runLogin(_ *types.GlobalFlags, flags *apiFlags, cmd *cobra.Command, _ []string) error {
	log.Debug().Msg("Running login command")

	if api.IsAlreadyLoggedIn() && !flags.ForceLogin {
		return errors.New(L("Refusing to overwrite existing login. Use --force to ignore this check."))
	}

	utils.AskIfMissing(&flags.Server, cmd.Flag("api-server").Usage, 0, 0, utils.IsWellFormedFQDN)
	utils.AskIfMissing(&flags.User, cmd.Flag("api-user").Usage, 0, 0, nil)
	utils.AskPasswordIfMissingOnce(&flags.Password, cmd.Flag("api-password").Usage, 0, 0)

	client, err := api.Init(&flags.ConnectionDetails)
	if err != nil {
		return err
	}
	if err := client.Login(); err != nil {
		return utils.Errorf(err, L("Failed to validate credentials."))
	}
	if err := api.StoreLoginCreds(client); err != nil {
		return err
	}

	log.Info().Msg(L("Login credentials verified."))
	return nil
}

func runLogout(_ *types.GlobalFlags, _ *apiFlags, _ *cobra.Command, _ []string) error {
	log.Debug().Msg("Running logout command")

	if err := api.RemoveLoginCreds(); err != nil {
		return err
	}
	log.Info().Msg(L("Successfully logged out"))
	return nil
}
07070100000004000081a400000000000000000000000168ed21dd00000602000000000000000000000000000000000000001700000000mgrctl/cmd/api/post.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package api

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func runPost(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
	log.Debug().Msgf("Running POST command %s", args[0])
	client, err := api.Init(&flags.ConnectionDetails)
	if err == nil {
		err = client.Login()
	}
	if err != nil {
		return utils.Errorf(err, L("unable to login to the server"))
	}

	path := args[0]
	options := args[1:]

	var data map[string]interface{}

	if len(options) > 1 {
		log.Debug().Msg("Multiple options specified, assuming non JSON data")
		data = map[string]interface{}{}
		for _, o := range options {
			s := strings.SplitN(o, "=", 2)
			data[s[0]] = s[1]
		}
	} else {
		if err := json.NewDecoder(strings.NewReader(args[1])).Decode(&data); err != nil {
			log.Debug().Msg("Failed to decode parameters as JSON, assuming key=value pairs")
		}
	}

	res, err := api.Post[interface{}](client, path, data)
	if err != nil {
		return utils.Errorf(err, L("error in query '%s'"), path)
	}

	if !res.Success {
		log.Error().Msg(res.Message)
	}
	out, err := json.MarshalIndent(res.Result, "", "  ")
	if err != nil {
		log.Fatal().Err(err)
	}
	fmt.Print(string(out))

	return nil
}
07070100000005000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000000f00000000mgrctl/cmd/api07070100000006000081a400000000000000000000000168ed21dd00000892000000000000000000000000000000000000001200000000mgrctl/cmd/cmd.go// SPDX-FileCopyrightText: 2025 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
	"os"
	"path"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/api"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/proxy"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/term"
	"github.com/uyuni-project/uyuni-tools/shared/completion"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// NewUyunictlCommand returns a new cobra.Command implementing the root command for mgrctl.
func NewUyunictlCommand() *cobra.Command {
	globalFlags := &types.GlobalFlags{}
	name := path.Base(os.Args[0])
	rootCmd := &cobra.Command{
		Use:          name,
		Short:        L("Uyuni control tool"),
		Long:         L("Tool to help managing Uyuni servers mainly through their API"),
		Version:      utils.Version,
		SilenceUsage: true, // Don't show usage help on errors
	}

	rootCmd.SetUsageTemplate(utils.GetLocalizedUsageTemplate())

	rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", L("configuration file path"))
	utils.AddLogLevelFlags(rootCmd, &globalFlags.LogLevel)

	rootCmd.PersistentPreRun = func(cmd *cobra.Command, _ []string) {
		// do not log if running the completion cmd as the output is redirect to create a file to source
		if cmd.Name() != "completion" {
			utils.LogInit((cmd.Name() != "exec" && cmd.Name() != "term") || globalFlags.LogLevel == "trace")
			utils.SetLogLevel(globalFlags.LogLevel)
			log.Info().Msgf(L("Starting %s"), strings.Join(os.Args, " "))
		}
	}

	apiCmd := api.NewCommand(globalFlags)
	rootCmd.AddCommand(apiCmd)
	rootCmd.AddCommand(exec.NewCommand(globalFlags))
	rootCmd.AddCommand(term.NewCommand(globalFlags))
	rootCmd.AddCommand(cp.NewCommand(globalFlags))
	rootCmd.AddCommand(completion.NewCommand(globalFlags))
	rootCmd.AddCommand(proxy.NewCommand(globalFlags))

	rootCmd.AddCommand(utils.GetConfigHelpCommand())

	return rootCmd
}
07070100000007000081a400000000000000000000000168ed21dd000006ff000000000000000000000000000000000000001400000000mgrctl/cmd/cp/cp.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package cp

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type flagpole struct {
	User    string
	Group   string
	Backend string
}

// NewCommand copy file to and from the containers.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	flags := &flagpole{}

	cpCmd := &cobra.Command{
		Use:   "cp [path/to/source.file] [path/to/destination.file]",
		Short: L("Copy files to and from the containers"),
		Long: L(`Takes a source and destination parameters.
	One of them can be prefixed with 'server:' to indicate the path is within the server pod.`),
		Args: cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			viper, err := utils.ReadConfig(cmd, utils.GlobalConfigFilename, globalFlags.ConfigPath)
			if err != nil {
				return err
			}
			if err := viper.Unmarshal(&flags); err != nil {
				return utils.Errorf(err, L("failed to unmarshall configuration"))
			}
			return run(flags, cmd, args)
		},
	}

	cpCmd.Flags().String("user", "", L("User or UID to set on the destination file"))
	cpCmd.Flags().String("group", "susemanager", L("Group or GID to set on the destination file"))

	utils.AddBackendFlag(cpCmd)
	return cpCmd
}

func run(flags *flagpole, _ *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	return cnx.Copy(args[0], args[1], flags.User, flags.Group)
}
07070100000008000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000000e00000000mgrctl/cmd/cp07070100000009000081a400000000000000000000000168ed21dd0000101a000000000000000000000000000000000000001800000000mgrctl/cmd/exec/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package exec

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared"
	"github.com/uyuni-project/uyuni-tools/shared/kubernetes"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/podman"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

type flagpole struct {
	Envs        []string `mapstructure:"env"`
	Interactive bool
	Tty         bool
	Backend     string
}

// NewCommand returns a new cobra.Command for exec.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	var flags flagpole

	execCmd := &cobra.Command{
		Use:   "exec '[command-to-run --with-args]'",
		Short: L("Execute commands inside the uyuni containers using 'sh -c'"),
		RunE: func(cmd *cobra.Command, args []string) error {
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
		},
	}

	execCmd.Flags().StringSliceP("env", "e", []string{},
		L("environment variables to pass to the command, separated by commas"),
	)
	execCmd.Flags().BoolP("interactive", "i", false, L("Pass stdin to the container"))
	execCmd.Flags().BoolP("tty", "t", false, L("Stdin is a TTY"))

	utils.AddBackendFlag(execCmd)
	return execCmd
}

func run(_ *types.GlobalFlags, flags *flagpole, _ *cobra.Command, args []string) error {
	cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter)
	podName, err := cnx.GetPodName()
	if err != nil {
		return err
	}

	command, err := cnx.GetCommand()
	if err != nil {
		return err
	}

	commandArgs := []string{"exec"}
	envs := []string{}
	envs = append(envs, flags.Envs...)
	if flags.Interactive {
		commandArgs = append(commandArgs, "-i")
		envs = append(envs, "ENV=/etc/sh.shrc.local")
	}
	if flags.Tty {
		commandArgs = append(commandArgs, "-t")
		envs = append(envs, utils.GetEnvironmentVarsList()...)
	}
	commandArgs = append(commandArgs, podName)

	if command == "kubectl" {
		namespace, err := cnx.GetNamespace("")
		if namespace == "" {
			log.Fatal().Err(err)
		}
		commandArgs = append(commandArgs, "-n", namespace, "-c", "uyuni", "--")
	}

	newEnv := []string{}
	for _, envValue := range envs {
		if !strings.Contains(envValue, "=") {
			if value, set := os.LookupEnv(envValue); set {
				newEnv = append(newEnv, fmt.Sprintf("%s=%s", envValue, value))
			}
		} else {
			newEnv = append(newEnv, envValue)
		}
	}
	if len(newEnv) > 0 {
		commandArgs = append(commandArgs, "env")
		commandArgs = append(commandArgs, newEnv...)
	}
	commandArgs = append(commandArgs, "sh", "-c", strings.Join(args, " "))
	err = RunRawCmd(command, commandArgs)
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			log.Info().Err(err).Msg(L("Command failed"))
			os.Exit(exitErr.ExitCode())
		}
	}
	log.Info().Msg(L("Command returned with exit code 0"))

	return nil
}

type copyWriter struct {
	Stream io.Writer
}

// Write writes an array of buffer in a stream.
func (l copyWriter) Write(p []byte) (n int, err error) {
	// Filter out kubectl line about terminated exit code
	if !strings.HasPrefix(string(p), "command terminated with exit code") {
		if _, err := l.Stream.Write(p); err != nil {
			return 0, utils.Errorf(err, L("cannot write"))
		}

		n = len(p)
		if n > 0 && p[n-1] == '\n' {
			// Trim CR added by stdlog.
			p = p[0 : n-1]
		}
		log.Debug().Msg(string(p))
	}
	return
}

// RunRawCmd runs a command, mapping stdout and start error, waiting and checking return code.
func RunRawCmd(command string, args []string) error {
	commandStr := fmt.Sprintf("%s %s", command, strings.Join(args, " "))
	log.Info().Msgf(L("Running %s"), commandStr)

	runCmd := exec.Command(command, args...)
	runCmd.Stdin = os.Stdin

	runCmd.Stdout = copyWriter{Stream: os.Stdout}
	runCmd.Stderr = copyWriter{Stream: os.Stderr}

	if err := runCmd.Start(); err != nil {
		log.Debug().Err(err).Msg("error starting command")
		return err
	}

	return runCmd.Wait()
}
0707010000000a000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000001000000000mgrctl/cmd/exec0707010000000b000081a400000000000000000000000168ed21dd00001f10000000000000000000000000000000000000001b00000000mgrctl/cmd/proxy/config.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/ssl"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// Specific flag names for proxy create config command.
const (
	proxyName      = "proxy-name"
	proxyPort      = "proxy-sshPort"
	server         = "proxy-parent"
	maxCache       = "proxy-maxCache"
	email          = "proxy-email"
	output         = "output"
	caCrt          = "ssl-ca-cert"
	caKey          = "ssl-ca-key"
	caPassword     = "ssl-ca-password"
	caIntermediate = "ssl-ca-intermediate"
	proxyCrt       = "ssl-proxy-cert"
	proxyKey       = "ssl-proxy-key"
	sslEmail       = "ssl-email"
)

type proxyFlags struct {
	Name     string
	Port     int `mapstructure:"sshPort"`
	Parent   string
	MaxCache int
	Email    string
}

type caFlags struct {
	types.SSLPair `mapstructure:",squash"`
	Password      string
	Intermediate  []string
}

type proxyConfigSSLFlags struct {
	types.SSLCertGenerationFlags `mapstructure:",squash"`
	Proxy                        types.SSLPair
	Ca                           caFlags
}

// proxyCreateConfigFlags is the structure containing the flags for proxy create config command.
type proxyCreateConfigFlags struct {
	ConnectionDetails api.ConnectionDetails `mapstructure:"api"`
	Proxy             proxyFlags
	Output            string
	SSL               proxyConfigSSLFlags
}

// proxyCreateConfigRequiredFields is a set of required fields for validation.
var proxyCreateConfigRequiredFields = []string{
	proxyName,
	server,
	email,
	caCrt,
}

func newCmd(globalFlags *types.GlobalFlags, run utils.CommandFunc[proxyCreateConfigFlags]) *cobra.Command {
	createConfigCmd := &cobra.Command{
		Use:   "config",
		Short: L("Create a proxy configuration file"),
		Long:  L("Create a proxy configuration file"),
		Example: `  Create a proxy configuration file providing certificates providing only required parameters

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@example.com" --ssl-ca-cert="root_ca.pem" --ssl-proxy-cert="proxy_crt.pem" \
		--ssl-proxy-key="proxy_key.pem"

  Create a proxy configuration file providing certificates providing  all parameters

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@example.com" --ssl-ca-cert="root_ca.pem" --ssl-proxy-cert="proxy_crt.pem" \
		--ssl-proxy-key="proxy_key.pem" \
		--ssl-ca-intermediate="intermediateCA_1.pem,intermediateCA_2.pem,intermediateCA_3.pem" \
		-o="proxy-config"

  or an alternative format:

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@example.com" --ssl-ca-cert="root_ca.pem" --ssl-proxy-cert="proxy_crt.pem" \
		--ssl-proxy-key="proxy_key.pem" \
		--ssl-ca-intermediate "intermediateCA_1.pem" --ssl-ca-intermediate "intermediateCA_2.pem" \
		--ssl-ca-intermediate "intermediateCA_3.pem" -o="proxy-config"

  Create a proxy configuration file with generated certificates providing only required parameters

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@org.com" --ssl-ca-cert="ca.pem" --ssl-ca-key="caKey.pem"

  Create a proxy configuration file with generated certificates providing only required parameters and ca password

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@org.com" --ssl-ca-cert="ca.pem" --ssl-ca-key="caKey.pem" --ssl-ca-password="secret"

  Create a proxy configuration file with generated certificates providing all parameters

    $ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@org.com" --ssl-ca-cert="ca.pem" --ssl-ca-key="caKey.pem" --ssl-ca-password="secret" \
		--ssl-cnames="proxy_a.example.com,proxy_b.example.com,proxy_c.example.com" --ssl-country="DE" \
		--ssl-state="Bayern" --ssl-city="Nuernberg" --ssl-org="orgExample" --ssl-ou="orgUnitExample" \
		--ssl-email="sslEmail@example.com" -o="proxy-config"

  or an alternative format:

	$ mgrctl proxy create config --proxy-name="proxy.example.com" --proxy-parent="server.example.com" \
		--proxy-email="admin@org.com" --ssl-ca-cert="ca.pem" --ssl-ca-key="caKey.pem" --ssl-ca-password="secret" \
		--ssl-cnames="proxy_a.example.com" --ssl-cnames="proxy_b.example.com" --ssl-cnames="proxy_c.example.com" \
		--ssl-country="DE" --ssl-state="Bayern" --ssl-city="Nuernberg" --ssl-org="orgExample" --ssl-ou="orgUnitExample" \
		--ssl-email="sslEmail@example.com" -o="proxy-config"

  Note that passing the CA password using --ssl-ca-password is not secure, use --config config.yaml with config.yaml
  containing the following as this will not persist in the shell history. Alternativaly the password can be defined in
  an UYUNI_SSL_CA_PASSWORD environment variable.

  ssl:
    ca:
	  password: secret
  `,
		RunE: func(cmd *cobra.Command, args []string) error {
			var flags proxyCreateConfigFlags
			return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, run)
		},
	}

	addFlags(createConfigCmd)

	// validations
	utils.MarkMandatoryFlags(createConfigCmd, proxyCreateConfigRequiredFields[:])
	createConfigCmd.MarkFlagsOneRequired(proxyCrt, caKey)
	createConfigCmd.MarkFlagsMutuallyExclusive(proxyCrt, caKey)

	return createConfigCmd
}

// NewConfigCommand creates the command for managing cache.
// Setup for subcommand to clear (the cache).
func NewConfigCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	return newCmd(globalFlags, proxyCreateConfigInit)
}

func addFlags(cmd *cobra.Command) {
	cmd.Flags().StringP(output, "o", "", L("Filename to write the configuration to (without extension)."))

	// Common flags in command scope
	cmd.Flags().String(proxyName, "", L("Unique DNS-resolvable FQDN of this proxy."))
	cmd.Flags().Int(proxyPort, 8022, L("SSH port the proxy listens on."))
	cmd.Flags().String(server, "", L("FQDN of the server to connect the proxy to."))
	cmd.Flags().Int(maxCache, 102400, L("Maximum cache size in MB."))
	cmd.Flags().String(email, "", L("Email of the proxy administrator"))
	cmd.Flags().String(caCrt, "", L("Path to the root CA certificate in PEM format."))

	// Specific flags for when providing proxy certificates
	cmd.Flags().String(proxyCrt, "", L("Path to the proxy certificate in PEM format."))
	cmd.Flags().String(proxyKey, "", L("Path to the proxy certificate private key in PEM format."))
	cmd.Flags().StringSliceP(caIntermediate, "i", []string{},
		L(`Path to an intermediate CA used to sign the proxy certicate in PEM format.
May be provided multiple times or separated by commas.`),
	)

	// Specific flags for when generating proxy certificates
	ssl.AddSSLGenerationFlags(cmd)
	cmd.Flags().String(sslEmail, "", L("Email to set in the SSL certificate"))

	cmd.Flags().String(caKey, "", L("Path to the private key of the CA to use to generate a new proxy certificate."))
	cmd.Flags().String(caPassword, "",
		L("Password of the CA private key, will be prompted if not passed."),
	)

	// Login API flags
	api.AddAPIFlags(cmd)

	// Setup flag groups
	commonGroup := "common"
	providingGroup := "providing"
	_ = utils.AddFlagHelpGroup(cmd,
		&utils.Group{ID: commonGroup, Title: L("Common Flags")},
		&utils.Group{ID: providingGroup, Title: L("Third party proxy certificates flags")},
	)

	_ = utils.AddFlagsToHelpGroupID(cmd, commonGroup, proxyName, proxyPort, server, maxCache, email, caCrt)
	_ = utils.AddFlagsToHelpGroupID(cmd, providingGroup, proxyCrt, proxyKey, caIntermediate)

	_ = utils.AddFlagsToHelpGroupID(cmd, "ssl", sslEmail)
}
0707010000000c000081a400000000000000000000000168ed21dd00001152000000000000000000000000000000000000001e00000000mgrctl/cmd/proxy/config_do.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"errors"

	"github.com/rs/zerolog/log"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/api/proxy"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

func proxyCreateConfigInit(
	_ *types.GlobalFlags,
	flags *proxyCreateConfigFlags,
	_ *cobra.Command,
	_ []string,
) error {
	return proxyCreateConfig(flags, api.Init, proxy.ContainerConfig, proxy.ContainerConfigGenerate)
}

// proxyCreateConfig command handler.
func proxyCreateConfig(
	flags *proxyCreateConfigFlags,
	apiInit func(*api.ConnectionDetails) (*api.APIClient, error),
	proxyConfig func(client *api.APIClient, request proxy.ProxyConfigRequest) (*[]int8, error),
	proxyConfigGenerate func(client *api.APIClient, request proxy.ProxyConfigGenerateRequest) (*[]int8, error),
) error {
	client, err := apiInit(&flags.ConnectionDetails)
	if err == nil {
		err = client.Login()
	}

	if err != nil {
		return utils.Errorf(err, L("failed to connect to the server"))
	}

	// handle CA certificate path
	caCertificate := string(utils.ReadFile(flags.SSL.Ca.Cert))

	// Check if ProxyCrt is provided to decide which configuration to run
	var data *[]int8
	if flags.SSL.Proxy.Cert != "" {
		data, err = handleProxyConfig(client, flags, caCertificate, proxyConfig)
	} else {
		data, err = handleProxyConfigGenerate(client, flags, caCertificate, proxyConfigGenerate)
	}

	if err != nil {
		return utils.Errorf(err, L("failed to execute proxy configuration api request"))
	}

	filename := GetFilename(flags.Output, flags.Proxy.Name)
	if err := utils.SaveBinaryData(filename, *data); err != nil {
		return utils.Errorf(err, L("error saving binary data: %v"), err)
	}
	log.Info().Msgf(L("Proxy configuration file saved as %s"), filename)

	return nil
}

// Helper function to handle proxy configuration.
func handleProxyConfig(
	client *api.APIClient,
	flags *proxyCreateConfigFlags,
	caCertificate string,
	proxyConfig func(client *api.APIClient, request proxy.ProxyConfigRequest) (*[]int8, error),
) (*[]int8, error) {
	// Custom validations
	if flags.SSL.Proxy.Key == "" {
		return nil, errors.New(L("flag proxyKey is required when flag proxyCrt is provided"))
	}

	// Read file paths for certificates and keys
	proxyCrt := string(utils.ReadFile(flags.SSL.Proxy.Cert))
	proxyKey := string(utils.ReadFile(flags.SSL.Proxy.Key))

	// Handle intermediate CAs
	var intermediateCAs []string
	for _, path := range flags.SSL.Ca.Intermediate {
		intermediateCAs = append(intermediateCAs, string(utils.ReadFile(path)))
	}

	// Prepare the request object & call the proxyConfig function
	request := proxy.ProxyConfigRequest{
		ProxyName:       flags.Proxy.Name,
		ProxyPort:       flags.Proxy.Port,
		Server:          flags.Proxy.Parent,
		MaxCache:        flags.Proxy.MaxCache,
		Email:           flags.Proxy.Email,
		RootCA:          caCertificate,
		ProxyCrt:        proxyCrt,
		ProxyKey:        proxyKey,
		IntermediateCAs: intermediateCAs,
	}

	return proxyConfig(client, request)
}

// Helper function to handle proxy configuration generation.
func handleProxyConfigGenerate(
	client *api.APIClient,
	flags *proxyCreateConfigFlags,
	caCertificate string,
	proxyConfigGenerate func(client *api.APIClient, request proxy.ProxyConfigGenerateRequest) (*[]int8, error),
) (*[]int8, error) {
	// CA key and password
	caKey := string(utils.ReadFile(flags.SSL.Ca.Key))

	var caPasswordRead string
	if flags.SSL.Ca.Password == "" {
		utils.AskPasswordIfMissingOnce(&caPasswordRead, L("Please enter SSL CA password"), 0, 0)
	} else {
		caPasswordRead = flags.SSL.Ca.Password
	}

	// Prepare the request object & call the proxyConfigGenerate function
	request := proxy.ProxyConfigGenerateRequest{
		ProxyName:  flags.Proxy.Name,
		ProxyPort:  flags.Proxy.Port,
		Server:     flags.Proxy.Parent,
		MaxCache:   flags.Proxy.MaxCache,
		Email:      flags.Proxy.Email,
		CaCrt:      caCertificate,
		CaKey:      caKey,
		CaPassword: caPasswordRead,
		Cnames:     flags.SSL.Cnames,
		Country:    flags.SSL.Country,
		State:      flags.SSL.State,
		City:       flags.SSL.City,
		Org:        flags.SSL.Org,
		OrgUnit:    flags.SSL.OU,
		SSLEmail:   flags.SSL.Email,
	}

	return proxyConfigGenerate(client, request)
}
0707010000000d000081a400000000000000000000000168ed21dd000036e3000000000000000000000000000000000000002300000000mgrctl/cmd/proxy/config_do_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"errors"
	"fmt"
	"path"
	"strings"
	"testing"

	"github.com/uyuni-project/uyuni-tools/shared/api"
	"github.com/uyuni-project/uyuni-tools/shared/api/mocks"
	proxyApi "github.com/uyuni-project/uyuni-tools/shared/api/proxy"
	"github.com/uyuni-project/uyuni-tools/shared/testutils"
	"github.com/uyuni-project/uyuni-tools/shared/types"

	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// common connection details (for generating the client).
var connectionDetails = api.ConnectionDetails{User: "testUser", Password: "testPwd", Server: "testServer"}

// dummy file contents.
const dummyCaCrtContents = "caCrt contents"
const dummyCaKeyContents = "caKey contents"
const dummyCaPasswordContents = "caPwd"
const dummyProxyCrtContents = "proxyCrt contents"
const dummyProxyKeyContents = "dummy proxyKey"
const dummyIntermediateCA1Contents = "dummy IntermediateCA 1 contents"
const dummyIntermediateCA2Contents = "dummy IntermediateCA 2 contents"

type TestFilePaths struct {
	OutputFilePath          string
	CaCrtFilePath           string
	CaKeyFilePath           string
	ProxyCrtFilePath        string
	ProxyKeyFilePath        string
	IntermediateCA1FilePath string
	IntermediateCA2FilePath string
}

// Helper function to mock a successful API call to login.
func mockSuccessfulLoginAPICall() func(conn *api.ConnectionDetails) (*api.APIClient, error) {
	return func(conn *api.ConnectionDetails) (*api.APIClient, error) {
		client, _ := api.Init(conn)
		client.Client = &mocks.MockClient{
			DoFunc: testutils.SuccessfulLoginTestDo,
		}
		return client, nil
	}
}

// Helper function to create a test files with given contents.
func setupTestFiles(t *testing.T, testDir string) TestFilePaths {
	outputFilePath := path.Join(testDir, t.Name()+".tar.gz")

	caCrtFilePath := createTestFile(testDir, "ca.pem", dummyCaCrtContents, t)
	caKeyFilePath := createTestFile(testDir, "caKey.pem", dummyCaKeyContents, t)
	proxyCrtFilePath := createTestFile(testDir, "proxyCrt.pem", dummyProxyCrtContents, t)
	proxyKeyFilePath := createTestFile(testDir, "proxyKey.txt", dummyProxyKeyContents, t)
	intermediateCA1FilePath := createTestFile(testDir, "intermediateCa1.pem", dummyIntermediateCA1Contents, t)
	intermediateCA2FilePath := createTestFile(testDir, "intermediateCa2.pem", dummyIntermediateCA2Contents, t)

	// Return all file paths in a struct
	return TestFilePaths{
		OutputFilePath:          outputFilePath,
		CaCrtFilePath:           caCrtFilePath,
		CaKeyFilePath:           caKeyFilePath,
		ProxyCrtFilePath:        proxyCrtFilePath,
		ProxyKeyFilePath:        proxyKeyFilePath,
		IntermediateCA1FilePath: intermediateCA1FilePath,
		IntermediateCA2FilePath: intermediateCA2FilePath,
	}
}

// tests a failure proxy create config generate command when no connection details are provided.
func TestFailProxyCreateConfigWhenNoConnectionDetailsAreProvided(t *testing.T) {
	// Setup
	testDir := t.TempDir()

	expectedOutputFilePath := path.Join(testDir, t.Name()+".tar.gz")
	flags := &proxyCreateConfigFlags{}
	expectedErrorMessage := "server URL is not provided"

	// Execute
	err := proxyCreateConfig(flags, api.Init, proxyApi.ContainerConfig, proxyApi.ContainerConfigGenerate)

	// Assertions
	testutils.AssertTrue(t, "Unexpected success execution of ProxyCreateConfig", err != nil)
	testutils.AssertTrue(t, "ProxyCreateConfig error message", strings.HasSuffix(err.Error(), expectedErrorMessage))
	testutils.AssertTrue(t, "File configuration file stored", !utils.FileExists(expectedOutputFilePath))
}

// tests a failure proxy create config generate command when login fails.
func TestFailProxyCreateConfigWhenLoginFails(t *testing.T) {
	// Setup structures and expected values
	testDir := t.TempDir()

	expectedOutputFilePath := path.Join(testDir, t.Name()+".tar.gz")
	flags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
	}
	expectedErrorMessage := "Either the password or username is incorrect."

	// Mock login api call to fail
	mockAPIFunc := func(conn *api.ConnectionDetails) (*api.APIClient, error) {
		client, _ := api.Init(conn)
		client.Client = &mocks.MockClient{
			DoFunc: testutils.FailedLoginTestDo,
		}
		return client, nil
	}

	// Execute
	err := proxyCreateConfig(flags, mockAPIFunc, proxyApi.ContainerConfig, proxyApi.ContainerConfigGenerate)

	// Assertions
	testutils.AssertTrue(t, "Unexpected success execution of ProxyCreateConfig", err != nil)
	testutils.AssertTrue(t, "ProxyCreateConfig error message", strings.HasSuffix(err.Error(), expectedErrorMessage))
	testutils.AssertTrue(t, "File configuration file stored", !utils.FileExists(expectedOutputFilePath))
}

// tests a failure proxy create config generate command when ProxyCrt is provided but ProxyKey is missing.
func TestFailProxyCreateConfigWhenProxyCrtIsProvidedButProxyKeyIsMissing(t *testing.T) {
	// Setup
	testDir := t.TempDir()

	testFiles := setupTestFiles(t, testDir)
	flags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
		SSL: proxyConfigSSLFlags{
			Ca: caFlags{
				SSLPair: types.SSLPair{Cert: testFiles.ProxyCrtFilePath},
			},
			Proxy: types.SSLPair{Cert: testFiles.ProxyCrtFilePath},
		},
	}
	expectedErrorMessage := "flag proxyKey is required when flag proxyCrt is provided"

	// Execute
	err := proxyCreateConfig(flags, mockSuccessfulLoginAPICall(), nil, nil)

	// Assertions
	testutils.AssertTrue(t, "Unexpected success execution of ProxyCreateConfig", err != nil)
	testutils.AssertTrue(t, "ProxyCreateConfig error message", strings.HasSuffix(err.Error(), expectedErrorMessage))
	testutils.AssertTrue(t, "File configuration file stored", !utils.FileExists(testFiles.OutputFilePath))
}

// tests a failure proxy create config command when proxy config request returns an error.
func TestFailProxyCreateConfigWhenProxyConfigApiRequestFails(t *testing.T) {
	// Setup
	testDir := t.TempDir()

	testFiles := setupTestFiles(t, testDir)
	mockContainerConfigflags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
		SSL: proxyConfigSSLFlags{
			Ca: caFlags{
				SSLPair: types.SSLPair{Cert: testFiles.CaCrtFilePath},
			},
			Proxy: types.SSLPair{
				Cert: testFiles.ProxyCrtFilePath,
				Key:  testFiles.ProxyKeyFilePath,
			},
		},
	}
	mockContainerConfigGenerateflags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
		SSL: proxyConfigSSLFlags{
			Ca: caFlags{
				SSLPair: types.SSLPair{
					Cert: testFiles.CaCrtFilePath,
					Key:  testFiles.CaKeyFilePath,
				},
				Password: dummyCaPasswordContents,
			},
		},
	}
	expectedReturnMessage := "Totally unexpected error"

	// Mock containerConfig api calls
	mockContainerConfig := func(_ *api.APIClient, _ proxyApi.ProxyConfigRequest) (*[]int8, error) {
		return nil, errors.New(expectedReturnMessage)
	}
	mockCreateConfigGenerate := func(_ *api.APIClient, _ proxyApi.ProxyConfigGenerateRequest) (*[]int8, error) {
		return nil, errors.New(expectedReturnMessage)
	}

	// Execute providing certs
	err := proxyCreateConfig(
		mockContainerConfigflags, mockSuccessfulLoginAPICall(), mockContainerConfig, mockCreateConfigGenerate,
	)

	// Assertions providing certs call
	testutils.AssertTrue(t, "Unexpected success execution of ProxyCreateConfig", err != nil)
	testutils.AssertTrue(t, "API proxy config return message", strings.HasSuffix(err.Error(), expectedReturnMessage))
	testutils.AssertTrue(t, "File configuration file stored", !utils.FileExists(testFiles.OutputFilePath))

	// Execute generate certs
	err = proxyCreateConfig(
		mockContainerConfigGenerateflags, mockSuccessfulLoginAPICall(), mockContainerConfig, mockCreateConfigGenerate,
	)

	// Assertions generate certs call
	testutils.AssertTrue(t, "Unexpected success execution of ProxyCreateConfig", err != nil)
	testutils.AssertTrue(t, "API proxy config return message", strings.HasSuffix(err.Error(), expectedReturnMessage))
	testutils.AssertTrue(t, "File configuration file stored", !utils.FileExists(testFiles.OutputFilePath))
}

// tests a successful proxy create config command when all parameters provided.
func TestSuccessProxyCreateConfigWhenAllParamsProvidedSuccess(t *testing.T) {
	// Setup
	testDir := t.TempDir()

	testFiles := setupTestFiles(t, testDir)

	output := path.Join(testDir, t.Name())
	expectedOutputFilePath := path.Join(testDir, t.Name()+".tar.gz")
	expectedConfigFileData := []int8{72, 105, 32, 77, 97, 114, 107, 33}

	flags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
		Proxy: proxyFlags{
			Name:     "testProxy",
			Port:     8080,
			Parent:   "testServer",
			MaxCache: 2048,
			Email:    "example@email.com",
		},
		Output: output,
		SSL: proxyConfigSSLFlags{
			Ca: caFlags{
				SSLPair:      types.SSLPair{Cert: testFiles.CaCrtFilePath},
				Intermediate: []string{testFiles.IntermediateCA1FilePath, testFiles.IntermediateCA2FilePath},
			},
			Proxy: types.SSLPair{
				Cert: testFiles.ProxyCrtFilePath,
				Key:  testFiles.ProxyKeyFilePath,
			},
		},
	}

	// Mock containerConfig api call
	mockContainerConfig := func(_ *api.APIClient, request proxyApi.ProxyConfigRequest) (*[]int8, error) {
		testutils.AssertEquals(t, "Unexpected proxyName", flags.Proxy.Name, request.ProxyName)
		testutils.AssertEquals(t, "Unexpected proxyPort", flags.Proxy.Port, request.ProxyPort)
		testutils.AssertEquals(t, "Unexpected server", flags.Proxy.Parent, request.Server)
		testutils.AssertEquals(t, "Unexpected maxCache", flags.Proxy.MaxCache, request.MaxCache)
		testutils.AssertEquals(t, "Unexpected email", flags.Proxy.Email, request.Email)
		testutils.AssertEquals(t, "Unexpected caCrt", dummyCaCrtContents, request.RootCA)
		testutils.AssertEquals(t, "Unexpected proxyCrt", dummyProxyCrtContents, request.ProxyCrt)
		testutils.AssertEquals(t, "Unexpected proxyKey", dummyProxyKeyContents, request.ProxyKey)
		testutils.AssertEquals(t, "Number of intermediateCAs", 2, len(request.IntermediateCAs))
		testutils.AssertEquals(t, "Unexpected intermediateCA", dummyIntermediateCA1Contents, request.IntermediateCAs[0])
		testutils.AssertEquals(t, "Unexpected intermediateCA", dummyIntermediateCA2Contents, request.IntermediateCAs[1])
		return &expectedConfigFileData, nil
	}

	// Execute
	err := proxyCreateConfig(flags, mockSuccessfulLoginAPICall(), mockContainerConfig, nil)

	// Assertions
	testutils.AssertTrue(t, "Unexpected error executing ProxyCreateConfig", err == nil)
	testutils.AssertTrue(t, "File configuration file was not stored", utils.FileExists(expectedOutputFilePath))

	storedConfigFile := testutils.ReadFileAsBinary(t, expectedOutputFilePath)
	testutils.AssertEquals(t, "File configuration binary doesn't match the response",
		fmt.Sprintf("%v", expectedConfigFileData),
		fmt.Sprintf("%v", storedConfigFile))
}

// tests a successful proxy create config command (with generated certificates) when all parameters provided.
func TestSuccessProxyCreateConfigGenerateWhenAllParamsProvidedSuccess(t *testing.T) {
	// Setup
	testDir := t.TempDir()

	testFiles := setupTestFiles(t, testDir)

	output := path.Join(testDir, t.Name())
	expectedOutputFilePath := path.Join(testDir, t.Name()+".tar.gz")
	expectedConfigFileData := []int8{72, 105, 32, 77, 97, 114, 107, 33}

	flags := &proxyCreateConfigFlags{
		ConnectionDetails: connectionDetails,
		Proxy: proxyFlags{
			Name:     "testProxy",
			Port:     8080,
			Parent:   "testServer",
			MaxCache: 2048,
			Email:    "example@email.com",
		},
		Output: output,
		SSL: proxyConfigSSLFlags{
			SSLCertGenerationFlags: types.SSLCertGenerationFlags{
				Cnames:  []string{"altNameA.example.com", "altNameB.example.com"},
				Country: "testCountry",
				State:   "exampleState",
				City:    "exampleCity",
				Org:     "exampleOrg",
				OU:      "exampleOrgUnit",
				Email:   "sslEmail@example.com",
			},
			Ca: caFlags{
				SSLPair: types.SSLPair{
					Cert: testFiles.CaCrtFilePath,
					Key:  testFiles.CaKeyFilePath,
				},
				Password: dummyCaPasswordContents,
			},
		},
	}

	// Mock api client & containerConfig
	mockCreateConfigGenerate := func(_ *api.APIClient, request proxyApi.ProxyConfigGenerateRequest) (*[]int8, error) {
		testutils.AssertEquals(t, "Unexpected proxyName", flags.Proxy.Name, request.ProxyName)
		testutils.AssertEquals(t, "Unexpected proxyPort", flags.Proxy.Port, request.ProxyPort)
		testutils.AssertEquals(t, "Unexpected server", flags.Proxy.Parent, request.Server)
		testutils.AssertEquals(t, "Unexpected maxCache", flags.Proxy.MaxCache, request.MaxCache)
		testutils.AssertEquals(t, "Unexpected email", flags.Proxy.Email, request.Email)
		testutils.AssertEquals(t, "Unexpected caCrt", dummyCaCrtContents, request.CaCrt)
		testutils.AssertEquals(t, "Unexpected caKey", dummyCaKeyContents, request.CaKey)
		testutils.AssertEquals(t, "Unexpected caPassword", dummyCaPasswordContents, request.CaPassword)
		testutils.AssertEquals(t, "Unexpected cnames", fmt.Sprintf("%v", flags.SSL.Cnames), fmt.Sprintf("%v", request.Cnames))
		testutils.AssertEquals(t, "Unexpected country", flags.SSL.Country, request.Country)
		testutils.AssertEquals(t, "Unexpected state", flags.SSL.State, request.State)
		testutils.AssertEquals(t, "Unexpected city", flags.SSL.City, request.City)
		testutils.AssertEquals(t, "Unexpected org", flags.SSL.Org, request.Org)
		testutils.AssertEquals(t, "Unexpected orgUnit", flags.SSL.OU, request.OrgUnit)
		testutils.AssertEquals(t, "Unexpected sslEmail", flags.SSL.Email, request.SSLEmail)
		return &expectedConfigFileData, nil
	}

	// Execute
	err := proxyCreateConfig(flags, mockSuccessfulLoginAPICall(), nil, mockCreateConfigGenerate)

	// Assertions
	testutils.AssertTrue(t, "Unexpected error executing ProxyCreateConfigGenerate", err == nil)
	testutils.AssertTrue(t, "File configuration file was not stored", utils.FileExists(expectedOutputFilePath))

	storedConfigFile := testutils.ReadFileAsBinary(t, expectedOutputFilePath)
	testutils.AssertEquals(t, "File configuration binary doesn't match the response",
		fmt.Sprintf("%v", expectedConfigFileData),
		fmt.Sprintf("%v", storedConfigFile))
}
0707010000000e000081a400000000000000000000000168ed21dd000010b8000000000000000000000000000000000000002000000000mgrctl/cmd/proxy/config_test.go// SPDX-FileCopyrightText: 2025 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"strings"
	"testing"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/shared/testutils"
	"github.com/uyuni-project/uyuni-tools/shared/testutils/flagstests"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

func getCommonArgs() []string {
	args := []string{
		"--proxy-name", "pxy1.test.com",
		"--proxy-sshPort", "1234",
		"--proxy-parent", "uyuni.test.com",
		"--proxy-maxCache", "123456",
		"--proxy-email", "admin@proxy.test.com",
		"--output", "path/to/output.tgz",
		"--ssl-ca-cert", "path/to/ca.crt",
	}
	args = append(args, flagstests.APIFlagsTestArgs...)
	return args
}

func assertCommonArgs(t *testing.T, flags *proxyCreateConfigFlags) {
	flagstests.AssertAPIFlags(t, &flags.ConnectionDetails)
	testutils.AssertEquals(t, "Unexpected proxy name", "pxy1.test.com", flags.Proxy.Name)
	testutils.AssertEquals(t, "Unexpected proxy SSH port", 1234, flags.Proxy.Port)
	testutils.AssertEquals(t, "Unexpected proxy parent", "uyuni.test.com", flags.Proxy.Parent)
	testutils.AssertEquals(t, "Unexpected proxy max cache", 123456, flags.Proxy.MaxCache)
	testutils.AssertEquals(t, "Unexpected proxy email", "admin@proxy.test.com", flags.Proxy.Email)
	testutils.AssertEquals(t, "Unexpected output path", "path/to/output.tgz", flags.Output)
	testutils.AssertEquals(t, "Unexpected SSL CA cert path", "path/to/ca.crt", flags.SSL.Ca.Cert)
}

func TestParamsParsingGeneratedCert(t *testing.T) {
	args := getCommonArgs()
	args = append(args,
		"--ssl-ca-cert", "path/to/ca.crt",
		"--ssl-ca-key", "path/to/ca.key",
		"--ssl-ca-password", "casecret",
		"--ssl-email", "ssl@test.com",
	)
	args = append(args, flagstests.SSLGenerationFlagsTestArgs...)

	conflictingFlags := []string{
		"--ssl-proxy-cert",
		"--ssl-proxy-key",
		"--ssl-ca-intermediate",
	}

	// Test function asserting that the args are properly parsed
	tester := func(_ *types.GlobalFlags, flags *proxyCreateConfigFlags, _ *cobra.Command, _ []string) error {
		assertCommonArgs(t, flags)
		flagstests.AssertSSLGenerationFlag(t, &flags.SSL.SSLCertGenerationFlags)
		testutils.AssertEquals(t, "Unexpected SSL CA cert path", "path/to/ca.crt", flags.SSL.Ca.Cert)
		testutils.AssertEquals(t, "Unexpected SSL CA key path", "path/to/ca.key", flags.SSL.Ca.Key)
		testutils.AssertEquals(t, "Unexpected SSL CA password", "casecret", flags.SSL.Ca.Password)
		testutils.AssertEquals(t, "Unexpected SSL email", "ssl@test.com", flags.SSL.Email)
		return nil
	}

	globalFlags := types.GlobalFlags{}
	cmd := newCmd(&globalFlags, tester)

	testutils.AssertHasAllFlagsIgnores(t, cmd, args, conflictingFlags)

	t.Logf("flags: %s", strings.Join(args, " "))
	cmd.SetArgs(args)
	if err := cmd.Execute(); err != nil {
		t.Errorf("command failed with error: %s", err)
	}
}

func TestParamsParsingProvidedCert(t *testing.T) {
	args := getCommonArgs()
	args = append(args,
		"--ssl-ca-intermediate", "path/to/ca1.crt",
		"--ssl-ca-intermediate", "path/to/ca2.crt",
		"--ssl-proxy-cert", "path/to/proxy.crt",
		"--ssl-proxy-key", "path/to/proxy.key",
	)

	conflictingFlags := []string{
		"--ssl-email",
		"--ssl-ca-key",
		"--ssl-ca-password",
		"--ssl-cname",
		"--ssl-country",
		"--ssl-state",
		"--ssl-city",
		"--ssl-org",
		"--ssl-ou",
	}

	// Test function asserting that the args are properly parsed
	tester := func(_ *types.GlobalFlags, flags *proxyCreateConfigFlags, _ *cobra.Command, _ []string) error {
		assertCommonArgs(t, flags)
		testutils.AssertEquals(t, "Unexpected SSL CA cert path", "path/to/ca.crt", flags.SSL.Ca.Cert)
		testutils.AssertEquals(t, "Unexpected SSL intermediate CA cert paths",
			[]string{"path/to/ca1.crt", "path/to/ca2.crt"}, flags.SSL.Ca.Intermediate,
		)
		testutils.AssertEquals(t, "Unexpected Proxy SSL cert path", "path/to/proxy.crt", flags.SSL.Proxy.Cert)
		testutils.AssertEquals(t, "Unexpected Proxy SSL key path", "path/to/proxy.key", flags.SSL.Proxy.Key)
		return nil
	}

	globalFlags := types.GlobalFlags{}
	cmd := newCmd(&globalFlags, tester)

	testutils.AssertHasAllFlagsIgnores(t, cmd, args, conflictingFlags)

	t.Logf("flags: %s", strings.Join(args, " "))
	cmd.SetArgs(args)
	if err := cmd.Execute(); err != nil {
		t.Errorf("command failed with error: %s", err)
	}
}
0707010000000f000081a400000000000000000000000168ed21dd00000391000000000000000000000000000000000000001a00000000mgrctl/cmd/proxy/proxy.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"github.com/spf13/cobra"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// NewCommand entry command for managing cache.
// Setup for subcommand to clear (the cache).
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	var cmd = &cobra.Command{
		Use:   "proxy",
		Short: L("Manage proxy configurations"),
		Long:  L("Manage proxy configurations"),
		Run: func(cmd *cobra.Command, _ []string) {
			_ = cmd.Help()
		},
	}

	var createCmd = &cobra.Command{
		Use:   "create",
		Short: L("Create proxy configurations"),
		Long:  L("Create proxy configurations"),
		Run: func(cmd *cobra.Command, _ []string) {
			_ = cmd.Help()
		},
	}

	createCmd.AddCommand(NewConfigCommand(globalFlags))

	cmd.AddCommand(createCmd)
	return cmd
}
07070100000010000081a400000000000000000000000168ed21dd000001d8000000000000000000000000000000000000001a00000000mgrctl/cmd/proxy/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"strings"
)

// GetFilename returns the filename to save a configuration file to.
// If an output filename is not specified, then the filename is based on the proxy name.
func GetFilename(output string, proxyName string) string {
	filename := output
	if filename == "" {
		filename = strings.Split(proxyName, ".")[0] + "-config"
	}
	return filename + ".tar.gz"
}
07070100000011000081a400000000000000000000000168ed21dd000003b1000000000000000000000000000000000000001f00000000mgrctl/cmd/proxy/utils_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package proxy

import (
	"path"
	"testing"

	"github.com/uyuni-project/uyuni-tools/shared/testutils"
)

// Test getFilename function.
func TestGetFilename(t *testing.T) {
	// Test when output is empty
	filename := GetFilename("", "testProxy.domain.com")
	testutils.AssertEquals(t, "", "testProxy-config.tar.gz", filename)

	// Test when output is provided
	filename = GetFilename("customOutput", "testProxy.domain.com")
	testutils.AssertEquals(t, "", "customOutput.tar.gz", filename)

	// Test when output is provided
	filename = GetFilename("/var/customOutputWitPath", "testProxy.domain.com")
	testutils.AssertEquals(t, "", "/var/customOutputWitPath.tar.gz", filename)
}

func createTestFile(dir string, filename string, content string, t *testing.T) string {
	filepath := path.Join(dir, filename)
	testutils.WriteFile(t, filepath, content)
	return filepath
}
07070100000012000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000001100000000mgrctl/cmd/proxy07070100000013000081a400000000000000000000000168ed21dd0000040e000000000000000000000000000000000000001800000000mgrctl/cmd/term/term.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package term

import (
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	. "github.com/uyuni-project/uyuni-tools/shared/l10n"
	"github.com/uyuni-project/uyuni-tools/shared/types"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

var newExecCmd = exec.NewCommand

// NewCommand returns a new cobra.Command for term.
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "term",
		Short: L("Run a terminal inside the server container"),
		RunE: func(cmd *cobra.Command, _ []string) error {
			execCmd := newExecCmd(globalFlags)
			execArgs := []string{"-i", "-t"}
			backend, err := cmd.Flags().GetString("backend")
			if err == nil {
				execArgs = append(execArgs, "--backend", backend)
			}
			if err := execCmd.Flags().Parse(execArgs); err != nil {
				return err
			}
			return execCmd.RunE(execCmd, []string{"bash"})
		},
	}

	utils.AddBackendFlag(cmd)
	return cmd
}
07070100000014000081a400000000000000000000000168ed21dd00000509000000000000000000000000000000000000001d00000000mgrctl/cmd/term/term_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package term

import (
	"errors"
	"testing"

	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
	"github.com/uyuni-project/uyuni-tools/shared/types"
)

// Ensure the term command properly delegates to the exec one.
func TestExecute(t *testing.T) {
	var globalFlags types.GlobalFlags

	newExecCmd = func(globalFlags *types.GlobalFlags) *cobra.Command {
		execCmd := exec.NewCommand(globalFlags)
		execCmd.RunE = func(cmd *cobra.Command, _ []string) error {
			if interactive, err := cmd.Flags().GetBool("interactive"); err != nil || !interactive {
				t.Error("interactive flag not passed")
			}
			if tty, err := cmd.Flags().GetBool("tty"); err != nil || !tty {
				t.Error("tty flag not passed")
			}
			if backend, err := cmd.Flags().GetString("backend"); err != nil || backend != "mybackend" {
				t.Error("backend flag not passed")
			}
			return errors.New("some error")
		}
		return execCmd
	}

	cmd := NewCommand(&globalFlags)
	if err := cmd.Flags().Parse([]string{"--backend", "mybackend"}); err != nil {
		t.Errorf("failed to parse flags: %s", err)
	}
	if err := cmd.RunE(cmd, []string{}); err.Error() != "some error" {
		t.Errorf("Unexpected error returned")
	}
}
07070100000015000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000001000000000mgrctl/cmd/term07070100000016000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000000b00000000mgrctl/cmd07070100000017000081a400000000000000000000000168ed21dd00000295000000000000000000000000000000000000000f00000000mgrctl/main.go// SPDX-FileCopyrightText: 2025 SUSE LLC
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"os"

	"github.com/chai2010/gettext-go"
	"github.com/spf13/cobra"
	"github.com/uyuni-project/uyuni-tools/mgrctl/cmd"
	l10n_utils "github.com/uyuni-project/uyuni-tools/shared/l10n/utils"
	"github.com/uyuni-project/uyuni-tools/shared/utils"
)

// Run runs the `mgrctl` root command.
func Run() error {
	gettext.BindLocale(gettext.New("mgrctl", utils.LocaleRoot, l10n_utils.New(utils.LocaleRoot)))
	cobra.EnableCaseInsensitive = true
	run := cmd.NewUyunictlCommand()

	return run.Execute()
}

func main() {
	if err := Run(); err != nil {
		os.Exit(1)
	}
}
07070100000018000041ed00000000000000000000000168ed21dd00000000000000000000000000000000000000000000000700000000mgrctl07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!
openSUSE Build Service is sponsored by