projectprovision

package
v1.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package projectprovision is octo-server's only outbound path to the two optional Project provisioning subsystems: octo-fleet and octo-drive.

No request handler may reach either subsystem. The Project worker is the caller of Ensure and CreateDriveSpace; config_provisioning.go imports this package only to resolve and validate targets at boot. Keeping that boundary in one package makes the no-egress-from-request-path rule checkable.

This package deliberately does not retry or log. Retry, backoff and the give-up decision belong to the durable outbox row, and callers receive targets that have already been resolved from configuration.

Fleet's Ensure protocol uses an opaque container_id as its idempotency key and authenticates with the per-target HMAC secret. That id is never included in errors or logs.

Drive's internal create protocol is intentionally separate: it authenticates with X-Internal-Token and sends the current Project name, octo_space_id, super_admin_uid and project_id. It does not send Fleet's container_id and does not read or persist Drive's remote space id. A same-project 409 is accepted only when Drive's conflict envelope names the requested project_id exactly; all other statuses remain failures for the worker to retry or abandon according to its category.

Index

Constants

View Source
const (
	HeaderSignature = octosign.HeaderSignature
	HeaderTimestamp = octosign.HeaderTimestamp
	HeaderEventID   = octosign.HeaderEventID
)

Fleet HMAC header names. They use the same canonical string and v1 HMAC as pkg/octosign, because a receiver that already verifies card callbacks can verify these with the code it has.

View Source
const HeaderInternalToken = "X-Internal-Token"

HeaderInternalToken carries an internal service token to a target that uses token authentication instead of the Fleet HMAC contract.

View Source
const MaxSkewSeconds = 300

MaxSkewSeconds is the timestamp window these vectors assume. A receiver may choose a smaller one; anything much larger widens the replay exposure described above.

Variables

This section is empty.

Functions

func Category

func Category(err error) string

Category extracts the low-cardinality failure label, or "" for a non-ensure error. Metric label values come from here so they can never be a free-form message.

func IsPublishedConformanceSecret

func IsPublishedConformanceSecret(s string) bool

IsPublishedConformanceSecret reports whether s is one of the secrets this file publishes.

Kept next to the literals it guards rather than in client.go, so adding a fifth vector with a new secret cannot leave the check behind: whoever adds the constant is editing this file.

Exported because modules/project signs an outbound feed to the SAME peer with the SAME pkg/octosign scheme, so the same published keys are the same forgery risk there. It must call this rather than re-declare the literals: a second copy is how one of them gets a fifth vector and the other does not.

func Summary

func Summary(err error) string

Summary returns the container-id-free failure text WITHOUT the category.

The caller already labels the row and the metric with its own outcome string, which is derived from Category — so including the category here produced `last_error = "transport_failed: projectprovision: transport_failed"`, the same label twice, in a 255-byte column that a human is sent to read and that the sweep appends to. This returns only the part the outcome does not already say:

transport_failed -> "dial tcp 10.0.0.4:8080: connect: connection refused"
target_5xx       -> "status 500"
invalid_request  -> ""            (nothing to add; the category IS the reason)

A non-EnsureError falls back to its own message, which is how a panic or a DB error keeps its text. Same container-id-freedom guarantee as Error(): the only request-derived field on EnsureError is `cause`, and this never reads it.

func ValidateTarget

func ValidateTarget(t Target) error

ValidateTarget rejects a destination that must not be used, at process start.

Scheme is restricted to http/https and userinfo is refused: an ensure URL is operator-supplied configuration, and a `https://user:pass@host/` form would put a credential into every log line that prints the URL. A path is required because a bare origin almost always means a truncated env value.

