diff --git a/agent/cmd/root.go b/agent/cmd/root.go deleted file mode 100644 index 6bfd484078..0000000000 --- a/agent/cmd/root.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var ( - cliExitInterceptor func(code int) -) - -var rootCmd = cobra.Command{ - Use: "agent", - Short: "Manages the tracetest agent", - Long: "Manages the tracetest agent", -} - -func init() { - rootCmd.AddCommand(&StartCmd) -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) - ExitCLI(1) - } -} - -func ExitCLI(errorCode int) { - if cliExitInterceptor != nil { - cliExitInterceptor(errorCode) - return - } - - os.Exit(errorCode) -} - -func RegisterCLIExitInterceptor(interceptor func(int)) { - cliExitInterceptor = interceptor -} diff --git a/agent/cmd/start.go b/agent/cmd/start.go deleted file mode 100644 index 44afce6e07..0000000000 --- a/agent/cmd/start.go +++ /dev/null @@ -1,46 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "os" - - "github.com/kubeshop/tracetest/agent/config" - "github.com/kubeshop/tracetest/agent/initialization" - "github.com/spf13/cobra" -) - -var ( - apiKey string - devMode bool -) - -var StartCmd = cobra.Command{ - Use: "start", - Short: "Start the local agent", - Long: "Start the local agent", - Run: func(cmd *cobra.Command, args []string) { - ctx := cmd.Context() - cfg, err := config.LoadConfig() - if err != nil { - fmt.Fprintln(os.Stderr, err) - ExitCLI(1) - } - - log.Printf("starting agent [%s] connecting to %s", cfg.Name, cfg.ServerURL) - - session, err := initialization.Start(ctx, cfg) - if err != nil { - fmt.Fprintln(os.Stderr, err) - ExitCLI(1) - } - - session.WaitUntilDisconnected() - session.Close() - }, -} - -func init() { - StartCmd.Flags().StringVarP(&apiKey, "apiKey", "", "", "the API key from the environment that will run the tests") - StartCmd.Flags().BoolVarP(&devMode, "devMode", "d", false, "starts a dev mode session on your private environment") -} diff --git a/agent/config/flags.go b/agent/config/flags.go new file mode 100644 index 0000000000..81b9ae82ed --- /dev/null +++ b/agent/config/flags.go @@ -0,0 +1,18 @@ +package config + +type Mode string + +const ( + Mode_Desktop Mode = "desktop" + Mode_Verbose Mode = "verbose" +) + +type Flags struct { + Endpoint string + OrganizationID string + EnvironmentID string + CI bool + AgentApiKey string + Token string + Mode Mode +} diff --git a/agent/initialization/session.go b/agent/initialization/session.go deleted file mode 100644 index 519486fd0e..0000000000 --- a/agent/initialization/session.go +++ /dev/null @@ -1,16 +0,0 @@ -package initialization - -import "github.com/kubeshop/tracetest/agent/client" - -type Session struct { - Token string - client *client.Client -} - -func (s *Session) Close() { - s.client.Close() -} - -func (s *Session) WaitUntilDisconnected() { - s.client.WaitUntilDisconnected() -} diff --git a/agent/main.go b/agent/main.go deleted file mode 100644 index 00b69981c2..0000000000 --- a/agent/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/kubeshop/tracetest/agent/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/agent/runner/environment.go b/agent/runner/environment.go new file mode 100644 index 0000000000..9b7857f7cd --- /dev/null +++ b/agent/runner/environment.go @@ -0,0 +1,48 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/kubeshop/tracetest/cli/config" + "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" +) + +type environment struct { + ID string `json:"id"` + Name string `json:"name"` + AgentApiKey string `json:"agentApiKey"` + OrganizationID string `json:"organizationID"` +} + +func (s *Runner) getEnvironment(ctx context.Context, cfg config.Config) (environment, error) { + resource, err := s.resources.Get("env") + if err != nil { + return environment{}, err + } + + resource = resource. + WithHttpClient(config.SetupHttpClient(cfg)). + WithOptions(resourcemanager.WithPrefixGetter(func() string { + return fmt.Sprintf("/organizations/%s/", cfg.OrganizationID) + })) + + resultFormat, err := resourcemanager.Formats.GetWithFallback("json", "json") + if err != nil { + return environment{}, err + } + + raw, err := resource.Get(ctx, cfg.EnvironmentID, resultFormat) + if err != nil { + return environment{}, err + } + + var env environment + err = json.Unmarshal([]byte(raw), &env) + if err != nil { + return environment{}, err + } + + return env, nil +} diff --git a/agent/runner/runner.go b/agent/runner/runner.go new file mode 100644 index 0000000000..84a545ca8a --- /dev/null +++ b/agent/runner/runner.go @@ -0,0 +1,86 @@ +package runner + +import ( + "context" + + agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/agent/ui" + + "github.com/kubeshop/tracetest/cli/config" + "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" +) + +type Runner struct { + configurator config.Configurator + resources *resourcemanager.Registry + ui ui.ConsoleUI + mode agentConfig.Mode +} + +func NewRunner(configurator config.Configurator, resources *resourcemanager.Registry, ui ui.ConsoleUI) *Runner { + return &Runner{ + configurator: configurator, + resources: resources, + ui: ui, + mode: agentConfig.Mode_Desktop, + } +} + +func (s *Runner) Run(ctx context.Context, cfg config.Config, flags agentConfig.Flags) error { + s.ui.Banner(config.Version) + s.ui.Println(`Tracetest start launches a lightweight agent. It enables you to run tests and collect traces with Tracetest. +Once started, Tracetest Agent exposes OTLP ports 4317 and 4318 to ingest traces via gRCP and HTTP.`) + + if flags.Token == "" || flags.AgentApiKey != "" { + s.configurator = s.configurator.WithOnFinish(s.onStartAgent) + } + + s.ui.Infof("Running in %s mode...", s.mode) + + s.mode = flags.Mode + + return s.configurator.Start(ctx, cfg, flags) +} + +func (s *Runner) onStartAgent(ctx context.Context, cfg config.Config) { + if cfg.AgentApiKey != "" { + err := s.StartAgent(ctx, cfg.AgentEndpoint, cfg.AgentApiKey, cfg.UIEndpoint) + if err != nil { + s.ui.Error(err.Error()) + } + + return + } + + env, err := s.getEnvironment(ctx, cfg) + if err != nil { + s.ui.Error(err.Error()) + } + + err = s.StartAgent(ctx, cfg.AgentEndpoint, env.AgentApiKey, cfg.UIEndpoint) + if err != nil { + s.ui.Error(err.Error()) + } +} + +func (s *Runner) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoint string) error { + cfg, err := agentConfig.LoadConfig() + if err != nil { + return err + } + + if endpoint != "" { + cfg.ServerURL = endpoint + } + + if agentApiKey != "" { + cfg.APIKey = agentApiKey + } + + if s.mode == agentConfig.Mode_Desktop { + return RunDesktopStrategy(ctx, cfg, s.ui, uiEndpoint) + } + + // TODO: Add verbose strategy + return nil +} diff --git a/agent/runner/runstrategy_desktop.go b/agent/runner/runstrategy_desktop.go new file mode 100644 index 0000000000..414f2a4092 --- /dev/null +++ b/agent/runner/runstrategy_desktop.go @@ -0,0 +1,70 @@ +package runner + +import ( + "context" + "errors" + "fmt" + + agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/cli/config" + + consoleUI "github.com/kubeshop/tracetest/agent/ui" +) + +func RunDesktopStrategy(ctx context.Context, cfg agentConfig.Config, ui consoleUI.ConsoleUI, uiEndpoint string) error { + ui.Infof("Starting Agent with name %s...", cfg.Name) + + isStarted := false + session := &Session{} + + var err error + + for !isStarted { + session, err = StartSession(ctx, cfg) + if err != nil && errors.Is(err, ErrOtlpServerStart) { + ui.Error("Tracetest Agent binds to the OpenTelemetry ports 4317 and 4318 which are used to receive trace information from your system. The agent tried to bind to these ports, but failed.") + shouldRetry := ui.Enter("Please stop the process currently listening on these ports and press enter to try again.") + + if !shouldRetry { + ui.Finish() + return err + } + + continue + } + + if err != nil { + return err + } + + isStarted = true + } + + claims, err := config.GetTokenClaims(session.Token) + if err != nil { + return err + } + + isOpen := true + message := `Agent is started! Leave the terminal open so tests can be run and traces gathered from this environment. +You can` + options := []consoleUI.Option{{ + Text: "Open Tracetest in a browser to this environment", + Fn: func(_ consoleUI.ConsoleUI) { + ui.OpenBrowser(fmt.Sprintf("%sorganizations/%s/environments/%s/dashboard", uiEndpoint, claims["organization_id"], claims["environment_id"])) + }, + }, { + Text: "Stop this agent", + Fn: func(_ consoleUI.ConsoleUI) { + isOpen = false + session.Close() + ui.Finish() + }, + }} + + for isOpen { + selected := ui.Select(message, options, 0) + selected.Fn(ui) + } + return nil +} diff --git a/agent/initialization/start.go b/agent/runner/session.go similarity index 84% rename from agent/initialization/start.go rename to agent/runner/session.go index acdb9d97a5..246eaabbe6 100644 --- a/agent/initialization/start.go +++ b/agent/runner/session.go @@ -1,4 +1,4 @@ -package initialization +package runner import ( "context" @@ -12,6 +12,7 @@ import ( "github.com/kubeshop/tracetest/agent/proto" "github.com/kubeshop/tracetest/agent/workers" "github.com/kubeshop/tracetest/agent/workers/poller" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) @@ -20,51 +21,23 @@ var ErrOtlpServerStart = errors.New("OTLP server start error") var logger *zap.Logger -func NewClient(ctx context.Context, config config.Config, traceCache collector.TraceCache) (*client.Client, error) { - if enableLogging() { - var err error - logger, err = zap.NewDevelopment() - if err != nil { - return nil, fmt.Errorf("could not create logger: %w", err) - } - } - - controlPlaneClient, err := client.Connect(ctx, config.ServerURL, - client.WithAPIKey(config.APIKey), - client.WithAgentName(config.Name), - client.WithLogger(logger), - ) - if err != nil { - return nil, err - } - - triggerWorker := workers.NewTriggerWorker(controlPlaneClient, workers.WithTraceCache(traceCache)) - pollingWorker := workers.NewPollerWorker(controlPlaneClient, workers.WithInMemoryDatastore( - poller.NewInMemoryDatastore(traceCache), - )) - dataStoreTestConnectionWorker := workers.NewTestConnectionWorker(controlPlaneClient) - - if enableLogging() { - triggerWorker.SetLogger(logger) - pollingWorker.SetLogger(logger) - dataStoreTestConnectionWorker.SetLogger(logger) - } +type Session struct { + Token string + client *client.Client +} - controlPlaneClient.OnDataStoreTestConnectionRequest(dataStoreTestConnectionWorker.Test) - controlPlaneClient.OnTriggerRequest(triggerWorker.Trigger) - controlPlaneClient.OnPollingRequest(pollingWorker.Poll) - controlPlaneClient.OnConnectionClosed(func(ctx context.Context, sr *proto.ShutdownRequest) error { - fmt.Printf("Server terminated the connection with the agent. Reason: %s\n", sr.Reason) - return controlPlaneClient.Close() - }) +func (s *Session) Close() { + s.client.Close() +} - return controlPlaneClient, nil +func (s *Session) WaitUntilDisconnected() { + s.client.WaitUntilDisconnected() } -// Start the agent with given configuration -func Start(ctx context.Context, cfg config.Config) (*Session, error) { +// Start the agent session with given configuration +func StartSession(ctx context.Context, cfg config.Config) (*Session, error) { traceCache := collector.NewTraceCache() - controlPlaneClient, err := NewClient(ctx, cfg, traceCache) + controlPlaneClient, err := newControlPlaneClient(ctx, cfg, traceCache) if err != nil { return nil, err } @@ -117,3 +90,44 @@ func StartCollector(ctx context.Context, config config.Config, traceCache collec func enableLogging() bool { return os.Getenv("TRACETEST_DEV") == "true" } + +func newControlPlaneClient(ctx context.Context, config config.Config, traceCache collector.TraceCache) (*client.Client, error) { + if enableLogging() { + var err error + logger, err = zap.NewDevelopment() + if err != nil { + return nil, fmt.Errorf("could not create logger: %w", err) + } + } + + controlPlaneClient, err := client.Connect(ctx, config.ServerURL, + client.WithAPIKey(config.APIKey), + client.WithAgentName(config.Name), + client.WithLogger(logger), + ) + if err != nil { + return nil, err + } + + triggerWorker := workers.NewTriggerWorker(controlPlaneClient, workers.WithTraceCache(traceCache)) + pollingWorker := workers.NewPollerWorker(controlPlaneClient, workers.WithInMemoryDatastore( + poller.NewInMemoryDatastore(traceCache), + )) + dataStoreTestConnectionWorker := workers.NewTestConnectionWorker(controlPlaneClient) + + if enableLogging() { + triggerWorker.SetLogger(logger) + pollingWorker.SetLogger(logger) + dataStoreTestConnectionWorker.SetLogger(logger) + } + + controlPlaneClient.OnDataStoreTestConnectionRequest(dataStoreTestConnectionWorker.Test) + controlPlaneClient.OnTriggerRequest(triggerWorker.Trigger) + controlPlaneClient.OnPollingRequest(pollingWorker.Poll) + controlPlaneClient.OnConnectionClosed(func(ctx context.Context, sr *proto.ShutdownRequest) error { + fmt.Printf("Server terminated the connection with the agent. Reason: %s\n", sr.Reason) + return controlPlaneClient.Close() + }) + + return controlPlaneClient, nil +} diff --git a/agent/ui/pterm.go b/agent/ui/pterm.go new file mode 100644 index 0000000000..71b4a2cbc2 --- /dev/null +++ b/agent/ui/pterm.go @@ -0,0 +1,192 @@ +package ui + +import ( + "fmt" + "os" + "os/exec" + "runtime" + + "github.com/pterm/pterm" + "github.com/pterm/pterm/putils" +) + +type ptermUI struct{} + +func (ui ptermUI) Banner(version string) { + pterm.Print("\n\n") + + pterm.DefaultBigText. + WithLetters(putils.LettersFromString("TraceTest")). + Render() + + pterm.Print(fmt.Sprintf("Version: %s", version)) + + pterm.Print("\n\n") + +} + +func (ui ptermUI) Panic(err error) { + pterm.Error.WithFatal(true).Println(err) +} + +func (ui ptermUI) Finish() { + os.Exit(0) +} + +func (ui ptermUI) Exit(msg string) { + pterm.Error.Println(msg) + os.Exit(1) +} + +func (ui ptermUI) Errorf(msg string, args ...any) { + ui.Error(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Warningf(msg string, args ...any) { + ui.Warning(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Infof(msg string, args ...any) { + ui.Info(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Printlnf(msg string, args ...any) { + ui.Println(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Error(msg string) { + pterm.Error.Println(msg) +} + +func (ui ptermUI) Warning(msg string) { + pterm.Warning.Println(msg) +} + +func (ui ptermUI) Info(msg string) { + pterm.Info.Println(msg) +} + +func (ui ptermUI) Success(msg string) { + pterm.Success.Println(msg) +} + +func (ui ptermUI) Println(msg string) { + pterm.Println(msg) +} + +func (ui ptermUI) Title(msg string) { + pterm.Println(pterm.Yellow("\n-> ", msg, "\n")) +} + +func (ui ptermUI) Green(msg string) string { + return pterm.Green(msg) +} + +func (ui ptermUI) Red(msg string) string { + return pterm.Red(msg) +} + +func (ui ptermUI) Confirm(msg string, defaultValue bool) bool { + confirm, err := (&pterm.InteractiveConfirmPrinter{ + DefaultValue: defaultValue, + DefaultText: msg, + TextStyle: &pterm.ThemeDefault.DefaultText, + ConfirmText: "Yes", + ConfirmStyle: &pterm.ThemeDefault.SuccessMessageStyle, + RejectText: "No", + RejectStyle: &pterm.ThemeDefault.ErrorMessageStyle, + SuffixStyle: &pterm.ThemeDefault.SecondaryStyle, + }). + Show() + if err != nil { + ui.Panic(err) + } + + return confirm +} + +func (ui ptermUI) Enter(msg string) bool { + confirm, err := (&pterm.InteractiveConfirmPrinter{ + DefaultText: msg, + DefaultValue: true, + TextStyle: &pterm.ThemeDefault.DefaultText, + ConfirmText: "Enter", + RejectText: "Cancel", + RejectStyle: &pterm.ThemeDefault.ErrorMessageStyle, + ConfirmStyle: &pterm.ThemeDefault.SuccessMessageStyle, + SuffixStyle: &pterm.ThemeDefault.SecondaryStyle, + }). + Show() + if err != nil { + ui.Panic(err) + } + + return confirm +} + +func (ui ptermUI) TextInput(msg, defaultValue string) string { + text := msg + if defaultValue != "" { + text = fmt.Sprintf("%s (default: %s)", msg, defaultValue) + } + text, err := (&pterm.InteractiveTextInputPrinter{ + TextStyle: &pterm.ThemeDefault.DefaultText, + DefaultText: text, + MultiLine: false, + }). + Show() + ui.Println("") + if err != nil { + ui.Panic(err) + } + + if text == "" { + return defaultValue + } + + return text +} + +func (ui ptermUI) Select(prompt string, options []Option, defaultIndex int) (selected Option) { + textOpts := make([]string, len(options)) + lookupMap := make(map[string]int) + + for ix, opt := range options { + textOpts[ix] = opt.Text + if _, ok := lookupMap[opt.Text]; ok { + panic(fmt.Sprintf("duplicated option %s", opt.Text)) + } + lookupMap[opt.Text] = ix + } + + selectedText, err := (&pterm.InteractiveSelectPrinter{ + TextStyle: &pterm.ThemeDefault.DefaultText, + DefaultText: prompt, + Options: textOpts, + OptionStyle: &pterm.ThemeDefault.DefaultText, + DefaultOption: textOpts[defaultIndex], + MaxHeight: 5, + Selector: ">", + SelectorStyle: &pterm.ThemeDefault.SecondaryStyle, + }). + Show() + if err != nil { + panic(err) + } + + selectedIx := lookupMap[selectedText] + return options[selectedIx] +} + +func (ui ptermUI) OpenBrowser(u string) error { + switch runtime.GOOS { + case "linux": + return exec.Command("xdg-open", u).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", u).Start() + case "darwin": + return exec.Command("open", u).Start() + default: + return fmt.Errorf("unsupported platform") + } +} diff --git a/agent/ui/ui.go b/agent/ui/ui.go new file mode 100644 index 0000000000..6067ab83f6 --- /dev/null +++ b/agent/ui/ui.go @@ -0,0 +1,38 @@ +package ui + +var DefaultUI ConsoleUI = &ptermUI{} + +type ConsoleUI interface { + Banner(version string) + + Panic(error) + Exit(string) + + Errorf(string, ...any) + Warningf(string, ...any) + Infof(string, ...any) + Printlnf(string, ...any) + + Error(string) + Warning(string) + Info(string) + Success(string) + Finish() + + Println(string) + Title(string) + OpenBrowser(string) error + + Green(string) string + Red(string) string + + Confirm(prompt string, defaultValue bool) bool + Enter(msg string) bool + Select(prompt string, options []Option, defaultIndex int) (selected Option) + TextInput(msg, defaultValue string) string +} + +type Option struct { + Text string + Fn func(ui ConsoleUI) +} diff --git a/cli/cmd/configure_cmd.go b/cli/cmd/configure_cmd.go index 23545d09e1..598cc4a877 100644 --- a/cli/cmd/configure_cmd.go +++ b/cli/cmd/configure_cmd.go @@ -4,6 +4,7 @@ import ( "context" "net/url" + agentConfig "github.com/kubeshop/tracetest/agent/config" "github.com/kubeshop/tracetest/cli/config" "github.com/spf13/cobra" ) @@ -22,7 +23,7 @@ var configureCmd = &cobra.Command{ PreRun: setupLogger, Run: WithResultHandler(WithParamsHandler(configParams)(func(cmd *cobra.Command, _ []string) (string, error) { ctx := context.Background() - flags := config.ConfigFlags{ + flags := agentConfig.Flags{ CI: configParams.CI, } config, err := config.LoadConfig("") diff --git a/cli/cmd/start_cmd.go b/cli/cmd/start_cmd.go index 3d8b7ea34f..e685e32bd5 100644 --- a/cli/cmd/start_cmd.go +++ b/cli/cmd/start_cmd.go @@ -5,13 +5,13 @@ import ( "os" agentConfig "github.com/kubeshop/tracetest/agent/config" - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/pkg/starter" + "github.com/kubeshop/tracetest/agent/runner" + "github.com/kubeshop/tracetest/agent/ui" "github.com/spf13/cobra" ) var ( - start = starter.NewStarter(configurator, resources) + agentRunner = runner.NewRunner(configurator, resources, ui.DefaultUI) defaultToken = os.Getenv("TRACETEST_TOKEN") defaultEndpoint = os.Getenv("TRACETEST_SERVER_URL") defaultAPIKey = os.Getenv("TRACETEST_API_KEY") @@ -27,12 +27,13 @@ var startCmd = &cobra.Command{ Run: WithResultHandler((func(_ *cobra.Command, _ []string) (string, error) { ctx := context.Background() - flags := config.ConfigFlags{ + flags := agentConfig.Flags{ OrganizationID: saveParams.organizationID, EnvironmentID: saveParams.environmentID, Endpoint: saveParams.endpoint, AgentApiKey: saveParams.agentApiKey, Token: saveParams.token, + Mode: agentConfig.Mode(saveParams.mode), } cfg, err := agentConfig.LoadConfig() @@ -44,7 +45,7 @@ var startCmd = &cobra.Command{ flags.AgentApiKey = cfg.APIKey } - err = start.Run(ctx, cliConfig, flags) + err = agentRunner.Run(ctx, cliConfig, flags) return "", err })), PostRun: teardownCommand, @@ -54,8 +55,9 @@ func init() { startCmd.Flags().StringVarP(&saveParams.organizationID, "organization", "", "", "organization id") startCmd.Flags().StringVarP(&saveParams.environmentID, "environment", "", "", "environment id") startCmd.Flags().StringVarP(&saveParams.agentApiKey, "api-key", "", defaultAPIKey, "agent api key") - startCmd.Flags().StringVarP(&saveParams.token, "token", "", defaultToken, "token api key") + startCmd.Flags().StringVarP(&saveParams.token, "token", "", defaultToken, "token authentication key") startCmd.Flags().StringVarP(&saveParams.endpoint, "endpoint", "e", defaultEndpoint, "set the value for the endpoint, so the CLI won't ask for this value") + startCmd.Flags().StringVarP(&saveParams.mode, "mode", "m", "desktop", "set how the agent will start") rootCmd.AddCommand(startCmd) } @@ -65,4 +67,5 @@ type saveParameters struct { endpoint string agentApiKey string token string + mode string } diff --git a/cli/config/config.go b/cli/config/config.go index c57f9dcee3..89ba800d98 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -21,15 +21,6 @@ var ( DefaultCloudPath = "/" ) -type ConfigFlags struct { - Endpoint string - OrganizationID string - EnvironmentID string - CI bool - AgentApiKey string - Token string -} - type Config struct { Scheme string `yaml:"scheme"` Endpoint string `yaml:"endpoint"` diff --git a/cli/config/configurator.go b/cli/config/configurator.go index 808b697b8c..71797a13d5 100644 --- a/cli/config/configurator.go +++ b/cli/config/configurator.go @@ -7,9 +7,13 @@ import ( "strings" "github.com/golang-jwt/jwt" + + agentConfig "github.com/kubeshop/tracetest/agent/config" + "github.com/kubeshop/tracetest/cli/analytics" "github.com/kubeshop/tracetest/cli/pkg/oauth" "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" + cliUI "github.com/kubeshop/tracetest/cli/ui" ) @@ -19,7 +23,7 @@ type Configurator struct { resources *resourcemanager.Registry ui cliUI.UI onFinish onFinishFn - flags ConfigFlags + flags agentConfig.Flags } func NewConfigurator(resources *resourcemanager.Registry) Configurator { @@ -28,7 +32,7 @@ func NewConfigurator(resources *resourcemanager.Registry) Configurator { ui.Success("Successfully configured Tracetest CLI") ui.Finish() } - flags := ConfigFlags{} + flags := agentConfig.Flags{} return Configurator{resources, ui, onFinish, flags} } @@ -38,7 +42,7 @@ func (c Configurator) WithOnFinish(onFinish onFinishFn) Configurator { return c } -func (c Configurator) Start(ctx context.Context, prev Config, flags ConfigFlags) error { +func (c Configurator) Start(ctx context.Context, prev Config, flags agentConfig.Flags) error { c.flags = flags serverURL := getFirstValidString(flags.Endpoint, prev.UIEndpoint, DefaultCloudEndpoint) if serverURL == "" { @@ -168,7 +172,7 @@ func (c Configurator) onOAuthFailure(err error) { c.ui.Exit(err.Error()) } -func (c Configurator) ShowOrganizationSelector(ctx context.Context, cfg Config, flags ConfigFlags) { +func (c Configurator) ShowOrganizationSelector(ctx context.Context, cfg Config, flags agentConfig.Flags) { cfg.OrganizationID = flags.OrganizationID if cfg.OrganizationID == "" && flags.AgentApiKey == "" { orgID, err := c.organizationSelector(ctx, cfg) diff --git a/cli/pkg/starter/starter.go b/cli/pkg/starter/starter.go deleted file mode 100644 index 5c98ff4eca..0000000000 --- a/cli/pkg/starter/starter.go +++ /dev/null @@ -1,165 +0,0 @@ -package starter - -import ( - "context" - "encoding/json" - "errors" - "fmt" - - agentConfig "github.com/kubeshop/tracetest/agent/config" - "github.com/kubeshop/tracetest/agent/initialization" - - "github.com/kubeshop/tracetest/cli/config" - "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" - "github.com/kubeshop/tracetest/cli/ui" -) - -type Starter struct { - configurator config.Configurator - resources *resourcemanager.Registry - ui ui.UI -} - -func NewStarter(configurator config.Configurator, resources *resourcemanager.Registry) *Starter { - ui := ui.DefaultUI - return &Starter{configurator, resources, ui} -} - -func (s *Starter) Run(ctx context.Context, cfg config.Config, flags config.ConfigFlags) error { - s.ui.Banner(config.Version) - s.ui.Println(`Tracetest start launches a lightweight agent. It enables you to run tests and collect traces with Tracetest. -Once started, Tracetest Agent exposes OTLP ports 4317 and 4318 to ingest traces via gRCP and HTTP.`) - - if flags.Token == "" || flags.AgentApiKey != "" { - s.configurator = s.configurator.WithOnFinish(s.onStartAgent) - } - - return s.configurator.Start(ctx, cfg, flags) -} - -func (s *Starter) onStartAgent(ctx context.Context, cfg config.Config) { - if cfg.AgentApiKey != "" { - err := s.StartAgent(ctx, cfg.AgentEndpoint, cfg.AgentApiKey, cfg.UIEndpoint) - if err != nil { - s.ui.Error(err.Error()) - } - - return - } - - env, err := s.getEnvironment(ctx, cfg) - if err != nil { - s.ui.Error(err.Error()) - } - - err = s.StartAgent(ctx, cfg.AgentEndpoint, env.AgentApiKey, cfg.UIEndpoint) - if err != nil { - s.ui.Error(err.Error()) - } -} - -type environment struct { - ID string `json:"id"` - Name string `json:"name"` - AgentApiKey string `json:"agentApiKey"` - OrganizationID string `json:"organizationID"` -} - -func (s *Starter) getEnvironment(ctx context.Context, cfg config.Config) (environment, error) { - resource, err := s.resources.Get("env") - if err != nil { - return environment{}, err - } - - resource = resource. - WithHttpClient(config.SetupHttpClient(cfg)). - WithOptions(resourcemanager.WithPrefixGetter(func() string { - return fmt.Sprintf("/organizations/%s/", cfg.OrganizationID) - })) - - resultFormat, err := resourcemanager.Formats.GetWithFallback("json", "json") - if err != nil { - return environment{}, err - } - - raw, err := resource.Get(ctx, cfg.EnvironmentID, resultFormat) - if err != nil { - return environment{}, err - } - - var env environment - err = json.Unmarshal([]byte(raw), &env) - if err != nil { - return environment{}, err - } - - return env, nil -} - -func (s *Starter) StartAgent(ctx context.Context, endpoint, agentApiKey, uiEndpoint string) error { - cfg, err := agentConfig.LoadConfig() - if err != nil { - return err - } - - if endpoint != "" { - cfg.ServerURL = endpoint - } - - if agentApiKey != "" { - cfg.APIKey = agentApiKey - } - - s.ui.Info(fmt.Sprintf("Starting Agent with name %s...", cfg.Name)) - - isStarted := false - session := &initialization.Session{} - for !isStarted { - session, err = initialization.Start(ctx, cfg) - if err != nil && errors.Is(err, initialization.ErrOtlpServerStart) { - s.ui.Error("Tracetest Agent binds to the OpenTelemetry ports 4317 and 4318 which are used to receive trace information from your system. The agent tried to bind to these ports, but failed.") - shouldRetry := s.ui.Enter("Please stop the process currently listening on these ports and press enter to try again.") - - if !shouldRetry { - s.ui.Finish() - return err - } - - continue - } - - if err != nil { - return err - } - - isStarted = true - } - - claims, err := config.GetTokenClaims(session.Token) - if err != nil { - return err - } - - isOpen := true - message := `Agent is started! Leave the terminal open so tests can be run and traces gathered from this environment. -You can` - options := []ui.Option{{ - Text: "Open Tracetest in a browser to this environment", - Fn: func(_ ui.UI) { - s.ui.OpenBrowser(fmt.Sprintf("%sorganizations/%s/environments/%s/dashboard", uiEndpoint, claims["organization_id"], claims["environment_id"])) - }, - }, { - Text: "Stop this agent", - Fn: func(_ ui.UI) { - isOpen = false - session.Close() - s.ui.Finish() - }, - }} - - for isOpen { - selected := s.ui.Select(message, options, 0) - selected.Fn(s.ui) - } - return nil -} diff --git a/cli/ui/ui.go b/cli/ui/ui.go index d1febec201..07ab4db0ce 100644 --- a/cli/ui/ui.go +++ b/cli/ui/ui.go @@ -18,6 +18,11 @@ type UI interface { Panic(error) Exit(string) + Errorf(string, ...any) + Warningf(string, ...any) + Infof(string, ...any) + Printlnf(string, ...any) + Error(string) Warning(string) Info(string) @@ -70,6 +75,22 @@ func (ui ptermUI) Exit(msg string) { os.Exit(1) } +func (ui ptermUI) Errorf(msg string, args ...any) { + ui.Error(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Warningf(msg string, args ...any) { + ui.Warning(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Infof(msg string, args ...any) { + ui.Info(fmt.Sprintf(msg, args...)) +} + +func (ui ptermUI) Printlnf(msg string, args ...any) { + ui.Println(fmt.Sprintf(msg, args...)) +} + func (ui ptermUI) Error(msg string) { pterm.Error.Println(msg) }