Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 11 additions & 77 deletions internal/api/serverless/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,7 @@ func (c *Client) ListGpuTypes(ctx context.Context) ([]GpuType, error) {
return nil, fmt.Errorf("list GPU types: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/gpu-types",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/gpu-types", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -236,13 +230,7 @@ func (c *Client) CreateDeployment(ctx context.Context, body DeploymentCreate) (*
return nil, fmt.Errorf("create deployment: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusCreated:
Expand Down Expand Up @@ -276,13 +264,7 @@ func (c *Client) ListDeployments(ctx context.Context, params *ListDeploymentsPar
return Page[Deployment]{}, fmt.Errorf("list deployments: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -314,13 +296,7 @@ func (c *Client) GetDeployment(ctx context.Context, deploymentID string) (*Deplo
return nil, fmt.Errorf("get deployment: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID,
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID, resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -351,13 +327,7 @@ func (c *Client) UpdateDeployment(ctx context.Context, deploymentID string, body
return nil, fmt.Errorf("update deployment: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID,
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID, resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -393,13 +363,7 @@ func (c *Client) ListEndpoints(ctx context.Context, deploymentID string, params
return Page[Endpoint]{}, fmt.Errorf("list endpoints: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/endpoints",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/endpoints", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -429,13 +393,7 @@ func (c *Client) ListVersions(ctx context.Context, deploymentID string, params *
return Page[Version]{}, fmt.Errorf("list versions: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/versions",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/versions", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -465,13 +423,7 @@ func (c *Client) GetVersion(ctx context.Context, deploymentID string, versionNum
return nil, fmt.Errorf("get version: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", fmt.Sprintf("/v1/deployments/%s/versions/%d", deploymentID, versionNumber),
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, fmt.Sprintf("/v1/deployments/%s/versions/%d", deploymentID, versionNumber), resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -501,13 +453,7 @@ func (c *Client) ListBuilds(ctx context.Context, deploymentID string, params *Li
return Page[Build]{}, fmt.Errorf("list builds: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/builds",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/builds", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -537,13 +483,7 @@ func (c *Client) GetBuild(ctx context.Context, deploymentID string, buildID uuid
return nil, fmt.Errorf("get build: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/builds/"+buildID.String(),
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/builds/"+buildID.String(), resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -573,13 +513,7 @@ func (c *Client) ListWorkers(ctx context.Context, deploymentID string, params *L
return Page[Worker]{}, fmt.Errorf("list workers: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/workers",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/workers", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down
25 changes: 3 additions & 22 deletions internal/api/serverless/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package serverless
import (
"context"
"fmt"
"log/slog"
"net/http"

"github.com/runware/runware-cli/internal/api/serverless/gen"
Expand Down Expand Up @@ -32,13 +31,7 @@ func (c *Client) ListDeploymentEnvironmentVariables(ctx context.Context, deploym
return Page[EnvironmentVariable]{}, fmt.Errorf("list environment variables: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/environment-variables",
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/environment-variables", resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -69,13 +62,7 @@ func (c *Client) UpdateDeploymentEnvironmentVariable(ctx context.Context, deploy
return nil, fmt.Errorf("update environment variable: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/environment-variables/"+key,
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/environment-variables/"+key, resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusOK:
Expand Down Expand Up @@ -110,13 +97,7 @@ func (c *Client) DeleteDeploymentEnvironmentVariable(ctx context.Context, deploy
return fmt.Errorf("delete environment variable: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", "/v1/deployments/"+deploymentID+"/environment-variables/"+key,
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}
c.logResponse(ctx, "/v1/deployments/"+deploymentID+"/environment-variables/"+key, resp.StatusCode(), resp.Body)

switch resp.StatusCode() {
case http.StatusNoContent:
Expand Down
85 changes: 85 additions & 0 deletions internal/api/serverless/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package serverless

import (
"context"
"encoding/json"
"log/slog"
"net/http"
)

const redactedValue = "[redacted]"

// logResponse writes a debug line for a control-plane response. Success bodies
// have JSON "value" fields redacted so plaintext env vars are not persisted in
// debug logs. Error bodies (problem details) are logged as returned. A nil body
// logs path and status only (used for secrets endpoints whose metadata is opaque).
func (c *Client) logResponse(ctx context.Context, path string, status int, body []byte) {
if c.logger == nil || !c.logger.Enabled(ctx, slog.LevelDebug) {
return
}
if body == nil {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", path,
"status", status,
)
return
}
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", path,
"status", status,
"bodyBytes", len(body),
"body", debugLogBody(status, body),
)
}

func debugLogBody(status int, body []byte) string {
if status >= http.StatusOK && status < http.StatusBadRequest {
return string(redactJSONValues(body))
}
return string(body)
}

// redactJSONValues replaces every object key "value" with "[redacted]".
// Returns the original bytes if the body is not JSON or has no such key.
func redactJSONValues(body []byte) []byte {
var v any
if err := json.Unmarshal(body, &v); err != nil {
return body
}
if !redactValueKeys(v) {
return body
}
out, err := json.Marshal(v)
if err != nil {
return body
}
return out
}

func redactValueKeys(v any) bool {
switch t := v.(type) {
case map[string]any:
changed := false
for k, child := range t {
if k == "value" {
t[k] = redactedValue
changed = true
continue
}
if redactValueKeys(child) {
changed = true
}
}
return changed
case []any:
changed := false
for _, child := range t {
if redactValueKeys(child) {
changed = true
}
}
return changed
default:
return false
}
}
92 changes: 92 additions & 0 deletions internal/api/serverless/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package serverless

import (
"bytes"
"context"
"encoding/json"
"log/slog"
"net/http"
"strings"
"testing"
)

func TestRedactJSONValues_EnvVarList(t *testing.T) {
in := []byte(`{"data":[{"key":"MY_KEY","value":"super-secret"}],"nextCursor":"page-2"}`)
out := redactJSONValues(in)
if strings.Contains(string(out), "super-secret") {
t.Fatalf("value leaked: %s", out)
}
if !strings.Contains(string(out), `"key":"MY_KEY"`) {
t.Fatalf("key should remain: %s", out)
}
if !strings.Contains(string(out), `"value":"[redacted]"`) {
t.Fatalf("value should be redacted: %s", out)
}
}

func TestRedactJSONValues_NestedDeploymentEnvVars(t *testing.T) {
in := []byte(`{"deploymentId":"my-app","environmentVariables":[{"key":"K","value":"v"}]}`)
out := redactJSONValues(in)
if strings.Contains(string(out), `"value":"v"`) {
t.Fatalf("nested value leaked: %s", out)
}
if !strings.Contains(string(out), `"deploymentId":"my-app"`) {
t.Fatalf("other fields should remain: %s", out)
}
}

func TestRedactJSONValues_UnchangedWithoutValueKey(t *testing.T) {
in := []byte(`{"data":[{"id":"h100","name":"H100"}]}`)
out := redactJSONValues(in)
if !bytes.Equal(out, in) {
t.Fatalf("expected original bytes, got %s", out)
}
}

func TestRedactJSONValues_InvalidJSON(t *testing.T) {
in := []byte("not-json")
if got := string(redactJSONValues(in)); got != "not-json" {
t.Fatalf("got %q", got)
}
}

func TestDebugLogBody_RedactsSuccessKeepsError(t *testing.T) {
secret := []byte(`{"key":"MY_KEY","value":"super-secret"}`)
got := debugLogBody(http.StatusOK, secret)
if strings.Contains(got, "super-secret") {
t.Fatalf("200 leaked value: %s", got)
}

problem := []byte(`{"title":"Unprocessable Entity","status":422,"detail":"RUNTIME is a reserved platform name"}`)
got = debugLogBody(http.StatusUnprocessableEntity, problem)
if got != string(problem) {
t.Fatalf("422 body should be unchanged, got %s", got)
}
}

func TestLogResponse_NilBodyOmitsBody(t *testing.T) {
var buf bytes.Buffer
c := &Client{
logger: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})),
}
c.logResponse(context.Background(), "/v1/secrets", http.StatusOK, nil)
out := buf.String()
if !strings.Contains(out, `"/v1/secrets"`) {
t.Fatalf("path missing: %s", out)
}
if strings.Contains(out, `"body"`) || strings.Contains(out, "bodyBytes") {
t.Fatalf("nil body should omit body attrs: %s", out)
}
}

func TestDebugLogBody_CreatedRedacts(t *testing.T) {
in := []byte(`{"environmentVariables":[{"key":"K","value":"hidden"}]}`)
got := debugLogBody(http.StatusCreated, in)
if strings.Contains(got, "hidden") {
t.Fatalf("201 leaked value: %s", got)
}
var parsed map[string]any
if err := json.Unmarshal([]byte(got), &parsed); err != nil {
t.Fatalf("redacted body should stay JSON: %v", err)
}
}
Loading