Files
DDI/server/internal/httpapi/h_agents.go
T

207 lines
5.6 KiB
Go

// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
package httpapi
import (
"bytes"
"io"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"git.beugsiud.de/beug.k/DDI/server/internal/service"
"git.beugsiud.de/beug.k/DDI/server/internal/store"
)
// intParam parses an optional integer query parameter within [lo, hi]
// (0 if absent).
func intParam(q url.Values, name string, lo, hi int64) (int64, error) {
v := q.Get(name)
if v == "" {
return 0, nil
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil || n < lo || n > hi {
return 0, errBadRequest("invalid_parameter", "invalid "+name)
}
return n, nil
}
func (a *API) listAgents(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
agents, err := a.svc.ListAgents(r.Context())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.AgentDTO]{Items: agents})
}
func (a *API) getAgent(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
ag, err := a.svc.GetAgent(r.Context(), r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, ag)
}
func (a *API) revokeAgent(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
ag, err := a.svc.RevokeAgent(r.Context(), rc.Actor, r.PathValue("id"))
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, ag)
}
func (a *API) deleteAgent(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
if err := a.svc.DeleteAgent(r.Context(), rc.Actor, r.PathValue("id")); err != nil {
return err
}
return noContent(w)
}
func (a *API) listEnrollmentTokens(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
tokens, err := a.svc.ListEnrollmentTokens(r.Context())
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, listResponse[service.EnrollmentTokenDTO]{Items: tokens})
}
type enrollmentTokenRequest struct {
Role string `json:"role"`
Description string `json:"description"`
ValidHours *int `json:"valid_hours"`
}
func (a *API) createEnrollmentToken(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req enrollmentTokenRequest
if err := decode(w, r, &req); err != nil {
return err
}
out, err := a.svc.CreateEnrollmentToken(r.Context(), rc.Actor, service.TokenInput{
Role: req.Role, Description: req.Description, ValidHours: req.ValidHours,
})
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, out)
}
func (a *API) deleteEnrollmentToken(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
if err := a.svc.DeleteEnrollmentToken(r.Context(), rc.Actor, r.PathValue("id")); err != nil {
return err
}
return noContent(w)
}
func (a *API) listRevisions(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
q := r.URL.Query()
limit, err := intParam(q, "limit", 1, service.MaxRevisionPage)
if err != nil {
return err
}
before, err := intParam(q, "before", 1, 1<<62)
if err != nil {
return err
}
page, err := a.svc.ListRevisions(r.Context(), int(limit), before)
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, page)
}
type revisionRequest struct {
Comment string `json:"comment"`
}
func (a *API) createRevision(w http.ResponseWriter, r *http.Request, rc *reqCtx) error {
var req revisionRequest
// The body is optional.
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBody))
if err != nil {
return errBadRequest("body_too_large", "request body too large")
}
if len(bytes.TrimSpace(body)) > 0 {
r.Body = io.NopCloser(bytes.NewReader(body))
if err := decode(w, r, &req); err != nil {
return err
}
}
rev, created, err := a.svc.CreateRevision(r.Context(), rc.Actor, req.Comment)
if err != nil {
return err
}
status := http.StatusOK
if created {
status = http.StatusCreated
}
return writeJSON(w, status, rev)
}
func (a *API) getRevision(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
n, err := strconv.ParseInt(r.PathValue("number"), 10, 64)
if err != nil {
return service.ErrNotFound
}
rev, err := a.svc.GetRevision(r.Context(), n)
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, rev)
}
func (a *API) listDeployments(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
q := r.URL.Query()
f := service.DeploymentFilter{AgentID: q.Get("agent_id")}
var err error
if f.Revision, err = intParam(q, "revision", 1, store.MaxRevision); err != nil {
return err
}
limit, err := intParam(q, "limit", 1, service.MaxDeploymentPage)
if err != nil {
return err
}
f.Limit = int(limit)
if f.BeforeID, err = intParam(q, "before_id", 1, 1<<62); err != nil {
return err
}
page, err := a.svc.ListDeployments(r.Context(), f)
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, page)
}
var leaseStates = map[string]bool{"active": true, "expired": true, "released": true, "declined": true}
func (a *API) listLeases(w http.ResponseWriter, r *http.Request, _ *reqCtx) error {
q := r.URL.Query()
f := service.LeaseFilter{NetworkID: q.Get("network_id"), State: q.Get("state"), Query: strings.TrimSpace(q.Get("q"))}
if f.State != "" && !leaseStates[f.State] {
return errBadRequest("invalid_parameter", "unknown lease state "+strconv.Quote(f.State))
}
if len(f.Query) > 200 {
return errBadRequest("invalid_parameter", "q is too long")
}
limit, err := intParam(q, "limit", 1, store.MaxLeasePage)
if err != nil {
return err
}
f.Limit = int(limit)
if v := q.Get("after"); v != "" {
ip, err := netip.ParseAddr(v)
if err != nil {
return errBadRequest("invalid_parameter", "after must be an IP address")
}
ip = ip.Unmap()
f.After = &ip
}
page, err := a.svc.ListLeases(r.Context(), f)
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, page)
}