// Package updatesocket exposes tera-updated only over a group-restricted Unix socket. package updatesocket import ( "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "strconv" "time" "tera-clone/services/tera-api/internal/update" ) const DefaultSocketPath = "/run/tera-updated/control.sock" type response struct { Status *update.Status `json:"status,omitempty"` Code string `json:"code,omitempty"` Error string `json:"error,omitempty"` } type Server struct { Controller update.Controller MaxBytes int64 } func (s Server) Serve(listener net.Listener) error { if s.Controller == nil || s.MaxBytes <= 0 { return errors.New("update controller and positive package limit are required") } mux := http.NewServeMux() mux.HandleFunc("GET /v1/status", s.handleStatus) mux.HandleFunc("POST /v1/upload", s.handleUpload) mux.HandleFunc("POST /v1/apply", s.handleApply) server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second} err := server.Serve(listener) if errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) { return nil } return err } func (s Server) handleStatus(w http.ResponseWriter, request *http.Request) { status, err := s.Controller.Status(request.Context()) s.write(w, status, err) } func (s Server) handleUpload(w http.ResponseWriter, request *http.Request) { if request.ContentLength <= 0 || request.ContentLength > s.MaxBytes { s.write(w, update.Status{}, &update.Error{Code: "update_size_invalid", Message: "package content length is missing or exceeds the configured maximum"}) return } request.Body = http.MaxBytesReader(w, request.Body, s.MaxBytes) status, err := s.Controller.Upload(request.Context(), update.UploadRequest{ Filename: request.Header.Get("X-Tera-Filename"), ContentLength: request.ContentLength, Actor: request.Header.Get("X-Tera-Actor"), Body: request.Body, }) s.write(w, status, err) } func (s Server) handleApply(w http.ResponseWriter, request *http.Request) { status, err := s.Controller.Apply(request.Context(), update.ApplyRequest{Actor: request.Header.Get("X-Tera-Actor"), OperationID: request.Header.Get("X-Tera-Operation-ID")}) s.write(w, status, err) } func (Server) write(w http.ResponseWriter, status update.Status, err error) { w.Header().Set("Content-Type", "application/json") value := response{Status: &status} if err != nil { value.Code, value.Error = update.Code(err), err.Error() if value.Code == "" { value.Code = "update_failed" } } _ = json.NewEncoder(w).Encode(value) } type Client struct { uploadClient *http.Client controlClient *http.Client } func NewClient(socketPath string) *Client { if socketPath == "" { socketPath = DefaultSocketPath } transport := &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { dialer := net.Dialer{Timeout: 5 * time.Second} return dialer.DialContext(ctx, "unix", socketPath) }, DisableKeepAlives: false, } return &Client{ uploadClient: &http.Client{Transport: transport}, controlClient: &http.Client{Transport: transport, Timeout: 10 * time.Second}, } } func (c *Client) Status(ctx context.Context) (update.Status, error) { request, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://tera-updated/v1/status", nil) return c.do(c.controlClient, request) } func (c *Client) Upload(ctx context.Context, value update.UploadRequest) (update.Status, error) { request, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://tera-updated/v1/upload", io.NopCloser(value.Body)) if err != nil { return update.Status{}, err } request.ContentLength = value.ContentLength request.Header.Set("Content-Type", "application/octet-stream") request.Header.Set("X-Tera-Filename", value.Filename) request.Header.Set("X-Tera-Actor", value.Actor) request.Header.Set("X-Tera-Content-Length", strconv.FormatInt(value.ContentLength, 10)) return c.do(c.uploadClient, request) } func (c *Client) Apply(ctx context.Context, value update.ApplyRequest) (update.Status, error) { request, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://tera-updated/v1/apply", nil) request.Header.Set("X-Tera-Actor", value.Actor) request.Header.Set("X-Tera-Operation-ID", value.OperationID) return c.do(c.controlClient, request) } func (c *Client) do(client *http.Client, request *http.Request) (update.Status, error) { result, err := client.Do(request) if err != nil { return update.Status{}, &update.Error{Code: "update_unavailable", Message: "tera-updated is not reachable"} } defer result.Body.Close() var value response if err := json.NewDecoder(io.LimitReader(result.Body, 256*1024)).Decode(&value); err != nil { return update.Status{}, &update.Error{Code: "update_unavailable", Message: "tera-updated returned an invalid response"} } if value.Error != "" { return statusValue(value.Status), &update.Error{Code: value.Code, Message: value.Error} } if value.Status == nil { return update.Status{}, fmt.Errorf("tera-updated returned no status") } return *value.Status, nil } func statusValue(value *update.Status) update.Status { if value == nil { return update.Status{} } return *value }