diff --git a/README.md b/README.md index 807e21e44..2591dbc85 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Install the Defang CLI from one of the following sources: ## Support - File any issues [here](https://github.com/DefangLabs/defang/issues) +- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions ## Command completion @@ -142,13 +143,12 @@ The Defang CLI recognizes the following environment variables: - `DEFANG_ISSUER` - The OAuth2 issuer to use for authentication; defaults to `https://auth.defang.io` - `DEFANG_MODEL_ID` - The model ID of the LLM to use for the generate/debug AI integration (Pro users only) - `DEFANG_NO_CACHE` - If set to `true`, disables pull-through caching of container images; defaults to `false` -- `DEFANG_ORG` - The name of the organization to use; defaults to the user's GitHub name +- `DEFANG_ORG` - The name of the organization to use; defaults to the user's personal org - `DEFANG_PREFIX` - The prefix to use for all BYOC resources; defaults to `Defang` - `DEFANG_PROVIDER` - The name of the cloud provider to use, `auto` (default), `aws`, `digitalocean`, `gcp`, or `defang` - `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend - `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only) - `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only) -- `DEFANG_TENANT` - The name of the tenant to use. - `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal - `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND` - `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values diff --git a/pkgs/defang/cli.nix b/pkgs/defang/cli.nix index c2e1fef8a..be04e2fba 100644 --- a/pkgs/defang/cli.nix +++ b/pkgs/defang/cli.nix @@ -7,7 +7,7 @@ buildGoModule { pname = "defang-cli"; version = "git"; src = ../../src; - vendorHash = "sha256-8caEfevVUKXsYA5YyVDuPZTqMnYXIZnC4kTTGfPmuqA="; # TODO: use fetchFromGitHub + vendorHash = "sha256-E+80Fv4XGAzq2PqvObqIWlATGsDMfx2we62fA7kLHSo="; # TODO: use fetchFromGitHub subPackages = [ "cmd/cli" ]; diff --git a/pkgs/npm/README.md b/pkgs/npm/README.md index 5a116fb04..2459c19e2 100644 --- a/pkgs/npm/README.md +++ b/pkgs/npm/README.md @@ -16,7 +16,8 @@ The Defang Command-Line Interface [(CLI)](https://docs.defang.io/docs/getting-st ## Support -- File any issues [here](https://github.com/DefangLabs/defang/issues) +- File any issues [right here on GitHub](https://github.com/DefangLabs/defang/issues) +- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions ## Environment Variables @@ -43,7 +44,6 @@ The Defang CLI recognizes the following environment variables: - `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend - `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only) - `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only) -- `DEFANG_TENANT` - The name of the tenant to use. - `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal - `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND` - `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values @@ -52,4 +52,3 @@ The Defang CLI recognizes the following environment variables: Environment variables will be loaded from a `.defangrc` file in the current directory, if it exists. This file follows the same format as a `.env` file: `KEY=VALUE` pairs on each line, lines starting with `#` are treated as comments and ignored. - diff --git a/src/README.md b/src/README.md index 5a116fb04..d0759463b 100644 --- a/src/README.md +++ b/src/README.md @@ -17,6 +17,7 @@ The Defang Command-Line Interface [(CLI)](https://docs.defang.io/docs/getting-st ## Support - File any issues [here](https://github.com/DefangLabs/defang/issues) +- Join our [Discord community](https://s.defang.io/discord) for real-time help and discussions ## Environment Variables @@ -43,7 +44,6 @@ The Defang CLI recognizes the following environment variables: - `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend - `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only) - `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only) -- `DEFANG_TENANT` - The name of the tenant to use. - `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal - `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND` - `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values @@ -52,4 +52,3 @@ The Defang CLI recognizes the following environment variables: Environment variables will be loaded from a `.defangrc` file in the current directory, if it exists. This file follows the same format as a `.env` file: `KEY=VALUE` pairs on each line, lines starting with `#` are treated as comments and ignored. - diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index 13cec4de1..35bfe6bbc 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -9,7 +9,7 @@ import ( "os/exec" "path/filepath" "regexp" - "sort" + "slices" "strings" "time" @@ -52,19 +52,18 @@ var ( cluster string colorMode = ColorAuto sourcePlatform = migrate.SourcePlatformUnspecified // default to auto-detecting the source platform - doDebug = false + doDebug = pkg.GetenvBool("DEFANG_DEBUG") hasTty = term.IsTerminal() && !pkg.GetenvBool("CI") hideUpdate = pkg.GetenvBool("DEFANG_HIDE_UPDATE") mode = Mode(defangv1.DeploymentMode_MODE_UNSPECIFIED) modelId = os.Getenv("DEFANG_MODEL_ID") // for Pro users only nonInteractive = !hasTty - org string - tenantFlag string + org = os.Getenv("DEFANG_ORG") providerID = cliClient.ProviderID(pkg.Getenv("DEFANG_PROVIDER", "auto")) - verbose = false + verbose = pkg.GetenvBool("DEFANG_VERBOSE") ) -func getCluster() string { +func getOrgCluster() string { if org == "" { return cluster } @@ -165,12 +164,11 @@ func SetupCommands(ctx context.Context, version string) { RootCmd.PersistentFlags().Var(&colorMode, "color", fmt.Sprintf(`colorize output; one of %v`, allColorModes)) RootCmd.PersistentFlags().StringVarP(&cluster, "cluster", "s", pcluster.DefangFabric, "Defang cluster to connect to") RootCmd.PersistentFlags().MarkHidden("cluster") - RootCmd.PersistentFlags().StringVar(&org, "org", os.Getenv("DEFANG_ORG"), "override GitHub organization name (tenant)") - RootCmd.PersistentFlags().StringVar(&tenantFlag, "tenant", "", "select tenant by name") + RootCmd.PersistentFlags().StringVar(&org, "org", "", "override organization name (tenant)") RootCmd.PersistentFlags().VarP(&providerID, "provider", "P", fmt.Sprintf(`bring-your-own-cloud provider; one of %v`, cliClient.AllProviders())) // RootCmd.Flag("provider").NoOptDefVal = "auto" NO this will break the "--provider aws" RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging") // backwards compat: only used by tail - RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", pkg.GetenvBool("DEFANG_DEBUG"), "debug logging for troubleshooting the CLI") + RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", false, "debug logging for troubleshooting the CLI") RootCmd.PersistentFlags().BoolVar(&dryrun.DoDryRun, "dry-run", false, "dry run (don't actually change anything)") RootCmd.PersistentFlags().BoolVarP(&nonInteractive, "non-interactive", "T", !hasTty, "disable interactive prompts / no TTY") RootCmd.PersistentFlags().StringP("project-name", "p", "", "project name") @@ -372,19 +370,10 @@ var RootCmd = &cobra.Command{ } } - // Configure tenant selection based on --tenant flag - if f := cmd.Root().Flag("tenant"); f != nil && f.Changed { - // Highest precedence: explicit --tenant flag - auth.SetSelectedTenantName(tenantFlag) - } else if envTenant := os.Getenv("DEFANG_TENANT"); strings.TrimSpace(envTenant) != "" { - // Next precedence: DEFANG_TENANT environment variable - auth.SetSelectedTenantName(envTenant) - } else { - // Default behavior: auto-select tenant by JWT subject if no explicit name is provided - auth.SetAutoSelectBySub(true) - } + // Configure tenant selection based on --org flag + auth.SetSelectedTenantName(org) - client, err = cli.Connect(ctx, getCluster()) + client, err = cli.Connect(ctx, getOrgCluster()) if v, err := client.GetVersions(ctx); err == nil { version := cmd.Root().Version // HACK to avoid circular dependency with RootCmd @@ -405,7 +394,7 @@ var RootCmd = &cobra.Command{ if nonInteractive { err = client.CheckLoginAndToS(ctx) } else { - err = login.InteractiveRequireLoginAndToS(ctx, client, getCluster()) + err = login.InteractiveRequireLoginAndToS(ctx, client, getOrgCluster()) } if err != nil { @@ -413,12 +402,12 @@ var RootCmd = &cobra.Command{ } // Ensure tenant is resolved post-login as we now have a token - if tok := pcluster.GetExistingToken(getCluster()); tok != "" { + if tok := pcluster.GetExistingToken(getOrgCluster()); tok != "" { if err2 := auth.ResolveAndSetTenantFromToken(ctx, tok); err2 != nil { return err2 } // log the tenant name and id - term.Debug("Selected tenant:", auth.GetSelectedTenantName(), "(", auth.GetSelectedTenantID(), ")") + term.Debugf("Selected tenant: %q (%s)", auth.GetSelectedTenantName(), auth.GetSelectedTenantID()) } return nil @@ -427,61 +416,49 @@ var RootCmd = &cobra.Command{ var tenantsCmd = &cobra.Command{ Use: "tenants", + Aliases: []string{"tenant", "orgs", "org"}, Args: cobra.NoArgs, Annotations: authNeededAnnotation, Short: "List tenants available to the logged-in user", RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - tok := pcluster.GetExistingToken(getCluster()) - if strings.TrimSpace(tok) == "" { - return errors.New("not logged in; run 'defang login'") - } + tok := pcluster.GetExistingToken(getOrgCluster()) tenants, err := auth.ListTenantsFromToken(ctx, tok) if err != nil { return err } - // Sort by name for stable output - sort.Slice(tenants, func(i, j int) bool { return strings.ToLower(tenants[i].Name) < strings.ToLower(tenants[j].Name) }) - if len(tenants) == 0 { - term.Info("No tenants found") + term.Warn("No tenants found") return nil } - currentID := auth.GetSelectedTenantID() - currentName := auth.GetSelectedTenantName() + // Sort by name for stable output + slices.SortStableFunc(tenants, func(a, b auth.Tenant) int { + return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) + }) - // Compute longest name for aligned output - maxNameLen := 0 - for _, t := range tenants { - if l := len(t.Name); l > maxNameLen { - maxNameLen = l - } - } + printTenants := make([]struct { + Active string + auth.Tenant + }, len(tenants)) - for _, t := range tenants { + currentID := auth.GetSelectedTenantID() + currentName := auth.GetSelectedTenantName() + for i, t := range tenants { + printTenants[i].Tenant = t selected := t.ID == currentID || (currentID == "" && t.Name == currentName && strings.TrimSpace(currentName) != "") - marker := "-" if selected { - marker = "*" // highlight selected - } - - var line string - if verbose { - line = fmt.Sprintf("%s %-*s (%s)\n", marker, maxNameLen, t.Name, t.ID) - } else { - line = fmt.Sprintf("%s %s\n", marker, t.Name) + printTenants[i].Active = "*" // highlight selected } + } - if selected { - term.Printc(term.BrightCyan, line) - } else { - term.Printc(term.InfoColor, line) - } + attrs := []string{"Active", "Name"} + if verbose { + attrs = append(attrs, "ID") } - return nil + return term.Table(printTenants, attrs) }, } @@ -493,11 +470,11 @@ var loginCmd = &cobra.Command{ trainingOptOut, _ := cmd.Flags().GetBool("training-opt-out") if nonInteractive { - if err := login.NonInteractiveGitHubLogin(cmd.Context(), client, getCluster()); err != nil { + if err := login.NonInteractiveGitHubLogin(cmd.Context(), client, getOrgCluster()); err != nil { return err } } else { - err := login.InteractiveLogin(cmd.Context(), client, getCluster()) + err := login.InteractiveLogin(cmd.Context(), client, getOrgCluster()) if err != nil { return err } @@ -614,7 +591,7 @@ var generateCmd = &cobra.Command{ Heroku: migrate.NewHerokuClient(), ModelID: modelId, Fabric: client, - Cluster: getCluster(), + Cluster: getOrgCluster(), } sample := "" @@ -642,7 +619,7 @@ var initCmd = &cobra.Command{ Heroku: migrate.NewHerokuClient(), ModelID: modelId, Fabric: client, - Cluster: getCluster(), + Cluster: getOrgCluster(), } if len(args) > 0 { diff --git a/src/cmd/cli/command/mcp.go b/src/cmd/cli/command/mcp.go index 4e2150501..e9219d4f9 100644 --- a/src/cmd/cli/command/mcp.go +++ b/src/cmd/cli/command/mcp.go @@ -71,7 +71,7 @@ set_config - This tool sets or updates configuration variables for a deployed ap `), ) - cluster := getCluster() + cluster := getOrgCluster() // Setup resources term.Debug("Setting up resources") diff --git a/src/go.mod b/src/go.mod index 07f92070b..251645563 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,7 +4,7 @@ go 1.24 toolchain go1.24.5 -replace github.com/spf13/cobra v1.8.0 => github.com/DefangLabs/cobra v1.8.0-defang +replace github.com/spf13/cobra v1.10.1 => github.com/DefangLabs/cobra v1.10.1-defang require ( cloud.google.com/go/artifactregistry v1.16.1 @@ -50,8 +50,8 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/ross96D/cancelreader v0.2.6 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.6 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.9 github.com/stretchr/testify v1.10.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/mod v0.18.0 diff --git a/src/go.sum b/src/go.sum index 8dec5999c..5e20bb6e5 100644 --- a/src/go.sum +++ b/src/go.sum @@ -36,8 +36,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/DefangLabs/cobra v1.8.0-defang h1:rTzAg1XbEk3yXUmQPumcwkLgi8iNCby5CjyG3sCwzKk= -github.com/DefangLabs/cobra v1.8.0-defang/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/DefangLabs/cobra v1.10.1-defang h1:Jsj/7J/hcEVnOnRB/qyNQgZY8pjAONfhHntw3w+UwQA= +github.com/DefangLabs/cobra v1.10.1-defang/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679 h1:qNT7R4qrN+5u5ajSbqSW1opHP4LA8lzA+ASyw5MQZjs= github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= @@ -301,8 +301,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/pkg/auth/tenant.go b/src/pkg/auth/tenant.go index 1e510aa69..93e0426af 100644 --- a/src/pkg/auth/tenant.go +++ b/src/pkg/auth/tenant.go @@ -5,9 +5,10 @@ import ( "encoding/json" "errors" "fmt" - "net/http" + "net/url" "strings" + "github.com/DefangLabs/defang/src/pkg/http" "github.com/golang-jwt/jwt/v5" ) @@ -27,7 +28,7 @@ var ( // SetSelectedTenantName stores the desired tenant name for selection. func SetSelectedTenantName(name string) { selectedTenantName = strings.TrimSpace(name) - autoSelectBySub = false + autoSelectBySub = name == "" } // SetAutoSelectBySub enables or disables auto-select by JWT sub. @@ -37,20 +38,15 @@ func SetAutoSelectBySub(enabled bool) { // subFromJWT extracts the "sub" claim from the given JWT without verification. func subFromJWT(token string) (string, error) { - var claims jwt.MapClaims + var claims jwt.RegisteredClaims _, _, err := new(jwt.Parser).ParseUnverified(token, &claims) if err != nil { return "", fmt.Errorf("failed to parse access token: %w", err) } - subVal, ok := claims["sub"] - if !ok { - return "", errors.New("token is missing subject (sub) claim") - } - sub, ok := subVal.(string) - if !ok || sub == "" { + if claims.Subject == "" { return "", errors.New("invalid subject (sub) claim in token") } - return sub, nil + return claims.Subject, nil } // GetSelectedTenantName returns the currently selected tenant name. @@ -64,20 +60,15 @@ func GetSelectedTenantID() string { return selectedTenantID } // issuerFromJWT extracts the "iss" claim from the given JWT without verification. func issuerFromJWT(token string) (string, error) { - var claims jwt.MapClaims + var claims jwt.RegisteredClaims _, _, err := new(jwt.Parser).ParseUnverified(token, &claims) if err != nil { return "", fmt.Errorf("failed to parse access token: %w", err) } - issVal, ok := claims["iss"] - if !ok { - return "", errors.New("token is missing issuer (iss) claim") - } - iss, ok := issVal.(string) - if !ok || iss == "" { + if claims.Issuer == "" { return "", errors.New("invalid issuer (iss) claim in token") } - return iss, nil + return claims.Issuer, nil } // userinfoTenant represents a tenant entry in the /userinfo payload. @@ -91,6 +82,32 @@ type userinfoResponse struct { AllTenants []userinfoTenant `json:"allTenants"` } +func invokeUserinfoEndpoint(ctx context.Context, accessToken string) (*userinfoResponse, error) { + iss, err := issuerFromJWT(accessToken) + if err != nil { + return nil, err + } + + url, _ := url.JoinPath(iss, "userinfo") + header := http.Header{} + header.Set("Accept", "application/json") + header.Set("Authorization", "Bearer "+accessToken) + resp, err := http.GetWithHeader(ctx, url, header) + if err != nil { + return nil, fmt.Errorf("userinfo request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("userinfo request failed: %s", resp.Status) + } + + var ui userinfoResponse + if err := json.NewDecoder(resp.Body).Decode(&ui); err != nil { + return nil, fmt.Errorf("failed to decode userinfo: %w", err) + } + return &ui, nil +} + // ResolveAndSetTenantFromToken resolves the tenant ID for the previously set tenant name // by calling issuer + "/userinfo" with the current access token. On success, it sets the // global selected tenant ID so subsequent Fabric requests include the header. @@ -116,27 +133,10 @@ func ResolveAndSetTenantFromToken(ctx context.Context, accessToken string) error return nil } - url := strings.TrimRight(iss, "/") + "/userinfo" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + ui, err := invokeUserinfoEndpoint(ctx, token) if err != nil { return err } - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("userinfo request failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("userinfo request failed: %s", resp.Status) - } - - var ui userinfoResponse - if err := json.NewDecoder(resp.Body).Decode(&ui); err != nil { - return fmt.Errorf("failed to decode userinfo: %w", err) - } if autoSelectBySub { sub, err := subFromJWT(token) @@ -197,32 +197,10 @@ func ListTenantsFromToken(ctx context.Context, accessToken string) ([]Tenant, er return nil, ErrNoAccessToken } - iss, err := issuerFromJWT(token) - if err != nil { - return nil, err - } - - url := strings.TrimRight(iss, "/") + "/userinfo" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + ui, err := invokeUserinfoEndpoint(ctx, token) if err != nil { return nil, err } - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Accept", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("userinfo request failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("userinfo request failed: %s", resp.Status) - } - - var ui userinfoResponse - if err := json.NewDecoder(resp.Body).Decode(&ui); err != nil { - return nil, fmt.Errorf("failed to decode userinfo: %w", err) - } tenants := make([]Tenant, 0, len(ui.AllTenants)) for _, t := range ui.AllTenants { diff --git a/src/pkg/cli/compose/validation.go b/src/pkg/cli/compose/validation.go index 378cf241f..89470dc01 100644 --- a/src/pkg/cli/compose/validation.go +++ b/src/pkg/cli/compose/validation.go @@ -8,7 +8,6 @@ import ( "path/filepath" "regexp" "slices" - "sort" "strconv" "strings" "time" @@ -38,8 +37,8 @@ func ValidateProject(project *composeTypes.Project) error { for _, svccfg := range project.Services { services = append(services, svccfg) } - sort.Slice(services, func(i, j int) bool { - return services[i].Name < services[j].Name + slices.SortFunc(services, func(a, b composeTypes.ServiceConfig) int { + return strings.Compare(a.Name, b.Name) }) var errs []error diff --git a/src/pkg/cli/deploymentsList.go b/src/pkg/cli/deploymentsList.go index 98ae27f4b..374a27975 100644 --- a/src/pkg/cli/deploymentsList.go +++ b/src/pkg/cli/deploymentsList.go @@ -2,7 +2,7 @@ package cli import ( "context" - "sort" + "slices" "strings" "time" @@ -60,8 +60,8 @@ func DeploymentsList(ctx context.Context, listType defangv1.DeploymentType, proj // TODO: allow user to specify sort order sortKeys[i] = strings.Join([]string{d.ProjectName, d.Provider, d.AccountId, d.Region}, "|") } - sort.SliceStable(sortKeys, func(i, j int) bool { - return sortKeys[i] < sortKeys[j] + slices.SortStableFunc(sortKeys, func(a, b string) int { + return strings.Compare(a, b) }) return term.Table(deployments, []string{"ProjectName", "Provider", "AccountId", "Region", "Deployment", "DeployedAt"}) diff --git a/src/pkg/cli/estimate.go b/src/pkg/cli/estimate.go index edc18fa47..c8877b38b 100644 --- a/src/pkg/cli/estimate.go +++ b/src/pkg/cli/estimate.go @@ -6,7 +6,7 @@ import ( "fmt" "io" "os" - "sort" + "slices" "strconv" "strings" "time" @@ -165,8 +165,8 @@ func prepareEstimateLineItemTableItems(lineItems []*defangv1.EstimateLineItem) [ } // sort line items by service + description - sort.Slice(tableItems, func(i, j int) bool { - return tableItems[i].Service+tableItems[i].Description < tableItems[j].Service+tableItems[j].Description + slices.SortFunc(tableItems, func(a, b EstimateLineItemTableItem) int { + return strings.Compare(a.Service+a.Description, b.Service+b.Description) }) return tableItems diff --git a/src/pkg/dns/resolver.go b/src/pkg/dns/resolver.go index dcd8c3b52..04f616ef8 100644 --- a/src/pkg/dns/resolver.go +++ b/src/pkg/dns/resolver.go @@ -6,7 +6,7 @@ import ( "fmt" "net" "slices" - "sort" + "strings" "github.com/DefangLabs/defang/src/pkg" "github.com/miekg/dns" @@ -76,7 +76,9 @@ func FindNSServers(ctx context.Context, domain string) ([]*net.NS, error) { index := pkg.RandomIndex(len(nsServers)) nsServer := nsServers[index].Host ns, err := ResolverAt(nsServer).LookupNS(ctx, domain) - sort.Slice(ns, func(i, j int) bool { return ns[i].Host < ns[j].Host }) + slices.SortFunc(ns, func(a, b *net.NS) int { + return strings.Compare(a.Host, b.Host) + }) if err != nil { if retries--; retries > 0 { continue diff --git a/src/pkg/http/get.go b/src/pkg/http/get.go index b701066b0..08a0f2e5c 100644 --- a/src/pkg/http/get.go +++ b/src/pkg/http/get.go @@ -7,6 +7,8 @@ import ( type Header = http.Header +const StatusOK = http.StatusOK + func GetWithContext(ctx context.Context, url string) (*http.Response, error) { hreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/src/pkg/types/tenant.go b/src/pkg/types/tenant.go index dd462bb89..3bcfda597 100644 --- a/src/pkg/types/tenant.go +++ b/src/pkg/types/tenant.go @@ -2,13 +2,24 @@ package types type TenantName string +// Set implements pflag.Value. +func (t *TenantName) Set(s string) error { + *t = TenantName(s) + return nil +} + +// Type implements pflag.Value. +func (t *TenantName) Type() string { + return "name|id" +} + const ( - DEFAULT_TENANT TenantName = "" // the default tenant (GitHub login) + DEFAULT_TENANT TenantName = "" // the default tenant ) func (t TenantName) String() string { if t == DEFAULT_TENANT { - return "default" + return "" } return string(t) }