Two things it deliberately does NOT do, both ACCEPTED postures rather than omissions a reader has to guess about:

  • It does not reject private, loopback or link-local hosts (169.254.169.254 and friends). The destination is a deploy-time operator value, not user input, so there is no untrusted party choosing it, and both real targets are in-cluster services on private addresses — a private-range denylist would reject every legitimate configuration. Same posture as internal/cardactiondispatch, whose route URLs are operator-registered for the same reason. It would have to change if an ensure URL ever became something a tenant could influence.
  • It permits `http://`. Reviewed and accepted deliberately (2026-09-07): both targets are in-cluster, and requiring TLS would block the ordinary in-cluster deployment. The cost is stated rather than hidden: on a plaintext link, an on-path observer inside the cluster sees the container id — which is a capability until R2/R3 land — and captures a replayable signed request. That is precisely why the receiver's timestamp-freshness clause below is a MUST and why the conformance vectors exist: with TLS declined, the receiver-side check is the layer that has to actually work.

Types

type AuthMode

type AuthMode uint8

AuthMode selects the wire authentication contract for a target.

AuthHMAC is the zero value for backwards compatibility with the fleet ensure client. AuthInternalToken is used by Drive's internal create route; the two credential fields are mutually exclusive and ValidateTarget enforces that separation.

