Skip to content
quarry Artifact [1c49b153ee]
Login

Artifact [1c49b153ee]

Artifact 1c49b153ee5a8cfa97ee31dab889cfe3cf29b1011ac30fc0b26fff42d2cebb90:


package api

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/s3"
)

// forgebucket.go is the storage the FORGE supplies.
//
// Attachments were bring-your-own and nothing else, so every repository
// had to be handed an endpoint and a key before a file could be put on
// a ticket. On a forge somebody runs for their friends that is the
// whole feature unavailable until each of them opens an account with a
// storage provider — the operator sets up quarry and dagnats, invites
// people, and watches the composer fail.
//
// So: the forge may bind one bucket, every repository without its own
// uses it, and binding your own stays a choice. The forge's binding
// lives under oltp.ForgeBucket, the empty name, which is the same
// convention the fleet default for capability categories already uses.
//
// Site admin only, and that is the correct scope rather than a
// concession: this is the operator's own storage and the operator's own
// bill, and it is the one thing here that is genuinely the forge's.

type forgeBucketIn struct {
	Endpoint   string `json:"endpoint" validate:"required,max=512"`
	Bucket     string `json:"bucket" validate:"required,max=200"`
	Region     string `json:"region" validate:"max=64"`
	AccessKey  string `json:"accessKey" validate:"required,max=200"`
	Secret     string `json:"secret" validate:"max=400"`
	PublicBase string `json:"publicBase" validate:"max=512"`
	Limit      int64  `json:"limit"`
}

type forgeBucketOut struct {
	Bound bool `json:"bound"`
	// Info describes the binding without its secret, the same shape a
	// repository's own binding is reported in.
	Info BucketInfo `json:"info"`
}

func (d Deps) registerForgeBucket(r *ops.Registry) error {
	if err := ops.Register(r, ops.Op[struct{}, forgeBucketOut]{
		Name:  "fleet.bucket.get",
		Caps:  "s",
		Doc:   "The storage the forge supplies to every repository that has not bound its own.",
		Kind:  ops.Query,
		Auth:  ops.Admin,
		Route: "GET /api/v1/fleet/bucket",
		Run: func(ctx context.Context, _ struct{}) (forgeBucketOut, error) {
			if d.Buckets == nil {
				return forgeBucketOut{}, fmt.Errorf("this deployment has no attachment storage: %w", ops.ErrUnavailable)
			}
			b, ok, err := d.Buckets.Bucket(oltp.ForgeBucket)
			if err != nil {
				return forgeBucketOut{}, err
			}
			if !ok {
				return forgeBucketOut{}, nil
			}
			info := bucketInfo("", b)
			info.Forge = true
			return forgeBucketOut{Bound: true, Info: info}, nil
		},
	}); err != nil {
		return err
	}
	if err := ops.Register(r, ops.Op[forgeBucketIn, forgeBucketOut]{
		Name: "fleet.bucket.set",
		Caps: "s",
		Doc: "Bind the storage the forge supplies. Every repository without a bucket of its own uses it, so a forge can host attachments for everybody on it. " +
			"Probed before it is written, like a repository's own binding; an empty secret keeps the stored one.",
		Kind:  ops.Command,
		Auth:  ops.Admin,
		Route: "POST /api/v1/fleet/bucket",
		Run: func(ctx context.Context, in forgeBucketIn) (forgeBucketOut, error) {
			if d.Buckets == nil {
				return forgeBucketOut{}, fmt.Errorf("this deployment has no attachment storage: %w", ops.ErrUnavailable)
			}
			in.Endpoint = strings.TrimRight(strings.TrimSpace(in.Endpoint), "/")
			if err := checkEndpoint(in.Endpoint); err != nil {
				return forgeBucketOut{}, fmt.Errorf("%v: %w", err, ops.ErrInvalid)
			}
			if in.PublicBase != "" {
				in.PublicBase = strings.TrimRight(strings.TrimSpace(in.PublicBase), "/")
				if u, perr := url.Parse(in.PublicBase); perr != nil || u.Host == "" ||
					(u.Scheme != "https" && u.Scheme != "http") {
					return forgeBucketOut{}, fmt.Errorf("the public base must be an http(s) url: %w", ops.ErrInvalid)
				}
			}
			if in.Region == "" {
				in.Region = "auto"
			}
			have, bound, err := d.Buckets.Bucket(oltp.ForgeBucket)
			if err != nil {
				return forgeBucketOut{}, err
			}
			if in.Secret == "" {
				if !bound {
					return forgeBucketOut{}, fmt.Errorf("a first binding needs the secret: %w", ops.ErrInvalid)
				}
				in.Secret = have.Secret
			}
			next := oltp.BucketBinding{Endpoint: in.Endpoint, Bucket: in.Bucket, Region: in.Region,
				AccessKey: in.AccessKey, Secret: in.Secret, PublicBase: in.PublicBase, LimitBytes: in.Limit}
			if next.LimitBytes <= 0 {
				next.LimitBytes = oltp.DefaultBucketLimit
			}
			// Probed before it is written, for the reason a repository's
			// own binding is: a binding that does not work is a form
			// error, not a row that fails every upload on the forge.
			probeCtx, cancel := context.WithTimeout(ctx, probeTimeout)
			defer cancel()
			bucket := s3.Bucket{Endpoint: next.Endpoint, Name: next.Bucket, Region: next.Region,
				AccessKey: next.AccessKey, Secret: next.Secret}
			if perr := bucket.Probe(probeCtx, &http.Client{Timeout: probeTimeout}, time.Now()); perr != nil {
				return forgeBucketOut{}, fmt.Errorf(
					"the bucket refused a probe (%v); check the endpoint, the bucket and the key's permissions: %w",
					perr, ops.ErrInvalid)
			}
			if err := d.Buckets.BindBucket(oltp.ForgeBucket, next); err != nil {
				return forgeBucketOut{}, fmt.Errorf("%v: %w", err, ops.ErrInvalid)
			}
			d.recordProbe(probeCtx, oltp.ForgeBucket, bucket, "")
			info := bucketInfo("", next)
			info.Forge = true
			return forgeBucketOut{Bound: true, Info: info}, nil
		},
	}); err != nil {
		return err
	}
	return ops.Register(r, ops.Op[struct{}, forgeBucketOut]{
		Name: "fleet.bucket.unset",
		Caps: "s",
		Doc: "Stop supplying storage. Repositories with a bucket of their own are unaffected; the rest lose attachments until they bind one. " +
			"Nothing is deleted: the objects stay where they are and the stubs stay in the DAG.",
		Kind:  ops.Command,
		Auth:  ops.Admin,
		Route: "POST /api/v1/fleet/bucket/unset",
		Run: func(ctx context.Context, _ struct{}) (forgeBucketOut, error) {
			if d.Buckets == nil {
				return forgeBucketOut{}, fmt.Errorf("this deployment has no attachment storage: %w", ops.ErrUnavailable)
			}
			if err := d.Buckets.UnbindBucket(oltp.ForgeBucket); err != nil {
				return forgeBucketOut{}, err
			}
			return forgeBucketOut{}, nil
		},
	})
}