From d91baa9dfbac49839c9120768842adc448a74894 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 20 May 2026 20:26:59 -0400 Subject: [PATCH] feat(api,sdk,cli): add unauthenticated version endpoint across the stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GET /api/ambient/v1/version as a pre-auth middleware plugin so the endpoint is accessible without authentication. The server version and build time are injected via ldflags at build time and returned as JSON. The Go and Python SDKs expose standalone FetchServerVersion functions that require only a base URL (no token/project). The CLI version subcommand now shows both client and server versions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- components/ambient-api-server/Dockerfile | 4 +- .../cmd/ambient-api-server/main.go | 1 + components/ambient-api-server/pkg/api/api.go | 5 ++ .../plugins/version/plugin.go | 33 ++++++++++ .../ambient-cli/cmd/acpctl/version/cmd.go | 28 +++++++- .../ambient-sdk/go-sdk/client/version_api.go | 66 +++++++++++++++++++ .../ambient_platform/_version_api.py | 32 +++++++++ 7 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 components/ambient-api-server/plugins/version/plugin.go create mode 100644 components/ambient-sdk/go-sdk/client/version_api.go create mode 100644 components/ambient-sdk/python-sdk/ambient_platform/_version_api.py diff --git a/components/ambient-api-server/Dockerfile b/components/ambient-api-server/Dockerfile index fb32013ac..33c4d5344 100755 --- a/components/ambient-api-server/Dockerfile +++ b/components/ambient-api-server/Dockerfile @@ -13,7 +13,9 @@ COPY plugins/ plugins/ COPY openapi/ openapi/ # Build the binary -RUN go build -ldflags="-s -w" -o ambient-api-server ./cmd/ambient-api-server +ARG GIT_VERSION=dev +ARG BUILD_TIME=unknown +RUN go build -ldflags="-s -w -X github.com/ambient-code/platform/components/ambient-api-server/pkg/api.Version=${GIT_VERSION} -X github.com/ambient-code/platform/components/ambient-api-server/pkg/api.BuildTime=${BUILD_TIME}" -o ambient-api-server ./cmd/ambient-api-server # Runtime stage FROM registry.access.redhat.com/ubi9/ubi-minimal:latest diff --git a/components/ambient-api-server/cmd/ambient-api-server/main.go b/components/ambient-api-server/cmd/ambient-api-server/main.go index 9af9e0215..4beb97521 100755 --- a/components/ambient-api-server/cmd/ambient-api-server/main.go +++ b/components/ambient-api-server/cmd/ambient-api-server/main.go @@ -26,6 +26,7 @@ import ( _ "github.com/ambient-code/platform/components/ambient-api-server/plugins/scheduledSessions" _ "github.com/ambient-code/platform/components/ambient-api-server/plugins/sessions" _ "github.com/ambient-code/platform/components/ambient-api-server/plugins/users" + _ "github.com/ambient-code/platform/components/ambient-api-server/plugins/version" ) func main() { diff --git a/components/ambient-api-server/pkg/api/api.go b/components/ambient-api-server/pkg/api/api.go index 139b2f2f3..f498d88de 100644 --- a/components/ambient-api-server/pkg/api/api.go +++ b/components/ambient-api-server/pkg/api/api.go @@ -14,3 +14,8 @@ const ( ) var NewID = trexapi.NewID + +var ( + Version = "dev" + BuildTime = "unknown" +) diff --git a/components/ambient-api-server/plugins/version/plugin.go b/components/ambient-api-server/plugins/version/plugin.go new file mode 100644 index 000000000..bffca901b --- /dev/null +++ b/components/ambient-api-server/plugins/version/plugin.go @@ -0,0 +1,33 @@ +package version + +import ( + "encoding/json" + "net/http" + "strings" + + localapi "github.com/ambient-code/platform/components/ambient-api-server/pkg/api" + pkgserver "github.com/openshift-online/rh-trex-ai/pkg/server" +) + +const versionPath = "/api/ambient/v1/version" + +func init() { + pkgserver.RegisterPreAuthMiddleware(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && strings.TrimSuffix(r.URL.Path, "/") == versionPath { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(versionResponse{ + Version: localapi.Version, + BuildTime: localapi.BuildTime, + }) + return + } + next.ServeHTTP(w, r) + }) + }) +} + +type versionResponse struct { + Version string `json:"version"` + BuildTime string `json:"build_time"` +} diff --git a/components/ambient-cli/cmd/acpctl/version/cmd.go b/components/ambient-cli/cmd/acpctl/version/cmd.go index f8c8f9fbd..90febb78a 100644 --- a/components/ambient-cli/cmd/acpctl/version/cmd.go +++ b/components/ambient-cli/cmd/acpctl/version/cmd.go @@ -1,19 +1,41 @@ -// Package version implements the version subcommand displaying build metadata. package version import ( + "context" "fmt" + "time" + "github.com/ambient-code/platform/components/ambient-cli/pkg/config" "github.com/ambient-code/platform/components/ambient-cli/pkg/info" + sdkclient "github.com/ambient-code/platform/components/ambient-sdk/go-sdk/client" "github.com/spf13/cobra" ) var Cmd = &cobra.Command{ Use: "version", - Short: "Print the version", + Short: "Print the client and server version", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, _ []string) { - fmt.Fprintf(cmd.OutOrStdout(), "acpctl %s (commit: %s, built: %s)\n", + fmt.Fprintf(cmd.OutOrStdout(), "Client: %s (commit: %s, built: %s)\n", info.Version, info.Commit, info.BuildDate) + + cfg, err := config.Load() + if err != nil { + return + } + apiURL := cfg.GetAPIUrl() + if apiURL == "" { + return + } + + ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) + defer cancel() + + sv, err := sdkclient.FetchServerVersion(ctx, apiURL, cfg.InsecureTLSVerify) + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "Server: unavailable (%v)\n", err) + return + } + fmt.Fprintf(cmd.OutOrStdout(), "Server: %s (built: %s)\n", sv.Version, sv.BuildTime) }, } diff --git a/components/ambient-sdk/go-sdk/client/version_api.go b/components/ambient-sdk/go-sdk/client/version_api.go new file mode 100644 index 000000000..9ab4c3bb0 --- /dev/null +++ b/components/ambient-sdk/go-sdk/client/version_api.go @@ -0,0 +1,66 @@ +package client + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type ServerVersion struct { + Version string `json:"version"` + BuildTime string `json:"build_time"` +} + +func (c *Client) ServerVersion(ctx context.Context) (*ServerVersion, error) { + var result ServerVersion + if err := c.do(ctx, http.MethodGet, "/version", nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func FetchServerVersion(ctx context.Context, baseURL string, insecureSkipVerify bool) (*ServerVersion, error) { + url := strings.TrimSuffix(baseURL, "/") + "/api/ambient/v1/version" + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Accept", "application/json") + + httpClient := &http.Client{Timeout: 10 * time.Second} + if insecureSkipVerify { + t := http.DefaultTransport.(*http.Transport).Clone() + if t.TLSClientConfig == nil { + t.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12} + } + t.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec + httpClient.Transport = t + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("server returned HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + var result ServerVersion + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("unmarshal response: %w", err) + } + return &result, nil +} diff --git a/components/ambient-sdk/python-sdk/ambient_platform/_version_api.py b/components/ambient-sdk/python-sdk/ambient_platform/_version_api.py new file mode 100644 index 000000000..2543b80fc --- /dev/null +++ b/components/ambient-sdk/python-sdk/ambient_platform/_version_api.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +import httpx + + +@dataclass(frozen=True) +class ServerVersion: + version: str = "" + build_time: str = "" + + @classmethod + def from_dict(cls, data: dict) -> ServerVersion: + return cls( + version=data.get("version", ""), + build_time=data.get("build_time", ""), + ) + + +def fetch_server_version( + base_url: str, + *, + timeout: float = 10.0, + verify_ssl: bool = True, +) -> ServerVersion: + url = base_url.rstrip("/") + "/api/ambient/v1/version" + with httpx.Client(timeout=timeout, verify=verify_ssl) as client: + response = client.get(url, headers={"Accept": "application/json"}) + response.raise_for_status() + return ServerVersion.from_dict(response.json())