diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 487c07e..07f066e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,7 +27,8 @@ jobs: - name: Validate release configuration env: PB_CLOUD_ORCHESTRATOR_URL: ${{ vars.PB_CLOUD_ORCHESTRATOR_URL }} - run: test -n "$PB_CLOUD_ORCHESTRATOR_URL" + PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN: ${{ secrets.PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN }} + run: test -n "$PB_CLOUD_ORCHESTRATOR_URL" && test -n "$PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 @@ -38,6 +39,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PB_CLOUD_ORCHESTRATOR_URL: ${{ vars.PB_CLOUD_ORCHESTRATOR_URL }} + PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN: ${{ secrets.PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN }} - name: Update Homebrew tap env: diff --git a/.goreleaser.yml b/.goreleaser.yml index 96ed2e8..2e2bfd0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -18,7 +18,7 @@ builds: - -trimpath - -tags=kqueue ldflags: - - -s -w -X main.Version=v{{ .Version }} -X main.Commit={{ .ShortCommit }} -X github.com/parseablehq/pb/pkg/config.CloudOrchestratorURL={{ .Env.PB_CLOUD_ORCHESTRATOR_URL }} + - -s -w -X main.Version=v{{ .Version }} -X main.Commit={{ .ShortCommit }} -X github.com/parseablehq/pb/pkg/config.CloudOrchestratorURL={{ .Env.PB_CLOUD_ORCHESTRATOR_URL }} -X github.com/parseablehq/pb/pkg/config.CloudOrchestratorAuthToken={{ .Env.PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN }} archives: - name_template: "pb_{{ .Version }}_{{ .Os }}_{{ .Arch }}" diff --git a/README.md b/README.md index 9e746ee..c370969 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Query production logs. Explore metrics. Stream new events. Save repeatable inves - Metrics metadata exploration: labels, series, cardinality, and TSDB stats - Live event tailing from Parseable datasets - Dataset, user, role, and profile management +- Human-readable terminal output and structured JSON for automation and agents ## Quick Start @@ -110,19 +111,14 @@ On macOS, a manually downloaded binary may be blocked on first run. Allow it onc ```bash xattr -d com.apple.quarantine /usr/local/bin/pb -``` - -**Go install:** - -```bash -go install github.com/parseablehq/pb@latest -``` **Verify:** `pb --help` ## Authentication -`pb login` creates a profile for a Parseable server and stores it locally. You can authenticate with username/password or an API key. +`pb` supports self-hosted Parseable and Parseable Cloud. Authentication details +are stored in a named local profile so subsequent commands can connect without +asking for credentials again. **Interactive login wizard:** @@ -130,13 +126,24 @@ go install github.com/parseablehq/pb@latest pb login ``` -The wizard asks for the server URL, auth method, credentials, and profile name. The first saved profile becomes the default. +Choose one of the following targets in the wizard: + +- **Self-hosted:** enter the Parseable URL and authenticate with a username and + password or an API key. +- **Parseable Cloud:** authenticate through the browser or with a Parseable + Cloud API key. The selected workspace is saved as a local profile. -**Add a profile without prompts:** +**Add a self-hosted profile without prompts:** ```bash pb profile add local http://localhost:8000 admin admin -pb profile add prod https://parseable.example.com +pb profile add local-key http://localhost:8000 --api-key psk_xxx +``` + +**Add a Parseable Cloud API-key profile without prompts:** + +```bash +pb cloud profile add --api-key psk_xxx --name production ``` **Manage profiles:** @@ -313,20 +320,30 @@ pb promql ps | Query metrics | `pb promql` | Run PromQL, inspect labels/series/cardinality | | Stream events | `pb tail` | Watch new events from a dataset | | Datasets | `pb dataset` | List, inspect, create, and remove datasets | -| Profiles | `pb login`, `pb profile`, `pb logout` | Manage Parseable server connections | +| Profiles | `pb login`, `pb cloud profile`, `pb profile`, `pb logout` | Manage self-hosted and Parseable Cloud connections | | Access control | `pb user`, `pb role` | Manage users and roles | | System | `pb status`, `pb version` | Check connectivity and versions | ## Automation -Use JSON output for scripts and CI: +Commands print human-readable text by default. Commands that support +`-o json` return structured output for scripts, CI, and agent workflows: ```bash +pb status -o json +pb profile list -o json +pb dataset list -o json pb sql run "SELECT count(*) FROM backend" --from=1h --output json pb promql run "up" --dataset otel_metrics --instant --output json ``` -For scripts and CI, omit `-i` so commands print machine-readable output instead of opening the terminal UI. +Use `pb --help` to check output support. For automation, omit `-i` +so SQL and PromQL commands print output instead of opening the terminal UI. + +> ⚠️ **Warning for agent access:** Create a dedicated read-only API key or role +> with the minimum permissions required for queries and metadata reads. Do not +> give an agent an administrator or shared human credential. Read-only access +> must be enforced by Parseable on the server. ## Documentation diff --git a/buildscripts/gen-ldflags.go b/buildscripts/gen-ldflags.go index 11e9be9..e80adee 100644 --- a/buildscripts/gen-ldflags.go +++ b/buildscripts/gen-ldflags.go @@ -32,6 +32,9 @@ func genLDFlags(version string) string { if orchestratorURL := os.Getenv("PB_CLOUD_ORCHESTRATOR_URL"); orchestratorURL != "" { ldflagsStr += " -X github.com/parseablehq/pb/pkg/config.CloudOrchestratorURL=" + orchestratorURL } + if orchestratorAuthToken := os.Getenv("PB_CLOUD_ORCHESTRATOR_AUTH_TOKEN"); orchestratorAuthToken != "" { + ldflagsStr += " -X github.com/parseablehq/pb/pkg/config.CloudOrchestratorAuthToken=" + orchestratorAuthToken + } return ldflagsStr } diff --git a/cmd/cloud.go b/cmd/cloud.go index cfe7be8..66e8001 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -26,6 +26,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/parseablehq/pb/pkg/config" + internalHTTP "github.com/parseablehq/pb/pkg/http" "github.com/parseablehq/pb/pkg/ui" "github.com/spf13/cobra" ) @@ -41,8 +42,15 @@ var ( cloudProfileName string cloudOrchestratorURL string cloudForceOverwrite bool + cloudOutputFormat string ) +type cloudProfileAddOutput struct { + Status string `json:"status"` + Name string `json:"name"` + Profile profileOutput `json:"profile"` +} + type cloudAPIKeyValidationResponse struct { OrgID string `json:"org_id"` OrgName string `json:"org_name"` @@ -196,14 +204,17 @@ var CloudProfileAddCmd = &cobra.Command{ Example: " pb cloud profile add --api-key psk_xxx\n pb cloud profile add --api-key psk_xxx --name prod", Short: "Add a Parseable Cloud profile", Long: "\nAdd a Parseable Cloud profile using an API key created in Parseable Cloud.", - RunE: func(_ *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { apiKey := strings.TrimSpace(cloudAPIKey) if apiKey == "" { return errors.New("api key is required. pass --api-key") } + if cloudOutputFormat != "text" && cloudOutputFormat != "json" { + return fmt.Errorf("unsupported output format %q (expected text or json)", cloudOutputFormat) + } orchestratorURL := cloudOrchestratorEndpoint() - result, err := validateCloudAPIKey(orchestratorURL, apiKey) + result, err := validateCloudAPIKey(cmd.Context(), orchestratorURL, apiKey) if err != nil { return err } @@ -226,13 +237,21 @@ var CloudProfileAddCmd = &cobra.Command{ return err } - fmt.Printf("Cloud profile %s added successfully\n", profileName) + if cloudOutputFormat == "json" { + return json.NewEncoder(cmd.OutOrStdout()).Encode(cloudProfileAddOutput{ + Status: "success", + Name: profileName, + Profile: safeProfileOutput(profile), + }) + } + + fmt.Fprintf(cmd.OutOrStdout(), "Cloud profile %s added successfully\n", profileName) if result.WorkspaceName != "" { - fmt.Printf("Workspace: %s (%s)\n", result.WorkspaceName, result.WorkspaceID) + fmt.Fprintf(cmd.OutOrStdout(), "Workspace: %s (%s)\n", result.WorkspaceName, result.WorkspaceID) } else { - fmt.Printf("Workspace: %s\n", result.WorkspaceID) + fmt.Fprintf(cmd.OutOrStdout(), "Workspace: %s\n", result.WorkspaceID) } - fmt.Printf("URL: %s\n", result.URL) + fmt.Fprintf(cmd.OutOrStdout(), "URL: %s\n", result.URL) return nil }, } @@ -242,6 +261,7 @@ func init() { CloudProfileAddCmd.Flags().StringVar(&cloudProfileName, "name", "", "profile name") CloudProfileAddCmd.Flags().StringVar(&cloudOrchestratorURL, "orchestrator-url", config.CloudOrchestratorURL, "Parseable Cloud orchestrator URL") CloudProfileAddCmd.Flags().BoolVar(&cloudForceOverwrite, "force", false, "overwrite existing profile") + CloudProfileAddCmd.Flags().StringVarP(&cloudOutputFormat, "output", "o", "text", "Output format (text|json)") CloudProfileCmd.AddCommand(CloudProfileAddCmd) CloudCmd.AddCommand(CloudProfileCmd) @@ -255,6 +275,9 @@ func cloudOrchestratorEndpoint() string { } func cloudProfileFromDeviceLogin(parent context.Context, orchestratorURL string) (*config.Profile, error) { + if strings.TrimSpace(orchestratorURL) == "" { + return nil, errors.New("cloud orchestrator URL is not configured") + } client := &http.Client{Timeout: 30 * time.Second} device, err := requestCloudDeviceCode(parent, client, orchestratorURL) if err != nil { @@ -394,6 +417,9 @@ func doCloudOAuthTokenRequest(ctx context.Context, client *http.Client, endpoint } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") + if err := internalHTTP.AddCloudOrchestratorAuth(req); err != nil { + return nil, err + } resp, err := client.Do(req) if err != nil { @@ -453,6 +479,9 @@ func doCloudJSON(ctx context.Context, client *http.Client, method, endpoint stri } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") + if err := internalHTTP.AddCloudOrchestratorAuth(req); err != nil { + return err + } resp, err := client.Do(req) if err != nil { return err @@ -471,16 +500,22 @@ func doCloudJSON(ctx context.Context, client *http.Client, method, endpoint stri return nil } -func validateCloudAPIKey(orchestratorURL, apiKey string) (*cloudAPIKeyValidationResponse, error) { - endpoint, err := url.JoinPath(orchestratorURL, "api/v1/apikey/validate") +func validateCloudAPIKey(ctx context.Context, orchestratorURL, apiKey string) (*cloudAPIKeyValidationResponse, error) { + if strings.TrimSpace(orchestratorURL) == "" { + return nil, errors.New("cloud orchestrator URL is not configured") + } + endpoint, err := url.JoinPath(orchestratorURL, "api/v1/cli/apikey/validate") if err != nil { return nil, fmt.Errorf("invalid orchestrator URL: %w", err) } - req, err := http.NewRequest(http.MethodGet, endpoint, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } - req.Header.Set("Authorization", "Bearer "+apiKey) + if err := internalHTTP.AddCloudOrchestratorAuth(req); err != nil { + return nil, err + } + req.Header.Set("x-api-key", apiKey) client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { diff --git a/cmd/cloud_profile_test.go b/cmd/cloud_profile_test.go index 83b72bb..d765667 100644 --- a/cmd/cloud_profile_test.go +++ b/cmd/cloud_profile_test.go @@ -1,11 +1,169 @@ package cmd import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" "testing" "github.com/parseablehq/pb/pkg/config" ) +func TestCloudProfileAddOutputDoesNotExposeCredentials(t *testing.T) { + secret := "psk_secret" + output := cloudProfileAddOutput{ + Status: "success", + Name: "agent", + Profile: safeProfileOutput(config.Profile{ + URL: "https://workspace.example.com", + Cloud: true, + APIKey: secret, + TenantID: "tenant-id", + OrchestratorURL: "https://orchestrator.example.com", + }), + } + + encoded, err := json.Marshal(output) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(encoded), secret) { + t.Fatal("cloud profile JSON output exposed the API key") + } +} + +func TestCloudProfileAddSupportsJSONOutput(t *testing.T) { + flag := CloudProfileAddCmd.Flags().Lookup("output") + if flag == nil || flag.Shorthand != "o" || flag.DefValue != "text" { + t.Fatalf("unexpected output flag: %#v", flag) + } +} + +func TestCloudRequestsRequireConfiguredOrchestratorURL(t *testing.T) { + if _, err := cloudProfileFromDeviceLogin(context.Background(), ""); err == nil || !strings.Contains(err.Error(), "URL is not configured") { + t.Fatalf("unexpected device login error: %v", err) + } + if _, err := validateCloudAPIKey(context.Background(), "", "api-key"); err == nil || !strings.Contains(err.Error(), "URL is not configured") { + t.Fatalf("unexpected API-key validation error: %v", err) + } +} + +func TestCloudProfileAddJSONFlow(t *testing.T) { + const secret = "psk_agent_secret" + oldAuthToken := config.CloudOrchestratorAuthToken + config.CloudOrchestratorAuthToken = "orchestrator-token" + t.Cleanup(func() { config.CloudOrchestratorAuthToken = oldAuthToken }) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/cli/apikey/validate" { + t.Fatalf("unexpected path %q", r.URL.Path) + } + if got := r.Header.Get("Authorization"); got != "Bearer orchestrator-token" { + t.Fatalf("unexpected authorization header %q", got) + } + if got := r.Header.Get("x-api-key"); got != secret { + t.Fatalf("unexpected x-api-key header %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "workspace_id":"workspace-id", + "workspace_name":"Agent Workspace", + "tenant_id":"tenant-id", + "url":"https://workspace.example.com", + "ingest_url":"https://ingest.example.com" + }`)) + })) + t.Cleanup(server.Close) + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + + var stdout bytes.Buffer + CloudCmd.SetOut(&stdout) + CloudCmd.SetErr(&stdout) + CloudCmd.SetArgs([]string{ + "profile", "add", + "--api-key", secret, + "--name", "agent", + "--orchestrator-url", server.URL, + "--output", "json", + }) + t.Cleanup(func() { + CloudCmd.SetArgs(nil) + CloudCmd.SetOut(nil) + CloudCmd.SetErr(nil) + _ = CloudProfileAddCmd.Flags().Set("api-key", "") + _ = CloudProfileAddCmd.Flags().Set("name", "") + _ = CloudProfileAddCmd.Flags().Set("orchestrator-url", config.CloudOrchestratorURL) + _ = CloudProfileAddCmd.Flags().Set("output", "text") + }) + + if err := CloudCmd.Execute(); err != nil { + t.Fatal(err) + } + if strings.Contains(stdout.String(), secret) { + t.Fatal("command output exposed the API key") + } + + var output cloudProfileAddOutput + if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + t.Fatalf("invalid JSON output %q: %v", stdout.String(), err) + } + if output.Status != "success" || output.Name != "agent" || output.Profile.WorkspaceID != "workspace-id" { + t.Fatalf("unexpected output: %#v", output) + } + + fileConfig, err := config.ReadConfigFromFile() + if err != nil { + t.Fatal(err) + } + profile := fileConfig.Profiles["agent"] + if profile.APIKey != secret || profile.WorkspaceID != "workspace-id" { + t.Fatalf("unexpected saved profile: %#v", profile) + } +} + +func TestCloudDeviceRequestsUseOrchestratorBearerToken(t *testing.T) { + oldAuthToken := config.CloudOrchestratorAuthToken + config.CloudOrchestratorAuthToken = "orchestrator-token" + t.Cleanup(func() { config.CloudOrchestratorAuthToken = oldAuthToken }) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer orchestrator-token" { + t.Fatalf("unexpected authorization header %q", got) + } + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api/v1/cli/device/code": + _, _ = w.Write([]byte(`{"device_code":"device-code","user_code":"user-code"}`)) + case "/api/v1/cli/oauth/token": + _, _ = w.Write([]byte(`{"access_token":"session","refresh_token":"refresh"}`)) + default: + t.Fatalf("unexpected path %q", r.URL.Path) + } + })) + t.Cleanup(server.Close) + + client := server.Client() + if _, err := requestCloudDeviceCode(context.Background(), client, server.URL); err != nil { + t.Fatal(err) + } + var tokens cloudDeviceTokenResponse + oauthErr, err := doCloudOAuthTokenRequest( + context.Background(), + client, + server.URL+"/api/v1/cli/oauth/token", + []byte(`{"grant_type":"refresh_token","refresh_token":"refresh"}`), + &tokens, + ) + if err != nil { + t.Fatal(err) + } + if oauthErr != nil { + t.Fatalf("unexpected OAuth error: %#v", oauthErr) + } +} + func TestCloudCommandsDoNotExposeDefaultFlag(t *testing.T) { if flag := CloudProfileAddCmd.Flags().Lookup("default"); flag != nil { t.Fatal("cloud profile add still exposes --default") diff --git a/cmd/login.go b/cmd/login.go index d4c0927..c750849 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -16,6 +16,7 @@ package cmd import ( + "context" "fmt" tea "github.com/charmbracelet/bubbletea" @@ -52,7 +53,7 @@ credentials. All settings are saved to ~/.config/pb/config.toml.`, m.Name = cloudProfileNameFromSession(profile) } } else if m.Profile.Cloud { - profile, err := cloudProfileFromAPIKey(m.Profile.APIKey) + profile, err := cloudProfileFromAPIKey(cmd.Context(), m.Profile.APIKey) if err != nil { return err } @@ -75,9 +76,9 @@ func printDeviceLoginSuccess(profileName string) { fmt.Println(" pb profile add [user] [pass]") } -func cloudProfileFromAPIKey(apiKey string) (*config.Profile, error) { +func cloudProfileFromAPIKey(ctx context.Context, apiKey string) (*config.Profile, error) { orchestratorURL := config.CloudOrchestratorURL - result, err := validateCloudAPIKey(orchestratorURL, apiKey) + result, err := validateCloudAPIKey(ctx, orchestratorURL, apiKey) if err != nil { return nil, err } diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index a265bdd..51f5124 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -9,9 +9,12 @@ package config import "fmt" -// CloudOrchestratorURL uses staging for local/development builds. Release -// builds replace it with the production URL through Go's -X linker flag. -var CloudOrchestratorURL = "ADD ORCHESTRATOR URL HERE" +// CloudOrchestratorURL and CloudOrchestratorAuthToken are populated in release +// binaries through Go's -X linker flag. +var ( + CloudOrchestratorURL string + CloudOrchestratorAuthToken string +) // Service and user-facing URL defaults used by pb. const ( diff --git a/pkg/http/http.go b/pkg/http/http.go index 546cced..bf3fe7e 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -105,6 +105,19 @@ func AddAuthHeaders(req *http.Request, profile *config.Profile) error { return nil } +// AddCloudOrchestratorAuth adds the bearer token required by CLI auth routes. +func AddCloudOrchestratorAuth(req *http.Request) error { + if req == nil { + return errors.New("request is nil") + } + token := strings.TrimSpace(config.CloudOrchestratorAuthToken) + if token == "" { + return errors.New("cloud orchestrator auth token is not configured") + } + req.Header.Set("Authorization", "Bearer "+token) + return nil +} + func debugRequestHeaders(req *http.Request) { if os.Getenv("PB_DEBUG_HTTP_HEADERS") == "" { return diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go index 9fc33fa..2224bd9 100644 --- a/pkg/http/http_test.go +++ b/pkg/http/http_test.go @@ -38,6 +38,9 @@ func TestNewRequestWithNilProfileReturnsError(t *testing.T) { } func TestCloudSessionRefreshesAndRetriesWorkspaceRequest(t *testing.T) { + oldAuthToken := config.CloudOrchestratorAuthToken + config.CloudOrchestratorAuthToken = "orchestrator-auth-token" + t.Cleanup(func() { config.CloudOrchestratorAuthToken = oldAuthToken }) t.Setenv("XDG_CONFIG_HOME", t.TempDir()) profile := config.Profile{ URL: "https://workspace.example.com", @@ -61,6 +64,9 @@ func TestCloudSessionRefreshesAndRetriesWorkspaceRequest(t *testing.T) { switch req.URL.Host { case "orchestrator.example.com": refreshCalls++ + if got := req.Header.Get("Authorization"); got != "Bearer orchestrator-auth-token" { + t.Fatalf("unexpected orchestrator authorization header %q", got) + } var payload cloudRefreshRequest if err := json.NewDecoder(req.Body).Decode(&payload); err != nil { t.Fatal(err) @@ -142,6 +148,9 @@ func TestCloudSessionDoesNotRefreshCrossOriginRequest(t *testing.T) { } func TestCloudSessionReturnsRefreshPersistenceError(t *testing.T) { + oldAuthToken := config.CloudOrchestratorAuthToken + config.CloudOrchestratorAuthToken = "orchestrator-auth-token" + t.Cleanup(func() { config.CloudOrchestratorAuthToken = oldAuthToken }) t.Setenv("XDG_CONFIG_HOME", t.TempDir()) profile := config.Profile{ URL: "https://workspace.example.com", @@ -186,3 +195,25 @@ func TestCloudSessionReturnsRefreshPersistenceError(t *testing.T) { t.Fatalf("rotated tokens were not retained in memory: %#v", profile) } } + +func TestAddCloudOrchestratorAuth(t *testing.T) { + oldAuthToken := config.CloudOrchestratorAuthToken + t.Cleanup(func() { config.CloudOrchestratorAuthToken = oldAuthToken }) + + req, err := http.NewRequest(http.MethodPost, "https://orchestrator.example.com/api/v1/cli/oauth/token", nil) + if err != nil { + t.Fatal(err) + } + config.CloudOrchestratorAuthToken = "test-token" + if err := AddCloudOrchestratorAuth(req); err != nil { + t.Fatal(err) + } + if got := req.Header.Get("Authorization"); got != "Bearer test-token" { + t.Fatalf("unexpected authorization header %q", got) + } + + config.CloudOrchestratorAuthToken = "" + if err := AddCloudOrchestratorAuth(req); err == nil { + t.Fatal("expected missing cloud orchestrator auth token error") + } +} diff --git a/pkg/http/refresh.go b/pkg/http/refresh.go index f31ccf3..f2d0d88 100644 --- a/pkg/http/refresh.go +++ b/pkg/http/refresh.go @@ -101,6 +101,9 @@ func (transport *cloudRefreshTransport) refresh(original *http.Request) error { } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") + if err := AddCloudOrchestratorAuth(req); err != nil { + return err + } resp, err := transport.base.RoundTrip(req) if err != nil { return err