package api import ( "bytes" "encoding/json" "io" "net/http" "regexp" "strings" "time" ) var releaseBuilderTagPattern = regexp.MustCompile(`^v\d+\.\d+\.\d+$`) type releaseBuilderRequest struct { Tag string `json:"tag"` Notes string `json:"notes"` Mandatory bool `json:"mandatory"` } func (s *Server) handleAdminReleaseBuilderStatus(w http.ResponseWriter, r *http.Request) { s.relayReleaseBuilder(w, r, http.MethodGet, "/v1/status", nil) } func (s *Server) handleAdminReleaseBuilderStart(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 16<<10) var request releaseBuilderRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { writeError(w, http.StatusBadRequest, "invalid release request") return } request.Tag = strings.TrimSpace(request.Tag) request.Notes = strings.TrimSpace(request.Notes) if request.Tag != "" && !releaseBuilderTagPattern.MatchString(request.Tag) { writeError(w, http.StatusBadRequest, "tag must be blank or look like v0.2.64") return } if len(request.Notes) > 4000 { writeError(w, http.StatusBadRequest, "release notes are too long") return } body, err := json.Marshal(request) if err != nil { writeError(w, http.StatusInternalServerError, "could not prepare release request") return } s.relayReleaseBuilder(w, r, http.MethodPost, "/v1/releases", body) } func (s *Server) relayReleaseBuilder(w http.ResponseWriter, incoming *http.Request, method, path string, body []byte) { if s.cfg.ReleaseBuilderURL == "" || s.cfg.ReleasePublishToken == "" { writeError(w, http.StatusServiceUnavailable, "the Docker release builder is not configured") return } request, err := http.NewRequestWithContext(incoming.Context(), method, s.cfg.ReleaseBuilderURL+path, bytes.NewReader(body)) if err != nil { writeError(w, http.StatusInternalServerError, "could not prepare builder request") return } request.Header.Set("Authorization", "Bearer "+s.cfg.ReleasePublishToken) if len(body) > 0 { request.Header.Set("Content-Type", "application/json") } client := &http.Client{Timeout: 10 * time.Second} response, err := client.Do(request) if err != nil { writeError(w, http.StatusServiceUnavailable, "the Docker release builder is not available") return } defer response.Body.Close() payload, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) if err != nil { writeError(w, http.StatusBadGateway, "could not read the Docker release builder response") return } w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(response.StatusCode) _, _ = w.Write(payload) }