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 }