Resources segregation by tenant #20
|
@ -48,7 +48,7 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
query = `
|
query = `
|
||||||
UPDATE agents SET tenant_id = $1, updated_at = $2
|
UPDATE agents SET tenant_id = $1, updated_at = $2 WHERE id = $3
|
||||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||||
ctx, query,
|
ctx, query,
|
||||||
tenantID,
|
tenantID,
|
||||||
now,
|
now,
|
||||||
|
agentID,
|
||||||
)
|
)
|
||||||
|
|
||||||
metadata := JSONMap{}
|
metadata := JSONMap{}
|
||||||
|
@ -85,8 +86,47 @@ func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach implements datastore.AgentRepository.
|
// Detach implements datastore.AgentRepository.
|
||||||
func (*AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
func (r *AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||||
panic("unimplemented")
|
var agent datastore.Agent
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
UPDATE agents SET tenant_id = null, updated_at = $1 WHERE id = $2
|
||||||
|
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(
|
||||||
|
ctx, query,
|
||||||
|
now,
|
||||||
|
agentID,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata := JSONMap{}
|
||||||
|
var rawKeySet []byte
|
||||||
|
|
||||||
|
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Metadata = metadata
|
||||||
|
|
||||||
|
keySet, err := jwk.Parse(rawKeySet)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.KeySet = &datastore.SerializableKeySet{keySet}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &agent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSpec implements datastore.AgentRepository.
|
// DeleteSpec implements datastore.AgentRepository.
|
||||||
|
|
|
@ -1,504 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/api"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
|
||||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
|
||||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
|
||||||
ErrCodeConflict api.ErrorCode = "conflict"
|
|
||||||
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
|
||||||
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type registerAgentRequest struct {
|
|
||||||
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
|
||||||
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
|
||||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
|
||||||
Signature string `json:"signature" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
registerAgentReq := ®isterAgentRequest{}
|
|
||||||
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
|
||||||
|
|
||||||
// Validate that the existing signature validates the request
|
|
||||||
|
|
||||||
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !validSignature {
|
|
||||||
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Create(
|
|
||||||
ctx,
|
|
||||||
registerAgentReq.Thumbprint,
|
|
||||||
keySet,
|
|
||||||
metadata,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agents, _, err := s.agentRepo.Query(
|
|
||||||
ctx,
|
|
||||||
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
|
||||||
datastore.WithAgentQueryLimit(1),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(agents) == 0 {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agentID := agents[0].ID
|
|
||||||
|
|
||||||
agent, err = s.agentRepo.Get(ctx, agentID)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not retrieve agent",
|
|
||||||
logger.CapturedE(err), logger.F("agentID", agentID),
|
|
||||||
)
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agent, err = s.agentRepo.Update(
|
|
||||||
ctx,
|
|
||||||
agents[0].ID,
|
|
||||||
datastore.WithAgentUpdateKeySet(keySet),
|
|
||||||
datastore.WithAgentUpdateMetadata(metadata),
|
|
||||||
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusCreated, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateAgentRequest struct {
|
|
||||||
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
|
||||||
Label *string `json:"label" validate:"omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
updateAgentReq := &updateAgentRequest{}
|
|
||||||
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
|
||||||
|
|
||||||
if updateAgentReq.Status != nil {
|
|
||||||
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateAgentReq.Label != nil {
|
|
||||||
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
|
||||||
}
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Update(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, ok := assertRequestUser(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
options := []datastore.AgentQueryOptionFunc{
|
|
||||||
datastore.WithAgentQueryLimit(int(limit)),
|
|
||||||
datastore.WithAgentQueryOffset(int(offset)),
|
|
||||||
datastore.WithAgentQueryTenantID(user.Tenant()),
|
|
||||||
}
|
|
||||||
|
|
||||||
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ids != nil {
|
|
||||||
agentIDs := func(ids []int64) []datastore.AgentID {
|
|
||||||
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
agentIDs = append(agentIDs, datastore.AgentID(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
return agentIDs
|
|
||||||
}(ids)
|
|
||||||
|
|
||||||
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if thumbprints != nil {
|
|
||||||
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
|
||||||
}
|
|
||||||
|
|
||||||
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if statuses != nil {
|
|
||||||
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
|
||||||
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
|
||||||
for _, status := range statuses {
|
|
||||||
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
return agentStatuses
|
|
||||||
}(statuses)
|
|
||||||
|
|
||||||
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
agents, total, err := s.agentRepo.Query(
|
|
||||||
ctx,
|
|
||||||
options...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agents []*datastore.Agent `json:"agents"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
}{
|
|
||||||
Agents: agents,
|
|
||||||
Total: total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
err := s.agentRepo.Delete(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
AgentID datastore.AgentID `json:"agentId"`
|
|
||||||
}{
|
|
||||||
AgentID: datastore.AgentID(agentID),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
agent, err := s.agentRepo.Get(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type claimAgentRequest struct {
|
|
||||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) claimAgent(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, ok := assertRequestUser(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
claimAgentReq := &claimAgentRequest{}
|
|
||||||
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
results, _, err := s.agentRepo.Query(
|
|
||||||
ctx,
|
|
||||||
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) > 1 {
|
|
||||||
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agent := results[0]
|
|
||||||
|
|
||||||
if agent.TenantID != nil {
|
|
||||||
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
agent, err = s.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Agent *datastore.Agent `json:"agent"`
|
|
||||||
}{
|
|
||||||
Agent: agent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
|
||||||
rawAgentID := chi.URLParam(r, "agentID")
|
|
||||||
|
|
||||||
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return datastore.AgentID(agentID), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
if rawValue != "" {
|
|
||||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
if rawValue != "" {
|
|
||||||
values := strings.Split(rawValue, ",")
|
|
||||||
|
|
||||||
return values, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
|
||||||
rawValue := r.URL.Query().Get(param)
|
|
||||||
|
|
||||||
if rawValue != "" {
|
|
||||||
rawValues := strings.Split(rawValue, ",")
|
|
||||||
values := make([]int64, 0, len(rawValues))
|
|
||||||
|
|
||||||
for _, rv := range rawValues {
|
|
||||||
value, err := strconv.ParseInt(rv, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return values, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue, true
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package server
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -142,7 +142,7 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
|
||||||
return user, true
|
return user, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
func (m *Mount) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
user, ok := assertRequestUser(w, r)
|
user, ok := assertRequestUser(w, r)
|
||||||
|
@ -150,7 +150,7 @@ func (s *Server) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentI
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
agent, err := s.agentRepo.Get(ctx, agentID)
|
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
|
@ -0,0 +1,77 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type claimAgentRequest struct {
|
||||||
|
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) claimAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
claimAgentReq := &claimAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results, _, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 1 {
|
||||||
|
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := results[0]
|
||||||
|
|
||||||
|
if agent.TenantID != nil {
|
||||||
|
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
err := m.agentRepo.Delete(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not delete agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
AgentID datastore.AgentID `json:"agentId"`
|
||||||
|
}{
|
||||||
|
AgentID: datastore.AgentID(agentID),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Get(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
specs, err := m.agentRepo.GetSpecs(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Specs []*datastore.Spec `json:"specs"`
|
||||||
|
}{
|
||||||
|
Specs: specs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteSpecRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSpecReq := &deleteSpecRequest{}
|
||||||
|
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
err := m.agentRepo.DeleteSpec(
|
||||||
|
ctx,
|
||||||
|
agentID,
|
||||||
|
deleteSpecReq.Name,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{
|
||||||
|
Name: deleteSpecReq.Name,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrCodeUnknownError api.ErrorCode = "unknown-error"
|
||||||
|
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||||
|
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||||
|
ErrCodeConflict api.ErrorCode = "conflict"
|
||||||
|
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
||||||
|
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
||||||
|
rawAgentID := chi.URLParam(r, "agentID")
|
||||||
|
|
||||||
|
agentID, err := strconv.ParseInt(rawAgentID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(r.Context(), "could not parse agent id", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return datastore.AgentID(agentID), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
||||||
|
rawSpecID := chi.URLParam(r, "")
|
||||||
|
|
||||||
|
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return datastore.SpecID(specID), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
if rawValue != "" {
|
||||||
|
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
if rawValue != "" {
|
||||||
|
values := strings.Split(rawValue, ",")
|
||||||
|
|
||||||
|
return values, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []int64) ([]int64, bool) {
|
||||||
|
rawValue := r.URL.Query().Get(param)
|
||||||
|
|
||||||
|
if rawValue != "" {
|
||||||
|
rawValues := strings.Split(rawValue, ",")
|
||||||
|
values := make([]int64, 0, len(rawValues))
|
||||||
|
|
||||||
|
for _, rv := range rawValues {
|
||||||
|
value, err := strconv.ParseInt(rv, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(r.Context(), "could not parse int slice param", logger.F("param", param), logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, true
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mount struct {
|
||||||
|
agentRepo datastore.AgentRepository
|
||||||
|
authenticators []auth.Authenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) Mount(r chi.Router) {
|
||||||
|
r.NotFound(m.notFound)
|
||||||
|
|
||||||
|
r.Post("/register", m.registerAgent)
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(auth.Middleware(m.authenticators...))
|
||||||
|
|
||||||
|
r.Route("/agents", func(r chi.Router) {
|
||||||
|
r.Post("/claim", m.claimAgent)
|
||||||
|
|
||||||
|
r.With(assertGlobalReadAccess).Get("/", m.queryAgents)
|
||||||
|
|
||||||
|
r.With(assertAgentReadAccess).Get("/{agentID}", m.getAgent)
|
||||||
|
r.With(assertAgentWriteAccess).Put("/{agentID}", m.updateAgent)
|
||||||
|
r.With(assertAgentWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||||
|
|
||||||
|
r.With(assertAgentReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||||
|
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
||||||
|
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) notFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMount(agentRepo datastore.AgentRepository, authenticators ...auth.Authenticator) *Mount {
|
||||||
|
return &Mount{agentRepo, authenticators}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Mount) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := assertRequestUser(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, ok := getIntQueryParam(w, r, "offset", 0)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []datastore.AgentQueryOptionFunc{
|
||||||
|
datastore.WithAgentQueryLimit(int(limit)),
|
||||||
|
datastore.WithAgentQueryOffset(int(offset)),
|
||||||
|
datastore.WithAgentQueryTenantID(user.Tenant()),
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ids != nil {
|
||||||
|
agentIDs := func(ids []int64) []datastore.AgentID {
|
||||||
|
agentIDs := make([]datastore.AgentID, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
agentIDs = append(agentIDs, datastore.AgentID(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentIDs
|
||||||
|
}(ids)
|
||||||
|
|
||||||
|
options = append(options, datastore.WithAgentQueryID(agentIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbprints, ok := getStringSliceValues(w, r, "thumbprints", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if thumbprints != nil {
|
||||||
|
options = append(options, datastore.WithAgentQueryThumbprints(thumbprints...))
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses, ok := getIntSliceValues(w, r, "statuses", nil)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if statuses != nil {
|
||||||
|
agentStatuses := func(statuses []int64) []datastore.AgentStatus {
|
||||||
|
agentStatuses := make([]datastore.AgentStatus, 0, len(statuses))
|
||||||
|
for _, status := range statuses {
|
||||||
|
agentStatuses = append(agentStatuses, datastore.AgentStatus(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentStatuses
|
||||||
|
}(statuses)
|
||||||
|
|
||||||
|
options = append(options, datastore.WithAgentQueryStatus(agentStatuses...))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
agents, total, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not list agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agents []*datastore.Agent `json:"agents"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}{
|
||||||
|
Agents: agents,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type registerAgentRequest struct {
|
||||||
|
KeySet json.RawMessage `json:"keySet" validate:"required"`
|
||||||
|
Metadata []metadata.Tuple `json:"metadata" validate:"required"`
|
||||||
|
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||||
|
Signature string `json:"signature" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
registerAgentReq := ®isterAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, registerAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
keySet, err := jwk.Parse(registerAgentReq.KeySet)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not parse key set", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = logger.With(ctx, logger.F("agentThumbprint", registerAgentReq.Thumbprint))
|
||||||
|
|
||||||
|
// Validate that the existing signature validates the request
|
||||||
|
|
||||||
|
validSignature, err := jwk.Verify(keySet, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate signature", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validSignature {
|
||||||
|
logger.Warn(ctx, "conflicting signature", logger.F("signature", registerAgentReq.Signature))
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := metadata.FromSorted(registerAgentReq.Metadata)
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Create(
|
||||||
|
ctx,
|
||||||
|
registerAgentReq.Thumbprint,
|
||||||
|
keySet,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, datastore.ErrAlreadyExist) {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not create agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, _, err := m.agentRepo.Query(
|
||||||
|
ctx,
|
||||||
|
datastore.WithAgentQueryThumbprints(registerAgentReq.Thumbprint),
|
||||||
|
datastore.WithAgentQueryLimit(1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve agents", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(agents) == 0 {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve matching agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentID := agents[0].ID
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Get(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not retrieve agent",
|
||||||
|
logger.CapturedE(err), logger.F("agentID", agentID),
|
||||||
|
)
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validSignature, err = jwk.Verify(agent.KeySet.Set, registerAgentReq.Signature, registerAgentReq.Thumbprint, registerAgentReq.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate signature using previous keyset", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeConflict, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validSignature {
|
||||||
|
logger.Error(ctx, "invalid signature")
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Update(
|
||||||
|
ctx,
|
||||||
|
agents[0].ID,
|
||||||
|
datastore.WithAgentUpdateKeySet(keySet),
|
||||||
|
datastore.WithAgentUpdateMetadata(metadata),
|
||||||
|
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusCreated, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type releaseAgentRequest struct {
|
||||||
|
AgentID int64 `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) releaseAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
releaseAgentReq := &releaseAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, releaseAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentID := datastore.AgentID(releaseAgentReq.AgentID)
|
||||||
|
|
||||||
|
if ok := m.assertTenantOwns(w, r, agentID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Get(ctx, agentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not retrieve agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err = m.agentRepo.Detach(ctx, agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not detach agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateAgentRequest struct {
|
||||||
|
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
|
||||||
|
Label *string `json:"label" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
updateAgentReq := &updateAgentRequest{}
|
||||||
|
if ok := api.Bind(w, r, updateAgentReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make([]datastore.AgentUpdateOptionFunc, 0)
|
||||||
|
|
||||||
|
if updateAgentReq.Status != nil {
|
||||||
|
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateAgentReq.Label != nil {
|
||||||
|
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := m.agentRepo.Update(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
options...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update agent", logger.CapturedE(err))
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Agent *datastore.Agent `json:"agent"`
|
||||||
|
}{
|
||||||
|
Agent: agent,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/api"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateSpecRequest struct {
|
||||||
|
spec.RawSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
agentID, ok := getAgentID(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
updateSpecReq := &updateSpecRequest{}
|
||||||
|
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
||||||
|
data := struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
var validationErr *spec.ValidationError
|
||||||
|
|
||||||
|
if errors.As(err, &validationErr) {
|
||||||
|
data.Message = validationErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := m.agentRepo.UpdateSpec(
|
||||||
|
ctx,
|
||||||
|
datastore.AgentID(agentID),
|
||||||
|
string(updateSpecReq.SpecName()),
|
||||||
|
updateSpecReq.SpecRevision(),
|
||||||
|
updateSpecReq.SpecData(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
||||||
|
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
||||||
|
|
||||||
|
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.DataResponse(w, http.StatusOK, struct {
|
||||||
|
Spec *datastore.Spec `json:"spec"`
|
||||||
|
}{
|
||||||
|
Spec: spec,
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,12 +10,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
"forge.cadoles.com/Cadoles/emissary/internal/config"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/server/api"
|
||||||
"github.com/antonmedv/expr"
|
"github.com/antonmedv/expr"
|
||||||
"github.com/antonmedv/expr/vm"
|
"github.com/antonmedv/expr/vm"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -101,28 +101,13 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||||
}
|
}
|
||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Post("/register", s.registerAgent)
|
apiMount := api.NewMount(
|
||||||
|
s.agentRepo,
|
||||||
r.Group(func(r chi.Router) {
|
|
||||||
r.Use(auth.Middleware(
|
|
||||||
thirdPartyAuth,
|
thirdPartyAuth,
|
||||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||||
))
|
)
|
||||||
|
|
||||||
r.Route("/agents", func(r chi.Router) {
|
apiMount.Mount(r)
|
||||||
r.Post("/claim", s.claimAgent)
|
|
||||||
|
|
||||||
r.With(assertGlobalReadAccess).Get("/", s.queryAgents)
|
|
||||||
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}", s.getAgent)
|
|
||||||
r.With(assertAgentWriteAccess).Put("/{agentID}", s.updateAgent)
|
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", s.deleteAgent)
|
|
||||||
|
|
||||||
r.With(assertAgentReadAccess).Get("/{agentID}/specs", s.getAgentSpecs)
|
|
||||||
r.With(assertAgentWriteAccess).Post("/{agentID}/specs", s.updateSpec)
|
|
||||||
r.With(assertAgentWriteAccess).Delete("/{agentID}/specs", s.deleteSpec)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.Info(ctx, "http server listening")
|
logger.Info(ctx, "http server listening")
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/api"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
|
||||||
)
|
|
||||||
|
|
||||||
type updateSpecRequest struct {
|
|
||||||
spec.RawSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) updateSpec(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
updateSpecReq := &updateSpecRequest{}
|
|
||||||
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := spec.Validate(ctx, updateSpecReq); err != nil {
|
|
||||||
data := struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
var validationErr *spec.ValidationError
|
|
||||||
|
|
||||||
if errors.As(err, &validationErr) {
|
|
||||||
data.Message = validationErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not validate spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, data)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, err := s.agentRepo.UpdateSpec(
|
|
||||||
ctx,
|
|
||||||
datastore.AgentID(agentID),
|
|
||||||
string(updateSpecReq.SpecName()),
|
|
||||||
updateSpecReq.SpecRevision(),
|
|
||||||
updateSpecReq.SpecData(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, datastore.ErrUnexpectedRevision) {
|
|
||||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeUnexpectedRevision, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not update spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Spec *datastore.Spec `json:"spec"`
|
|
||||||
}{
|
|
||||||
Spec: spec,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
specs, err := s.agentRepo.GetSpecs(ctx, agentID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not list specs", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Specs []*datastore.Spec `json:"specs"`
|
|
||||||
}{
|
|
||||||
Specs: specs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type deleteSpecRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
|
||||||
agentID, ok := getAgentID(w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSpecReq := &deleteSpecRequest{}
|
|
||||||
if ok := api.Bind(w, r, deleteSpecReq); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
err := s.agentRepo.DeleteSpec(
|
|
||||||
ctx,
|
|
||||||
agentID,
|
|
||||||
deleteSpecReq.Name,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
|
||||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(ctx, "could not delete spec", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}{
|
|
||||||
Name: deleteSpecReq.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
|
||||||
rawSpecID := chi.URLParam(r, "")
|
|
||||||
|
|
||||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithStack(err)
|
|
||||||
logger.Error(r.Context(), "could not parse spec id", logger.CapturedE(err))
|
|
||||||
|
|
||||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return datastore.SpecID(specID), true
|
|
||||||
}
|
|
Loading…
Reference in New Issue