API d’introspection des définitions de spécifications #23
1
Makefile
1
Makefile
|
@ -20,6 +20,7 @@ clean:
|
||||||
rm -f emissary.sqlite*
|
rm -f emissary.sqlite*
|
||||||
rm -f server-key.json
|
rm -f server-key.json
|
||||||
rm -f agent-key.json
|
rm -f agent-key.json
|
||||||
|
rm -f state.json
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: test-go ## Executing tests
|
test: test-go ## Executing tests
|
||||||
|
|
|
@ -38,7 +38,7 @@ func (c *Controller) Name() string {
|
||||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
appSpec := spec.NewSpec()
|
appSpec := spec.NewSpec()
|
||||||
|
|
||||||
if err := state.GetSpec(spec.Name, appSpec); err != nil {
|
if err := state.GetSpec(spec.Name, spec.Version, appSpec); err != nil {
|
||||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||||
logger.Info(ctx, "could not find app spec")
|
logger.Info(ctx, "could not find app spec")
|
||||||
|
|
||||||
|
@ -50,7 +50,12 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "retrieved spec", logger.F("spec", appSpec.SpecName()), logger.F("revision", appSpec.SpecRevision()))
|
logger.Info(
|
||||||
|
ctx, "retrieved spec",
|
||||||
|
logger.F("name", appSpec.SpecDefinitionName()),
|
||||||
|
logger.F("version", appSpec.SpecDefinitionVersion()),
|
||||||
|
logger.F("revision", appSpec.SpecRevision()),
|
||||||
|
)
|
||||||
|
|
||||||
c.updateApps(ctx, appSpec)
|
c.updateApps(ctx, appSpec)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
var schema []byte
|
var schema []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := spec.Register(Name, schema); err != nil {
|
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||||
"title": "AppSpec",
|
"title": "AppSpec",
|
||||||
"description": "Emissary 'App' specification",
|
"description": "Emissary 'App' specification",
|
||||||
|
@ -78,7 +78,9 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["defaultUrlTemplate"],
|
"required": [
|
||||||
|
"defaultUrlTemplate"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"unexpectedHostRedirect": {
|
"unexpectedHostRedirect": {
|
||||||
|
@ -94,7 +96,10 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["acceptedHostPatterns", "hostTarget"],
|
"required": [
|
||||||
|
"acceptedHostPatterns",
|
||||||
|
"hostTarget"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
|
@ -104,7 +109,10 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"key": {
|
"key": {
|
||||||
"type": ["object", "string"]
|
"type": [
|
||||||
|
"object",
|
||||||
|
"string"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"signingAlgorithm": {
|
"signingAlgorithm": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
@ -6,7 +6,10 @@ import (
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Name spec.Name = "app.emissary.cadoles.com"
|
const (
|
||||||
|
Name string = "app.emissary.cadoles.com"
|
||||||
|
Version string = "0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
|
@ -56,10 +59,14 @@ type AppURLResolving struct {
|
||||||
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return Name
|
return Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return Version
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
return s.Revision
|
return s.Revision
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||||
func TestValidator(t *testing.T) {
|
func TestValidator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
validator := spec.NewValidator()
|
ctx := context.Background()
|
||||||
if err := validator.Register(Name, schema); err != nil {
|
|
||||||
|
repo := memory.NewSpecDefinitionRepository()
|
||||||
|
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator := spec.NewValidator(repo)
|
||||||
|
|
||||||
for _, tc := range validatorTestCases {
|
for _, tc := range validatorTestCases {
|
||||||
func(tc validatorTestCase) {
|
func(tc validatorTestCase) {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
|
|
@ -33,7 +33,7 @@ func (c *Controller) Name() string {
|
||||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
mdnsSpec := mdns.NewSpec()
|
mdnsSpec := mdns.NewSpec()
|
||||||
|
|
||||||
if err := state.GetSpec(mdns.Name, mdnsSpec); err != nil {
|
if err := state.GetSpec(mdns.Name, mdns.Version, mdnsSpec); err != nil {
|
||||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||||
logger.Info(ctx, "could not find mdns spec")
|
logger.Info(ctx, "could not find mdns spec")
|
||||||
|
|
||||||
|
@ -45,7 +45,11 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "retrieved spec", logger.F("spec", mdnsSpec.SpecName()), logger.F("revision", mdnsSpec.SpecRevision()))
|
logger.Info(ctx, "retrieved spec",
|
||||||
|
logger.F("name", mdnsSpec.SpecDefinitionName()),
|
||||||
|
logger.F("version", mdnsSpec.SpecDefinitionVersion()),
|
||||||
|
logger.F("revision", mdnsSpec.SpecRevision()),
|
||||||
|
)
|
||||||
|
|
||||||
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
|
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
|
||||||
return errors.Wrap(err, "could not update responder")
|
return errors.Wrap(err, "could not update responder")
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
var schema []byte
|
var schema []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := spec.Register(Name, schema); err != nil {
|
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
|
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
|
||||||
"title": "MDNSSpec",
|
"title": "MDNSSpec",
|
||||||
"description": "Emissary 'MDNS' specification",
|
"description": "Emissary 'MDNS' specification",
|
||||||
|
|
|
@ -4,7 +4,10 @@ import (
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Name spec.Name = "mdns.emissary.cadoles.com"
|
const (
|
||||||
|
Name string = "mdns.emissary.cadoles.com"
|
||||||
|
Version string = "0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
|
@ -19,10 +22,14 @@ type Service struct {
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return Name
|
return Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return Version
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
return s.Revision
|
return s.Revision
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||||
func TestValidator(t *testing.T) {
|
func TestValidator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
validator := spec.NewValidator()
|
ctx := context.Background()
|
||||||
if err := validator.Register(Name, schema); err != nil {
|
|
||||||
|
repo := memory.NewSpecDefinitionRepository()
|
||||||
|
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator := spec.NewValidator(repo)
|
||||||
|
|
||||||
for _, tc := range validatorTestCases {
|
for _, tc := range validatorTestCases {
|
||||||
func(tc validatorTestCase) {
|
func(tc validatorTestCase) {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
var schema []byte
|
var schema []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := spec.Register(Name, schema); err != nil {
|
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
"$id": "https://sysupgrade.openwrt.emissary.cadoles.com/spec.json",
|
"$id": "https://sysupgrade.openwrt.emissary.cadoles.com/spec.json",
|
||||||
"title": "SysUpgradeSpec",
|
"title": "SysUpgradeSpec",
|
||||||
"description": "Emissary 'SysUpgrade' specification",
|
"description": "Emissary 'SysUpgrade' specification",
|
||||||
|
@ -15,6 +15,10 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["url", "sha256sum", "version"],
|
"required": [
|
||||||
|
"url",
|
||||||
|
"sha256sum",
|
||||||
|
"version"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
|
@ -4,7 +4,10 @@ import (
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Name spec.Name = "sysupgrade.openwrt.emissary.cadoles.com"
|
const (
|
||||||
|
Name string = "sysupgrade.openwrt.emissary.cadoles.com"
|
||||||
|
Version string = "0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
|
@ -13,10 +16,14 @@ type Spec struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return Name
|
return Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return Version
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
return s.Revision
|
return s.Revision
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||||
func TestValidator(t *testing.T) {
|
func TestValidator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
validator := spec.NewValidator()
|
ctx := context.Background()
|
||||||
if err := validator.Register(Name, schema); err != nil {
|
|
||||||
|
repo := memory.NewSpecDefinitionRepository()
|
||||||
|
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator := spec.NewValidator(repo)
|
||||||
|
|
||||||
for _, tc := range validatorTestCases {
|
for _, tc := range validatorTestCases {
|
||||||
func(tc validatorTestCase) {
|
func(tc validatorTestCase) {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ func (*SysUpgradeController) Name() string {
|
||||||
func (c *SysUpgradeController) Reconcile(ctx context.Context, state *agent.State) error {
|
func (c *SysUpgradeController) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
sysSpec := sysupgrade.NewSpec()
|
sysSpec := sysupgrade.NewSpec()
|
||||||
|
|
||||||
if err := state.GetSpec(sysupgrade.Name, sysSpec); err != nil {
|
if err := state.GetSpec(sysupgrade.Name, sysupgrade.Version, sysSpec); err != nil {
|
||||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||||
logger.Info(ctx, "could not find sysupgrade spec, doing nothing")
|
logger.Info(ctx, "could not find sysupgrade spec, doing nothing")
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (*UCIController) Name() string {
|
||||||
func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error {
|
func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
uciSpec := ucispec.NewSpec()
|
uciSpec := ucispec.NewSpec()
|
||||||
|
|
||||||
if err := state.GetSpec(ucispec.NameUCI, uciSpec); err != nil {
|
if err := state.GetSpec(ucispec.Name, ucispec.Version, uciSpec); err != nil {
|
||||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||||
logger.Info(ctx, "could not find uci spec, doing nothing")
|
logger.Info(ctx, "could not find uci spec, doing nothing")
|
||||||
|
|
||||||
|
@ -37,7 +37,11 @@ func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "retrieved spec", logger.F("spec", uciSpec.SpecName()), logger.F("revision", uciSpec.SpecRevision()))
|
logger.Info(ctx, "retrieved spec",
|
||||||
|
logger.F("name", uciSpec.SpecDefinitionName()),
|
||||||
|
logger.F("version", uciSpec.SpecDefinitionVersion()),
|
||||||
|
logger.F("revision", uciSpec.SpecRevision()),
|
||||||
|
)
|
||||||
|
|
||||||
if c.currentSpecRevision == uciSpec.SpecRevision() {
|
if c.currentSpecRevision == uciSpec.SpecRevision() {
|
||||||
logger.Info(ctx, "spec revision did not change, doing nothing")
|
logger.Info(ctx, "spec revision did not change, doing nothing")
|
||||||
|
|
|
@ -9,13 +9,12 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
trackedSpecRevisions map[spec.Name]int
|
trackedSpecRevisions map[string]map[string]int
|
||||||
filename string
|
filename string
|
||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
@ -78,8 +77,14 @@ func (c *Controller) specChanged(specs agent.Specs) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, spec := range specs {
|
for name, specVersions := range specs {
|
||||||
trackedRevision, exists := c.trackedSpecRevisions[name]
|
trackedSpecs, exists := c.trackedSpecRevisions[name]
|
||||||
|
if !exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for version, spec := range specVersions {
|
||||||
|
trackedRevision, exists := trackedSpecs[version]
|
||||||
if !exists {
|
if !exists {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -89,25 +94,22 @@ func (c *Controller) specChanged(specs agent.Specs) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for trackedSpecName, trackedRevision := range c.trackedSpecRevisions {
|
|
||||||
spec, exists := specs[trackedSpecName]
|
|
||||||
if !exists {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if trackedRevision != spec.SpecRevision() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) trackSpecsRevisions(specs agent.Specs) {
|
func (c *Controller) trackSpecsRevisions(specs agent.Specs) {
|
||||||
c.trackedSpecRevisions = make(map[spec.Name]int)
|
c.trackedSpecRevisions = make(map[string]map[string]int)
|
||||||
|
|
||||||
for name, spec := range specs {
|
for name, specVersions := range specs {
|
||||||
c.trackedSpecRevisions[name] = spec.SpecRevision()
|
if _, exists := c.trackedSpecRevisions[name]; !exists {
|
||||||
|
c.trackedSpecRevisions[name] = make(map[string]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
for version, spec := range specVersions {
|
||||||
|
c.trackedSpecRevisions[name][version] = spec.SpecRevision()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +169,7 @@ func (c *Controller) writeState(ctx context.Context, state *agent.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
if err := ioutil.WriteFile(name, data, os.ModePerm); err != nil {
|
if err := os.WriteFile(name, data, os.ModePerm); err != nil {
|
||||||
return errors.Errorf("cannot write data to temporary file %q: %v", name, err)
|
return errors.Errorf("cannot write data to temporary file %q: %v", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +215,7 @@ func (c *Controller) writeState(ctx context.Context, state *agent.State) error {
|
||||||
func NewController(filename string) *Controller {
|
func NewController(filename string) *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
filename: filename,
|
filename: filename,
|
||||||
trackedSpecRevisions: make(map[spec.Name]int),
|
trackedSpecRevisions: make(map[string]map[string]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ func (c *Controller) Name() string {
|
||||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
proxySpec := spec.NewSpec()
|
proxySpec := spec.NewSpec()
|
||||||
|
|
||||||
if err := state.GetSpec(spec.NameProxy, proxySpec); err != nil {
|
if err := state.GetSpec(spec.Name, spec.Version, proxySpec); err != nil {
|
||||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||||
logger.Info(ctx, "could not find proxy spec")
|
logger.Info(ctx, "could not find proxy spec")
|
||||||
|
|
||||||
|
@ -42,7 +42,12 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "retrieved spec", logger.F("spec", proxySpec.SpecName()), logger.F("revision", proxySpec.SpecRevision()))
|
logger.Info(
|
||||||
|
ctx, "retrieved spec",
|
||||||
|
logger.F("name", proxySpec.SpecDefinitionName()),
|
||||||
|
logger.F("version", proxySpec.SpecDefinitionVersion()),
|
||||||
|
logger.F("revision", proxySpec.SpecRevision()),
|
||||||
|
)
|
||||||
|
|
||||||
c.updateProxies(ctx, proxySpec)
|
c.updateProxies(ctx, proxySpec)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
var ErrSpecNotFound = errors.New("spec not found")
|
var ErrSpecNotFound = errors.New("spec not found")
|
||||||
|
|
||||||
type Specs map[spec.Name]spec.Spec
|
type Specs map[string]map[string]spec.Spec
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
agentID datastore.AgentID
|
agentID datastore.AgentID
|
||||||
|
@ -20,27 +20,35 @@ type State struct {
|
||||||
|
|
||||||
func NewState() *State {
|
func NewState() *State {
|
||||||
return &State{
|
return &State{
|
||||||
specs: make(map[spec.Name]spec.Spec),
|
specs: make(map[string]map[string]spec.Spec),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) MarshalJSON() ([]byte, error) {
|
func (s *State) MarshalJSON() ([]byte, error) {
|
||||||
state := struct {
|
state := struct {
|
||||||
ID datastore.AgentID `json:"agentId"`
|
ID datastore.AgentID `json:"agentId"`
|
||||||
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
|
Specs map[string]map[string]*spec.RawSpec `json:"specs"`
|
||||||
}{
|
}{
|
||||||
ID: s.agentID,
|
ID: s.agentID,
|
||||||
Specs: func(specs map[spec.Name]spec.Spec) map[spec.Name]*spec.RawSpec {
|
Specs: func(specs map[string]map[string]spec.Spec) map[string]map[string]*spec.RawSpec {
|
||||||
rawSpecs := make(map[spec.Name]*spec.RawSpec)
|
rawSpecs := make(map[string]map[string]*spec.RawSpec)
|
||||||
|
|
||||||
for name, sp := range specs {
|
for name, versions := range specs {
|
||||||
rawSpecs[name] = &spec.RawSpec{
|
if _, exists := rawSpecs[name]; !exists {
|
||||||
Name: sp.SpecName(),
|
rawSpecs[name] = make(map[string]*spec.RawSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
for version, sp := range versions {
|
||||||
|
rawSpecs[name][version] = &spec.RawSpec{
|
||||||
|
DefinitionName: sp.SpecDefinitionName(),
|
||||||
|
DefinitionVersion: sp.SpecDefinitionVersion(),
|
||||||
Revision: sp.SpecRevision(),
|
Revision: sp.SpecRevision(),
|
||||||
Data: sp.SpecData(),
|
Data: sp.SpecData(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return rawSpecs
|
return rawSpecs
|
||||||
}(s.specs),
|
}(s.specs),
|
||||||
}
|
}
|
||||||
|
@ -56,18 +64,23 @@ func (s *State) MarshalJSON() ([]byte, error) {
|
||||||
func (s *State) UnmarshalJSON(data []byte) error {
|
func (s *State) UnmarshalJSON(data []byte) error {
|
||||||
state := struct {
|
state := struct {
|
||||||
AgentID datastore.AgentID `json:"agentId"`
|
AgentID datastore.AgentID `json:"agentId"`
|
||||||
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
|
Specs map[string]map[string]*spec.RawSpec `json:"specs"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &state); err != nil {
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.specs = func(rawSpecs map[spec.Name]*spec.RawSpec) Specs {
|
s.specs = func(rawSpecs map[string]map[string]*spec.RawSpec) Specs {
|
||||||
specs := make(Specs)
|
specs := make(Specs)
|
||||||
|
|
||||||
for name, raw := range rawSpecs {
|
for name, versions := range rawSpecs {
|
||||||
specs[name] = spec.Spec(raw)
|
if _, exists := specs[name]; !exists {
|
||||||
|
specs[name] = make(map[string]spec.Spec)
|
||||||
|
}
|
||||||
|
for version, raw := range versions {
|
||||||
|
specs[name][version] = spec.Spec(raw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return specs
|
return specs
|
||||||
|
@ -85,23 +98,36 @@ func (s *State) Specs() Specs {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) ClearSpecs() *State {
|
func (s *State) ClearSpecs() *State {
|
||||||
s.specs = make(map[spec.Name]spec.Spec)
|
s.specs = make(map[string]map[string]spec.Spec)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) SetSpec(sp spec.Spec) *State {
|
func (s *State) SetSpec(sp spec.Spec) *State {
|
||||||
if s.specs == nil {
|
if s.specs == nil {
|
||||||
s.specs = make(map[spec.Name]spec.Spec)
|
s.specs = make(map[string]map[string]spec.Spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.specs[sp.SpecName()] = sp
|
name := sp.SpecDefinitionName()
|
||||||
|
|
||||||
|
if _, exists := s.specs[name]; !exists {
|
||||||
|
s.specs[name] = make(map[string]spec.Spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := sp.SpecDefinitionVersion()
|
||||||
|
|
||||||
|
s.specs[name][version] = sp
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) GetSpec(name spec.Name, dest any) error {
|
func (s *State) GetSpec(name string, version string, dest any) error {
|
||||||
spec, exists := s.specs[name]
|
versions, exists := s.specs[name]
|
||||||
|
if !exists {
|
||||||
|
return errors.WithStack(ErrSpecNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, exists := versions[version]
|
||||||
if !exists {
|
if !exists {
|
||||||
return errors.WithStack(ErrSpecNotFound)
|
return errors.WithStack(ErrSpecNotFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
||||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/flag"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -23,6 +22,11 @@ func DeleteCommand() *cli.Command {
|
||||||
Name: "spec-name",
|
Name: "spec-name",
|
||||||
Usage: "use `NAME` as specification's 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 {
|
Action: func(ctx *cli.Context) error {
|
||||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||||
|
@ -37,14 +41,19 @@ func DeleteCommand() *cli.Command {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
specName, err := assertSpecName(ctx)
|
specDefName, err := assertSpecDefName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefVersion, err := assertSpecDefVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||||
|
|
||||||
specName, err = client.DeleteAgentSpec(ctx.Context, agentID, specName)
|
specDefName, specDefVersion, err = client.DeleteAgentSpec(ctx.Context, agentID, specDefName, specDefVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(apierr.Wrap(err))
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
}
|
}
|
||||||
|
@ -54,9 +63,11 @@ func DeleteCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, struct {
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, struct {
|
||||||
Name spec.Name `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
}{
|
}{
|
||||||
Name: specName,
|
Name: specDefName,
|
||||||
|
Version: specDefVersion,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,9 +36,7 @@ func GetCommand() *cli.Command {
|
||||||
return errors.WithStack(apierr.Wrap(err))
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
hints := format.Hints{
|
hints := specHints(baseFlags.OutputMode)
|
||||||
OutputMode: 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, clientFlag.AsAnySlice(specs)...); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package spec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format"
|
||||||
|
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||||
|
)
|
||||||
|
|
||||||
|
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("Data", "Data"),
|
||||||
|
format.NewProp("CreatedAt", "CreatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
|
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,11 @@ func UpdateCommand() *cli.Command {
|
||||||
Name: "spec-name",
|
Name: "spec-name",
|
||||||
Usage: "use `NAME` as specification's name",
|
Usage: "use `NAME` as specification's name",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "spec-version",
|
||||||
|
Usage: "use `VERSION` as specification's version",
|
||||||
|
Value: "0.0.0",
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "spec-data",
|
Name: "spec-data",
|
||||||
Usage: "use `DATA` as specification's data, '-' to read from STDIN",
|
Usage: "use `DATA` as specification's data, '-' to read from STDIN",
|
||||||
|
@ -44,7 +49,12 @@ func UpdateCommand() *cli.Command {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
specName, err := assertSpecName(ctx)
|
specDefName, err := assertSpecDefName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefVersion, err := assertSpecDefVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
@ -71,7 +81,7 @@ func UpdateCommand() *cli.Command {
|
||||||
var existingSpec spec.Spec
|
var existingSpec spec.Spec
|
||||||
|
|
||||||
for _, s := range specs {
|
for _, s := range specs {
|
||||||
if s.SpecName() != specName {
|
if s.SpecDefinitionName() != specDefName || s.SpecDefinitionVersion() != specDefVersion {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,23 +110,18 @@ func UpdateCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
rawSpec := &spec.RawSpec{
|
rawSpec := &spec.RawSpec{
|
||||||
Name: specName,
|
DefinitionName: specDefName,
|
||||||
|
DefinitionVersion: specDefVersion,
|
||||||
Revision: revision,
|
Revision: revision,
|
||||||
Data: specData,
|
Data: specData,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := spec.Validate(ctx.Context, rawSpec); err != nil {
|
|
||||||
return errors.WithStack(apierr.Wrap(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, err := client.UpdateAgentSpec(ctx.Context, agentID, rawSpec)
|
spec, err := client.UpdateAgentSpec(ctx.Context, agentID, rawSpec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(apierr.Wrap(err))
|
return errors.WithStack(apierr.Wrap(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
hints := format.Hints{
|
hints := specHints(baseFlags.OutputMode)
|
||||||
OutputMode: baseFlags.OutputMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil {
|
if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
@ -127,14 +132,24 @@ func UpdateCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertSpecName(ctx *cli.Context) (spec.Name, error) {
|
func assertSpecDefName(ctx *cli.Context) (string, error) {
|
||||||
specName := ctx.String("spec-name")
|
specDefName := ctx.String("spec-name")
|
||||||
|
|
||||||
if specName == "" {
|
if specDefName == "" {
|
||||||
return "", errors.New("flag 'spec-name' is required")
|
return "", errors.New("flag 'spec-name' is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec.Name(specName), nil
|
return specDefName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSpecDefVersion(ctx *cli.Context) (string, error) {
|
||||||
|
specDefVersion := ctx.String("spec-version")
|
||||||
|
|
||||||
|
if specDefVersion == "" {
|
||||||
|
return "", errors.New("flag 'spec-name' is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return specDefVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertSpecData(ctx *cli.Context) (map[string]any, error) {
|
func assertSpecData(ctx *cli.Context) (map[string]any, error) {
|
||||||
|
|
|
@ -18,9 +18,9 @@ type AgentRepository interface {
|
||||||
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
|
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
|
||||||
Delete(ctx context.Context, id AgentID) error
|
Delete(ctx context.Context, id AgentID) error
|
||||||
|
|
||||||
UpdateSpec(ctx context.Context, id AgentID, name string, revision int, data map[string]any) (*Spec, 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)
|
GetSpecs(ctx context.Context, id AgentID) ([]*Spec, error)
|
||||||
DeleteSpec(ctx context.Context, id AgentID, name string) error
|
DeleteSpec(ctx context.Context, id AgentID, name string, version string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentQueryOptionFunc func(*AgentQueryOptions)
|
type AgentQueryOptionFunc func(*AgentQueryOptions)
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type specDefRecord struct {
|
||||||
|
Schema []byte
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecDefinitionRepository struct {
|
||||||
|
definitions map[string]map[string]specDefRecord
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Delete(ctx context.Context, name string, version string) error {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
versions, exists := r.definitions[name]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(versions, version)
|
||||||
|
|
||||||
|
r.definitions[name] = versions
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Get(ctx context.Context, name string, version string) (*datastore.SpecDefinition, error) {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
versions, exists := r.definitions[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, exists := versions[version]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDef := datastore.SpecDefinition{
|
||||||
|
SpecDefinitionHeader: datastore.SpecDefinitionHeader{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
CreatedAt: rec.CreatedAt,
|
||||||
|
UpdatedAt: rec.UpdatedAt,
|
||||||
|
},
|
||||||
|
Schema: rec.Schema[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
return &specDef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Query(ctx context.Context, opts ...datastore.SpecDefinitionQueryOptionFunc) ([]datastore.SpecDefinitionHeader, int, error) {
|
||||||
|
options := &datastore.SpecDefinitionQueryOptions{}
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
specDefs := make([]datastore.SpecDefinitionHeader, 0)
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for name, versions := range r.definitions {
|
||||||
|
for version, rec := range versions {
|
||||||
|
count++
|
||||||
|
|
||||||
|
matches := true
|
||||||
|
|
||||||
|
if options.Names != nil && !slices.Contains(options.Names, name) {
|
||||||
|
matches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Versions != nil && !slices.Contains(options.Versions, version) {
|
||||||
|
matches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Offset != nil && count < *options.Offset {
|
||||||
|
matches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Limit != nil && len(specDefs) >= *options.Limit {
|
||||||
|
matches = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefs = append(specDefs, datastore.SpecDefinitionHeader{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
CreatedAt: rec.CreatedAt,
|
||||||
|
UpdatedAt: rec.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return specDefs, count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Upsert(ctx context.Context, name string, version string, schema []byte) (*datastore.SpecDefinition, error) {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
versions, exists := r.definitions[name]
|
||||||
|
if !exists {
|
||||||
|
versions = make(map[string]specDefRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
rec, exists := versions[version]
|
||||||
|
if !exists {
|
||||||
|
rec = specDefRecord{
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
Schema: schema[:],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rec.UpdatedAt = now
|
||||||
|
rec.Schema = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
versions[version] = rec
|
||||||
|
r.definitions[name] = versions
|
||||||
|
|
||||||
|
specDef := datastore.SpecDefinition{
|
||||||
|
SpecDefinitionHeader: datastore.SpecDefinitionHeader{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
CreatedAt: rec.CreatedAt,
|
||||||
|
UpdatedAt: rec.UpdatedAt,
|
||||||
|
},
|
||||||
|
Schema: rec.Schema[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
return &specDef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpecDefinitionRepository() *SpecDefinitionRepository {
|
||||||
|
return &SpecDefinitionRepository{
|
||||||
|
definitions: make(map[string]map[string]specDefRecord),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ datastore.SpecDefinitionRepository = &SpecDefinitionRepository{}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMemorySpecDefinitionRepository(t *testing.T) {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
repo := NewSpecDefinitionRepository()
|
||||||
|
testsuite.TestSpecDefinitionRepository(t, repo)
|
||||||
|
}
|
|
@ -2,15 +2,14 @@ package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SpecID int64
|
type SpecID int64
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
ID SpecID `json:"id"`
|
ID SpecID `json:"id"`
|
||||||
Name string `json:"name"`
|
DefinitionName string `json:"name"`
|
||||||
|
DefinitionVersion string `json:"version"`
|
||||||
Data map[string]any `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
@ -19,8 +18,12 @@ type Spec struct {
|
||||||
AgentID AgentID `json:"agentId"`
|
AgentID AgentID `json:"agentId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return spec.Name(s.Name)
|
return s.DefinitionName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return s.DefinitionVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpecDefinitionHeader struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecDefinition struct {
|
||||||
|
SpecDefinitionHeader
|
||||||
|
|
||||||
|
Schema []byte `json:"schema"`
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpecDefinitionRepository interface {
|
||||||
|
Upsert(ctx context.Context, name string, version string, schema []byte) (*SpecDefinition, error)
|
||||||
|
Get(ctx context.Context, name string, version string) (*SpecDefinition, error)
|
||||||
|
Delete(ctx context.Context, name string, version string) error
|
||||||
|
|
||||||
|
Query(ctx context.Context, opts ...SpecDefinitionQueryOptionFunc) ([]SpecDefinitionHeader, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecDefinitionQueryOptionFunc func(*SpecDefinitionQueryOptions)
|
||||||
|
|
||||||
|
type SpecDefinitionQueryOptions struct {
|
||||||
|
Limit *int
|
||||||
|
Offset *int
|
||||||
|
Names []string
|
||||||
|
Versions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecDefinitionQueryLimit(limit int) SpecDefinitionQueryOptionFunc {
|
||||||
|
return func(opts *SpecDefinitionQueryOptions) {
|
||||||
|
opts.Limit = &limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecDefinitionQueryOffset(offset int) SpecDefinitionQueryOptionFunc {
|
||||||
|
return func(opts *SpecDefinitionQueryOptions) {
|
||||||
|
opts.Offset = &offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecDefinitionQueryNames(names ...string) SpecDefinitionQueryOptionFunc {
|
||||||
|
return func(opts *SpecDefinitionQueryOptions) {
|
||||||
|
opts.Names = names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSpecDefinitionQueryVersions(versions ...string) SpecDefinitionQueryOptionFunc {
|
||||||
|
return func(opts *SpecDefinitionQueryOptions) {
|
||||||
|
opts.Versions = versions
|
||||||
|
}
|
||||||
|
}
|
|
@ -128,7 +128,7 @@ func (r *AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSpec implements datastore.AgentRepository.
|
// DeleteSpec implements datastore.AgentRepository.
|
||||||
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
|
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string, version string) error {
|
||||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
exists, err := r.agentExists(ctx, tx, agentID)
|
exists, err := r.agentExists(ctx, tx, agentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -139,9 +139,9 @@ func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.Agen
|
||||||
return errors.WithStack(datastore.ErrNotFound)
|
return errors.WithStack(datastore.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
|
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2 AND version = $3`
|
||||||
|
|
||||||
if _, err = tx.ExecContext(ctx, query, agentID, name); err != nil {
|
if _, err = tx.ExecContext(ctx, query, agentID, name, version); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT id, name, revision, data, created_at, updated_at
|
SELECT id, name, version, revision, data, created_at, updated_at, agent_id, tenant_id
|
||||||
FROM specs
|
FROM specs
|
||||||
WHERE agent_id = $1
|
WHERE agent_id = $1
|
||||||
`
|
`
|
||||||
|
@ -191,10 +191,14 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
||||||
|
|
||||||
data := JSONMap{}
|
data := JSONMap{}
|
||||||
|
|
||||||
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
|
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 {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tenantID.Valid {
|
||||||
|
spec.TenantID = datastore.TenantID(tenantID.String)
|
||||||
|
}
|
||||||
spec.Data = data
|
spec.Data = data
|
||||||
|
|
||||||
specs = append(specs, spec)
|
specs = append(specs, spec)
|
||||||
|
@ -214,7 +218,7 @@ func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentI
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSpec implements datastore.AgentRepository.
|
// UpdateSpec implements datastore.AgentRepository.
|
||||||
func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.AgentID, name string, revision int, data map[string]any) (*datastore.Spec, error) {
|
func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.AgentID, name string, version string, revision int, data map[string]any) (*datastore.Spec, error) {
|
||||||
spec := &datastore.Spec{}
|
spec := &datastore.Spec{}
|
||||||
|
|
||||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
@ -230,23 +234,24 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO specs (agent_id, name, revision, data, created_at, updated_at, tenant_id)
|
INSERT INTO specs (agent_id, name, version, revision, data, created_at, updated_at, tenant_id)
|
||||||
VALUES($1, $2, $3, $4, $5, $5, ( SELECT tenant_id FROM agents WHERE id = $1 ))
|
VALUES($1, $2, $3, $4, $5, $6, $6, ( SELECT tenant_id FROM agents WHERE id = $1 ))
|
||||||
ON CONFLICT (agent_id, name) DO UPDATE SET
|
ON CONFLICT (agent_id, name, version) DO UPDATE SET
|
||||||
data = $4, updated_at = $5, revision = specs.revision + 1
|
data = $5, updated_at = $6, revision = specs.revision + 1, tenant_id = ( SELECT tenant_id FROM agents WHERE id = $1 )
|
||||||
WHERE revision = $3
|
WHERE revision = $4
|
||||||
RETURNING "id", "name", "revision", "data", "created_at", "updated_at"
|
RETURNING "id", "name", "version", "revision", "data", "created_at", "updated_at", "tenant_id", "agent_id"
|
||||||
`
|
`
|
||||||
|
|
||||||
args := []any{agentID, name, revision, JSONMap(data), now}
|
args := []any{agentID, name, version, revision, JSONMap(data), now}
|
||||||
|
|
||||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||||
|
|
||||||
row := tx.QueryRowContext(ctx, query, args...)
|
row := tx.QueryRowContext(ctx, query, args...)
|
||||||
|
|
||||||
data := JSONMap{}
|
data := JSONMap{}
|
||||||
|
var tenantID sql.NullString
|
||||||
|
|
||||||
err = row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
|
err = row.Scan(&spec.ID, &spec.DefinitionName, &spec.DefinitionVersion, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt, &tenantID, &spec.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return errors.WithStack(datastore.ErrUnexpectedRevision)
|
return errors.WithStack(datastore.ErrUnexpectedRevision)
|
||||||
|
@ -255,6 +260,10 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tenantID.Valid {
|
||||||
|
spec.TenantID = datastore.TenantID(tenantID.String)
|
||||||
|
}
|
||||||
|
|
||||||
spec.Data = data
|
spec.Data = data
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -301,6 +310,10 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.TenantIDs != nil && len(options.TenantIDs) > 0 {
|
if options.TenantIDs != nil && len(options.TenantIDs) > 0 {
|
||||||
|
if filters != "" {
|
||||||
|
filters += " AND "
|
||||||
|
}
|
||||||
|
|
||||||
filter, newArgs, newParamIndex := inFilter("tenant_id", paramIndex, options.TenantIDs)
|
filter, newArgs, newParamIndex := inFilter("tenant_id", paramIndex, options.TenantIDs)
|
||||||
filters += filter
|
filters += filter
|
||||||
paramIndex = newParamIndex
|
paramIndex = newParamIndex
|
||||||
|
|
|
@ -7,6 +7,42 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type JSON struct {
|
||||||
|
value any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JSON) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
switch typ := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
data = typ
|
||||||
|
case string:
|
||||||
|
data = []byte(typ)
|
||||||
|
default:
|
||||||
|
return errors.Errorf("unexpected type '%T'", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &j.value); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JSON) Value() (driver.Value, error) {
|
||||||
|
data, err := json.Marshal(j.value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
type JSONMap map[string]any
|
type JSONMap map[string]any
|
||||||
|
|
||||||
func (j *JSONMap) Scan(value interface{}) error {
|
func (j *JSONMap) Scan(value interface{}) error {
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpecDefinitionRepository struct {
|
||||||
|
repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Delete(ctx context.Context, name string, version string) error {
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
if exists, err := r.specDefinitionExists(ctx, tx, name, version); !exists {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `DELETE FROM spec_definitions WHERE name = $1 AND version = $2`
|
||||||
|
_, err := tx.ExecContext(ctx, query, name, version)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Get(ctx context.Context, name string, version string) (*datastore.SpecDefinition, error) {
|
||||||
|
var specDef datastore.SpecDefinition
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
query := `
|
||||||
|
SELECT "name", "version", "schema", "created_at", "updated_at"
|
||||||
|
FROM spec_definitions
|
||||||
|
WHERE name = $1 AND version = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, query, name, version)
|
||||||
|
|
||||||
|
if err := row.Scan(&specDef.Name, &specDef.Version, &specDef.Schema, &specDef.CreatedAt, &specDef.UpdatedAt); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &specDef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Query(ctx context.Context, opts ...datastore.SpecDefinitionQueryOptionFunc) ([]datastore.SpecDefinitionHeader, int, error) {
|
||||||
|
options := &datastore.SpecDefinitionQueryOptions{}
|
||||||
|
for _, fn := range opts {
|
||||||
|
fn(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefs := make([]datastore.SpecDefinitionHeader, 0)
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
query := `SELECT name, version, created_at, updated_at FROM spec_definitions`
|
||||||
|
|
||||||
|
limit := 10
|
||||||
|
if options.Limit != nil {
|
||||||
|
limit = *options.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
if options.Offset != nil {
|
||||||
|
offset = *options.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := ""
|
||||||
|
paramIndex := 3
|
||||||
|
args := []any{offset, limit}
|
||||||
|
|
||||||
|
if options.Names != nil && len(options.Names) > 0 {
|
||||||
|
filter, newArgs, newParamIndex := inFilter("name", paramIndex, options.Names)
|
||||||
|
filters += filter
|
||||||
|
paramIndex = newParamIndex
|
||||||
|
args = append(args, newArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Versions != nil && len(options.Versions) > 0 {
|
||||||
|
if filters != "" {
|
||||||
|
filters += " AND "
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, newArgs, _ := inFilter("version", paramIndex, options.Versions)
|
||||||
|
filters += filter
|
||||||
|
args = append(args, newArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters != "" {
|
||||||
|
filters = ` WHERE ` + filters
|
||||||
|
}
|
||||||
|
|
||||||
|
query += filters + ` LIMIT $2 OFFSET $1`
|
||||||
|
|
||||||
|
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
logger.Error(ctx, "could not close rows", logger.CapturedE(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
sdh := datastore.SpecDefinitionHeader{}
|
||||||
|
|
||||||
|
if err := rows.Scan(&sdh.Name, &sdh.Version, &sdh.CreatedAt, &sdh.UpdatedAt); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefs = append(specDefs, sdh)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, `SELECT count(*) FROM spec_definitions `+filters, args...)
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return specDefs, count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert implements datastore.SpecDefinitionRepository.
|
||||||
|
func (r *SpecDefinitionRepository) Upsert(ctx context.Context, name string, version string, schema []byte) (*datastore.SpecDefinition, error) {
|
||||||
|
var specDef datastore.SpecDefinition
|
||||||
|
|
||||||
|
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT INTO spec_definitions (name, version, schema, created_at, updated_at)
|
||||||
|
VALUES($1, $2, $3, $4, $4)
|
||||||
|
ON CONFLICT(name, version) DO UPDATE SET schema = $3, updated_at = $4
|
||||||
|
RETURNING "name", "version", "schema", "created_at", "updated_at"
|
||||||
|
`
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(
|
||||||
|
ctx, query,
|
||||||
|
name, version, schema, now, now,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := row.Scan(&specDef.Name, &specDef.Version, &specDef.Schema, &specDef.CreatedAt, &specDef.UpdatedAt); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &specDef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SpecDefinitionRepository) specDefinitionExists(ctx context.Context, tx *sql.Tx, name string, version string) (bool, error) {
|
||||||
|
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM spec_definitions WHERE name = $1 AND version = $2`, name, version)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return false, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return false, errors.WithStack(datastore.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpecDefinitionRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *SpecDefinitionRepository {
|
||||||
|
return &SpecDefinitionRepository{
|
||||||
|
repository: repository{db, sqliteBusyRetryMaxAttempts},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ datastore.SpecDefinitionRepository = &SpecDefinitionRepository{}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSQLiteSpecDefinitionRepository(t *testing.T) {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
|
||||||
|
file := "testdata/spec_definition_repository_test.sqlite"
|
||||||
|
|
||||||
|
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||||
|
|
||||||
|
migr, err := migrate.New("../../../migrations", "sqlite", "sqlite://"+dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migr.Up(); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := NewSpecDefinitionRepository(db, 5)
|
||||||
|
|
||||||
|
testsuite.TestSpecDefinitionRepository(t, repo)
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ import (
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSQLiteTeantRepository(t *testing.T) {
|
func TestSQLiteTenantRepository(t *testing.T) {
|
||||||
logger.SetLevel(logger.LevelDebug)
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
|
||||||
file := "testdata/tenant_repository_test.sqlite"
|
file := "testdata/tenant_repository_test.sqlite"
|
||||||
|
|
|
@ -50,7 +50,7 @@ var agentRepositoryTestCases = []agentRepositoryTestCase{
|
||||||
var unexistantAgentID datastore.AgentID = 9999
|
var unexistantAgentID datastore.AgentID = 9999
|
||||||
var specData map[string]any
|
var specData map[string]any
|
||||||
|
|
||||||
agent, err := repo.UpdateSpec(ctx, unexistantAgentID, string(spec.Name), 0, specData)
|
agent, err := repo.UpdateSpec(ctx, unexistantAgentID, spec.Name, spec.Version, 0, specData)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return errors.New("error should not be nil")
|
return errors.New("error should not be nil")
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ var agentRepositoryTestCases = []agentRepositoryTestCase{
|
||||||
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
|
||||||
var unexistantAgentID datastore.AgentID = 9999
|
var unexistantAgentID datastore.AgentID = 9999
|
||||||
|
|
||||||
err := repo.DeleteSpec(ctx, unexistantAgentID, string(spec.Name))
|
err := repo.DeleteSpec(ctx, unexistantAgentID, spec.Name, spec.Version)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return errors.New("error should not be nil")
|
return errors.New("error should not be nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpecDefinitionRepository(t *testing.T, repo datastore.SpecDefinitionRepository) {
|
||||||
|
t.Run("Cases", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
runSpecDefinitionRepositoryTests(t, repo)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package testsuite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type specDefinitionRepositoryTestCase struct {
|
||||||
|
Name string
|
||||||
|
Skip bool
|
||||||
|
Run func(ctx context.Context, repo datastore.SpecDefinitionRepository) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var specDefinitionRepositoryTestCases = []specDefinitionRepositoryTestCase{
|
||||||
|
{
|
||||||
|
Name: "Create a spec definition",
|
||||||
|
Run: func(ctx context.Context, repo datastore.SpecDefinitionRepository) error {
|
||||||
|
schema := []byte("{}")
|
||||||
|
name := "net.example.foo"
|
||||||
|
version := "0.0.0"
|
||||||
|
|
||||||
|
specDef, err := repo.Upsert(ctx, name, version, schema)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if specDef.CreatedAt.IsZero() {
|
||||||
|
return errors.Errorf("specDef.CreatedAt should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if specDef.UpdatedAt.IsZero() {
|
||||||
|
return errors.Errorf("specDef.UpdatedAt should not be zero time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := name, specDef.Name; e != g {
|
||||||
|
return errors.Errorf("specDef.Name: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := version, specDef.Version; e != g {
|
||||||
|
return errors.Errorf("specDef.Name: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := schema, specDef.Schema; !reflect.DeepEqual(e, g) {
|
||||||
|
return errors.Errorf("specDef.Schema: expected '%v', got '%v'", e, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSpecDefinitionRepositoryTests(t *testing.T, repo datastore.SpecDefinitionRepository) {
|
||||||
|
for _, tc := range specDefinitionRepositoryTestCases {
|
||||||
|
func(tc specDefinitionRepositoryTestCase) {
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if tc.Skip {
|
||||||
|
t.SkipNow()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := tc.Run(ctx, repo); err != nil {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(tc)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,42 +9,12 @@ import (
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"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 {
|
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)
|
agentID, ok := getAgentID(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
@ -61,6 +31,7 @@ func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx,
|
ctx,
|
||||||
agentID,
|
agentID,
|
||||||
deleteSpecReq.Name,
|
deleteSpecReq.Name,
|
||||||
|
deleteSpecReq.Version,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, datastore.ErrNotFound) {
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
|
@ -79,7 +50,9 @@ func (m *Mount) deleteSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, struct {
|
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,
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
return datastore.AgentID(agentID), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpecID(w http.ResponseWriter, r *http.Request) (datastore.SpecID, bool) {
|
func getSpecDefinitionNameAndVersion(w http.ResponseWriter, r *http.Request) (string, string, bool) {
|
||||||
rawSpecID := chi.URLParam(r, "specID")
|
specDefName := chi.URLParam(r, "specDefName")
|
||||||
|
specDefVersion := chi.URLParam(r, "specDefVersion")
|
||||||
|
|
||||||
specID, err := strconv.ParseInt(rawSpecID, 10, 64)
|
return specDefName, specDefVersion, true
|
||||||
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 getTenantID(w http.ResponseWriter, r *http.Request) (datastore.TenantID, bool) {
|
func getTenantID(w http.ResponseWriter, r *http.Request) (datastore.TenantID, bool) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
type Mount struct {
|
type Mount struct {
|
||||||
agentRepo datastore.AgentRepository
|
agentRepo datastore.AgentRepository
|
||||||
tenantRepo datastore.TenantRepository
|
tenantRepo datastore.TenantRepository
|
||||||
|
specDefRepo datastore.SpecDefinitionRepository
|
||||||
authenticators []auth.Authenticator
|
authenticators []auth.Authenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +36,8 @@ func (m *Mount) Mount(r chi.Router) {
|
||||||
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}", m.deleteAgent)
|
||||||
|
|
||||||
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
r.With(assertAgentOrUserWithReadAccess).Get("/{agentID}/specs", m.getAgentSpecs)
|
||||||
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateSpec)
|
r.With(assertUserWithWriteAccess).Post("/{agentID}/specs", m.updateAgentSpec)
|
||||||
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteSpec)
|
r.With(assertUserWithWriteAccess).Delete("/{agentID}/specs", m.deleteAgentSpec)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/tenants", func(r chi.Router) {
|
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(assertAdminOrTenantWriteAccess).Put("/{tenantID}", m.updateTenant)
|
||||||
r.With(assertAdminAccess).Delete("/{tenantID}", m.deleteTenant)
|
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)
|
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, authenticators ...auth.Authenticator) *Mount {
|
func NewMount(agentRepo datastore.AgentRepository, tenantRepo datastore.TenantRepository, specDefRepo datastore.SpecDefinitionRepository, authenticators ...auth.Authenticator) *Mount {
|
||||||
return &Mount{agentRepo, tenantRepo, authenticators}
|
return &Mount{agentRepo, tenantRepo, specDefRepo, authenticators}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
ErrCodeUnexpectedRevision api.ErrorCode = "unexpected-revision"
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateSpecRequest struct {
|
type updateAgentSpecRequest struct {
|
||||||
spec.RawSpec
|
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)
|
agentID, ok := getAgentID(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
@ -26,12 +26,18 @@ func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
updateSpecReq := &updateSpecRequest{}
|
updateSpecReq := &updateAgentSpecRequest{}
|
||||||
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
if ok := api.Bind(w, r, updateSpecReq); !ok {
|
||||||
return
|
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 {
|
data := struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}{}
|
}{}
|
||||||
|
@ -53,7 +59,8 @@ func (m *Mount) updateSpec(w http.ResponseWriter, r *http.Request) {
|
||||||
spec, err := m.agentRepo.UpdateSpec(
|
spec, err := m.agentRepo.UpdateSpec(
|
||||||
ctx,
|
ctx,
|
||||||
datastore.AgentID(agentID),
|
datastore.AgentID(agentID),
|
||||||
string(updateSpecReq.SpecName()),
|
updateSpecReq.SpecDefinitionName(),
|
||||||
|
updateSpecReq.SpecDefinitionVersion(),
|
||||||
updateSpecReq.SpecRevision(),
|
updateSpecReq.SpecRevision(),
|
||||||
updateSpecReq.SpecData(),
|
updateSpecReq.SpecData(),
|
||||||
)
|
)
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/setup"
|
"forge.cadoles.com/Cadoles/emissary/internal/setup"
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) initRepositories(ctx context.Context) error {
|
func (s *Server) initRepositories(ctx context.Context) error {
|
||||||
|
@ -18,8 +20,35 @@ func (s *Server) initRepositories(ctx context.Context) error {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
specDefRepo, err := setup.NewSpecDefinitionRepository(ctx, s.conf.Database)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
s.agentRepo = agentRepo
|
s.agentRepo = agentRepo
|
||||||
s.tenantRepo = tenantRepo
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ type Server struct {
|
||||||
conf config.ServerConfig
|
conf config.ServerConfig
|
||||||
agentRepo datastore.AgentRepository
|
agentRepo datastore.AgentRepository
|
||||||
tenantRepo datastore.TenantRepository
|
tenantRepo datastore.TenantRepository
|
||||||
|
specDefRepo datastore.SpecDefinitionRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
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
|
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))
|
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.conf.HTTP.Host, s.conf.HTTP.Port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- errors.WithStack(err)
|
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(
|
apiMount := api.NewMount(
|
||||||
s.agentRepo,
|
s.agentRepo,
|
||||||
s.tenantRepo,
|
s.tenantRepo,
|
||||||
|
s.specDefRepo,
|
||||||
userAuth,
|
userAuth,
|
||||||
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
|
||||||
)
|
)
|
||||||
|
|
|
@ -104,3 +104,40 @@ func NewTenantRepository(ctx context.Context, conf config.DatabaseConfig) (datas
|
||||||
|
|
||||||
return tenantRepository, nil
|
return tenantRepository, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSpecDefinitionRepository(ctx context.Context, conf config.DatabaseConfig) (datastore.SpecDefinitionRepository, error) {
|
||||||
|
driver := string(conf.Driver)
|
||||||
|
dsn := string(conf.DSN)
|
||||||
|
|
||||||
|
var specDefRepository datastore.SpecDefinitionRepository
|
||||||
|
|
||||||
|
logger.Debug(ctx, "initializing spec definition repository", logger.F("driver", driver), logger.F("dsn", dsn))
|
||||||
|
|
||||||
|
switch driver {
|
||||||
|
case config.DatabaseDriverPostgres:
|
||||||
|
// TODO
|
||||||
|
// pool, err := openPostgresPool(ctx, dsn)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, errors.WithStack(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// entryRepository = postgres.NewEntryRepository(pool)
|
||||||
|
case config.DatabaseDriverSQLite:
|
||||||
|
url, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open(driver, url.Host+url.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefRepository = sqlite.NewSpecDefinitionRepository(db, 5)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unsupported database driver '%s'", driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
return specDefRepository, nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Equals(a Spec, b Spec) (bool, error) {
|
func Equals(a Spec, b Spec) (bool, error) {
|
||||||
if a.SpecName() != b.SpecName() {
|
if a.SpecDefinitionName() != b.SpecDefinitionName() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.SpecDefinitionVersion() != b.SpecDefinitionVersion() {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
package spec
|
|
||||||
|
|
||||||
type Name string
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
var schema []byte
|
var schema []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := spec.Register(NameProxy, schema); err != nil {
|
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
"$id": "https://proxy.emissary.cadoles.com/spec.json",
|
"$id": "https://proxy.emissary.cadoles.com/spec.json",
|
||||||
"title": "ProxySpec",
|
"title": "ProxySpec",
|
||||||
"description": "Emissary 'Proxy' specification",
|
"description": "Emissary 'Proxy' specification",
|
||||||
|
@ -26,16 +26,24 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["hostPattern", "target"]
|
"required": [
|
||||||
|
"hostPattern",
|
||||||
|
"target"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["address", "mappings"],
|
"required": [
|
||||||
|
"address",
|
||||||
|
"mappings"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["proxies"],
|
"required": [
|
||||||
|
"proxies"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
|
@ -2,7 +2,10 @@ package proxy
|
||||||
|
|
||||||
import "forge.cadoles.com/Cadoles/emissary/internal/spec"
|
import "forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
|
|
||||||
const NameProxy spec.Name = "proxy.emissary.cadoles.com"
|
const (
|
||||||
|
Name string = "proxy.emissary.cadoles.com"
|
||||||
|
Version = "0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
type ID string
|
type ID string
|
||||||
|
|
||||||
|
@ -21,8 +24,12 @@ type ProxyMapping struct {
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return NameProxy
|
return Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return Version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -37,11 +38,15 @@ var validatorTestCases = []validatorTestCase{
|
||||||
func TestValidator(t *testing.T) {
|
func TestValidator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
validator := spec.NewValidator()
|
ctx := context.Background()
|
||||||
if err := validator.Register(NameProxy, schema); err != nil {
|
|
||||||
|
repo := memory.NewSpecDefinitionRepository()
|
||||||
|
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator := spec.NewValidator(repo)
|
||||||
|
|
||||||
for _, tc := range validatorTestCases {
|
for _, tc := range validatorTestCases {
|
||||||
func(tc validatorTestCase) {
|
func(tc validatorTestCase) {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package spec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/qri-io/jsonschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
definitions map[string]map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Register(name string, version string, schema []byte) error {
|
||||||
|
// Assert that provided schema is valid
|
||||||
|
if err := json.Unmarshal(schema, &jsonschema.Schema{}); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefinitions, exists := r.definitions[name]
|
||||||
|
if !exists {
|
||||||
|
specDefinitions = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
specDefinitions[version] = schema
|
||||||
|
|
||||||
|
r.definitions[name] = specDefinitions
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Walk(fn func(name string, version string, schema []byte) error) error {
|
||||||
|
for name, specDefinitions := range r.definitions {
|
||||||
|
for version, schema := range specDefinitions {
|
||||||
|
if err := fn(name, version, schema); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
definitions: make(map[string]map[string][]byte),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRegistry = NewRegistry()
|
||||||
|
|
||||||
|
func Register(name string, version string, schema []byte) error {
|
||||||
|
if err := defaultRegistry.Register(name, version, schema); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Walk(fn func(name string, version string, schema []byte) error) error {
|
||||||
|
if err := defaultRegistry.Walk(fn); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,19 +1,27 @@
|
||||||
package spec
|
package spec
|
||||||
|
|
||||||
|
const DefaultVersion = "0.0.0"
|
||||||
|
|
||||||
type Spec interface {
|
type Spec interface {
|
||||||
SpecName() Name
|
SpecDefinitionName() string
|
||||||
|
SpecDefinitionVersion() string
|
||||||
SpecRevision() int
|
SpecRevision() int
|
||||||
SpecData() map[string]any
|
SpecData() map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawSpec struct {
|
type RawSpec struct {
|
||||||
Name Name `json:"name"`
|
DefinitionName string `json:"name"`
|
||||||
|
DefinitionVersion string `json:"version"`
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
Data map[string]any `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RawSpec) SpecName() Name {
|
func (s *RawSpec) SpecDefinitionName() string {
|
||||||
return s.Name
|
return s.DefinitionName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RawSpec) SpecDefinitionVersion() string {
|
||||||
|
return s.DefinitionVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RawSpec) SpecRevision() int {
|
func (s *RawSpec) SpecRevision() int {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
var schema []byte
|
var schema []byte
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := spec.Register(NameUCI, schema); err != nil {
|
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
"$id": "https://uci.emissary.cadoles.com/spec.json",
|
"$id": "https://uci.emissary.cadoles.com/spec.json",
|
||||||
"title": "UCISpec",
|
"title": "UCISpec",
|
||||||
"description": "Emissary 'UCI' specification",
|
"description": "Emissary 'UCI' specification",
|
||||||
|
@ -15,7 +15,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["packages"],
|
"required": [
|
||||||
|
"packages"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"postImportCommands": {
|
"postImportCommands": {
|
||||||
|
@ -33,12 +35,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "args"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"args"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["config", "postImportCommands"],
|
"required": [
|
||||||
|
"config",
|
||||||
|
"postImportCommands"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"package": {
|
"package": {
|
||||||
|
@ -54,7 +62,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "configs"],
|
"required": [
|
||||||
|
"name",
|
||||||
|
"configs"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
@ -74,11 +85,15 @@
|
||||||
"$ref": "#/$defs/option"
|
"$ref": "#/$defs/option"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ "type": "null" }
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"option": {
|
"option": {
|
||||||
|
@ -86,7 +101,10 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["list", "option"]
|
"enum": [
|
||||||
|
"list",
|
||||||
|
"option"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -95,7 +113,11 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["type", "name", "value"],
|
"required": [
|
||||||
|
"type",
|
||||||
|
"name",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,10 @@ import (
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
const NameUCI spec.Name = "uci.emissary.cadoles.com"
|
const (
|
||||||
|
Name string = "uci.emissary.cadoles.com"
|
||||||
|
Version string = "0.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
|
@ -18,8 +21,12 @@ type UCIPostImportCommand struct {
|
||||||
Args []string `json:"args"`
|
Args []string `json:"args"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecName() spec.Name {
|
func (s *Spec) SpecDefinitionName() string {
|
||||||
return NameUCI
|
return Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Spec) SpecDefinitionVersion() string {
|
||||||
|
return Version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Spec) SpecRevision() int {
|
func (s *Spec) SpecRevision() int {
|
||||||
|
|
|
@ -3,9 +3,10 @@ package uci
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -32,17 +33,21 @@ var validatorTestCases = []validatorTestCase{
|
||||||
func TestValidator(t *testing.T) {
|
func TestValidator(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
validator := spec.NewValidator()
|
ctx := context.Background()
|
||||||
if err := validator.Register(NameUCI, schema); err != nil {
|
|
||||||
|
repo := memory.NewSpecDefinitionRepository()
|
||||||
|
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validator := spec.NewValidator(repo)
|
||||||
|
|
||||||
for _, tc := range validatorTestCases {
|
for _, tc := range validatorTestCases {
|
||||||
func(tc validatorTestCase) {
|
func(tc validatorTestCase) {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
rawSpec, err := ioutil.ReadFile(tc.Source)
|
rawSpec, err := os.ReadFile(tc.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("+%v", errors.WithStack(err))
|
t.Fatalf("+%v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,31 +4,37 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/qri-io/jsonschema"
|
"github.com/qri-io/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Validator struct {
|
type Validator struct {
|
||||||
schemas map[Name]*jsonschema.Schema
|
repo datastore.SpecDefinitionRepository
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Validator) Register(name Name, rawSchema []byte) error {
|
|
||||||
schema := &jsonschema.Schema{}
|
|
||||||
if err := json.Unmarshal(rawSchema, schema); err != nil {
|
|
||||||
return errors.Wrapf(err, "could not register spec shema '%s'", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.schemas[name] = schema
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) Validate(ctx context.Context, spec Spec) error {
|
func (v *Validator) Validate(ctx context.Context, spec Spec) error {
|
||||||
schema, exists := v.schemas[spec.SpecName()]
|
name := spec.SpecDefinitionName()
|
||||||
if !exists {
|
|
||||||
|
version := spec.SpecDefinitionVersion()
|
||||||
|
if version == "" {
|
||||||
|
version = DefaultVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
specDef, err := v.repo.Get(ctx, name, version)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, datastore.ErrNotFound) {
|
||||||
return errors.WithStack(ErrUnknownSchema)
|
return errors.WithStack(ErrUnknownSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := &jsonschema.Schema{}
|
||||||
|
if err := json.Unmarshal(specDef.Schema, schema); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
state := schema.Validate(ctx, map[string]any(spec.SpecData()))
|
state := schema.Validate(ctx, map[string]any(spec.SpecData()))
|
||||||
if !state.IsValid() {
|
if !state.IsValid() {
|
||||||
return errors.WithStack(&ValidationError{*state.Errs})
|
return errors.WithStack(&ValidationError{*state.Errs})
|
||||||
|
@ -37,26 +43,8 @@ func (v *Validator) Validate(ctx context.Context, spec Spec) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidator() *Validator {
|
func NewValidator(repo datastore.SpecDefinitionRepository) *Validator {
|
||||||
return &Validator{
|
return &Validator{
|
||||||
schemas: make(map[Name]*jsonschema.Schema),
|
repo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultValidator = NewValidator()
|
|
||||||
|
|
||||||
func Register(name Name, rawSchema []byte) error {
|
|
||||||
if err := defaultValidator.Register(name, rawSchema); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Validate(ctx context.Context, spec Spec) error {
|
|
||||||
if err := defaultValidator.Validate(ctx, spec); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE spec_definitions;
|
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE
|
||||||
|
spec_definitions (
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
version TEXT NOT NULL,
|
||||||
|
schema TEXT NOT NULL,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
UNIQUE (name, version)
|
||||||
|
);
|
|
@ -0,0 +1,35 @@
|
||||||
|
ALTER TABLE specs
|
||||||
|
RENAME TO _specs;
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
specs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
agent_id INTEGER,
|
||||||
|
tenant_id TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
revision INTEGER DEFAULT 0,
|
||||||
|
data TEXT,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants (id),
|
||||||
|
FOREIGN KEY (agent_id) REFERENCES agents (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE (agent_id, name) ON CONFLICT REPLACE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
specs
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
agent_id,
|
||||||
|
tenant_id,
|
||||||
|
name,
|
||||||
|
revision,
|
||||||
|
data,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
_specs;
|
||||||
|
|
||||||
|
DROP TABLE _specs;
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- Add unique constraint on name/version to specs
|
||||||
|
ALTER TABLE specs
|
||||||
|
RENAME TO _specs;
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
specs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
agent_id INTEGER,
|
||||||
|
tenant_id TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
version TEXT NOT NULL,
|
||||||
|
revision INTEGER DEFAULT 0,
|
||||||
|
data TEXT,
|
||||||
|
created_at datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants (id),
|
||||||
|
FOREIGN KEY (agent_id) REFERENCES agents (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE (agent_id, name, version) ON CONFLICT REPLACE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
specs
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
agent_id,
|
||||||
|
tenant_id,
|
||||||
|
name,
|
||||||
|
"0.0.0",
|
||||||
|
revision,
|
||||||
|
data,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
_specs;
|
||||||
|
|
||||||
|
DROP TABLE _specs;
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Spec = spec.Spec
|
Spec = spec.Spec
|
||||||
SpecName = spec.Name
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
|
@ -4,30 +4,32 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) DeleteAgentSpec(ctx context.Context, agentID AgentID, name SpecName, funcs ...OptionFunc) (SpecName, error) {
|
func (c *Client) DeleteAgentSpec(ctx context.Context, agentID AgentID, name string, version string, funcs ...OptionFunc) (string, string, error) {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
Name spec.Name `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
}{
|
}{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Version: version,
|
||||||
}
|
}
|
||||||
|
|
||||||
response := withResponse[struct {
|
response := withResponse[struct {
|
||||||
Name spec.Name `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
}]()
|
}]()
|
||||||
|
|
||||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||||
|
|
||||||
if err := c.apiDelete(ctx, path, payload, &response, funcs...); err != nil {
|
if err := c.apiDelete(ctx, path, payload, &response, funcs...); err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Error != nil {
|
if response.Error != nil {
|
||||||
return "", errors.WithStack(response.Error)
|
return "", "", errors.WithStack(response.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.Data.Name, nil
|
return response.Data.Name, response.Data.Version, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetAgentSpecs(ctx context.Context, agentID AgentID, funcs ...OptionFunc) ([]Spec, error) {
|
func (c *Client) GetAgentSpecs(ctx context.Context, agentID AgentID, funcs ...OptionFunc) ([]Spec, error) {
|
||||||
response := withResponse[struct {
|
response := withResponse[struct {
|
||||||
Specs []*spec.RawSpec `json:"specs"`
|
Specs []*datastore.Spec `json:"specs"`
|
||||||
}]()
|
}]()
|
||||||
|
|
||||||
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
path := fmt.Sprintf("/api/v1/agents/%d/specs", agentID)
|
||||||
|
|
|
@ -6,17 +6,18 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) UpdateAgentSpec(ctx context.Context, agentID AgentID, spc Spec, funcs ...OptionFunc) (Spec, error) {
|
func (c *Client) UpdateAgentSpec(ctx context.Context, agentID AgentID, spc Spec, funcs ...OptionFunc) (Spec, error) {
|
||||||
payload := struct {
|
payload := struct {
|
||||||
Name spec.Name `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
Revision int `json:"revision"`
|
Revision int `json:"revision"`
|
||||||
Data metadata.Metadata `json:"data"`
|
Data metadata.Metadata `json:"data"`
|
||||||
}{
|
}{
|
||||||
Name: spc.SpecName(),
|
Name: spc.SpecDefinitionName(),
|
||||||
|
Version: spc.SpecDefinitionVersion(),
|
||||||
Revision: spc.SpecRevision(),
|
Revision: spc.SpecRevision(),
|
||||||
Data: spc.SpecData(),
|
Data: spc.SpecData(),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue