feat: add spec definition api with versioning
All checks were successful
arcad/emissary/pipeline/head This commit looks good
arcad/emissary/pipeline/pr-master This commit looks good

This commit is contained in:
2024-03-12 16:22:35 +01:00
parent 0b34b485da
commit f612721b4e
69 changed files with 1468 additions and 273 deletions

View File

@ -9,42 +9,12 @@ import (
"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"`
Name string `json:"name" validate:"required"`
Version string `json:"version" validate:"required"`
}
func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
func (m *Mount) deleteAgentSpec(w http.ResponseWriter, r *http.Request) {
agentID, ok := getAgentID(w, r)
if !ok {
return
@ -61,6 +31,7 @@ func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
ctx,
agentID,
deleteSpecReq.Name,
deleteSpecReq.Version,
)
if err != nil {
if errors.Is(err, datastore.ErrNotFound) {
@ -78,8 +49,10 @@ func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
}
api.DataResponse(w, http.StatusOK, struct {
Name string `json:"name"`
Name string `json:"name"`
Version string `json:"version"`
}{
Name: deleteSpecReq.Name,
Name: deleteSpecReq.Name,
Version: deleteSpecReq.Version,
})
}

View File

@ -0,0 +1,41 @@
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,
})
}

View File

@ -0,0 +1,44 @@
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) getSpecDefinition(w http.ResponseWriter, r *http.Request) {
specDefName, specDefVersion, ok := getSpecDefinitionNameAndVersion(w, r)
if !ok {
return
}
ctx := r.Context()
specDef, err := m.specDefRepo.Get(
ctx,
specDefName,
specDefVersion,
)
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 {
SpecDefinition *datastore.SpecDefinition `json:"specDefinition"`
}{
SpecDefinition: specDef,
})
}

View File

@ -35,20 +35,11 @@ func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool
return datastore.AgentID(agentID), true
}
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
rawSpecID := chi.URLParam(r, "specID")
func getSpecDefinitionNameAndVersion(w http.ResponseWriter, r *http.Request) (string, string, bool) {
specDefName := chi.URLParam(r, "specDefName")
specDefVersion := chi.URLParam(r, "specDefVersion")
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
return specDefName, specDefVersion, true
}
func getTenantID(w http.ResponseWriter, r *http.Request) (datastore.TenantID, bool) {

View File

@ -12,6 +12,7 @@ import (
type Mount struct {
agentRepo datastore.AgentRepository
tenantRepo datastore.TenantRepository
specDefRepo datastore.SpecDefinitionRepository
authenticators []auth.Authenticator
}
@ -35,8 +36,8 @@ func (m *Mount) Mount(r chi.Router) {
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateSpec)
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateAgentSpec)
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteAgentSpec)
})
r.Route("/tenants", func(r chi.Router) {
@ -46,6 +47,11 @@ func (m *Mount) Mount(r chi.Router) {
r.With(assertAdminOrTenantWriteAccess).Put("/{tenantID}", m.updateTenant)
r.With(assertAdminAccess).Delete("/{tenantID}", m.deleteTenant)
})
r.Route("/specs", func(r chi.Router) {
r.With(assertQueryAccess).Get("/", m.querySpecDefinitions)
r.With(assertQueryAccess).Get("/{specDefName}/{specDefVersion}", m.getSpecDefinition)
})
})
}
@ -53,6 +59,6 @@ func (m *Mount) notFound(w http.ResponseWriter, r *http.Request) {
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
}
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, authenticators ...auth.Authenticator) *Mount {
return &Mount{agentRepo, tenantRepo, authenticators}
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, specDefRepo datastore.SpecDefinitionRepository, authenticators ...auth.Authenticator) *Mount {
return &Mount{agentRepo, tenantRepo, specDefRepo, authenticators}
}

View File

@ -0,0 +1,67 @@
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) querySpecDefinitions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
limit, ok := getIntQueryParam(w, r, "limit", 10)
if !ok {
return
}
offset, ok := getIntQueryParam(w, r, "offset", 0)
if !ok {
return
}
options := []datastore.SpecDefinitionQueryOptionFunc{
datastore.WithSpecDefinitionQueryLimit(int(limit)),
datastore.WithSpecDefinitionQueryOffset(int(offset)),
}
names, ok := getStringSliceValues(w, r, "names", nil)
if !ok {
return
}
if len(names) > 0 {
options = append(options, datastore.WithSpecDefinitionQueryNames(names...))
}
versions, ok := getStringSliceValues(w, r, "versions", nil)
if !ok {
return
}
if len(names) > 0 {
options = append(options, datastore.WithSpecDefinitionQueryVersions(versions...))
}
specDefinitions, total, err := m.specDefRepo.Query(
ctx,
options...,
)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "could not list spec definitions", logger.CapturedE(err))
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
return
}
api.DataResponse(w, http.StatusOK, struct {
SpecDefinitions []datastore.SpecDefinitionHeader `json:"specDefinitions"`
Total int `json:"total"`
}{
SpecDefinitions: specDefinitions,
Total: total,
})
}

View File

@ -14,11 +14,11 @@ const (
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
)
type updateSpecRequest struct {
type updateAgentSpecRequest struct {
spec.RawSpec
}
func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
func (m *Mount) updateAgentSpec(w http.ResponseWriter, r *http.Request) {
agentID, ok := getAgentID(w, r)
if !ok {
return
@ -26,12 +26,18 @@ func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
updateSpecReq := &updateSpecRequest{}
updateSpecReq := &updateAgentSpecRequest{}
if ok := api.Bind(w, r, updateSpecReq); !ok {
return
}
if err := spec.Validate(ctx, updateSpecReq); err != nil {
if updateSpecReq.DefinitionVersion == "" {
updateSpecReq.DefinitionVersion = spec.DefaultVersion
}
validator := spec.NewValidator(m.specDefRepo)
if err := validator.Validate(ctx, updateSpecReq); err != nil {
data := struct {
Message string `json:"message"`
}{}
@ -53,7 +59,8 @@ func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
spec, err := m.agentRepo.UpdateSpec(
ctx,
datastore.AgentID(agentID),
string(updateSpecReq.SpecName()),
updateSpecReq.SpecDefinitionName(),
updateSpecReq.SpecDefinitionVersion(),
updateSpecReq.SpecRevision(),
updateSpecReq.SpecData(),
)