const (
	AuthHMAC AuthMode = iota
	AuthInternalToken
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the outbound HTTP client. One per process.

func NewClient

func NewClient(transport http.RoundTripper, clock func() time.Time) *Client

NewClient builds the client. transport and clock are injectable so tests drive it without a network, which is the only reason they are parameters.

Proxy is cleared and redirects are refused, for the same two reasons internal/cardactiondispatch/http.go clears them: the destinations are exact, operator-registered URLs, so honouring HTTP(S)_PROXY would let a deployment-level setting redirect provisioning traffic, and following a redirect would re-send the signed body to a host the signature was not computed for.

func (*Client) CreateDriveSpace

func (c *Client) CreateDriveSpace(ctx context.Context, target Target, req DriveRequest) (DriveSpaceResponse, error)

CreateDriveSpace calls Drive's internal create route exactly once.

Drive owns the remote space id, so this method sends only the project mapping fields and returns no remote identifier. A 409 is idempotent only when the response envelope names the same project_id that was requested; every other 409 remains a target failure.

func (*Client) Ensure

func (c *Client) Ensure(ctx context.Context, target Target, req EnsureRequest) (EnsureResponse, error)

Ensure calls one target's ensure endpoint exactly once.

The receiver's contract is get-first / create / duplicate-key downgrade, keyed on the supplied container_id (brief P-1), so this is safe to replay: at-least-once delivery converges instead of manufacturing a second container. The receiver owes two more things that this side cannot enforce — see "What the receiver must do".

The signature's event-id slot carries containerEventID(container_id), i.e. the hash, not the id. See the package comment for why a capability does not go in a header.

type ConformanceVector

type ConformanceVector struct {
	// Name is stable; quote it in the receiver's test so a failure is greppable across
	// repositories.
	Name string
	// Secret is the per-target HMAC secret the receiver should be configured with for
	// this vector. Note that WrongSecret's signature was produced with a DIFFERENT
	// secret — the receiver still uses Secret and must reject.
	Secret string
	// Method / Path / Timestamp / EventID / Body are the request as it arrives on the
	// wire. Body is the exact byte sequence; do not reformat the JSON.
	Method    string
	Path      string
	Timestamp string
	EventID   string
	Body      string
	// Signature is the X-Octo-Signature header value as sent.
	Signature string
	// NowUnix is the receiver's clock when the request arrives. With
	// MaxSkewSeconds as the configured window, MustAccept is the required verdict.
	NowUnix int64
	// MustAccept is the verdict. False means the request must be refused — with a 4xx,
	// and without creating or touching any container.
	MustAccept bool
	// Why records which MUST clause the vector exercises.
	Why string
}

ConformanceVector is one fixed request plus the verdict the receiver must reach.

func ConformanceVectors

func ConformanceVectors() []ConformanceVector

ConformanceVectors returns the four vectors, in the order a receiver should run them.

type DriveRequest

type DriveRequest struct {
	Name          string `json:"name"`
	OctoSpaceID   string `json:"octo_space_id"`
	SuperAdminUID string `json:"super_admin_uid"`
	ProjectID     string `json:"project_id"`
}

DriveRequest is the body accepted by Drive's internal create route.

ProjectID is the stable project-to-space mapping key. ContainerID is deliberately absent: Drive owns its space id and this client never reads or persists it.

type DriveSpaceResponse

type DriveSpaceResponse struct {
	Duplicate bool
}

DriveSpaceResponse reports the only Drive result the caller needs. A 201 is a new remote space; an exact same-project 409 is an idempotent duplicate. The remote Drive id is intentionally ignored.

type EnsureError

type EnsureError struct {
	Category string
	Status   int
	// Detail is a bounded, container-id-free reason. It is either derived from a
	// transport inner error or a fixed response-classification constant; it is
	// never derived from a request or a response body.
	//
	// It exists because category-plus-status is empty for the failure an operator hits
	// first. A target that is down produced `last_error = "transport_failed:
	// projectprovision: transport_failed"` — the outcome label twice — while the actual
	// reason (connection refused vs DNS vs TLS vs deadline) sat in `cause`, reachable
	// only through Unwrap, which nothing calls. That field is what the runbook sends a
	// human to read, the sweep was deliberately changed to APPEND to it because it is the
	// only durable per-row evidence, and this package has no logger by design — so for an
	// OOM-killed pod there is no log line to fall back on either.
	//
	// Why a separate field rather than folding `cause` into Error(): `cause` is whatever
	// the standard library produced, and at two of the construction sites that is a
	// function of OUR request — encode_failed wraps a json.Marshal error over an
	// EnsureRequest, which carries the container id. json.Marshal of an all-string struct
	// cannot realistically fail, but "cannot realistically" is not the bar for a value the
	// package comment calls a capability. Transport-derived Detail comes only from the
	// transport error's INNER error, which describes the network and structurally cannot
	// contain the request. Response classifications use fixed constants rather than parsing
	// errors, because a parser error can quote response-body bytes. The container-id-freedom
	// of last_error stays a property of construction, not an argument about stdlib formatting.
	Detail string
	// contains filtered or unexported fields
}

EnsureError carries a low-cardinality category so the worker can label a metric and write a bounded last_error without ever touching the request body.

func (*EnsureError) Error

func (e *EnsureError) Error() string

Error is built from the category, the status and the bounded transport Detail — never from the request. It must stay that way: this string lands in octo_project_provisioning.last_error, and the container id is a capability (see the package comment and the Detail field).

func (*EnsureError) Unwrap

func (e *EnsureError) Unwrap() error

type EnsureRequest

type EnsureRequest struct {
	ContainerID string `json:"container_id"`
	ProjectID   string `json:"project_id"`
	OctoSpaceID string `json:"octo_space_id"`
	Name        string `json:"name"`
	IssuePrefix string `json:"issue_prefix,omitempty"`
}

EnsureRequest is the Fleet wire body. Drive has a separate body type because its project mapping is keyed by project_id and does not use container_id.

type EnsureResponse

type EnsureResponse struct {
	ContainerID string `json:"container_id"`
	Slug        string `json:"slug,omitempty"`
}

EnsureResponse is what Fleet returns. Drive's remote space id is intentionally not represented because the Project outbox is keyed by project_id.

type Target

type Target struct {
	// Name is the low-cardinality target label ("fleet" / "drive"). It reaches
	// metrics and log lines, so it must stay an enum and never be a URL.
	Name string
	// EnsureURL is the absolute POST endpoint. Fleet uses its HMAC ensure route;
	// Drive uses /v1/internal/drive/spaces with AuthInternalToken.
	EnsureURL string
	// Auth selects the target's wire authentication. The zero value is HMAC.
	Auth AuthMode
	// Secret is the per-target Fleet HMAC secret.
	Secret string
	// InternalToken is the Drive internal-route token. It must not be used as
	// an HMAC secret or sent to the Fleet route.
	InternalToken string
	// Timeout bounds one call; zero means defaultTimeout.
	Timeout time.Duration
}

Target is one fully resolved destination. Built by the caller from configuration at process start; ValidateTarget is what makes "resolved" mean something.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL