Parent directory

status.go

4636 bytes
  1package pages
  2
  3import (
  4	"bytes"
  5	"context"
  6	"encoding/json"
  7	"io"
  8	"net/http"
  9	"net/url"
 10	"time"
 11)
 12
 13const (
 14	buildStatusRequestBatchSize = 100
 15	maxBuildStatusResponseSize  = 1 << 20
 16)
 17
 18var buildStatusHTTPClient = &http.Client{Timeout: 2 * time.Second}
 19
 20type repositoryBuildStatus struct {
 21	Label   string
 22	Class   string
 23	Present bool
 24}
 25
 26type workerBuildStatusRequest struct {
 27	Repositories []string `json:"repositories"`
 28}
 29
 30type workerBuildStatusResponse struct {
 31	Statuses map[string]workerBuildStatus `json:"statuses"`
 32}
 33
 34type workerBuildStatus struct {
 35	SHA      string    `json:"sha"`
 36	State    string    `json:"state"`
 37	QueuedAt time.Time `json:"queued_at"`
 38}
 39
 40// LoadBuildStatuses obtains current worker status without allowing an unavailable worker to fail a page.
 41func LoadBuildStatuses(ctx context.Context, workerURL string, repositories []string) map[string]repositoryBuildStatus {
 42	statuses := make(map[string]repositoryBuildStatus, len(repositories))
 43	for _, repository := range repositories {
 44		statuses[repository] = noBuildStatus()
 45	}
 46	if workerURL == "" || len(repositories) == 0 {
 47		return statuses
 48	}
 49
 50	endpoint, err := buildStatusEndpoint(workerURL)
 51	if err != nil {
 52		markBuildStatusUnavailable(statuses, repositories)
 53		return statuses
 54	}
 55	for start := 0; start < len(repositories); start += buildStatusRequestBatchSize {
 56		end := start + buildStatusRequestBatchSize
 57		if end > len(repositories) {
 58			end = len(repositories)
 59		}
 60		batch := repositories[start:end]
 61		workerStatuses, err := requestBuildStatuses(ctx, endpoint, batch)
 62		if err != nil {
 63			markBuildStatusUnavailable(statuses, batch)
 64			continue
 65		}
 66		for repository, status := range workerStatuses {
 67			statuses[repository] = status
 68		}
 69	}
 70	return statuses
 71}
 72
 73func buildStatusEndpoint(rawURL string) (string, error) {
 74	base, err := url.Parse(rawURL)
 75	if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" {
 76		return "", ErrNotFound
 77	}
 78	return base.ResolveReference(&url.URL{Path: "/build-statuses"}).String(), nil
 79}
 80
 81func requestBuildStatuses(ctx context.Context, endpoint string, repositories []string) (map[string]repositoryBuildStatus, error) {
 82	body, err := json.Marshal(workerBuildStatusRequest{Repositories: repositories})
 83	if err != nil {
 84		return nil, err
 85	}
 86	request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
 87	if err != nil {
 88		return nil, err
 89	}
 90	request.Header.Set("Content-Type", "application/json")
 91	response, err := buildStatusHTTPClient.Do(request)
 92	if err != nil {
 93		return nil, err
 94	}
 95	defer response.Body.Close()
 96	if response.StatusCode != http.StatusOK {
 97		return nil, ErrNotFound
 98	}
 99
100	limitedBody := &io.LimitedReader{R: response.Body, N: maxBuildStatusResponseSize + 1}
101	decoder := json.NewDecoder(limitedBody)
102	decoder.DisallowUnknownFields()
103	var payload workerBuildStatusResponse
104	if err := decoder.Decode(&payload); err != nil || payload.Statuses == nil || ensureNoAdditionalJSON(decoder) != nil || limitedBody.N == 0 {
105		return nil, ErrNotFound
106	}
107	requested := make(map[string]struct{}, len(repositories))
108	for _, repository := range repositories {
109		requested[repository] = struct{}{}
110	}
111	statuses := make(map[string]repositoryBuildStatus, len(payload.Statuses))
112	for repository, status := range payload.Statuses {
113		if _, ok := requested[repository]; !ok || !ValidCommitHash(status.SHA) || status.QueuedAt.IsZero() {
114			return nil, ErrNotFound
115		}
116		badge, ok := buildStatusBadge(status.State)
117		if !ok {
118			return nil, ErrNotFound
119		}
120		statuses[repository] = badge
121	}
122	return statuses, nil
123}
124
125func ensureNoAdditionalJSON(decoder *json.Decoder) error {
126	var extra any
127	if err := decoder.Decode(&extra); err != io.EOF {
128		return ErrNotFound
129	}
130	return nil
131}
132
133func buildStatusBadge(state string) (repositoryBuildStatus, bool) {
134	switch state {
135	case "queued":
136		return repositoryBuildStatus{Label: "Queued", Class: "build-status-queued", Present: true}, true
137	case "running":
138		return repositoryBuildStatus{Label: "Building", Class: "build-status-running", Present: true}, true
139	case "passed":
140		return repositoryBuildStatus{Label: "Passed", Class: "build-status-passed", Present: true}, true
141	case "failed":
142		return repositoryBuildStatus{Label: "Failed", Class: "build-status-failed", Present: true}, true
143	default:
144		return repositoryBuildStatus{}, false
145	}
146}
147
148func noBuildStatus() repositoryBuildStatus {
149	return repositoryBuildStatus{}
150}
151
152func markBuildStatusUnavailable(statuses map[string]repositoryBuildStatus, repositories []string) {
153	for _, repository := range repositories {
154		statuses[repository] = repositoryBuildStatus{}
155	}
156}