Skip to content

Commit 62d8e57

Browse files
Build docker image, always pull credentials from TIGER_PUBLIC_KEY and TIGER_SECRET_KEY if present
1 parent 4ed6666 commit 62d8e57

24 files changed

+707
-652
lines changed

.goreleaser.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,29 @@ nfpms:
9393
lintian_overrides:
9494
- statically-linked-binary
9595

96+
dockers_v2:
97+
- id: docker
98+
images:
99+
- ghcr.io/timescale/tiger-cli
100+
tags:
101+
- "{{ .Version }}"
102+
- '{{ if eq .Prerelease "" }}{{ .Major }}{{ end }}'
103+
- '{{ if eq .Prerelease "" }}{{ .Major }}.{{ .Minor }}{{ end }}'
104+
- '{{ if eq .Prerelease "" }}latest{{ end }}'
105+
labels:
106+
org.opencontainers.image.created: "{{.Date}}"
107+
org.opencontainers.image.name: "{{.ProjectName}}"
108+
org.opencontainers.image.title: "{{.ProjectName}}"
109+
org.opencontainers.image.revision: "{{.FullCommit}}"
110+
org.opencontainers.image.source: "https://github.com/timescale/tiger-cli"
111+
org.opencontainers.image.url: "https://github.com/timescale/tiger-cli"
112+
org.opencontainers.image.version: "{{.Version}}"
113+
io.modelcontextprotocol.server.name: "io.github.timescale/tiger-cli"
114+
build_args:
115+
BINARY_SOURCE: release
116+
platforms:
117+
- linux/amd64
118+
- linux/arm64
96119

97120
# S3 Blob Storage Configuration
98121
blobs:

