feat: add spec definition api with versioning
This commit is contained in:
@ -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,
|
||||
})
|
||||
}
|
41
internal/server/api/get_agent_specs.go
Normal file
41
internal/server/api/get_agent_specs.go
Normal 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,
|
||||
})
|
||||
}
|
44
internal/server/api/get_spec_definition.go
Normal file
44
internal/server/api/get_spec_definition.go
Normal 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,
|
||||
})
|
||||
}
|
@ -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) {
|
||||
|
@ -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}
|
||||
}
|
||||
|
67
internal/server/api/query_spec_definitions.go
Normal file
67
internal/server/api/query_spec_definitions.go
Normal 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,
|
||||
})
|
||||
}
|
@ -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(),
|
||||
)
|
@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/setup"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func (s *Server) initRepositories(ctx context.Context) error {
|
||||
@ -18,8 +20,35 @@ func (s *Server) initRepositories(ctx context.Context) error {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
specDefRepo, err := setup.NewSpecDefinitionRepository(ctx, s.conf.Database)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.agentRepo = agentRepo
|
||||
s.tenantRepo = tenantRepo
|
||||
s.specDefRepo = specDefRepo
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initSpecDefinitions(ctx context.Context) error {
|
||||
err := spec.Walk(func(name, version string, schema []byte) error {
|
||||
logger.Debug(
|
||||
ctx, "updating spec definition",
|
||||
logger.F("name", name),
|
||||
logger.F("version", version),
|
||||
)
|
||||
if _, err := s.specDefRepo.Upsert(ctx, name, version, schema); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -28,9 +28,10 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
conf config.ServerConfig
|
||||
agentRepo datastore.AgentRepository
|
||||
tenantRepo datastore.TenantRepository
|
||||
conf config.ServerConfig
|
||||
agentRepo datastore.AgentRepository
|
||||
tenantRepo datastore.TenantRepository
|
||||
specDefRepo datastore.SpecDefinitionRepository
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||
@ -57,6 +58,12 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.initSpecDefinitions(ctx); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.conf.HTTP.Host, s.conf.HTTP.Port))
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
@ -105,6 +112,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
apiMount := api.NewMount(
|
||||
s.agentRepo,
|
||||
s.tenantRepo,
|
||||
s.specDefRepo,
|
||||
userAuth,
|
||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||
)
|
||||
|
Reference in New Issue
Block a user