feat(server): default steps seed-on-boot + admin re-sync
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mrhid6/vantage/server/internal/db"
|
||||
"github.com/mrhid6/vantage/server/internal/models"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
// DefaultStepsDir returns the directory holding default step JSON files.
|
||||
func DefaultStepsDir() string {
|
||||
dir := os.Getenv("VANTAGE_DEFAULT_STEPS_DIR")
|
||||
if dir == "" {
|
||||
dir = filepath.Join("data", "default-steps")
|
||||
}
|
||||
_ = os.MkdirAll(dir, 0700)
|
||||
return dir
|
||||
}
|
||||
|
||||
// readDefaultStepFiles parses every *.json in the defaults dir into
|
||||
// source=default library steps (with slug set). Non-json and invalid files are
|
||||
// skipped silently; a slug is derived from the step name.
|
||||
func readDefaultStepFiles() ([]models.WorkflowStep, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(DefaultStepsDir(), "*.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []models.WorkflowStep{}
|
||||
for _, path := range matches {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s, err := ParseStepDoc(b)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.Source = "default"
|
||||
s.Slug = Slugify(s.Name)
|
||||
if s.Slug == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SeedDefaultSteps upserts default steps from disk keyed on {slug, source}.
|
||||
// Re-sync overwrites default-step content; user steps are never touched.
|
||||
func SeedDefaultSteps() (created, updated int, err error) {
|
||||
steps, err := readDefaultStepFiles()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
col := db.Col("workflow_steps")
|
||||
for _, s := range steps {
|
||||
filter := bson.M{"slug": s.Slug, "source": "default"}
|
||||
set := bson.M{
|
||||
"name": s.Name,
|
||||
"description": s.Description,
|
||||
"interpreter": s.Interpreter,
|
||||
"script": s.Script,
|
||||
"declared_outputs": s.DeclaredOutputs,
|
||||
"declared_inputs": s.DeclaredInputs,
|
||||
"secret_refs": s.SecretRefs,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
res, uerr := col.UpdateOne(ctx, filter, bson.M{
|
||||
"$set": set,
|
||||
"$setOnInsert": bson.M{
|
||||
"step_id": uuid.New().String(),
|
||||
"slug": s.Slug,
|
||||
"source": "default",
|
||||
"created_at": time.Now(),
|
||||
},
|
||||
}, options.UpdateOne().SetUpsert(true))
|
||||
if uerr != nil {
|
||||
return created, updated, uerr
|
||||
}
|
||||
if res.UpsertedCount > 0 {
|
||||
created++
|
||||
} else if res.ModifiedCount > 0 {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
return created, updated, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultStepsDirEnv(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "ds")
|
||||
t.Setenv("VANTAGE_DEFAULT_STEPS_DIR", dir)
|
||||
got := DefaultStepsDir()
|
||||
if got != dir {
|
||||
t.Fatalf("got %q want %q", got, dir)
|
||||
}
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
t.Fatalf("dir not created: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDefaultStepFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("VANTAGE_DEFAULT_STEPS_DIR", dir)
|
||||
good := `{"kind":"vantage.step/v1","name":"Ping Host","interpreter":"bash","script":"ping -c1 x=1 >> $WORKFLOW_ENV"}`
|
||||
os.WriteFile(filepath.Join(dir, "ping.json"), []byte(good), 0600)
|
||||
os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("ignore me"), 0600)
|
||||
|
||||
steps, err := readDefaultStepFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(steps) != 1 {
|
||||
t.Fatalf("want 1 step, got %d", len(steps))
|
||||
}
|
||||
if steps[0].Slug != "ping-host" || steps[0].Source != "default" {
|
||||
t.Fatalf("bad seed step: %+v", steps[0])
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,13 @@ func EnsureWorkflowIndexes() error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Col("workflow_steps").Indexes().CreateOne(ctx, mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "slug", Value: 1}},
|
||||
Options: options.Index().SetUnique(true).
|
||||
SetPartialFilterExpression(bson.M{"source": "default"}),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Col("workflows").Indexes().CreateOne(ctx, mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "workflow_id", Value: 1}}, Options: options.Index().SetUnique(true),
|
||||
}); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user