Skip to content

Commit bb173cc

Browse files
committed
Add private connector mutation support
1 parent c512c7c commit bb173cc

2 files changed

Lines changed: 518 additions & 11 deletions

File tree

cmd/private-connector/main.go

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/tls"
67
"errors"
@@ -28,8 +29,10 @@ import (
2829
"github.com/terion-name/air3/internal/mtls"
2930
"github.com/terion-name/air3/internal/natsclient"
3031
"github.com/terion-name/air3/internal/pending"
32+
"github.com/terion-name/air3/internal/s3api"
3133
"github.com/terion-name/air3/internal/s3fetch"
3234
"github.com/terion-name/air3/internal/tickets"
35+
"github.com/terion-name/air3/internal/uploadsource"
3336
)
3437

3538
const ingestTransportBufferBytes = 256 * 1024
@@ -42,6 +45,41 @@ type ingestSender interface {
4245
Send(context.Context, tickets.Ticket, ingestMetadata, io.Reader) error
4346
}
4447

48+
type uploadSourceOpener interface {
49+
Open(context.Context, tickets.Ticket) (*openedUploadSource, error)
50+
}
51+
52+
type openedUploadSource struct {
53+
Body io.ReadCloser
54+
ContentLength int64
55+
}
56+
57+
type httpUploadSourceOpener struct {
58+
client *http.Client
59+
}
60+
61+
func (o httpUploadSourceOpener) Open(ctx context.Context, ticket tickets.Ticket) (*openedUploadSource, error) {
62+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ticket.UploadSourceURL, nil)
63+
if err != nil {
64+
return nil, fmt.Errorf("create upload source request: %w", err)
65+
}
66+
req.Header.Set(uploadsource.TokenHeader, ticket.UploadToken)
67+
68+
client := o.client
69+
if client == nil {
70+
client = http.DefaultClient
71+
}
72+
resp, err := client.Do(req)
73+
if err != nil {
74+
return nil, fmt.Errorf("get upload source: %w", err)
75+
}
76+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
77+
_ = resp.Body.Close()
78+
return nil, fmt.Errorf("upload source returned status %d", resp.StatusCode)
79+
}
80+
return &openedUploadSource{Body: resp.Body, ContentLength: resp.ContentLength}, nil
81+
}
82+
4583
type closeableIngestSender interface {
4684
Close() error
4785
}
@@ -55,11 +93,12 @@ func closeIngestSender(sender any) error {
5593
}
5694

5795
type connector struct {
58-
cfg config.ConnectorConfig
59-
fetcher objectFetcher
60-
sender ingestSender
61-
logger *slog.Logger
62-
now func() time.Time
96+
cfg config.ConnectorConfig
97+
fetcher objectFetcher
98+
sender ingestSender
99+
uploadOpener uploadSourceOpener
100+
logger *slog.Logger
101+
now func() time.Time
63102
}
64103

65104
var errTicketWorkerPoolClosed = errors.New("ticket worker pool is closed")
@@ -193,7 +232,16 @@ func run(ctx context.Context, logger *slog.Logger) error {
193232
logger.Warn("ingest sender close failed", "error", safeLogError(err))
194233
}
195234
}()
196-
worker := newConnector(cfg, fetcher, sender, logger)
235+
uploadClient, err := ingestHTTPClient(cfg.MTLS, cfg.Timeouts.StreamTimeout, cfg.IngestDisableHTTP2, cfg.IngestPoolSize)
236+
if err != nil {
237+
return err
238+
}
239+
defer func() {
240+
if err := closeHTTPClient(uploadClient); err != nil {
241+
logger.Warn("upload source client close failed", "error", safeLogError(err))
242+
}
243+
}()
244+
worker := newConnectorWithUploadSourceOpener(cfg, fetcher, sender, httpUploadSourceOpener{client: uploadClient}, logger)
197245

198246
natsCtx, cancelNATS := context.WithTimeout(ctx, 10*time.Second)
199247
defer cancelNATS()
@@ -228,13 +276,20 @@ func run(ctx context.Context, logger *slog.Logger) error {
228276
}
229277

230278
func newConnector(cfg config.ConnectorConfig, fetcher objectFetcher, sender ingestSender, logger *slog.Logger) *connector {
279+
return newConnectorWithUploadSourceOpener(cfg, fetcher, sender, httpUploadSourceOpener{client: http.DefaultClient}, logger)
280+
}
281+
282+
func newConnectorWithUploadSourceOpener(cfg config.ConnectorConfig, fetcher objectFetcher, sender ingestSender, uploadOpener uploadSourceOpener, logger *slog.Logger) *connector {
231283
if logger == nil {
232284
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
233285
}
234286
if sender == nil {
235287
sender = httpIngestSender{client: http.DefaultClient}
236288
}
237-
return &connector{cfg: cfg, fetcher: fetcher, sender: sender, logger: logger, now: time.Now}
289+
if uploadOpener == nil {
290+
uploadOpener = httpUploadSourceOpener{client: http.DefaultClient}
291+
}
292+
return &connector{cfg: cfg, fetcher: fetcher, sender: sender, uploadOpener: uploadOpener, logger: logger, now: time.Now}
238293
}
239294

