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// Mirrors returns the MirrorService client.
64func (c *Client) Mirrors() ermiteconnect.MirrorServiceClient {
65 return ermiteconnect.NewMirrorServiceClient(c.http, c.base, c.opts...)
66}
67
68// Repos returns the RepoService client.
69func (c *Client) Repos() ermiteconnect.RepoServiceClient {
70 return ermiteconnect.NewRepoServiceClient(c.http, c.base, c.opts...)
71}
72
73// Runs returns the RunService client.
74func (c *Client) Runs() ermiteconnect.RunServiceClient {
75 return ermiteconnect.NewRunServiceClient(c.http, c.base, c.opts...)
76}
77
78// Artifacts returns the ArtifactService client.
79func (c *Client) Artifacts() ermiteconnect.ArtifactServiceClient {
80 return ermiteconnect.NewArtifactServiceClient(c.http, c.base, c.opts...)
81}
82
83// Releases returns the ReleaseService client.
84func (c *Client) Releases() ermiteconnect.ReleaseServiceClient {
85 return ermiteconnect.NewReleaseServiceClient(c.http, c.base, c.opts...)
86}
87
88// Tokens returns the TokenService client.
89func (c *Client) Tokens() ermiteconnect.TokenServiceClient {
90 return ermiteconnect.NewTokenServiceClient(c.http, c.base, c.opts...)
91}
92
93// Sessions returns the SessionService client.
94func (c *Client) Sessions() ermiteconnect.SessionServiceClient {
95 return ermiteconnect.NewSessionServiceClient(c.http, c.base, c.opts...)
96}
97
98// Events returns the EventService client.
99func (c *Client) Events() ermiteconnect.EventServiceClient {
100 return ermiteconnect.NewEventServiceClient(c.http, c.base, c.opts...)
101}
102
103// MergeRequests returns the MergeRequestService client.
104func (c *Client) MergeRequests() ermiteconnect.MergeRequestServiceClient {
105 return ermiteconnect.NewMergeRequestServiceClient(c.http, c.base, c.opts...)
106}
107
108// Issues returns the IssueService client.
109func (c *Client) Issues() ermiteconnect.IssueServiceClient {
110 return ermiteconnect.NewIssueServiceClient(c.http, c.base, c.opts...)
111}
112
113// Secrets returns the SecretService client.
114func (c *Client) Secrets() ermiteconnect.SecretServiceClient {
115 return ermiteconnect.NewSecretServiceClient(c.http, c.base, c.opts...)
116}
117
118// Variables returns the VariableService client.
119func (c *Client) Variables() ermiteconnect.VariableServiceClient {
120 return ermiteconnect.NewVariableServiceClient(c.http, c.base, c.opts...)
121}
122
123// Environments returns the EnvironmentService client.
124func (c *Client) Environments() ermiteconnect.EnvironmentServiceClient {
125 return ermiteconnect.NewEnvironmentServiceClient(c.http, c.base, c.opts...)
126}
127
128// TrustedKeys returns the TrustedKeyService client.
129func (c *Client) TrustedKeys() ermiteconnect.TrustedKeyServiceClient {
130 return ermiteconnect.NewTrustedKeyServiceClient(c.http, c.base, c.opts...)
131}
132
133// Server returns the ServerService client.
134func (c *Client) Server() ermiteconnect.ServerServiceClient {
135 return ermiteconnect.NewServerServiceClient(c.http, c.base, c.opts...)
136}
137
138// headers sets the credentials and the user agent on every call and bounds the
139// unary ones.
140type headers struct {
141 token string
142 agent string
143 timeout time.Duration
144}
145
146func (h headers) set(hd http.Header) {
147 if h.token != "" {
148 hd.Set("Authorization", "Bearer "+h.token)
149 }
150 if h.agent != "" {
151 hd.Set("User-Agent", h.agent)
152 }
153}
154
155func (h headers) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
156 return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
157 if req.Spec().IsClient {
158 h.set(req.Header())
159 if h.timeout > 0 {
160 var cancel context.CancelFunc
161 ctx, cancel = context.WithTimeout(ctx, h.timeout)
162 defer cancel()
163 }
164 }
165 return next(ctx, req)
166 }
167}
168
169func (h headers) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
170 return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn {
171 conn := next(ctx, spec)
172 h.set(conn.RequestHeader())
173 return conn
174 }
175}
176
177func (headers) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
178 return next
179}