Dockerfile

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
ARG GO_VERSION=1.25
2+
ARG BINARY_SOURCE=builder
3+
4+
# When performing a multi-platform build, leverage Go's built-in support for
5+
# cross-compilation instead of relying on emulation (which is much slower).
6+
# See: https://docs.docker.com/build/building/multi-platform/#cross-compiling-a-go-application
7+
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS builder
8+
ARG TARGETOS
9+
ARG TARGETARCH
10+
11+
# Download dependencies to local module cache
12+
WORKDIR /src
13+
RUN --mount=type=bind,source=go.sum,target=go.sum \
14+
--mount=type=bind,source=go.mod,target=go.mod \
15+
--mount=type=cache,target=/go/pkg/mod \
16+
go mod download -x
17+
18+
# Build static executable
19+
RUN --mount=type=bind,target=. \
20+
--mount=type=cache,target=/go/pkg/mod \
21+
--mount=type=cache,target=/root/.cache/go-build \
22+
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o /bin/tiger ./cmd/tiger
23+
24+
# When building Docker images via GoReleaser, to binaries are built externally
25+
# and copied in. See: https://goreleaser.com/customization/dockers_v2/
26+
FROM scratch AS release
27+
ARG TARGETPLATFORM
28+
COPY ${TARGETPLATFORM}/tiger /bin/tiger
29+
30+
# Either the 'builder' or 'release' stage, depending on whether we're building
31+
# the binaries in Docker or outside via GoReleaser.
32+
FROM ${BINARY_SOURCE} AS binary_source
33+
34+
# Create final alpine image
35+
FROM alpine:3.23 AS final
36+
37+
# Install psql for sake of `tiger db connect`
38+
RUN apk add --update --no-cache postgresql-client
39+
40+
# Create non-root user/group
41+
RUN addgroup -g 1000 tiger && adduser -u 1000 -G tiger -D tiger
42+
USER tiger
43+
WORKDIR /home/tiger
44+
45+
# Set env vars to control default Tiger CLI behavior
46+
ENV TIGER_PASSWORD_STORAGE=pgpass
47+
ENV TIGER_CONFIG_DIR=/home/tiger/.config/tiger
48+
49+
# Declare config file mount points
50+
VOLUME /home/tiger/.config/tiger
51+
VOLUME /home/tiger/.pgpass
52+
53+
# Copy binary to final image
54+
COPY --from=binary_source /bin/tiger /usr/local/bin/tiger
55+
56+
ENTRYPOINT ["tiger"]
57+
CMD ["mcp", "start"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ Environment variables override configuration file values. All variables use the
244244
- `TIGER_DOCS_MCP` - Enable/disable docs MCP proxy
245245
- `TIGER_OUTPUT` - Output format: `json`, `yaml`, or `table`
246246
- `TIGER_PASSWORD_STORAGE` - Password storage method: `keyring`, `pgpass`, or `none`
247+
- `TIGER_PUBLIC_KEY` - Public key to use for authentication (takes priority over stored credentials)
248+
- `TIGER_SECRET_KEY` - Secret key to use for authentication (takes priority over stored credentials)
247249
- `TIGER_SERVICE_ID` - Default service ID
248250
- `TIGER_VERSION_CHECK_INTERVAL` - How often the CLI will check for new versions, 0 to disable
249251

docker-compose.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
services:
2+
tiger-cli:
3+
build:
4+
context: .
5+
dockerfile: ./Dockerfile
6+
develop:
7+
watch:
8+
- action: rebuild
9+
path: ./
10+
image: ghcr.io/timescale/tiger-cli:${TAG:-latest}
11+
container_name: tiger-cli
12+
environment:
13+
- TIGER_PUBLIC_KEY
14+
- TIGER_SECRET_KEY
15+
ports:
16+
- 8080:8080
17+
volumes:
18+
- tiger-config:/home/tiger/.config/tiger
19+
- tiger-pgpass:/home/tiger/.pgpass
20+
21+
volumes:
22+
tiger-config:
23+
tiger-pgpass:

internal/tiger/analytics/analytics.go

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ type Analytics struct {
3737
client *api.ClientWithResponses
3838
}
3939

40-
// New initializes a new [Analytics] instance.
40+
// New initializes a new [Analytics] instance. The [config.Config] parameters
41+
// is required, but the others are optional. Analytics won't be sent if the
42+
// [api.ClientWithResponses] is nil.
4143
func New(cfg *config.Config, client *api.ClientWithResponses, projectID string) *Analytics {
4244
return &Analytics{
4345
config: cfg,
@@ -46,26 +48,6 @@ func New(cfg *config.Config, client *api.ClientWithResponses, projectID string)
4648
}
4749
}
4850

49-
// TryInit tries to load credentials to initialize an [Analytics]
50-
// instance. It returns an instance with a nil client if credentials do not
51-
// exist or it otherwise fails to create a new client. This function is
52-
// intended to be used when the caller does not otherwise need an API client to
53-
// function, but would use one if available to track analytics events.
54-
// Otherwise, call NewAnalytics directly.
55-
func TryInit(cfg *config.Config) *Analytics {
56-
apiKey, projectID, err := config.GetCredentials()
57-
if err != nil {
58-
return New(cfg, nil, "")
59-
}
60-
61-
client, err := api.NewTigerClient(cfg, apiKey)
62-
if err != nil {
63-
return New(cfg, nil, projectID)
64-
}
65-
66-
return New(cfg, client, projectID)
67-
}
68-
6951
// Option is a function that modifies analytics event properties. Options are
7052
// passed to Track and Identify methods to customize the data sent with events.
7153
type Option func(properties map[string]any)

internal/tiger/cmd/auth.go

Lines changed: 16 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"bufio"
55
"context"
6+
"errors"
67
"fmt"
78
"io"
89
"os"
@@ -15,15 +16,14 @@ import (
1516
"golang.org/x/text/cases"
1617
"golang.org/x/text/language"
1718

18-
"github.com/timescale/tiger-cli/internal/tiger/analytics"
1919
"github.com/timescale/tiger-cli/internal/tiger/api"
2020
"github.com/timescale/tiger-cli/internal/tiger/common"
2121
"github.com/timescale/tiger-cli/internal/tiger/config"
2222
"github.com/timescale/tiger-cli/internal/tiger/util"
2323
)
2424

25-
// validateAndGetAuthInfo can be overridden for testing
26-
var validateAndGetAuthInfo = validateAndGetAuthInfoImpl
25+
// validateAPIKey can be overridden for testing
26+
var validateAPIKey = common.ValidateAPIKey
2727

2828
// nextStepsMessage is the message shown after successful login
2929
const nextStepsMessage = `
@@ -118,9 +118,15 @@ Examples:
118118
// Combine the keys in the format "public:secret" for storage
119119
apiKey := fmt.Sprintf("%s:%s", creds.publicKey, creds.secretKey)
120120

121+
// Create API client
122+
client, err := api.NewTigerClient(cfg, apiKey)
123+
if err != nil {
124+
return fmt.Errorf("failed to create client: %w", err)
125+
}
126+
121127
// Validate the API key and get auth info by calling the /auth/info endpoint
122128
fmt.Fprintln(cmd.OutOrStdout(), "Validating API key...")
123-
authInfo, err := validateAndGetAuthInfo(cmd.Context(), cfg, apiKey)
129+
authInfo, err := validateAPIKey(cmd.Context(), cfg, client)
124130
if err != nil {
125131
return fmt.Errorf("API key validation failed: %w", err)
126132
}
@@ -178,28 +184,20 @@ func buildStatusCmd() *cobra.Command {
178184
RunE: func(cmd *cobra.Command, args []string) error {
179185
cmd.SilenceUsage = true
180186

181-
// Get config
182-
cfg, err := config.Load()
183-
if err != nil {
184-
return fmt.Errorf("failed to load config: %w", err)
185-
}
186-
187-
apiKey, _, err := config.GetCredentials()
187+
// Load config and API client
188+
cfg, err := common.LoadConfig(cmd.Context())
188189
if err != nil {
190+
if errors.Is(err, config.ErrNotLoggedIn) {
191+
return common.ExitWithCode(common.ExitAuthenticationError, config.ErrNotLoggedIn)
192+
}
189193
return err
190194
}
191195

192-
// Create API client
193-
client, err := api.NewTigerClient(cfg, apiKey)
194-
if err != nil {
195-
return fmt.Errorf("failed to create API client: %w", err)
196-
}
197-
198196
// Make API call to get auth information
199197
ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
200198
defer cancel()
201199

202-
resp, err := client.GetAuthInfoWithResponse(ctx)
200+
resp, err := cfg.Client.GetAuthInfoWithResponse(ctx)
203201
if err != nil {
204202
return fmt.Errorf("failed to get auth information: %w", err)
205203
}
@@ -316,45 +314,3 @@ func promptForCredentials(ctx context.Context, consoleURL string, creds credenti
316314

317315
return creds, nil
318316
}
319-
320-
// validateAndGetAuthInfoImpl validates the API key and returns authentication information
321-
// by calling the /auth/info endpoint.
322-
func validateAndGetAuthInfoImpl(ctx context.Context, cfg *config.Config, apiKey string) (*api.AuthInfo, error) {
323-
client, err := api.NewTigerClient(cfg, apiKey)
324-
if err != nil {
325-
return nil, fmt.Errorf("failed to create client: %w", err)
326-
}
327-
328-
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
329-
defer cancel()
330-
331-
// Call the /auth/info endpoint to validate credentials and get auth info
332-
resp, err := client.GetAuthInfoWithResponse(ctx)
333-
if err != nil {
334-
return nil, fmt.Errorf("API call failed: %w", err)
335-
}
336-
337-
// Check the response status
338-
if resp.StatusCode() != 200 {
339-
if resp.JSON4XX != nil {
340-
return nil, resp.JSON4XX
341-
}
342-
return nil, fmt.Errorf("unexpected API response: %d", resp.StatusCode())
343-
}
344-
345-
if resp.JSON200 == nil {
346-
return nil, fmt.Errorf("empty response from API")
347-
}
348-
349-
authInfo := resp.JSON200
350-
351-
// Identify the user with analytics
352-
a := analytics.New(cfg, client, authInfo.ApiKey.Project.Id)
353-
a.Identify(
354-
analytics.Property("userId", authInfo.ApiKey.IssuingUser.Id),
355-
analytics.Property("email", string(authInfo.ApiKey.IssuingUser.Email)),
356-
analytics.Property("planType", authInfo.ApiKey.Project.PlanType),
357-
)
358-
359-
return authInfo, nil
360-
}

0 commit comments

Comments
 (0)