v0.26.0 df1db36

client.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package client
4
5import (
6	"context"
7	"fmt"
8	"net"
9	"net/http"
10	"time"
11
12	"connectrpc.com/connect"
13
14	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
15)
16
17// Client gives access to the services of one ermite server. Every call carries
18// the bearer token and the user agent; unary calls are bounded by the timeout.
19// It is safe for concurrent use.
20type Client struct {
21	cfg  Config
22	base string
23	http *http.Client
24	opts []connect.ClientOption
25}
26
27// New builds a Client from a validated configuration.
28func New(cfg Config) (*Client, error) {
29	u, err := ParseURL(cfg.URL)
30	if err != nil {
31		return nil, fmt.Errorf("%s: %w", EnvURL, err)
32	}
33	if cfg.Timeout <= 0 {
34		cfg.Timeout = DefaultTimeout
35	}
36	cfg.URL = u.String()
37	hc := &http.Client{
38		Transport: transport(),
39		// A redirect would replay a request elsewhere: report it instead.
40		CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
41	}
42	return &Client{
43		cfg:  cfg,
44		base: u.JoinPath(APIPath).String(),
45		http: hc,
46		opts: []connect.ClientOption{connect.WithInterceptors(headers{token: cfg.Token, agent: cfg.UserAgent, timeout: cfg.Timeout})},
47	}, nil
48}
49
50// transport bounds connection setup but not the exchange, which may be a
51// long-lived stream. HTTP/2 pings detect a connection that died silently.
52func transport() *http.Transport {
53	t := http.DefaultTransport.(*http.Transport).Clone()
54	t.DialContext = (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext
55	t.TLSHandshakeTimeout = 10 * time.Second
56	t.HTTP2 = &http.HTTP2Config{SendPingTimeout: 30 * time.Second, PingTimeout: 15 * time.Second}
57	return t
58}
59
60// URL is the server address the client talks to.
61func (c *Client) URL() string { return c.cfg.URL }
62
63// Repos returns the RepoService client.
64func (c *Client) Repos() ermiteconnect.RepoServiceClient {
65	return ermiteconnect.NewRepoServiceClient(c.http, c.base, c.opts...)
66}
67
68// Runs returns the RunService client.
69func (c *Client) Runs() ermiteconnect.RunServiceClient {
70	return ermiteconnect.NewRunServiceClient(c.http, c.base, c.opts...)
71}
72
73// Artifacts returns the ArtifactService client.
74func (c *Client) Artifacts() ermiteconnect.ArtifactServiceClient {
75	return ermiteconnect.NewArtifactServiceClient(c.http, c.base, c.opts...)
76}
77
78// Tokens returns the TokenService client.
79func (c *Client) Tokens() ermiteconnect.TokenServiceClient {
80	return ermiteconnect.NewTokenServiceClient(c.http, c.base, c.opts...)
81}
82
83// Sessions returns the SessionService client.
84func (c *Client) Sessions() ermiteconnect.SessionServiceClient {
85	return ermiteconnect.NewSessionServiceClient(c.http, c.base, c.opts...)
86}
87
88// Events returns the EventService client.
89func (c *Client) Events() ermiteconnect.EventServiceClient {
90	return ermiteconnect.NewEventServiceClient(c.http, c.base, c.opts...)
91}
92
93// MergeRequests returns the MergeRequestService client.
94func (c *Client) MergeRequests() ermiteconnect.MergeRequestServiceClient {
95	return ermiteconnect.NewMergeRequestServiceClient(c.http, c.base, c.opts...)
96}
97
98// Secrets returns the SecretService client.
99func (c *Client) Secrets() ermiteconnect.SecretServiceClient {
100	return ermiteconnect.NewSecretServiceClient(c.http, c.base, c.opts...)
101}
102
103// Variables returns the VariableService client.
104func (c *Client) Variables() ermiteconnect.VariableServiceClient {
105	return ermiteconnect.NewVariableServiceClient(c.http, c.base, c.opts...)
106}
107
108// Environments returns the EnvironmentService client.
109func (c *Client) Environments() ermiteconnect.EnvironmentServiceClient {
110	return ermiteconnect.NewEnvironmentServiceClient(c.http, c.base, c.opts...)
111}
112
113// TrustedKeys returns the TrustedKeyService client.
114func (c *Client) TrustedKeys() ermiteconnect.TrustedKeyServiceClient {
115	return ermiteconnect.NewTrustedKeyServiceClient(c.http, c.base, c.opts...)
116}
117
118// Server returns the ServerService client.
119func (c *Client) Server() ermiteconnect.ServerServiceClient {
120	return ermiteconnect.NewServerServiceClient(c.http, c.base, c.opts...)
121}
122
123// headers sets the credentials and the user agent on every call and bounds the
124// unary ones.
125type headers struct {
126	token   string
127	agent   string
128	timeout time.Duration
129}
130
131func (h headers) set(hd http.Header) {
132	if h.token != "" {
133		hd.Set("Authorization", "Bearer "+h.token)
134	}
135	if h.agent != "" {
136		hd.Set("User-Agent", h.agent)
137	}
138}
139
140func (h headers) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
141	return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
142		if req.Spec().IsClient {
143			h.set(req.Header())
144			if h.timeout > 0 {
145				var cancel context.CancelFunc
146				ctx, cancel = context.WithTimeout(ctx, h.timeout)
147				defer cancel()
148			}
149		}
150		return next(ctx, req)
151	}
152}
153
154func (h headers) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
155	return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn {
156		conn := next(ctx, spec)
157		h.set(conn.RequestHeader())
158		return conn
159	}
160}
161
162func (headers) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
163	return next
164}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close