240295
func (c *connector) handleTicket(ctx context.Context, ticket tickets.Ticket) error {
@@ -245,7 +300,30 @@ func (c *connector) handleTicket(ctx context.Context, ticket tickets.Ticket) err
245300
ticketCtx, cancel := context.WithDeadline(ctx, deadline)
246301
defer cancel()
247302

248-
obj, err := c.fetcher.Fetch(ticketCtx, s3fetch.Request{Method: ticket.Method, Operation: ticket.Operation, Bucket: ticket.Bucket, Key: ticket.Key, Range: ticket.Range, List: ticket.List})
303+
op, err := tickets.ResolveOperation(ticket.Method, ticket.Operation)
304+
if err != nil {
305+
return err
306+
}
307+
if isMutationOperation(op) && !c.cfg.MutationsEnabled {
308+
return c.sendS3Error(ticketCtx, ticket, http.StatusMethodNotAllowed, "MethodNotAllowed", "mutations are disabled")
309+
}
310+
311+
req := s3fetch.Request{Method: ticket.Method, Operation: op, Bucket: ticket.Bucket, Key: ticket.Key, Range: ticket.Range, List: ticket.List}
312+
if op == tickets.OperationPutObject {
313+
upload, err := c.uploadOpener.Open(ticketCtx, ticket)
314+
if err != nil || upload == nil || upload.Body == nil {
315+
return c.sendS3Error(ticketCtx, ticket, http.StatusServiceUnavailable, "ServiceUnavailable", "upload source is unavailable")
316+
}
317+
defer upload.Body.Close()
318+
if upload.ContentLength >= 0 && ticket.ContentLength != nil && upload.ContentLength != *ticket.ContentLength {
319+
return c.sendS3Error(ticketCtx, ticket, http.StatusBadRequest, "InvalidRequest", "upload source content length does not match ticket content length")
320+
}
321+
req.Body = upload.Body
322+
req.ContentLength = ticket.ContentLength
323+
req.ContentType = ticket.ContentType
324+
}
325+
326+
obj, err := c.fetcher.Fetch(ticketCtx, req)
249327
if err != nil {
250328
status := statusForFetchError(err)
251329
return c.sender.Send(ticketCtx, ticket, metadataForStatus(status), http.NoBody)
@@ -254,7 +332,7 @@ func (c *connector) handleTicket(ctx context.Context, ticket tickets.Ticket) err
254332

255333
metadata := metadataForObject(obj)
256334
body := obj.Body
257-
if ticket.Method == http.MethodHead {
335+
if op == tickets.OperationHeadObject {
258336
body = http.NoBody
259337
}
260338
return c.sender.Send(ticketCtx, ticket, metadata, body)
@@ -275,15 +353,53 @@ func (c *connector) validateTicket(ticket tickets.Ticket) error {
275353
if !bucketAllowed(ticket.Bucket, c.cfg.AllowedBuckets) || !bucketAllowed(ticket.Bucket, c.cfg.S3.AllowedBuckets) {
276354
return errors.New("ticket bucket is not allowed")
277355
}
278-
if ticket.Method != http.MethodGet && ticket.Method != http.MethodHead {
279-
return errors.New("ticket method is not supported")
356+
op, err := tickets.ResolveOperation(ticket.Method, ticket.Operation)
357+
if err != nil {
358+
return err
359+
}
360+
if !operationAllowed(op) {
361+
return errors.New("ticket operation is not supported")
280362
}
281363
if err := validateIngestURL(ticket.IngestURL); err != nil {
282364
return err
283365
}
284366
return nil
285367
}
286368

369+
func operationAllowed(op tickets.Operation) bool {
370+
switch op {
371+
case tickets.OperationGetObject, tickets.OperationHeadObject, tickets.OperationListObjectsV2, tickets.OperationPutObject, tickets.OperationDeleteObject:
372+
return true
373+
default:
374+
return false
375+
}
376+
}
377+
378+
func isMutationOperation(op tickets.Operation) bool {
379+
return op == tickets.OperationPutObject || op == tickets.OperationDeleteObject
380+
}
381+
382+
func (c *connector) sendS3Error(ctx context.Context, ticket tickets.Ticket, status int, code, message string) error {
383+
body, err := s3api.RenderErrorXML(s3api.ErrorResponse{
384+
Code: code,
385+
Message: message,
386+
Resource: s3ErrorResource(ticket),
387+
RequestID: ticket.RequestID,
388+
})
389+
if err != nil {
390+
return fmt.Errorf("render s3 error xml: %w", err)
391+
}
392+
metadata := ingestMetadata{StatusCode: status, ContentType: "application/xml", ContentLength: int64(len(body))}
393+
return c.sender.Send(ctx, ticket, metadata, bytes.NewReader(body))
394+
}
395+
396+
func s3ErrorResource(ticket tickets.Ticket) string {
397+
if ticket.Key == "" {
398+
return "/" + ticket.Bucket
399+
}
400+
return "/" + ticket.Bucket + "/" + ticket.Key
401+
}
402+
287403
type httpIngestSender struct {
288404
client *http.Client
289405
}

0 commit comments

Comments
 (0)