Labels on deployment markers
Go · Go · beginner · modification
Lets CI pipelines attach labels to a deployment (region, commit, who triggered it) so the deploy dashboard can filter markers by them. Labels are validated up front: at most 20, keys in a strict snake_case alphabet, values capped at 200 bytes, with a plain 400 for anything else. The server stamps a `source` label with the pipeline's name on every deployment, overwriting whatever the client sent, so a pipeline can't post markers as another one. The shared GitHub Actions deploy step now sends `region` and `commit`.
Go 1.22 service, standard library only (`net/http` method-pattern `ServeMux`, `log/slog`). `cmd/server/main.go` (unchanged) registers `POST /api/deployments` behind `auth.RequirePipeline`, which checks the pipeline's bearer token, answers 401 itself for a bad one, and puts the `auth.Pipeline` (`ID int64`, `Name string`: the pipeline's unique name, at most 60 ASCII characters) into the request context. The mux is served by a plain `http.Server` with no other middleware. `Deployment` (existing) already has `Labels map[string]string`: the `labels jsonb not null default '{}'` column, the field and `Store.Insert` writing it shipped in the previous PR, and the dashboard's filters read it. `Handler` (existing) holds `Store Store` and `Log *slog.Logger`.
Requirements
- `POST /api/deployments` (existing) records a deployment reported by a CI pipeline. The body gains an optional `labels` field: a JSON object of string keys to string values, which may also be absent or `null`. Keys are 1 to 40 characters of `a-z`, `0-9` and `_`, starting with a letter; values are strings of at most 200 bytes (empty allowed); a request carries at most 20 labels, a `source` key the client sends included. Labels that break these rules answer 400 with a short plain-text reason and record nothing.
- Every recorded deployment carries a `source` label set to the name of the pipeline whose token sent the request, overwriting any `source` the client sent, so the dashboard can filter by pipeline and one pipeline cannot post as another. The stored labels are the request's labels plus `source`.
- Pipelines adopt labels at their own pace: a request in the existing format keeps getting 201 with the new deployment's id, as before.
- Unchanged: the request is decoded from the body through a 64 KiB read limit, and one that does not decode into the expected object (malformed JSON, a field of the wrong type, the limit reached mid-object) answers 400, and anything after the object is ignored, as in every handler of this service (bodies arrive as valid UTF-8: the gateway rejects others); `service` and `version` are required (at most 100 bytes each), `environment` is `staging` or `production`, `finished_at` is a required RFC 3339 timestamp; a store error is logged and answers 500 `internal error`.
Files touched
- internal/deploys/handler.go
--- internal/deploys/handler.go
import (
"encoding/json"
+ "errors"
+ "fmt"
"net/http"
+ "regexp"
"time"
)
-const maxBodyBytes = 64 << 10
+const (
+ maxBodyBytes = 64 << 10
+ maxLabels = 20
+ maxLabelValueLen = 200
+)
type createRequest struct {
- Service string `json:"service"`
- Version string `json:"version"`
- Environment string `json:"environment"`
- FinishedAt time.Time `json:"finished_at"`
+ Service string `json:"service"`
+ Version string `json:"version"`
+ Environment string `json:"environment"`
+ FinishedAt time.Time `json:"finished_at"`
+ Labels map[string]string `json:"labels"`
}