Compare commits
1 Commits
2024.3.13-
...
develop
Author | SHA1 | Date |
---|---|---|
wpetit | 85ccf2e1df |
|
@ -45,17 +45,29 @@ func (c *Controller) reconcileAgent(ctx context.Context, client *client.Client,
|
|||
return nil
|
||||
}
|
||||
|
||||
specs, err := client.GetAgentSpecs(ctx, agent.ID)
|
||||
specHeaders, err := client.QueryAgentSpecs(ctx, agent.ID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not retrieve agent specs", logger.CapturedE(err))
|
||||
logger.Error(ctx, "could not query agent specs", logger.CapturedE(err))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
state.ClearSpecs()
|
||||
|
||||
for _, spec := range specs {
|
||||
for _, sh := range specHeaders {
|
||||
spec, err := client.GetAgentSpec(ctx, agent.ID, sh.DefinitionName, sh.DefinitionVersion)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx, "could not retrieve agent spec",
|
||||
logger.F("specName", sh.DefinitionName),
|
||||
logger.F("specVersion", sh.DefinitionVersion),
|
||||
logger.CapturedE(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
state.SetSpec(spec)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,18 @@ import (
|
|||
func GetCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "get",
|
||||
Usage: "Get agent specifications",
|
||||
Flags: agentFlag.WithAgentFlags(),
|
||||
Usage: "Get agent specification",
|
||||
Flags: agentFlag.WithAgentFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "spec-name",
|
||||
Usage: "use `NAME` as specification's name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "spec-version",
|
||||
Usage: "use `VERSION` as specification's version",
|
||||
Value: "0.0.0",
|
||||
},
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
agentID, err := agentFlag.AssertAgentID(ctx)
|
||||
|
@ -29,16 +39,26 @@ func GetCommand() *cli.Command {
|
|||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
specDefName, err := assertSpecDefName(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
specDefVersion, err := assertSpecDefVersion(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
|
||||
spec, err := client.GetAgentSpec(ctx.Context, agentID, specDefName, specDefVersion)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
hints := specHints(baseFlags.OutputMode)
|
||||
|
||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(specs)...); err != nil {
|
||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,28 @@ import (
|
|||
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||
)
|
||||
|
||||
func specHeaderHints(outputMode format.OutputMode) format.Hints {
|
||||
return format.Hints{
|
||||
OutputMode: outputMode,
|
||||
Props: []format.Prop{
|
||||
format.NewProp("ID", "ID"),
|
||||
format.NewProp("DefinitionName", "Def. Name"),
|
||||
format.NewProp("DefinitionVersion", "Def. Version"),
|
||||
format.NewProp("Revision", "Revision"),
|
||||
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func specHints(outputMode format.OutputMode) format.Hints {
|
||||
return format.Hints{
|
||||
OutputMode: outputMode,
|
||||
Props: []format.Prop{
|
||||
format.NewProp("ID", "ID"),
|
||||
format.NewProp("Revision", "Revision"),
|
||||
format.NewProp("DefinitionName", "Def. Name"),
|
||||
format.NewProp("DefinitionVersion", "Def. Version"),
|
||||
format.NewProp("Revision", "Revision"),
|
||||
format.NewProp("Data", "Data"),
|
||||
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package spec
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func QueryCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "query",
|
||||
Usage: "Query agent specifications",
|
||||
Flags: agentFlag.WithAgentFlags(),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
agentID, err := agentFlag.AssertAgentID(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := clientFlag.GetToken(baseFlags)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
specs, err := client.QueryAgentSpecs(ctx.Context, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
hints := specHeaderHints(baseFlags.OutputMode)
|
||||
|
||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(specs)...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ func Root() *cli.Command {
|
|||
Usage: "Specifications related commands",
|
||||
Subcommands: []*cli.Command{
|
||||
GetCommand(),
|
||||
QueryCommand(),
|
||||
UpdateCommand(),
|
||||
DeleteCommand(),
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
|
@ -73,21 +74,12 @@ func UpdateCommand() *cli.Command {
|
|||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
specs, err := client.GetAgentSpecs(ctx.Context, agentID)
|
||||
existingSpec, err := client.GetAgentSpec(ctx.Context, agentID, specDefName, specDefVersion)
|
||||
if err != nil {
|
||||
var apiErr api.Error
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != api.ErrCodeNotFound {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
var existingSpec spec.Spec
|
||||
|
||||
for _, s := range specs {
|
||||
if s.SpecDefinitionName() != specDefName || s.SpecDefinitionVersion() != specDefVersion {
|
||||
continue
|
||||
}
|
||||
|
||||
existingSpec = s
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
revision := 0
|
||||
|
|
|
@ -19,7 +19,8 @@ type AgentRepository interface {
|
|||
Delete(ctx context.Context, id AgentID) error
|
||||
|
||||
UpdateSpec(ctx context.Context, id AgentID, name string, version string, revision int, data map[string]any) (*Spec, error)
|
||||
GetSpecs(ctx context.Context, id AgentID) ([]*Spec, error)
|
||||
QuerySpecs(ctx context.Context, id AgentID) ([]*SpecHeader, error)
|
||||
GetSpec(ctx context.Context, id AgentID, name string, version string) (*Spec, error)
|
||||
DeleteSpec(ctx context.Context, id AgentID, name string, version string) error
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,10 @@ import (
|
|||
|
||||
type SpecID int64
|
||||
|
||||
type Spec struct {
|
||||
type SpecHeader struct {
|
||||
ID SpecID `json:"id"`
|
||||
DefinitionName string `json:"name"`
|
||||
DefinitionVersion string `json:"version"`
|
||||
Data map[string]any `json:"data"`
|
||||
Revision int `json:"revision"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
@ -18,6 +17,12 @@ type Spec struct {
|
|||
AgentID AgentID `json:"agentId"`
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
SpecHeader
|
||||
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecDefinitionName() string {
|
||||
return s.DefinitionName
|
||||
}
|
||||
|
|
|
@ -154,9 +154,44 @@ func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.Agen
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetSpec implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) GetSpec(ctx context.Context, agentID datastore.AgentID, name string, version string) (*datastore.Spec, error) {
|
||||
var spec datastore.Spec
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
query := `SELECT id, name, version, revision, data, created_at, updated_at, agent_id, tenant_id FROM specs WHERE agent_id = $1 AND name = $2 AND version = $3`
|
||||
|
||||
row := tx.QueryRowContext(ctx, query, agentID, name, version)
|
||||
|
||||
var data JSONMap
|
||||
|
||||
if err := row.Scan(&spec.ID, &spec.DefinitionName, &spec.DefinitionVersion, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt, &spec.AgentID, &spec.TenantID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
spec.Data = data
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// GetSpecs implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentID) ([]*datastore.Spec, error) {
|
||||
specs := make([]*datastore.Spec, 0)
|
||||
func (r *AgentRepository) QuerySpecs(ctx context.Context, agentID datastore.AgentID) ([]*datastore.SpecHeader, error) {
|
||||
specs := make([]*datastore.SpecHeader, 0)
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
exists, err := r.agentExists(ctx, tx, agentID)
|
||||
|
@ -169,7 +204,7 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
|||
}
|
||||
|
||||
query := `
|
||||
SELECT id, name, version, revision, data, created_at, updated_at, agent_id, tenant_id
|
||||
SELECT id, name, version, revision, created_at, updated_at, agent_id, tenant_id
|
||||
FROM specs
|
||||
WHERE agent_id = $1
|
||||
`
|
||||
|
@ -187,19 +222,16 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
|||
}()
|
||||
|
||||
for rows.Next() {
|
||||
spec := &datastore.Spec{}
|
||||
|
||||
data := JSONMap{}
|
||||
spec := &datastore.SpecHeader{}
|
||||
|
||||
var tenantID sql.NullString
|
||||
if err := rows.Scan(&spec.ID, &spec.DefinitionName, &spec.DefinitionVersion, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt, &spec.AgentID, &tenantID); err != nil {
|
||||
if err := rows.Scan(&spec.ID, &spec.DefinitionName, &spec.DefinitionVersion, &spec.Revision, &spec.CreatedAt, &spec.UpdatedAt, &spec.AgentID, &tenantID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if tenantID.Valid {
|
||||
spec.TenantID = datastore.TenantID(tenantID.String)
|
||||
}
|
||||
spec.Data = data
|
||||
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
|
|
@ -84,11 +84,11 @@ var agentRepositoryTestCases = []agentRepositoryTestCase{
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "Try to get specs of an unexistant agent",
|
||||
Name: "Try to query specs of an unexistant agent",
|
||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||
var unexistantAgentID datastore.AgentID = 9999
|
||||
|
||||
specs, err := repo.GetSpecs(ctx, unexistantAgentID)
|
||||
specs, err := repo.QuerySpecs(ctx, unexistantAgentID)
|
||||
if err == nil {
|
||||
return errors.New("error should not be nil")
|
||||
}
|
||||
|
|
|
@ -4,20 +4,24 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func (m *Mount) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Mount) getAgentSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
specName := chi.URLParam(r, "specName")
|
||||
specVersion := chi.URLParam(r, "specVersion")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
specs, err := m.agentRepo.GetSpecs(ctx, agentID)
|
||||
spec, err := m.agentRepo.GetSpec(ctx, agentID, specName, specVersion)
|
||||
if err != nil {
|
||||
if errors.Is(err, datastore.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
@ -34,8 +38,8 @@ func (m *Mount) getAgentSpecs(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Specs []*datastore.Spec `json:"specs"`
|
||||
Spec *datastore.Spec `json:"spec"`
|
||||
}{
|
||||
Specs: specs,
|
||||
Spec: spec,
|
||||
})
|
||||
}
|
|
@ -35,7 +35,8 @@ func (m *Mount) Mount(r chi.Router) {
|
|||
r.With(assertAgentOrUserWithWriteAccess).Put("/{agentID}", m.updateAgent)
|
||||
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||
|
||||
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.queryAgentSpec)
|
||||
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs/{specName}/{specVersion}", m.getAgentSpec)
|
||||
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateAgentSpec)
|
||||
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteAgentSpec)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
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) queryAgentSpec(w http.ResponseWriter, r *http.Request) {
|
||||
agentID, ok := getAgentID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
specHeaders, err := m.agentRepo.QuerySpecs(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 query specs", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Retro-compatibility mode: return full specs temporarily
|
||||
specs := make([]*datastore.Spec, 0, len(specHeaders))
|
||||
for _, sh := range specHeaders {
|
||||
spec, err := m.agentRepo.GetSpec(ctx, agentID, sh.DefinitionName, sh.DefinitionVersion)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not query specs", logger.CapturedE(err))
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
}
|
||||
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Specs []*datastore.Spec `json:"specs"`
|
||||
}{
|
||||
Specs: specs,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) GetAgentSpec(ctx context.Context, agentID AgentID, specName string, specVersion string, funcs ...OptionFunc) (*datastore.Spec, error) {
|
||||
response := withResponse[struct {
|
||||
Spec *datastore.Spec `json:"spec"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d/specs/%s/%s", agentID, specName, specVersion)
|
||||
|
||||
if err := c.apiGet(ctx, path, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if response.Error != nil {
|
||||
return nil, errors.WithStack(response.Error)
|
||||
}
|
||||
|
||||
return response.Data.Spec, nil
|
||||
}
|
|
@ -5,13 +5,12 @@ import (
|
|||
"fmt"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) GetAgentSpecs(ctx context.Context, agentID AgentID, funcs ...OptionFunc) ([]Spec, error) {
|
||||
func (c *Client) QueryAgentSpecs(ctx context.Context, agentID AgentID, funcs ...OptionFunc) ([]*datastore.SpecHeader, error) {
|
||||
response := withResponse[struct {
|
||||
Specs []*datastore.Spec `json:"specs"`
|
||||
Specs []*datastore.SpecHeader `json:"specs"`
|
||||
}]()
|
||||
|
||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||
|
@ -24,10 +23,5 @@ func (c *Client) GetAgentSpecs(ctx context.Context, agentID AgentID, funcs ...Op
|
|||
return nil, errors.WithStack(response.Error)
|
||||
}
|
||||
|
||||
specs := make([]spec.Spec, 0, len(response.Data.Specs))
|
||||
for _, s := range response.Data.Specs {
|
||||
specs = append(specs, spec.Spec(s))
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
return response.Data.Specs, nil
|
||||
}
|
Loading…
Reference in New Issue