From 650b94ea7c93d8fc3b931f79f223cea3c2152e02 Mon Sep 17 00:00:00 2001 From: Ales Rechtorik Date: Sat, 16 Aug 2025 19:51:03 +0200 Subject: [PATCH] feat: add ZEROPS_TOKEN environment variable authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow bypassing zcli login by setting ZEROPS_TOKEN environment variable. When env token is used, automatically fetch and use default region. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/cmdBuilder/createRunFunc.go | 88 +++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/cmdBuilder/createRunFunc.go b/src/cmdBuilder/createRunFunc.go index fd059b3..f56fa91 100644 --- a/src/cmdBuilder/createRunFunc.go +++ b/src/cmdBuilder/createRunFunc.go @@ -4,15 +4,19 @@ import ( "context" "fmt" "os" + "time" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/zeropsio/zcli/src/cliStorage" + "github.com/zeropsio/zcli/src/constants" "github.com/zeropsio/zcli/src/entity" "github.com/zeropsio/zcli/src/flagParams" + "github.com/zeropsio/zcli/src/httpClient" "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zcli/src/optional" "github.com/zeropsio/zcli/src/printer" + "github.com/zeropsio/zcli/src/region" "github.com/zeropsio/zcli/src/uxBlock" "github.com/zeropsio/zcli/src/uxHelpers" getVersion "github.com/zeropsio/zcli/src/version" @@ -20,6 +24,10 @@ import ( "github.com/zeropsio/zerops-go/types/uuid" ) +const ( + EnvTokenKey = "ZEROPS_TOKEN" //nolint:gosec // Environment variable name, not a credential +) + type GuestCmdData struct { CliStorage *cliStorage.Handler UxBlocks *uxBlock.Blocks @@ -44,6 +52,78 @@ type LoggedUserCmdData struct { VpnKeys map[uuid.ProjectId]entity.VpnKey } +// resolveRegion finds a region by name or returns the default region. +// This is a simplified, non-interactive version of the login region logic. +func resolveRegion(regions []region.Item, selectedRegion string) (region.Item, error) { + if selectedRegion == "" { + for _, reg := range regions { + if reg.IsDefault { + return reg, nil + } + } + return region.Item{}, errors.New("no default region available") + } + + for _, reg := range regions { + if reg.Name == selectedRegion { + return reg, nil + } + } + + return region.Item{}, errors.Errorf("region '%s' not found", selectedRegion) +} + +func resolveAuthenticationData(ctx context.Context, storedData cliStorage.Data, storage *cliStorage.Handler) (string, region.Item, error) { + token := storedData.Token + regionData := storedData.RegionData + + envToken := os.Getenv(EnvTokenKey) + usingEnvToken := false + + if token == "" && envToken != "" { + token = envToken + usingEnvToken = true + } + + if token == "" { + return "", region.Item{}, errors.New(i18n.T(i18n.UnauthenticatedUser)) + } + + // Use default region for env tokens OR when region data is missing + if usingEnvToken || regionData.Address == "" { + // Use the same region fetching logic as login command + regionRetriever := region.New(httpClient.New(ctx, httpClient.Config{HttpTimeout: time.Minute * 5})) + regions, err := regionRetriever.RetrieveAllFromURL(ctx, constants.DefaultRegionUrl) + if err != nil { + return "", region.Item{}, errors.Wrap(err, "failed to retrieve regions") + } + + // Use our simplified region resolution (no interactive mode) + resolvedRegion, err := resolveRegion(regions, "") + if err != nil { + if usingEnvToken { + return "", region.Item{}, errors.Wrap(err, "environment token requires default region") + } + return "", region.Item{}, errors.Wrap(err, "failed to resolve region data") + } + regionData = resolvedRegion + + // Auto-login: Save env token and region like login command does + if usingEnvToken { + _, err = storage.Update(func(data cliStorage.Data) cliStorage.Data { + data.Token = token + data.RegionData = regionData + return data + }) + if err != nil { + return "", region.Item{}, errors.Wrap(err, "failed to save authentication data") + } + } + } + + return token, regionData, nil +} + func createCmdRunFunc( cmd *Cmd, flagParams *flagParams.Handler, @@ -85,12 +165,12 @@ func createCmdRunFunc( storedData := cliStorage.Data() - token := storedData.Token - if token == "" { + token, regionData, err := resolveAuthenticationData(ctx, storedData, cliStorage) + if err != nil { if cmd.guestRunFunc != nil { return cmd.guestRunFunc(ctx, guestCmdData) } - return errors.New(i18n.T(i18n.UnauthenticatedUser)) + return err } // user is logged in but there is only the guest run func @@ -103,7 +183,7 @@ func createCmdRunFunc( VpnKeys: storedData.VpnKeys, } - cmdData.RestApiClient = zeropsRestApiClient.NewAuthorizedClient(token, "https://"+storedData.RegionData.Address) + cmdData.RestApiClient = zeropsRestApiClient.NewAuthorizedClient(token, "https://"+regionData.Address) if cmd.scopeLevel != nil { if err := cmd.scopeLevel.LoadSelectedScope(ctx, cmd, cmdData); err != nil {