github.com/kubeshop/testkube@v1.17.23/cmd/kubectl-testkube/commands/common/helper.go (about)

     1  package common
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os/exec"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/skratchdot/open-golang/open"
    12  	"github.com/spf13/cobra"
    13  
    14  	"github.com/kubeshop/testkube/cmd/kubectl-testkube/config"
    15  	"github.com/kubeshop/testkube/internal/migrations"
    16  	cloudclient "github.com/kubeshop/testkube/pkg/cloud/client"
    17  	"github.com/kubeshop/testkube/pkg/cloudlogin"
    18  	"github.com/kubeshop/testkube/pkg/migrator"
    19  	"github.com/kubeshop/testkube/pkg/process"
    20  	"github.com/kubeshop/testkube/pkg/ui"
    21  )
    22  
    23  type HelmOptions struct {
    24  	Name, Namespace, Chart, Values string
    25  	NoMinio, NoMongo, NoConfirm    bool
    26  	MinioReplicas, MongoReplicas   int
    27  
    28  	Master config.Master
    29  	// For debug
    30  	DryRun         bool
    31  	MultiNamespace bool
    32  	NoOperator     bool
    33  }
    34  
    35  const (
    36  	github = "GitHub"
    37  	gitlab = "GitLab"
    38  )
    39  
    40  func (o HelmOptions) GetApiURI() string {
    41  	return o.Master.URIs.Api
    42  }
    43  
    44  func GetCurrentKubernetesContext() (string, error) {
    45  	kubectl, err := exec.LookPath("kubectl")
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  
    50  	out, err := process.Execute(kubectl, "config", "current-context")
    51  	if err != nil {
    52  		return "", err
    53  	}
    54  
    55  	return strings.TrimSpace(string(out)), nil
    56  }
    57  
    58  func HelmUpgradeOrInstallTestkubeCloud(options HelmOptions, cfg config.Data, isMigration bool) error {
    59  	// disable mongo and minio for cloud
    60  	options.NoMinio = true
    61  	options.NoMongo = true
    62  
    63  	// use config if set
    64  	if cfg.CloudContext.AgentKey != "" && options.Master.AgentToken == "" {
    65  		options.Master.AgentToken = cfg.CloudContext.AgentKey
    66  	}
    67  
    68  	if options.Master.AgentToken == "" {
    69  		return fmt.Errorf("agent key and agent uri are required, please pass it with `--agent-token` and `--agent-uri` flags")
    70  	}
    71  
    72  	helmPath, err := exec.LookPath("helm")
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	// repo update
    78  	args := []string{"repo", "add", "kubeshop", "https://kubeshop.github.io/helm-charts"}
    79  	_, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun})
    80  	if err != nil && !strings.Contains(err.Error(), "Error: repository name (kubeshop) already exists, please specify a different name") {
    81  		ui.WarnOnError("adding testkube repo", err)
    82  	}
    83  
    84  	_, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: []string{"repo", "update"}, DryRun: options.DryRun})
    85  	ui.ExitOnError("updating helm repositories", err)
    86  
    87  	// upgrade cloud
    88  	args = []string{
    89  		"upgrade", "--install", "--create-namespace",
    90  		"--namespace", options.Namespace,
    91  		"--set", "testkube-api.cloud.url=" + options.Master.URIs.Agent,
    92  		"--set", "testkube-api.cloud.key=" + options.Master.AgentToken,
    93  		"--set", "testkube-api.cloud.uiURL=" + options.Master.URIs.Ui,
    94  		"--set", "testkube-logs.pro.url=" + options.Master.URIs.Logs,
    95  		"--set", "testkube-logs.pro.key=" + options.Master.AgentToken,
    96  	}
    97  	if isMigration {
    98  		args = append(args, "--set", "testkube-api.cloud.migrate=true")
    99  	}
   100  
   101  	if options.Master.EnvId != "" {
   102  		args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.envId=%s", options.Master.EnvId))
   103  		args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.envId=%s", options.Master.EnvId))
   104  	}
   105  	if options.Master.OrgId != "" {
   106  		args = append(args, "--set", fmt.Sprintf("testkube-api.cloud.orgId=%s", options.Master.OrgId))
   107  		args = append(args, "--set", fmt.Sprintf("testkube-logs.pro.orgId=%s", options.Master.OrgId))
   108  	}
   109  
   110  	args = append(args, "--set", fmt.Sprintf("global.features.logsV2=%v", options.Master.Features.LogsV2))
   111  
   112  	args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace))
   113  	args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator))
   114  	args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo))
   115  	args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio))
   116  
   117  	args = append(args, "--set", fmt.Sprintf("testkube-api.minio.replicas=%d", options.MinioReplicas))
   118  	args = append(args, "--set", fmt.Sprintf("mongodb.replicas=%d", options.MongoReplicas))
   119  
   120  	args = append(args, options.Name, options.Chart)
   121  
   122  	if options.Values != "" {
   123  		args = append(args, "--values", options.Values)
   124  	}
   125  
   126  	out, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun})
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	ui.Debug("Helm command output:")
   132  	ui.Debug(helmPath, args...)
   133  
   134  	ui.Debug("Helm install testkube output", string(out))
   135  
   136  	return nil
   137  }
   138  
   139  func HelmUpgradeOrInstalTestkube(options HelmOptions) error {
   140  	helmPath, err := exec.LookPath("helm")
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	ui.Info("Helm installing testkube framework")
   146  	args := []string{"repo", "add", "kubeshop", "https://kubeshop.github.io/helm-charts"}
   147  	_, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun})
   148  	if err != nil && !strings.Contains(err.Error(), "Error: repository name (kubeshop) already exists, please specify a different name") {
   149  		ui.WarnOnError("adding testkube repo", err)
   150  	}
   151  
   152  	_, err = process.ExecuteWithOptions(process.Options{Command: helmPath, Args: []string{"repo", "update"}, DryRun: options.DryRun})
   153  	ui.ExitOnError("updating helm repositories", err)
   154  
   155  	args = []string{"upgrade", "--install", "--create-namespace", "--namespace", options.Namespace}
   156  	args = append(args, "--set", fmt.Sprintf("testkube-api.multinamespace.enabled=%t", options.MultiNamespace))
   157  	args = append(args, "--set", fmt.Sprintf("testkube-operator.enabled=%t", !options.NoOperator))
   158  	args = append(args, "--set", fmt.Sprintf("mongodb.enabled=%t", !options.NoMongo))
   159  	args = append(args, "--set", fmt.Sprintf("testkube-api.minio.enabled=%t", !options.NoMinio))
   160  	if options.NoMinio {
   161  		args = append(args, "--set", "testkube-api.logs.storage=mongo")
   162  	} else {
   163  		args = append(args, "--set", "testkube-api.logs.storage=minio")
   164  	}
   165  
   166  	args = append(args, "--set", fmt.Sprintf("global.features.logsV2=%v", options.Master.Features.LogsV2))
   167  
   168  	args = append(args, options.Name, options.Chart)
   169  
   170  	if options.Values != "" {
   171  		args = append(args, "--values", options.Values)
   172  	}
   173  
   174  	out, err := process.ExecuteWithOptions(process.Options{Command: helmPath, Args: args, DryRun: options.DryRun})
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	ui.Debug("Helm install testkube output", string(out))
   180  	return nil
   181  }
   182  
   183  func PopulateHelmFlags(cmd *cobra.Command, options *HelmOptions) {
   184  	cmd.Flags().StringVar(&options.Chart, "chart", "kubeshop/testkube", "chart name (usually you don't need to change it)")
   185  	cmd.Flags().StringVar(&options.Name, "name", "testkube", "installation name (usually you don't need to change it)")
   186  	cmd.Flags().StringVar(&options.Namespace, "namespace", "testkube", "namespace where to install")
   187  	cmd.Flags().StringVar(&options.Values, "values", "", "path to Helm values file")
   188  
   189  	cmd.Flags().BoolVar(&options.NoMinio, "no-minio", false, "don't install MinIO")
   190  	cmd.Flags().BoolVar(&options.NoMongo, "no-mongo", false, "don't install MongoDB")
   191  	cmd.Flags().BoolVar(&options.NoConfirm, "no-confirm", false, "don't ask for confirmation - unatended installation mode")
   192  	cmd.Flags().BoolVar(&options.DryRun, "dry-run", false, "dry run mode - only print commands that would be executed")
   193  }
   194  
   195  func PopulateLoginDataToContext(orgID, envID, token, refreshToken string, options HelmOptions, cfg config.Data) error {
   196  	if options.Master.AgentToken != "" {
   197  		cfg.CloudContext.AgentKey = options.Master.AgentToken
   198  	}
   199  	if options.Master.URIs.Api != "" {
   200  		cfg.CloudContext.AgentUri = options.Master.URIs.Api
   201  	}
   202  	if options.Master.URIs.Ui != "" {
   203  		cfg.CloudContext.UiUri = options.Master.URIs.Ui
   204  	}
   205  	if options.Master.URIs.Api != "" {
   206  		cfg.CloudContext.ApiUri = options.Master.URIs.Api
   207  	}
   208  	cfg.ContextType = config.ContextTypeCloud
   209  	cfg.CloudContext.OrganizationId = orgID
   210  	cfg.CloudContext.EnvironmentId = envID
   211  	cfg.CloudContext.TokenType = config.TokenTypeOIDC
   212  	if token != "" {
   213  		cfg.CloudContext.ApiKey = token
   214  	}
   215  	if refreshToken != "" {
   216  		cfg.CloudContext.RefreshToken = refreshToken
   217  	}
   218  
   219  	cfg, err := PopulateOrgAndEnvNames(cfg, orgID, envID, options.Master.URIs.Api)
   220  	if err != nil {
   221  		return errors.Wrap(err, "error populating org and env names")
   222  	}
   223  
   224  	return config.Save(cfg)
   225  }
   226  
   227  func PopulateAgentDataToContext(options HelmOptions, cfg config.Data) error {
   228  	updated := false
   229  	if options.Master.AgentToken != "" {
   230  		cfg.CloudContext.AgentKey = options.Master.AgentToken
   231  		updated = true
   232  	}
   233  	if options.Master.URIs.Api != "" {
   234  		cfg.CloudContext.AgentUri = options.Master.URIs.Api
   235  		updated = true
   236  	}
   237  	if options.Master.URIs.Ui != "" {
   238  		cfg.CloudContext.UiUri = options.Master.URIs.Ui
   239  		updated = true
   240  	}
   241  	if options.Master.URIs.Api != "" {
   242  		cfg.CloudContext.ApiUri = options.Master.URIs.Api
   243  		updated = true
   244  	}
   245  	if options.Master.IdToken != "" {
   246  		cfg.CloudContext.ApiKey = options.Master.IdToken
   247  		updated = true
   248  	}
   249  	if options.Master.EnvId != "" {
   250  		cfg.CloudContext.EnvironmentId = options.Master.EnvId
   251  		updated = true
   252  	}
   253  	if options.Master.OrgId != "" {
   254  		cfg.CloudContext.OrganizationId = options.Master.OrgId
   255  		updated = true
   256  	}
   257  
   258  	if updated {
   259  		return config.Save(cfg)
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func IsUserLoggedIn(cfg config.Data, options HelmOptions) bool {
   266  	if options.Master.URIs.Api != cfg.CloudContext.ApiUri {
   267  		//different environment
   268  		return false
   269  	}
   270  
   271  	if cfg.CloudContext.ApiKey != "" && cfg.CloudContext.RefreshToken != "" {
   272  		// users with refresh token don't need to login again
   273  		// since on expired token they will be logged in automatically
   274  		return true
   275  	}
   276  	return false
   277  }
   278  func UpdateTokens(cfg config.Data, token, refreshToken string) error {
   279  	var updated bool
   280  	if token != cfg.CloudContext.ApiKey {
   281  		cfg.CloudContext.ApiKey = token
   282  		updated = true
   283  	}
   284  	if refreshToken != cfg.CloudContext.RefreshToken {
   285  		cfg.CloudContext.RefreshToken = refreshToken
   286  		updated = true
   287  	}
   288  
   289  	if updated {
   290  		return config.Save(cfg)
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func KubectlScaleDeployment(namespace, deployment string, replicas int) (string, error) {
   297  	kubectl, err := exec.LookPath("kubectl")
   298  	if err != nil {
   299  		return "", err
   300  	}
   301  
   302  	// kubectl patch --namespace=$n deployment $1 -p "{\"spec\":{\"replicas\": $2}}"
   303  	out, err := process.Execute(kubectl, "patch", "--namespace", namespace, "deployment", deployment, "-p", fmt.Sprintf("{\"spec\":{\"replicas\": %d}}", replicas))
   304  	if err != nil {
   305  		return "", err
   306  	}
   307  
   308  	return strings.TrimSpace(string(out)), nil
   309  }
   310  
   311  func RunMigrations(cmd *cobra.Command) (hasMigrations bool, err error) {
   312  	client, _, err := GetClient(cmd)
   313  	ui.ExitOnError("getting client", err)
   314  
   315  	info, err := client.GetServerInfo()
   316  	ui.ExitOnError("getting server info", err)
   317  
   318  	if info.Version == "" {
   319  		ui.Failf("Can't detect cluster version")
   320  	}
   321  
   322  	ui.Info("Available migrations for", info.Version)
   323  	results := migrations.Migrator.GetValidMigrations(info.Version, migrator.MigrationTypeClient)
   324  	if len(results) == 0 {
   325  		ui.Warn("No migrations available for", info.Version)
   326  		return false, nil
   327  	}
   328  
   329  	for _, migration := range results {
   330  		fmt.Printf("- %+v - %s\n", migration.Version(), migration.Info())
   331  	}
   332  
   333  	return true, migrations.Migrator.Run(info.Version, migrator.MigrationTypeClient)
   334  }
   335  
   336  func PopulateOrgAndEnvNames(cfg config.Data, orgId, envId, apiUrl string) (config.Data, error) {
   337  	if orgId != "" {
   338  		cfg.CloudContext.OrganizationId = orgId
   339  		// reset env when the org is changed
   340  		if envId == "" {
   341  			cfg.CloudContext.EnvironmentId = ""
   342  		}
   343  	}
   344  	if envId != "" {
   345  		cfg.CloudContext.EnvironmentId = envId
   346  	}
   347  
   348  	orgClient := cloudclient.NewOrganizationsClient(apiUrl, cfg.CloudContext.ApiKey)
   349  	org, err := orgClient.Get(cfg.CloudContext.OrganizationId)
   350  	if err != nil {
   351  		return cfg, errors.Wrap(err, "error getting organization")
   352  	}
   353  
   354  	envsClient := cloudclient.NewEnvironmentsClient(apiUrl, cfg.CloudContext.ApiKey, cfg.CloudContext.OrganizationId)
   355  	env, err := envsClient.Get(cfg.CloudContext.EnvironmentId)
   356  	if err != nil {
   357  		return cfg, errors.Wrap(err, "error getting environment")
   358  	}
   359  
   360  	cfg.CloudContext.OrganizationName = org.Name
   361  	cfg.CloudContext.EnvironmentName = env.Name
   362  
   363  	return cfg, nil
   364  }
   365  
   366  func PopulateCloudConfig(cfg config.Data, apiKey string, opts *HelmOptions) config.Data {
   367  	if apiKey != "" {
   368  		cfg.CloudContext.ApiKey = apiKey
   369  	}
   370  
   371  	cfg.CloudContext.ApiUri = opts.Master.URIs.Api
   372  	cfg.CloudContext.UiUri = opts.Master.URIs.Ui
   373  	cfg.CloudContext.AgentUri = opts.Master.URIs.Agent
   374  
   375  	var err error
   376  	cfg, err = PopulateOrgAndEnvNames(cfg, opts.Master.OrgId, opts.Master.EnvId, opts.Master.URIs.Api)
   377  	if err != nil {
   378  		ui.Failf("Error populating org and env names: %s", err)
   379  	}
   380  
   381  	return cfg
   382  }
   383  
   384  func LoginUser(authUri string) (string, string, error) {
   385  	ui.H1("Login")
   386  	connectorID := ui.Select("Choose your login method", []string{github, gitlab})
   387  
   388  	authUrl, tokenChan, err := cloudlogin.CloudLogin(context.Background(), authUri, strings.ToLower(connectorID))
   389  	if err != nil {
   390  		return "", "", fmt.Errorf("cloud login: %w", err)
   391  	}
   392  
   393  	ui.Paragraph("Your browser should open automatically. If not, please open this link in your browser:")
   394  	ui.Link(authUrl)
   395  	ui.Paragraph("(just login and get back to your terminal)")
   396  	ui.Paragraph("")
   397  
   398  	if ok := ui.Confirm("Continue"); !ok {
   399  		return "", "", fmt.Errorf("login cancelled")
   400  	}
   401  
   402  	// open browser with login page and redirect to localhost
   403  	open.Run(authUrl)
   404  
   405  	idToken, refreshToken, err := uiGetToken(tokenChan)
   406  	if err != nil {
   407  		return "", "", fmt.Errorf("getting token")
   408  	}
   409  	return idToken, refreshToken, nil
   410  }
   411  
   412  func uiGetToken(tokenChan chan cloudlogin.Tokens) (string, string, error) {
   413  	// wait for token received to browser
   414  	s := ui.NewSpinner("waiting for auth token")
   415  
   416  	var token cloudlogin.Tokens
   417  	select {
   418  	case token = <-tokenChan:
   419  		s.Success()
   420  	case <-time.After(5 * time.Minute):
   421  		s.Fail("Timeout waiting for auth token")
   422  		return "", "", fmt.Errorf("timeout waiting for auth token")
   423  	}
   424  	ui.NL()
   425  
   426  	return token.IDToken, token.RefreshToken, nil
   427  }