From 832f231bd66c1225a4e9c88010ac6e0eef669630 Mon Sep 17 00:00:00 2001 From: Nofre Date: Thu, 15 May 2025 11:04:02 +0200 Subject: [PATCH 01/13] chore(cmd): small update in apply cmd, removed cobra useless outputs --- Taskfile.yml | 8 +++++++- cmd/apply.go | 19 ++++++++++--------- cmd/version.go | 6 ++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index d42e7dc..ae6f44e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,7 +2,7 @@ version: '3' vars: BINARY_NAME: qube - BUILD_DIR: ./bin + BUILD_DIR: C:/Users/admin/go/bin MAIN: . VERSION: sh: git describe --tags --abbrev=0 2>/dev/null || echo "dev" @@ -13,6 +13,12 @@ tasks: - task: build build: + desc: 🔧 Build Qube CLI + cmds: + - echo "🔧 Building {{.BINARY_NAME}} version {{.VERSION}}" + - go build -ldflags="-X github.com/apiqube/cli/cmd.version={{.VERSION}}" -o={{.BUILD_DIR}}/{{.BINARY_NAME}}.exe {{.MAIN}} + + build-versioned: desc: 🔧 Build Qube CLI cmds: - echo "🔧 Building {{.BINARY_NAME}} version {{.VERSION}}" diff --git a/cmd/apply.go b/cmd/apply.go index 038fb61..f5d3138 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -1,13 +1,12 @@ package cmd import ( - "slices" - "time" - "github.com/apiqube/cli/internal/manifests/depends" "github.com/apiqube/cli/internal/ui" "github.com/apiqube/cli/internal/yaml" "github.com/spf13/cobra" + "slices" + "time" ) func init() { @@ -16,14 +15,12 @@ func init() { } var applyCmd = &cobra.Command{ - Use: "apply", - Short: "Apply resources from manifest file", + Use: "apply", + Short: "Apply resources from manifest file", + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { ui.Init() - defer func() { - time.Sleep(time.Millisecond * 100) - ui.Stop() - }() file, err := cmd.Flags().GetString("file") if err != nil { @@ -69,4 +66,8 @@ var applyCmd = &cobra.Command{ return nil }, + PostRun: func(cmd *cobra.Command, args []string) { + time.Sleep(time.Millisecond * 500) + ui.Stop() + }, } diff --git a/cmd/version.go b/cmd/version.go index 5685857..e7a1c0b 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -9,8 +9,10 @@ import ( var version = "dev" var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number", + Use: "version", + Short: "Print the version number", + SilenceUsage: true, + SilenceErrors: true, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Qube CLI Version: ", version) }, From 12ee9cd4b882c721c4798b8e9c8b9e059f31200e Mon Sep 17 00:00:00 2001 From: Nofre Date: Thu, 15 May 2025 12:29:57 +0200 Subject: [PATCH 02/13] feat(manifest): added ability to multi manifest reading from one yaml --- cmd/apply.go | 4 +- examples/simple/execution-plan.json | 8 --- go.mod | 1 + go.sum | 2 + internal/manifests/depends/plan.go | 24 ++++++--- internal/manifests/interface.go | 5 ++ internal/yaml/loader.go | 24 ++++++--- internal/yaml/parse.go | 80 ++++++++++++++++------------- internal/yaml/saver.go | 27 ++++++---- 9 files changed, 107 insertions(+), 68 deletions(-) delete mode 100644 examples/simple/execution-plan.json diff --git a/cmd/apply.go b/cmd/apply.go index f5d3138..2d8e35d 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -47,7 +47,7 @@ var applyCmd = &cobra.Command{ ui.Spinner(true, "Generating execution plan") - if err = depends.GeneratePlan("./examples/simple", mans); err != nil { + if err = depends.GeneratePlan(mans); err != nil { ui.Errorf("Failed to generate plan: %s", err.Error()) return err } @@ -56,7 +56,7 @@ var applyCmd = &cobra.Command{ ui.Print("Execution plan generated successfully") ui.Spinner(true, "Saving manifests...") - if err := yaml.SaveManifests(file, mans...); err != nil { + if err := yaml.SaveManifestsAsCombined(mans...); err != nil { ui.Error("Failed to save manifests: " + err.Error()) return err } diff --git a/examples/simple/execution-plan.json b/examples/simple/execution-plan.json deleted file mode 100644 index f9f0800..0000000 --- a/examples/simple/execution-plan.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "order": [ - "default.Server.simple-server", - "default.Service.simple-service", - "default.HttpTest.simple-http-test", - "default.HttpLoadTest.simple-http-load-test" - ] -} \ No newline at end of file diff --git a/go.mod b/go.mod index c6e8442..37fc68b 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( + github.com/adrg/xdg v0.5.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect diff --git a/go.sum b/go.sum index 8f7db3f..be7de85 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/internal/manifests/depends/plan.go b/internal/manifests/depends/plan.go index 05283c1..bb67102 100644 --- a/internal/manifests/depends/plan.go +++ b/internal/manifests/depends/plan.go @@ -3,6 +3,7 @@ package depends import ( "encoding/json" "fmt" + "github.com/adrg/xdg" "os" "path/filepath" @@ -13,7 +14,7 @@ type ExecutionPlan struct { Order []string `json:"order"` } -func GeneratePlan(outPath string, manifests []manifests.Manifest) error { +func GeneratePlan(manifests []manifests.Manifest) error { graph, _, err := BuildDependencyGraph(manifests) if err != nil { return err @@ -24,10 +25,14 @@ func GeneratePlan(outPath string, manifests []manifests.Manifest) error { return err } - return SaveExecutionPlan(outPath, order) + return SaveExecutionPlan(manifests[0].GetNamespace(), order) } -func SaveExecutionPlan(path string, order []string) error { +func SaveExecutionPlan(namespace string, order []string) error { + if namespace == "" { + namespace = "default" + } + plan := ExecutionPlan{Order: order} data, err := json.MarshalIndent(plan, "", " ") @@ -35,9 +40,16 @@ func SaveExecutionPlan(path string, order []string) error { return fmt.Errorf("failed to marshal plan: %w", err) } - if err = os.MkdirAll(path, 0o755); err != nil { - return fmt.Errorf("failed to create dir: %w", err) + fileName := fmt.Sprintf("/plan-%s.json", namespace) + + filePath, err := xdg.DataFile(manifests.ExecutionPlansDirPath + fileName) + if err != nil { + return fmt.Errorf("failed to build path: %w", err) + } + + if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return err } - return os.WriteFile(filepath.Join(path, "execution-plan.json"), data, 0o644) + return os.WriteFile(filePath, data, 0o644) } diff --git a/internal/manifests/interface.go b/internal/manifests/interface.go index ef01d3a..7dce67c 100644 --- a/internal/manifests/interface.go +++ b/internal/manifests/interface.go @@ -1,5 +1,10 @@ package manifests +const ( + CombinedManifestsDirPath = "qube/manifests/combined" + ExecutionPlansDirPath = "qube/plans/" +) + const ( DefaultNamespace = "default" diff --git a/internal/yaml/loader.go b/internal/yaml/loader.go index 8dc7edd..c98d485 100644 --- a/internal/yaml/loader.go +++ b/internal/yaml/loader.go @@ -2,6 +2,7 @@ package yaml import ( "fmt" + "github.com/apiqube/cli/internal/ui" "os" "path/filepath" "strings" @@ -10,18 +11,19 @@ import ( ) func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { - var mans []manifests.Manifest - files, err := os.ReadDir(dir) if err != nil { return nil, err } - var manifest manifests.Manifest + var manifestsSet = make(map[string]struct{}) + var parsedManifests []manifests.Manifest + var result []manifests.Manifest + var content []byte for _, file := range files { - if file.IsDir() || file.Name() == "combined.yaml" || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { + if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { continue } @@ -30,13 +32,21 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { return nil, err } - manifest, err = ParseManifest(content) + parsedManifests, err = ParseManifests(content) if err != nil { return nil, fmt.Errorf("in file %s: %w", file.Name(), err) } - mans = append(mans, manifest) + for _, m := range parsedManifests { + if _, ok := manifestsSet[m.GetID()]; ok { + ui.Errorf("Duplicate manifest: %s", m.GetID()) + continue + } + + manifestsSet[m.GetID()] = struct{}{} + result = append(result, m) + } } - return mans, nil + return result, nil } diff --git a/internal/yaml/parse.go b/internal/yaml/parse.go index deef425..eb0ba2e 100644 --- a/internal/yaml/parse.go +++ b/internal/yaml/parse.go @@ -1,13 +1,14 @@ package yaml import ( + "bytes" "fmt" - "github.com/apiqube/cli/internal/manifests" "github.com/apiqube/cli/internal/manifests/kinds/load" "github.com/apiqube/cli/internal/manifests/kinds/server" "github.com/apiqube/cli/internal/manifests/kinds/service" "github.com/apiqube/cli/internal/manifests/kinds/tests" + "github.com/apiqube/cli/internal/ui" "gopkg.in/yaml.v3" ) @@ -15,49 +16,58 @@ type RawManifest struct { Kind string `yaml:"kind"` } -func ParseManifest(data []byte) (manifests.Manifest, error) { - var raw RawManifest - if err := yaml.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("failed to read kind: %w", err) - } - - var manifest manifests.Manifest +func ParseManifests(data []byte) ([]manifests.Manifest, error) { + docs := bytes.Split(data, []byte("\n---")) + var results []manifests.Manifest - switch raw.Kind { - case manifests.ServerManifestKind: - var m server.Server - - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err + for _, doc := range docs { + doc = bytes.TrimSpace(doc) + if len(doc) == 0 { + continue } - manifest = &m - case manifests.ServiceManifestKind: - var m service.Service - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err + var raw RawManifest + if err := yaml.Unmarshal(doc, &raw); err != nil { + return nil, fmt.Errorf("failed to decode raw manifest: %w", err) } - manifest = &m - case manifests.HttpTestManifestKind: - var m tests.Http - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } + var manifest manifests.Manifest - manifest = &m - case manifests.HttpLoadTestManifestKind: - var m load.Http + switch raw.Kind { + case manifests.ServerManifestKind: + var m server.Server + if err := yaml.Unmarshal(doc, m.Default()); err != nil { + return nil, err + } + manifest = &m - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } + case manifests.ServiceManifestKind: + var m service.Service + if err := yaml.Unmarshal(doc, m.Default()); err != nil { + return nil, err + } + manifest = &m + + case manifests.HttpTestManifestKind: + var m tests.Http + if err := yaml.Unmarshal(doc, m.Default()); err != nil { + return nil, err + } + manifest = &m - manifest = &m + case manifests.HttpLoadTestManifestKind: + var m load.Http + if err := yaml.Unmarshal(doc, m.Default()); err != nil { + return nil, err + } + manifest = &m + + default: + ui.Errorf("Unknown manifest kind %s", raw.Kind) + } - default: - return nil, fmt.Errorf("unsupported kind: %s", raw.Kind) + results = append(results, manifest) } - return manifest, nil + return results, nil } diff --git a/internal/yaml/saver.go b/internal/yaml/saver.go index 1026c33..6b94a90 100644 --- a/internal/yaml/saver.go +++ b/internal/yaml/saver.go @@ -3,22 +3,30 @@ package yaml import ( "bytes" "fmt" - "os" - "path/filepath" - + "github.com/adrg/xdg" "github.com/apiqube/cli/internal/manifests" "gopkg.in/yaml.v3" + "os" + "path/filepath" ) -func SaveManifests(dir string, manifests ...manifests.Manifest) error { - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("failed to create dir: %w", err) +func SaveManifestsAsCombined(mans ...manifests.Manifest) error { + fileName := fmt.Sprintf("/combined-%s.yaml", mans[0].GetNamespace()) + + filePath, err := xdg.DataFile(manifests.CombinedManifestsDirPath + fileName) + if err != nil { + panic(err) + } + + if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return err } var buf bytes.Buffer + var data []byte - for i, manifest := range manifests { - data, err := yaml.Marshal(manifest) + for i, manifest := range mans { + data, err = yaml.Marshal(manifest) if err != nil { return fmt.Errorf("failed to marshal manifest %d: %w", i, err) } @@ -30,8 +38,7 @@ func SaveManifests(dir string, manifests ...manifests.Manifest) error { buf.Write(data) } - outputPath := filepath.Join(dir, "combined.yaml") - if err := os.WriteFile(outputPath, buf.Bytes(), 0o644); err != nil { + if err = os.WriteFile(filePath, buf.Bytes(), 0o644); err != nil { return fmt.Errorf("failed to write file: %w", err) } From 221a5cf6a3ded0eb48ea89be9e0f0fe06f4e9972 Mon Sep 17 00:00:00 2001 From: Nofre Date: Thu, 15 May 2025 12:38:57 +0200 Subject: [PATCH 03/13] chore(style): adjustments in errors style --- cmd/apply.go | 13 ++++++------- internal/manifests/depends/plan.go | 4 ++++ internal/ui/styles.go | 9 +++------ internal/yaml/loader.go | 6 +++++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index 2d8e35d..a6ff279 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -19,13 +19,13 @@ var applyCmd = &cobra.Command{ Short: "Apply resources from manifest file", SilenceErrors: true, SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { + Run: func(cmd *cobra.Command, args []string) { ui.Init() file, err := cmd.Flags().GetString("file") if err != nil { ui.Errorf("Failed to parse --file: %s", err.Error()) - return err + return } ui.Printf("Applying manifests from: %s", file) @@ -33,8 +33,9 @@ var applyCmd = &cobra.Command{ mans, err := yaml.LoadManifestsFromDir(file) if err != nil { + ui.Spinner(false) ui.Errorf("Failed to load manifests: %s", err.Error()) - return err + return } ui.Spinner(false) @@ -49,7 +50,7 @@ var applyCmd = &cobra.Command{ if err = depends.GeneratePlan(mans); err != nil { ui.Errorf("Failed to generate plan: %s", err.Error()) - return err + return } ui.Spinner(false) @@ -58,13 +59,11 @@ var applyCmd = &cobra.Command{ if err := yaml.SaveManifestsAsCombined(mans...); err != nil { ui.Error("Failed to save manifests: " + err.Error()) - return err + return } ui.Spinner(false) ui.Println("Manifests applied successfully") - - return nil }, PostRun: func(cmd *cobra.Command, args []string) { time.Sleep(time.Millisecond * 500) diff --git a/internal/manifests/depends/plan.go b/internal/manifests/depends/plan.go index bb67102..db62bba 100644 --- a/internal/manifests/depends/plan.go +++ b/internal/manifests/depends/plan.go @@ -15,6 +15,10 @@ type ExecutionPlan struct { } func GeneratePlan(manifests []manifests.Manifest) error { + if len(manifests) == 0 { + return fmt.Errorf("no manifests to generate plan") + } + graph, _, err := BuildDependencyGraph(manifests) if err != nil { return err diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 6932e8e..f30793b 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -18,7 +18,7 @@ var ( Foreground(lipgloss.Color("#5fd700")) errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ff0000")). + Foreground(lipgloss.Color("#d70000")). Bold(true) warningStyle = lipgloss.NewStyle(). @@ -26,8 +26,7 @@ var ( Bold(true) infoStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00afff")). - Bold(false) + Foreground(lipgloss.Color("#00afff")) snippetStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("230")). @@ -41,12 +40,10 @@ var ( Background(lipgloss.Color("236")) progressTextStyle = lipgloss.NewStyle(). - Bold(true). Foreground(lipgloss.Color("255")) loaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("212")). - Bold(true) + Foreground(lipgloss.Color("212")) spinnerStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#ff0087")) diff --git a/internal/yaml/loader.go b/internal/yaml/loader.go index c98d485..0b70891 100644 --- a/internal/yaml/loader.go +++ b/internal/yaml/loader.go @@ -39,7 +39,7 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { for _, m := range parsedManifests { if _, ok := manifestsSet[m.GetID()]; ok { - ui.Errorf("Duplicate manifest: %s", m.GetID()) + ui.Warningf("Duplicate manifest: %s", m.GetID()) continue } @@ -48,5 +48,9 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { } } + if len(result) == 0 { + return nil, fmt.Errorf("manifests not found in %s", dir) + } + return result, nil } From cee58f9e5647b3a54daf58ac59e583a7337d328a Mon Sep 17 00:00:00 2001 From: Nofre Date: Thu, 15 May 2025 20:40:56 +0200 Subject: [PATCH 04/13] chore(cmd): small adjustments --- cmd/apply.go | 9 --------- examples/simple/http_test_second.yaml | 29 +++++++++++++++++++++++++++ internal/yaml/loader.go | 11 ++++++---- 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 examples/simple/http_test_second.yaml diff --git a/cmd/apply.go b/cmd/apply.go index a6ff279..47af1a7 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -5,7 +5,6 @@ import ( "github.com/apiqube/cli/internal/ui" "github.com/apiqube/cli/internal/yaml" "github.com/spf13/cobra" - "slices" "time" ) @@ -38,16 +37,8 @@ var applyCmd = &cobra.Command{ return } - ui.Spinner(false) ui.Printf("Loaded %d manifests", len(mans)) - slices.Reverse(mans) - for i, man := range mans { - ui.Printf("#%d ID: %s", i+1, man.GetID()) - } - - ui.Spinner(true, "Generating execution plan") - if err = depends.GeneratePlan(mans); err != nil { ui.Errorf("Failed to generate plan: %s", err.Error()) return diff --git a/examples/simple/http_test_second.yaml b/examples/simple/http_test_second.yaml new file mode 100644 index 0000000..baf0c90 --- /dev/null +++ b/examples/simple/http_test_second.yaml @@ -0,0 +1,29 @@ +version: 1 + +kind: HttpTest + +metadata: + name: not-simple-http-test + namespace: not-simple + +spec: + server: simple-server + cases: + - name: user-login + method: GET + endpoint: /login + body: + email: "example_email" + password: "example_password" + expected: + code: 200 + + - name: user-fetch + method: GET + endpoint: /users/{id} + expected: + code: 404 + message: "User not found" + +dependsOn: + - default.Service.simple-service diff --git a/internal/yaml/loader.go b/internal/yaml/loader.go index 0b70891..3b704dc 100644 --- a/internal/yaml/loader.go +++ b/internal/yaml/loader.go @@ -19,6 +19,7 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { var manifestsSet = make(map[string]struct{}) var parsedManifests []manifests.Manifest var result []manifests.Manifest + var counter int var content []byte @@ -39,12 +40,14 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { for _, m := range parsedManifests { if _, ok := manifestsSet[m.GetID()]; ok { - ui.Warningf("Duplicate manifest: %s", m.GetID()) - continue + ui.Infof("Manifest: %s loaded", m.GetID()) + } else { + ui.Infof("Manifest: %s cached", m.GetID()) + manifestsSet[m.GetID()] = struct{}{} + result = append(result, m) } - manifestsSet[m.GetID()] = struct{}{} - result = append(result, m) + counter++ } } From b2aefba1bb263c82072a8ce8d920fb8ed7ef7f90 Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 00:21:45 +0200 Subject: [PATCH 05/13] chore(db): added badger db as main storage, mod tiding, cleaning, linting --- cmd/apply.go | 15 +- go.mod | 18 +- go.sum | 52 ++++- internal/manifest/depends/dependencies.go | 40 ++++ .../{manifests => manifest}/depends/plan.go | 19 +- .../{manifests => manifest}/depends/sort.go | 8 +- internal/{manifests => manifest}/interface.go | 2 +- .../{manifests => manifest}/kinds/base.go | 0 .../kinds/load/http.go | 10 +- .../kinds/server/server.go | 10 +- .../kinds/service/service.go | 10 +- .../kinds/tests/http.go | 10 +- internal/{manifests => manifest}/validate.go | 2 +- internal/manifests/depends/dependencies.go | 40 ---- internal/store/db.go | 180 ++++++++++++++++++ internal/ui/ui.go | 8 + internal/yaml/loader.go | 13 +- internal/yaml/parse.go | 55 +++--- internal/yaml/saver.go | 13 +- 19 files changed, 379 insertions(+), 126 deletions(-) create mode 100644 internal/manifest/depends/dependencies.go rename internal/{manifests => manifest}/depends/plan.go (67%) rename internal/{manifests => manifest}/depends/sort.go (78%) rename internal/{manifests => manifest}/interface.go (96%) rename internal/{manifests => manifest}/kinds/base.go (100%) rename internal/{manifests => manifest}/kinds/load/http.go (87%) rename internal/{manifests => manifest}/kinds/server/server.go (75%) rename internal/{manifests => manifest}/kinds/service/service.go (85%) rename internal/{manifests => manifest}/kinds/tests/http.go (87%) rename internal/{manifests => manifest}/validate.go (91%) delete mode 100644 internal/manifests/depends/dependencies.go create mode 100644 internal/store/db.go diff --git a/cmd/apply.go b/cmd/apply.go index 47af1a7..24d3261 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -1,11 +1,12 @@ package cmd import ( - "github.com/apiqube/cli/internal/manifests/depends" + "time" + + "github.com/apiqube/cli/internal/manifest/depends" "github.com/apiqube/cli/internal/ui" "github.com/apiqube/cli/internal/yaml" "github.com/spf13/cobra" - "time" ) func init() { @@ -20,6 +21,7 @@ var applyCmd = &cobra.Command{ SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { ui.Init() + defer ui.StopWithTimeout(time.Millisecond * 250) file, err := cmd.Flags().GetString("file") if err != nil { @@ -37,12 +39,15 @@ var applyCmd = &cobra.Command{ return } + ui.Spinner(false) ui.Printf("Loaded %d manifests", len(mans)) - if err = depends.GeneratePlan(mans); err != nil { + var order []string + if order, err = depends.GeneratePlan(mans); err != nil { ui.Errorf("Failed to generate plan: %s", err.Error()) return } + _ = order ui.Spinner(false) ui.Print("Execution plan generated successfully") @@ -56,8 +61,4 @@ var applyCmd = &cobra.Command{ ui.Spinner(false) ui.Println("Manifests applied successfully") }, - PostRun: func(cmd *cobra.Command, args []string) { - time.Sleep(time.Millisecond * 500) - ui.Stop() - }, } diff --git a/go.mod b/go.mod index 37fc68b..e73d042 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,30 @@ module github.com/apiqube/cli go 1.24.3 require ( + github.com/adrg/xdg v0.5.3 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/lipgloss v1.1.0 + github.com/dgraph-io/badger/v4 v4.7.0 github.com/spf13/cobra v1.9.1 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/adrg/xdg v0.5.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -29,7 +37,13 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index be7de85..dcc493e 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= @@ -17,10 +19,35 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= +github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -35,27 +62,46 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/manifest/depends/dependencies.go b/internal/manifest/depends/dependencies.go new file mode 100644 index 0000000..41276f7 --- /dev/null +++ b/internal/manifest/depends/dependencies.go @@ -0,0 +1,40 @@ +package depends + +import ( + "fmt" + + "github.com/apiqube/cli/internal/manifest" +) + +type Node struct { + ID string + Manifest manifest.Manifest + Depends []string +} + +func BuildDependencyGraph(mans []manifest.Manifest) (map[string][]string, map[string]manifest.Manifest, error) { + graph := make(map[string][]string) + idToManifest := make(map[string]manifest.Manifest) + + for _, m := range mans { + id := m.GetID() + idToManifest[id] = m + graph[id] = []string{} + } + + for id, m := range idToManifest { + for _, dep := range m.GetDependsOn() { + if dep == id { + return nil, nil, fmt.Errorf("m %s cannot depend on itself", id) + } + + if _, ok := idToManifest[dep]; !ok { + return nil, nil, fmt.Errorf("m %s depends on unknown m %s", id, dep) + } + + graph[id] = append(graph[id], dep) + } + } + + return graph, idToManifest, nil +} diff --git a/internal/manifests/depends/plan.go b/internal/manifest/depends/plan.go similarity index 67% rename from internal/manifests/depends/plan.go rename to internal/manifest/depends/plan.go index db62bba..2116c44 100644 --- a/internal/manifests/depends/plan.go +++ b/internal/manifest/depends/plan.go @@ -3,33 +3,34 @@ package depends import ( "encoding/json" "fmt" - "github.com/adrg/xdg" "os" "path/filepath" - "github.com/apiqube/cli/internal/manifests" + "github.com/adrg/xdg" + + "github.com/apiqube/cli/internal/manifest" ) type ExecutionPlan struct { Order []string `json:"order"` } -func GeneratePlan(manifests []manifests.Manifest) error { +func GeneratePlan(manifests []manifest.Manifest) ([]string, error) { if len(manifests) == 0 { - return fmt.Errorf("no manifests to generate plan") + return nil, fmt.Errorf("no manifests to generate plan") } graph, _, err := BuildDependencyGraph(manifests) if err != nil { - return err + return nil, err } order, err := TopoSort(graph) if err != nil { - return err + return nil, err } - return SaveExecutionPlan(manifests[0].GetNamespace(), order) + return order, nil } func SaveExecutionPlan(namespace string, order []string) error { @@ -46,12 +47,12 @@ func SaveExecutionPlan(namespace string, order []string) error { fileName := fmt.Sprintf("/plan-%s.json", namespace) - filePath, err := xdg.DataFile(manifests.ExecutionPlansDirPath + fileName) + filePath, err := xdg.DataFile(manifest.ExecutionPlansDirPath + fileName) if err != nil { return fmt.Errorf("failed to build path: %w", err) } - if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + if err = os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { return err } diff --git a/internal/manifests/depends/sort.go b/internal/manifest/depends/sort.go similarity index 78% rename from internal/manifests/depends/sort.go rename to internal/manifest/depends/sort.go index 8733062..55cec60 100644 --- a/internal/manifests/depends/sort.go +++ b/internal/manifest/depends/sort.go @@ -3,7 +3,7 @@ package depends import ( "fmt" - "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifest" ) func TopoSort(graph map[string][]string) ([]string, error) { @@ -39,13 +39,13 @@ func TopoSort(graph map[string][]string) ([]string, error) { return result, nil } -func SortManifestsByExecutionOrder(mans []manifests.Manifest, order []string) ([]manifests.Manifest, error) { - idMap := make(map[string]manifests.Manifest) +func SortManifestsByExecutionOrder(mans []manifest.Manifest, order []string) ([]manifest.Manifest, error) { + idMap := make(map[string]manifest.Manifest) for _, m := range mans { idMap[m.GetID()] = m } - sorted := make([]manifests.Manifest, 0, len(order)) + sorted := make([]manifest.Manifest, 0, len(order)) for _, id := range order { m, ok := idMap[id] diff --git a/internal/manifests/interface.go b/internal/manifest/interface.go similarity index 96% rename from internal/manifests/interface.go rename to internal/manifest/interface.go index 7dce67c..02fe2fd 100644 --- a/internal/manifests/interface.go +++ b/internal/manifest/interface.go @@ -1,4 +1,4 @@ -package manifests +package manifest const ( CombinedManifestsDirPath = "qube/manifests/combined" diff --git a/internal/manifests/kinds/base.go b/internal/manifest/kinds/base.go similarity index 100% rename from internal/manifests/kinds/base.go rename to internal/manifest/kinds/base.go diff --git a/internal/manifests/kinds/load/http.go b/internal/manifest/kinds/load/http.go similarity index 87% rename from internal/manifests/kinds/load/http.go rename to internal/manifest/kinds/load/http.go index 3825656..0cb205f 100644 --- a/internal/manifests/kinds/load/http.go +++ b/internal/manifest/kinds/load/http.go @@ -4,13 +4,13 @@ import ( "fmt" "time" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifest/kinds" ) var ( - _ manifests.Manifest = (*Http)(nil) - _ manifests.Defaultable[*Http] = (*Http)(nil) + _ manifest.Manifest = (*Http)(nil) + _ manifest.Defaultable[*Http] = (*Http)(nil) ) type Http struct { @@ -68,7 +68,7 @@ func (h *Http) GetDependsOn() []string { } func (h *Http) Default() *Http { - h.Namespace = manifests.DefaultNamespace + h.Namespace = manifest.DefaultNamespace return h } diff --git a/internal/manifests/kinds/server/server.go b/internal/manifest/kinds/server/server.go similarity index 75% rename from internal/manifests/kinds/server/server.go rename to internal/manifest/kinds/server/server.go index 75da5e7..38ed89a 100644 --- a/internal/manifests/kinds/server/server.go +++ b/internal/manifest/kinds/server/server.go @@ -3,13 +3,13 @@ package server import ( "fmt" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifest/kinds" ) var ( - _ manifests.Manifest = (*Server)(nil) - _ manifests.Defaultable[*Server] = (*Server)(nil) + _ manifest.Manifest = (*Server)(nil) + _ manifest.Defaultable[*Server] = (*Server)(nil) ) type Server struct { @@ -42,7 +42,7 @@ func (s *Server) GetDependsOn() []string { } func (s *Server) Default() *Server { - s.Namespace = manifests.DefaultNamespace + s.Namespace = manifest.DefaultNamespace s.Spec.Headers = map[string]string{ "Content-Type": "application/json", } diff --git a/internal/manifests/kinds/service/service.go b/internal/manifest/kinds/service/service.go similarity index 85% rename from internal/manifests/kinds/service/service.go rename to internal/manifest/kinds/service/service.go index 0678362..8e90f75 100644 --- a/internal/manifests/kinds/service/service.go +++ b/internal/manifest/kinds/service/service.go @@ -3,13 +3,13 @@ package service import ( "fmt" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifest/kinds" ) var ( - _ manifests.Manifest = (*Service)(nil) - _ manifests.Defaultable[*Service] = (*Service)(nil) + _ manifest.Manifest = (*Service)(nil) + _ manifest.Defaultable[*Service] = (*Service)(nil) ) type Service struct { @@ -58,7 +58,7 @@ func (s *Service) GetDependsOn() []string { } func (s *Service) Default() *Service { - s.Namespace = manifests.DefaultNamespace + s.Namespace = manifest.DefaultNamespace return s } diff --git a/internal/manifests/kinds/tests/http.go b/internal/manifest/kinds/tests/http.go similarity index 87% rename from internal/manifests/kinds/tests/http.go rename to internal/manifest/kinds/tests/http.go index 47888f1..3370972 100644 --- a/internal/manifests/kinds/tests/http.go +++ b/internal/manifest/kinds/tests/http.go @@ -4,13 +4,13 @@ import ( "fmt" "time" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifest/kinds" ) var ( - _ manifests.Manifest = (*Http)(nil) - _ manifests.Defaultable[*Http] = (*Http)(nil) + _ manifest.Manifest = (*Http)(nil) + _ manifest.Defaultable[*Http] = (*Http)(nil) ) type Http struct { @@ -68,7 +68,7 @@ func (h *Http) GetDependsOn() []string { } func (h *Http) Default() *Http { - h.Namespace = manifests.DefaultNamespace + h.Namespace = manifest.DefaultNamespace return h } diff --git a/internal/manifests/validate.go b/internal/manifest/validate.go similarity index 91% rename from internal/manifests/validate.go rename to internal/manifest/validate.go index e4eae33..0cd8177 100644 --- a/internal/manifests/validate.go +++ b/internal/manifest/validate.go @@ -1,4 +1,4 @@ -package manifests +package manifest import "github.com/asaskevich/govalidator" diff --git a/internal/manifests/depends/dependencies.go b/internal/manifests/depends/dependencies.go deleted file mode 100644 index 1eff1e1..0000000 --- a/internal/manifests/depends/dependencies.go +++ /dev/null @@ -1,40 +0,0 @@ -package depends - -import ( - "fmt" - - "github.com/apiqube/cli/internal/manifests" -) - -type Node struct { - ID string - Manifest manifests.Manifest - Depends []string -} - -func BuildDependencyGraph(mans []manifests.Manifest) (map[string][]string, map[string]manifests.Manifest, error) { - graph := make(map[string][]string) - idToManifest := make(map[string]manifests.Manifest) - - for _, m := range mans { - id := m.GetID() - idToManifest[id] = m - graph[id] = []string{} - } - - for id, manifest := range idToManifest { - for _, dep := range manifest.GetDependsOn() { - if dep == id { - return nil, nil, fmt.Errorf("manifest %s cannot depend on itself", id) - } - - if _, ok := idToManifest[dep]; !ok { - return nil, nil, fmt.Errorf("manifest %s depends on unknown manifest %s", id, dep) - } - - graph[id] = append(graph[id], dep) - } - } - - return graph, idToManifest, nil -} diff --git a/internal/store/db.go b/internal/store/db.go new file mode 100644 index 0000000..bfe5c69 --- /dev/null +++ b/internal/store/db.go @@ -0,0 +1,180 @@ +package store + +import ( + "errors" + "fmt" + "os" + "strings" + "sync" + + "github.com/adrg/xdg" + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/ui" + parcer "github.com/apiqube/cli/internal/yaml" + "github.com/dgraph-io/badger/v4" + "gopkg.in/yaml.v3" +) + +const ( + BadgerDatabaseDirPath = "qube/storage" +) + +var ( + manifestListKeyPrefix = "manifest_list:" +) + +var ( + instance *Storage + once sync.Once +) + +type Storage struct { + db *badger.DB + enabled bool + initialized bool +} + +func Init() { + once.Do(func() { + path, err := xdg.DataFile(BadgerDatabaseDirPath) + if err != nil { + ui.Errorf("Failed to open database: %v", err) + return + } + + if err = os.MkdirAll(path, os.ModePerm); err != nil { + ui.Errorf("Failed to create database: %v", err) + return + } + + db, err := badger.Open(badger.DefaultOptions(path).WithLogger(nil)) + if err != nil { + ui.Errorf("Failed to open database: %v", err) + return + } + + instance = &Storage{ + db: db, + enabled: true, + initialized: true, + } + }) +} + +func Stop() { + if instance != nil && instance.initialized { + instance.enabled = false + instance.initialized = false + if err := instance.db.Close(); err != nil { + ui.Errorf("Failed to close database: %v", err) + } + instance = nil + } +} + +func IsEnabled() bool { + return instance != nil && instance.enabled +} + +func LoadManifestList() ([]string, error) { + if !IsEnabled() { + return nil, nil + } + + var manifestList []string + + err := instance.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = []byte(manifestListKeyPrefix) + + it := txn.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + key := it.Item().Key() + manifestList = append(manifestList, strings.TrimPrefix(string(key), manifestListKeyPrefix)) + } + + return nil + }) + + return manifestList, err +} + +func SaveManifests(mans ...manifest.Manifest) error { + if !IsEnabled() { + return nil + } + + return instance.db.Update(func(txn *badger.Txn) error { + var data []byte + var err error + + for _, m := range mans { + data, err = yaml.Marshal(m) + if err != nil { + return err + } + + if err = txn.Set(genManifestKey(m.GetID()), data); err != nil { + return err + } + + if err = txn.Set(genManifestListKey(m.GetID()), nil); err != nil { + return err + } + } + + return nil + }) +} + +func LoadManifests(ids ...string) ([]manifest.Manifest, error) { + if !IsEnabled() { + return nil, nil + } + + var results []manifest.Manifest + var rErr error + + err := instance.db.View(func(txn *badger.Txn) error { + var item *badger.Item + var err error + + for _, id := range ids { + item, err = txn.Get(genManifestKey(id)) + if errors.Is(err, badger.ErrKeyNotFound) { + rErr = errors.Join(rErr, fmt.Errorf("manifest %s not found", id)) + continue + } else if err != nil { + rErr = errors.Join(rErr, err) + continue + } + + var mans []manifest.Manifest + + if err = item.Value(func(data []byte) error { + if mans, err = parcer.ParseManifests(data); err != nil { + return err + } + + results = append(results, mans...) + return nil + }); err != nil { + rErr = errors.Join(rErr, err) + } + } + + return nil + }) + + return results, errors.Join(rErr, err) +} + +func genManifestKey(id string) []byte { + return []byte(id) +} + +func genManifestListKey(id string) []byte { + return []byte(fmt.Sprintf("%s%s", manifestListKeyPrefix, id)) +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 09c0874..3850a27 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -101,6 +101,14 @@ func Stop() { } } +func StopWithTimeout(timeout time.Duration) { + if instance != nil && instance.initialized { + time.AfterFunc(timeout, func() { + Stop() + }) + } +} + func IsEnabled() bool { return instance != nil && instance.enabled } diff --git a/internal/yaml/loader.go b/internal/yaml/loader.go index 3b704dc..e55ff78 100644 --- a/internal/yaml/loader.go +++ b/internal/yaml/loader.go @@ -2,23 +2,24 @@ package yaml import ( "fmt" - "github.com/apiqube/cli/internal/ui" "os" "path/filepath" "strings" - "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/ui" + + "github.com/apiqube/cli/internal/manifest" ) -func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { +func LoadManifestsFromDir(dir string) ([]manifest.Manifest, error) { files, err := os.ReadDir(dir) if err != nil { return nil, err } - var manifestsSet = make(map[string]struct{}) - var parsedManifests []manifests.Manifest - var result []manifests.Manifest + manifestsSet := make(map[string]struct{}) + var parsedManifests []manifest.Manifest + var result []manifest.Manifest var counter int var content []byte diff --git a/internal/yaml/parse.go b/internal/yaml/parse.go index eb0ba2e..eed29ab 100644 --- a/internal/yaml/parse.go +++ b/internal/yaml/parse.go @@ -3,11 +3,12 @@ package yaml import ( "bytes" "fmt" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds/load" - "github.com/apiqube/cli/internal/manifests/kinds/server" - "github.com/apiqube/cli/internal/manifests/kinds/service" - "github.com/apiqube/cli/internal/manifests/kinds/tests" + + "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifest/kinds/load" + "github.com/apiqube/cli/internal/manifest/kinds/server" + "github.com/apiqube/cli/internal/manifest/kinds/service" + "github.com/apiqube/cli/internal/manifest/kinds/tests" "github.com/apiqube/cli/internal/ui" "gopkg.in/yaml.v3" ) @@ -16,9 +17,9 @@ type RawManifest struct { Kind string `yaml:"kind"` } -func ParseManifests(data []byte) ([]manifests.Manifest, error) { +func ParseManifests(data []byte) ([]manifest.Manifest, error) { docs := bytes.Split(data, []byte("\n---")) - var results []manifests.Manifest + var results []manifest.Manifest for _, doc := range docs { doc = bytes.TrimSpace(doc) @@ -28,45 +29,45 @@ func ParseManifests(data []byte) ([]manifests.Manifest, error) { var raw RawManifest if err := yaml.Unmarshal(doc, &raw); err != nil { - return nil, fmt.Errorf("failed to decode raw manifest: %w", err) + return nil, fmt.Errorf("failed to decode raw s: %w", err) } - var manifest manifests.Manifest + var m manifest.Manifest switch raw.Kind { - case manifests.ServerManifestKind: - var m server.Server - if err := yaml.Unmarshal(doc, m.Default()); err != nil { + case manifest.ServerManifestKind: + var s server.Server + if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } - manifest = &m + m = &s - case manifests.ServiceManifestKind: - var m service.Service - if err := yaml.Unmarshal(doc, m.Default()); err != nil { + case manifest.ServiceManifestKind: + var s service.Service + if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } - manifest = &m + m = &s - case manifests.HttpTestManifestKind: - var m tests.Http - if err := yaml.Unmarshal(doc, m.Default()); err != nil { + case manifest.HttpTestManifestKind: + var h tests.Http + if err := yaml.Unmarshal(doc, h.Default()); err != nil { return nil, err } - manifest = &m + m = &h - case manifests.HttpLoadTestManifestKind: - var m load.Http - if err := yaml.Unmarshal(doc, m.Default()); err != nil { + case manifest.HttpLoadTestManifestKind: + var h load.Http + if err := yaml.Unmarshal(doc, h.Default()); err != nil { return nil, err } - manifest = &m + m = &h default: - ui.Errorf("Unknown manifest kind %s", raw.Kind) + ui.Errorf("Unknown s kind %s", raw.Kind) } - results = append(results, manifest) + results = append(results, m) } return results, nil diff --git a/internal/yaml/saver.go b/internal/yaml/saver.go index 6b94a90..40fd872 100644 --- a/internal/yaml/saver.go +++ b/internal/yaml/saver.go @@ -3,22 +3,23 @@ package yaml import ( "bytes" "fmt" - "github.com/adrg/xdg" - "github.com/apiqube/cli/internal/manifests" - "gopkg.in/yaml.v3" "os" "path/filepath" + + "github.com/adrg/xdg" + "github.com/apiqube/cli/internal/manifest" + "gopkg.in/yaml.v3" ) -func SaveManifestsAsCombined(mans ...manifests.Manifest) error { +func SaveManifestsAsCombined(mans ...manifest.Manifest) error { fileName := fmt.Sprintf("/combined-%s.yaml", mans[0].GetNamespace()) - filePath, err := xdg.DataFile(manifests.CombinedManifestsDirPath + fileName) + filePath, err := xdg.DataFile(manifest.CombinedManifestsDirPath + fileName) if err != nil { panic(err) } - if err = os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + if err = os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { return err } From a9b8043a6c05d7d1158c721b38bd77060cde0a77 Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 02:50:55 +0200 Subject: [PATCH 06/13] chore(collections): added priority queue collection --- internal/collections/priority_queue.go | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 internal/collections/priority_queue.go diff --git a/internal/collections/priority_queue.go b/internal/collections/priority_queue.go new file mode 100644 index 0000000..140c556 --- /dev/null +++ b/internal/collections/priority_queue.go @@ -0,0 +1,34 @@ +package collections + +import "container/heap" + +type PriorityQueue[T any] struct { + nodes []T + less func(a, b T) bool +} + +func NewPriorityQueue[T any](less func(a, b T) bool) *PriorityQueue[T] { + return &PriorityQueue[T]{less: less} +} + +func (pq *PriorityQueue[T]) Len() int { return len(pq.nodes) } + +func (pq *PriorityQueue[T]) Less(i, j int) bool { + return pq.less(pq.nodes[i], pq.nodes[j]) +} + +func (pq *PriorityQueue[T]) Swap(i, j int) { + pq.nodes[i], pq.nodes[j] = pq.nodes[j], pq.nodes[i] +} + +func (pq *PriorityQueue[T]) Push(x any) { + pq.nodes = append(pq.nodes, x) + heap.Fix(pq, len(pq.nodes)-1) +} + +func (pq *PriorityQueue[T]) Pop() any { + item := pq.nodes[0] + pq.nodes = pq.nodes[1:] + heap.Fix(pq, 0) + return item +} From ad427389919ea8b96e8e8e41cc270ccc01a71a2c Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 02:52:12 +0200 Subject: [PATCH 07/13] refactor(deps): refactored and removed old way to build dependencies graph with execution order --- internal/manifest/depends/dependencies.go | 107 ++++++++++++++++++---- internal/manifest/depends/sort.go | 60 ------------ 2 files changed, 90 insertions(+), 77 deletions(-) delete mode 100644 internal/manifest/depends/sort.go diff --git a/internal/manifest/depends/dependencies.go b/internal/manifest/depends/dependencies.go index 41276f7..96a39a8 100644 --- a/internal/manifest/depends/dependencies.go +++ b/internal/manifest/depends/dependencies.go @@ -1,40 +1,113 @@ package depends import ( + "container/heap" "fmt" + "github.com/apiqube/cli/internal/collections" + "strings" "github.com/apiqube/cli/internal/manifest" ) +var priorityOrder = map[string]int{ + "Values": 100, + "ConfigMap": 90, + "Server": 50, + "Service": 30, +} + +type GraphResult struct { + Graph map[string][]string + ExecutionOrder []string +} + type Node struct { ID string - Manifest manifest.Manifest - Depends []string + Priority int } -func BuildDependencyGraph(mans []manifest.Manifest) (map[string][]string, map[string]manifest.Manifest, error) { +func BuildGraphWithPriority(manifests []manifest.Manifest) (*GraphResult, error) { graph := make(map[string][]string) - idToManifest := make(map[string]manifest.Manifest) + inDegree := make(map[string]int) + idToNode := make(map[string]manifest.Manifest) + nodePriority := make(map[string]int) + + for _, node := range manifests { + id := node.GetID() + idToNode[id] = node + inDegree[id] = 0 - for _, m := range mans { - id := m.GetID() - idToManifest[id] = m - graph[id] = []string{} + parts := strings.Split(id, ".") + if len(parts) >= 2 { + kind := parts[1] + nodePriority[id] = getPriority(kind) + } } - for id, m := range idToManifest { - for _, dep := range m.GetDependsOn() { - if dep == id { - return nil, nil, fmt.Errorf("m %s cannot depend on itself", id) + for _, node := range manifests { + id := node.GetID() + for _, depID := range node.GetDependsOn() { + if depID == id { + return nil, fmt.Errorf("цикл: %s зависит от самого себя", id) } + graph[depID] = append(graph[depID], id) + inDegree[id]++ + } + } - if _, ok := idToManifest[dep]; !ok { - return nil, nil, fmt.Errorf("m %s depends on unknown m %s", id, dep) - } + priorityQueue := collections.NewPriorityQueue[*Node](func(a, b *Node) bool { + return a.Priority > b.Priority + }) - graph[id] = append(graph[id], dep) + for id, degree := range inDegree { + if degree == 0 { + heap.Push(priorityQueue, &Node{ + ID: id, + Priority: nodePriority[id], + }) } } - return graph, idToManifest, nil + var order []string + for priorityQueue.Len() > 0 { + current := heap.Pop(priorityQueue).(*Node).ID + order = append(order, current) + + for _, neighbor := range graph[current] { + inDegree[neighbor]-- + if inDegree[neighbor] == 0 { + heap.Push(priorityQueue, &Node{ + ID: neighbor, + Priority: nodePriority[neighbor], + }) + } + } + } + + if len(order) != len(manifests) { + cyclicNodes := findCyclicNodes(inDegree) + return nil, fmt.Errorf("циклы в зависимостях: %v", cyclicNodes) + } + + return &GraphResult{ + Graph: graph, + ExecutionOrder: order, + }, nil +} + +func getPriority(kind string) int { + if p, ok := priorityOrder[kind]; ok { + return p + } + return 0 +} + +func findCyclicNodes(inDegree map[string]int) []string { + cyclicNodes := make([]string, 0) + for id, degree := range inDegree { + if degree > 0 { + cyclicNodes = append(cyclicNodes, id) + } + } + return cyclicNodes } diff --git a/internal/manifest/depends/sort.go b/internal/manifest/depends/sort.go deleted file mode 100644 index 55cec60..0000000 --- a/internal/manifest/depends/sort.go +++ /dev/null @@ -1,60 +0,0 @@ -package depends - -import ( - "fmt" - - "github.com/apiqube/cli/internal/manifest" -) - -func TopoSort(graph map[string][]string) ([]string, error) { - visited := make(map[string]bool) - temp := make(map[string]bool) - var result []string - - var visit func(string) error - visit = func(n string) error { - if temp[n] { - return fmt.Errorf("circular dependency detected at %s", n) - } - if !visited[n] { - temp[n] = true - for _, dep := range graph[n] { - if err := visit(dep); err != nil { - return err - } - } - visited[n] = true - temp[n] = false - result = append(result, n) - } - return nil - } - - for node := range graph { - if err := visit(node); err != nil { - return nil, err - } - } - - return result, nil -} - -func SortManifestsByExecutionOrder(mans []manifest.Manifest, order []string) ([]manifest.Manifest, error) { - idMap := make(map[string]manifest.Manifest) - for _, m := range mans { - idMap[m.GetID()] = m - } - - sorted := make([]manifest.Manifest, 0, len(order)) - - for _, id := range order { - m, ok := idMap[id] - if !ok { - return nil, fmt.Errorf("manifest %s not found in loaded manifests", id) - } - - sorted = append(sorted, m) - } - - return sorted, nil -} From 6e83b9d3f2cb55da5345b3e5765b65de4b8dec99 Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 02:52:32 +0200 Subject: [PATCH 08/13] refactor(deps): some deleting --- internal/manifest/depends/plan.go | 60 ------------------------------- internal/manifest/validate.go | 11 ------ 2 files changed, 71 deletions(-) delete mode 100644 internal/manifest/depends/plan.go delete mode 100644 internal/manifest/validate.go diff --git a/internal/manifest/depends/plan.go b/internal/manifest/depends/plan.go deleted file mode 100644 index 2116c44..0000000 --- a/internal/manifest/depends/plan.go +++ /dev/null @@ -1,60 +0,0 @@ -package depends - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/adrg/xdg" - - "github.com/apiqube/cli/internal/manifest" -) - -type ExecutionPlan struct { - Order []string `json:"order"` -} - -func GeneratePlan(manifests []manifest.Manifest) ([]string, error) { - if len(manifests) == 0 { - return nil, fmt.Errorf("no manifests to generate plan") - } - - graph, _, err := BuildDependencyGraph(manifests) - if err != nil { - return nil, err - } - - order, err := TopoSort(graph) - if err != nil { - return nil, err - } - - return order, nil -} - -func SaveExecutionPlan(namespace string, order []string) error { - if namespace == "" { - namespace = "default" - } - - plan := ExecutionPlan{Order: order} - - data, err := json.MarshalIndent(plan, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal plan: %w", err) - } - - fileName := fmt.Sprintf("/plan-%s.json", namespace) - - filePath, err := xdg.DataFile(manifest.ExecutionPlansDirPath + fileName) - if err != nil { - return fmt.Errorf("failed to build path: %w", err) - } - - if err = os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { - return err - } - - return os.WriteFile(filePath, data, 0o644) -} diff --git a/internal/manifest/validate.go b/internal/manifest/validate.go deleted file mode 100644 index 0cd8177..0000000 --- a/internal/manifest/validate.go +++ /dev/null @@ -1,11 +0,0 @@ -package manifest - -import "github.com/asaskevich/govalidator" - -func ValidateManifest(manifest any) error { - if ok, err := govalidator.ValidateStruct(manifest); !ok && err != nil { - return err - } - - return nil -} From ee94ab3d03746535b8172da1de3763c35501ccde Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 13:56:57 +0200 Subject: [PATCH 09/13] refactor: big moving scripts --- cmd/{ => cli}/apply.go | 18 +- cmd/cli/root.go | 32 ++++ cmd/{ => cli}/version.go | 2 +- cmd/main.go | 18 ++ cmd/root.go | 16 -- go.mod | 2 +- go.sum | 4 +- .../{ => core}/collections/priority_queue.go | 0 .../manifests}/depends/dependencies.go | 16 +- internal/core/manifests/interface.go | 75 ++++++++ internal/core/manifests/kinds/base.go | 26 +++ internal/core/manifests/kinds/load/http.go | 173 ++++++++++++++++++ .../manifests}/kinds/server/server.go | 10 +- .../manifests}/kinds/service/service.go | 10 +- .../manifests}/kinds/tests/http.go | 10 +- .../core/manifests/kinds/values/values.go | 1 + internal/{ => core}/store/db.go | 10 +- internal/{ => core}/yaml/loader.go | 8 +- internal/{ => core}/yaml/parse.go | 24 +-- internal/{ => core}/yaml/saver.go | 6 +- internal/manifest/interface.go | 27 --- internal/manifest/kinds/base.go | 13 -- internal/manifest/kinds/load/http.go | 74 -------- main.go | 7 - 24 files changed, 383 insertions(+), 199 deletions(-) rename cmd/{ => cli}/apply.go (82%) create mode 100644 cmd/cli/root.go rename cmd/{ => cli}/version.go (96%) create mode 100644 cmd/main.go delete mode 100644 cmd/root.go rename internal/{ => core}/collections/priority_queue.go (100%) rename internal/{manifest => core/manifests}/depends/dependencies.go (80%) create mode 100644 internal/core/manifests/interface.go create mode 100644 internal/core/manifests/kinds/base.go create mode 100644 internal/core/manifests/kinds/load/http.go rename internal/{manifest => core/manifests}/kinds/server/server.go (75%) rename internal/{manifest => core/manifests}/kinds/service/service.go (85%) rename internal/{manifest => core/manifests}/kinds/tests/http.go (87%) create mode 100644 internal/core/manifests/kinds/values/values.go rename internal/{ => core}/store/db.go (93%) rename internal/{ => core}/yaml/loader.go (84%) rename internal/{ => core}/yaml/parse.go (65%) rename internal/{ => core}/yaml/saver.go (78%) delete mode 100644 internal/manifest/interface.go delete mode 100644 internal/manifest/kinds/base.go delete mode 100644 internal/manifest/kinds/load/http.go delete mode 100644 main.go diff --git a/cmd/apply.go b/cmd/cli/apply.go similarity index 82% rename from cmd/apply.go rename to cmd/cli/apply.go index 24d3261..2c825de 100644 --- a/cmd/apply.go +++ b/cmd/cli/apply.go @@ -1,11 +1,9 @@ -package cmd +package cli import ( - "time" - - "github.com/apiqube/cli/internal/manifest/depends" + "github.com/apiqube/cli/internal/core/manifests/depends" + "github.com/apiqube/cli/internal/core/yaml" "github.com/apiqube/cli/internal/ui" - "github.com/apiqube/cli/internal/yaml" "github.com/spf13/cobra" ) @@ -20,9 +18,6 @@ var applyCmd = &cobra.Command{ SilenceErrors: true, SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { - ui.Init() - defer ui.StopWithTimeout(time.Millisecond * 250) - file, err := cmd.Flags().GetString("file") if err != nil { ui.Errorf("Failed to parse --file: %s", err.Error()) @@ -42,12 +37,13 @@ var applyCmd = &cobra.Command{ ui.Spinner(false) ui.Printf("Loaded %d manifests", len(mans)) - var order []string - if order, err = depends.GeneratePlan(mans); err != nil { + var result *depends.GraphResult + if result, err = depends.BuildGraphWithPriority(mans); err != nil { ui.Errorf("Failed to generate plan: %s", err.Error()) return } - _ = order + + _ = result ui.Spinner(false) ui.Print("Execution plan generated successfully") diff --git a/cmd/cli/root.go b/cmd/cli/root.go new file mode 100644 index 0000000..4acf3e9 --- /dev/null +++ b/cmd/cli/root.go @@ -0,0 +1,32 @@ +package cli + +import ( + "fmt" + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/internal/ui" + "github.com/spf13/cobra" + "time" +) + +var rootCmd = &cobra.Command{ + Use: "qube", + Short: "ApiQube is a powerful test manager for apps and APIs", + PreRun: func(cmd *cobra.Command, args []string) { + fmt.Println("START !!!") + ui.Init() + store.Init() + }, + PostRun: func(cmd *cobra.Command, args []string) { + store.Stop() + ui.StopWithTimeout(time.Millisecond * 250) + fmt.Println("FINISH !!!") + }, +} + +func Execute() { + cobra.CheckErr(rootCmd.Execute()) +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/cmd/version.go b/cmd/cli/version.go similarity index 96% rename from cmd/version.go rename to cmd/cli/version.go index e7a1c0b..38acbc7 100644 --- a/cmd/version.go +++ b/cmd/cli/version.go @@ -1,4 +1,4 @@ -package cmd +package cli import ( "fmt" diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..3759a88 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/internal/ui" + "github.com/dgraph-io/badger/v4/badger/cmd" + "time" +) + +func main() { + ui.Init() + defer ui.StopWithTimeout(time.Microsecond * 250) + + store.Init() + defer store.Stop() + + cmd.Execute() +} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 5f77738..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,16 +0,0 @@ -package cmd - -import "github.com/spf13/cobra" - -var rootCmd = &cobra.Command{ - Use: "qube", - Short: "ApiQube is a powerful test manager for apps and APIs", -} - -func Execute() { - cobra.CheckErr(rootCmd.Execute()) -} - -func init() { - rootCmd.AddCommand(versionCmd) -} diff --git a/go.mod b/go.mod index e73d042..ec769e7 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.3 require ( github.com/adrg/xdg v0.5.3 - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 + github.com/brianvoe/gofakeit/v7 v7.2.1 github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 diff --git a/go.sum b/go.sum index dcc493e..be27707 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/brianvoe/gofakeit/v7 v7.2.1 h1:AGojgaaCdgq4Adzrd2uWdbGNDyX6MWNhHdQBraNfOHI= +github.com/brianvoe/gofakeit/v7 v7.2.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= diff --git a/internal/collections/priority_queue.go b/internal/core/collections/priority_queue.go similarity index 100% rename from internal/collections/priority_queue.go rename to internal/core/collections/priority_queue.go diff --git a/internal/manifest/depends/dependencies.go b/internal/core/manifests/depends/dependencies.go similarity index 80% rename from internal/manifest/depends/dependencies.go rename to internal/core/manifests/depends/dependencies.go index 96a39a8..fab4170 100644 --- a/internal/manifest/depends/dependencies.go +++ b/internal/core/manifests/depends/dependencies.go @@ -6,7 +6,7 @@ import ( "github.com/apiqube/cli/internal/collections" "strings" - "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifests" ) var priorityOrder = map[string]int{ @@ -26,13 +26,13 @@ type Node struct { Priority int } -func BuildGraphWithPriority(manifests []manifest.Manifest) (*GraphResult, error) { +func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) { graph := make(map[string][]string) inDegree := make(map[string]int) - idToNode := make(map[string]manifest.Manifest) + idToNode := make(map[string]manifests.Manifest) nodePriority := make(map[string]int) - for _, node := range manifests { + for _, node := range mans { id := node.GetID() idToNode[id] = node inDegree[id] = 0 @@ -44,11 +44,11 @@ func BuildGraphWithPriority(manifests []manifest.Manifest) (*GraphResult, error) } } - for _, node := range manifests { + for _, node := range mans { id := node.GetID() for _, depID := range node.GetDependsOn() { if depID == id { - return nil, fmt.Errorf("цикл: %s зависит от самого себя", id) + return nil, fmt.Errorf("dependency error: %s manifest cannot depend on itself", id) } graph[depID] = append(graph[depID], id) inDegree[id]++ @@ -84,9 +84,9 @@ func BuildGraphWithPriority(manifests []manifest.Manifest) (*GraphResult, error) } } - if len(order) != len(manifests) { + if len(order) != len(mans) { cyclicNodes := findCyclicNodes(inDegree) - return nil, fmt.Errorf("циклы в зависимостях: %v", cyclicNodes) + return nil, fmt.Errorf("dependency error: сyclic dependency: %v", cyclicNodes) } return &GraphResult{ diff --git a/internal/core/manifests/interface.go b/internal/core/manifests/interface.go new file mode 100644 index 0000000..3908a47 --- /dev/null +++ b/internal/core/manifests/interface.go @@ -0,0 +1,75 @@ +package manifests + +import ( + "github.com/apiqube/cli/internal/manifests/kinds" + "time" +) + +const ( + CombinedManifestsDirPath = "qube/manifests/combined" + ExecutionPlansDirPath = "qube/plans/" +) + +const ( + DefaultNamespace = "default" + + ServerManifestKind = "Server" + ServiceManifestKind = "Service" + HttpTestManifestKind = "HttpTest" + HttpLoadTestManifestKind = "HttpLoadTest" +) + +type Manifest interface { + GetID() string + GetKind() string + GetName() string + GetNamespace() string + GetDependsOn() []string +} + +var _ kinds.Meta + +type Meta interface { + GetHash() string + SetHash(hash string) + + GetVersion() uint8 + SetVersion(version uint8) + IncVersion() + + GetCreatedAt() time.Time + SetCreatedAt(createdAt time.Time) + + GetCreatedBy() string + SetCreatedBy(createdBy string) + + GetUpdatedAt() time.Time + SetUpdatedAt(updatedAt time.Time) + + GetUpdatedBy() string + SetUpdatedBy(updatedBy string) + + GetUsedBy() string + SetUsedBy(usedBy string) + + GetLastApplied() time.Time + SetLastApplied(lastApplied time.Time) +} + +type Defaultable[T Manifest] interface { + Default() T +} + +type Prepare interface { + Prepare() +} + +type Marshaler interface { + MarshalYAML() ([]byte, error) + MarshalJSON() ([]byte, error) +} + +type Unmarshaler interface { + UnmarshalYAML([]byte) error + UnmarshalJSON([]byte) error +} diff --git a/internal/core/manifests/kinds/base.go b/internal/core/manifests/kinds/base.go new file mode 100644 index 0000000..6ac64ad --- /dev/null +++ b/internal/core/manifests/kinds/base.go @@ -0,0 +1,26 @@ +package kinds + +import "time" + +type Metadata struct { + Name string `yaml:"name" json:"name" valid:"required,alpha"` + Namespace string `yaml:"namespace" json:"namespace" valid:"required,alpha"` +} + +type BaseManifest struct { + Version uint8 `yaml:"version" json:"version" valid:"required,numeric"` + Kind string `yaml:"kind" json:"kind" valid:"required,alpha,in(Server|Service|HttpTest|HttpLoadTest)"` + Metadata `yaml:"metadata" json:"metadata"` + DependsOn []string `yaml:"dependsOn,omitempty" json:"dependsOn,omitempty"` +} + +type Meta struct { + Hash string `yaml:"-" json:"hash"` + Version uint8 `yaml:"-" json:"version"` + CreatedAt time.Time `yaml:"-" json:"createdAt"` + CreatedBy string `yaml:"-" json:"createdBy"` + UpdatedAt time.Time `yaml:"-" json:"updatedAt"` + UpdatedBy string `yaml:"-" json:"updatedBy"` + UsedBy string `yaml:"-" json:"usedBy"` + LastApplied time.Time `yaml:"-" json:"lastApplied"` +} diff --git a/internal/core/manifests/kinds/load/http.go b/internal/core/manifests/kinds/load/http.go new file mode 100644 index 0000000..78f5603 --- /dev/null +++ b/internal/core/manifests/kinds/load/http.go @@ -0,0 +1,173 @@ +package load + +import ( + "encoding/json" + "fmt" + "github.com/brianvoe/gofakeit/v7" + "gopkg.in/yaml.v3" + "math" + "time" + + "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Http)(nil) + _ manifests.Defaultable[*Http] = (*Http)(nil) + _ manifests.Marshaler = (*Http)(nil) + _ manifests.Unmarshaler = (*Http)(nil) + _ manifests.Meta = (*Http)(nil) +) + +type Http struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + Server string `yaml:"server,omitempty" json:"server,omitempty"` + Cases []HttpCase `yaml:"cases" json:"cases" valid:"required,length(1|100)"` + } `yaml:"spec" json:"spec" valid:"required"` + + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +type HttpCase struct { + Name string `yaml:"name" json:"name" valid:"required"` + Method string `yaml:"method" json:"method" valid:"required,uppercase,in(GET|POST|PUT|DELETE)"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` + Url string `yaml:"url,omitempty" json:"url,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + Body map[string]interface{} `yaml:"body,omitempty" json:"body,omitempty"` + Expected *HttpExpect `yaml:"expected,omitempty" json:"expected,omitempty"` + Extract *HttpExtractRule `yaml:"extract,omitempty" json:"extract,omitempty"` + Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` + Async bool `yaml:"async,omitempty" json:"async,omitempty"` + Repeats int `yaml:"repeats,omitempty" json:"repeats,omitempty"` +} + +type HttpExpect struct { + Code int `yaml:"code" json:"code" valid:"required,range(0|599)"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Data map[string]interface{} `yaml:"data,omitempty" json:"data,omitempty"` +} + +type HttpExtractRule struct { + Path string `yaml:"path,omitempty" json:"path,omitempty"` + Value string `yaml:"value,omitempty" json:"value,omitempty"` +} + +func (h *Http) GetID() string { + return fmt.Sprintf("%s.%s.%s", h.Namespace, h.Kind, h.Name) +} + +func (h *Http) GetKind() string { + return h.Kind +} + +func (h *Http) GetName() string { + return h.Name +} + +func (h *Http) GetNamespace() string { + return h.Namespace +} + +func (h *Http) GetDependsOn() []string { + return h.DependsOn +} + +func (h *Http) Default() *Http { + h.Namespace = manifests.DefaultNamespace + + return h +} + +func (h *Http) MarshalJSON() ([]byte, error) { + return json.Marshal(h) +} + +func (h *Http) UnmarshalJSON(bytes []byte) error { + return json.Unmarshal(bytes, h) +} + +func (h *Http) MarshalYAML() ([]byte, error) { + return yaml.Marshal(h) +} + +func (h *Http) UnmarshalYAML(bytes []byte) error { + return yaml.Unmarshal(bytes, h) +} + +func (h *Http) GetHash() string { + return h.Meta.Hash +} + +func (h *Http) SetHash(hash string) { + h.Meta.Hash = hash +} + +func (h *Http) GetVersion() uint8 { + return h.Meta.Version +} + +func (h *Http) SetVersion(version uint8) { + h.Meta.Version = version +} + +func (h *Http) IncVersion() { + if h.Meta.Version < math.MaxUint8 { + h.Meta.Version++ + } +} + +func (h *Http) GetCreatedAt() time.Time { + return h.Meta.CreatedAt +} + +func (h *Http) SetCreatedAt(createdAt time.Time) { + h.Meta.CreatedAt = createdAt +} + +func (h *Http) GetCreatedBy() string { + return h.Meta.CreatedBy +} + +func (h *Http) SetCreatedBy(createdBy string) { + h.Meta.CreatedBy = createdBy +} + +func (h *Http) GetUpdatedAt() time.Time { + return h.Meta.UpdatedAt +} + +func (h *Http) SetUpdatedAt(updatedAt time.Time) { + h.Meta.UpdatedAt = updatedAt +} + +func (h *Http) GetUpdatedBy() string { + return h.Meta.CreatedBy +} + +func (h *Http) SetUpdatedBy(updatedBy string) { + h.Meta.UpdatedBy = updatedBy +} + +func (h *Http) GetUsedBy() string { + return h.Meta.UsedBy +} + +func (h *Http) SetUsedBy(usedBy string) { + h.Meta.UsedBy = usedBy +} + +func (h *Http) GetLastApplied() time.Time { + return h.Meta.LastApplied +} + +func (h *Http) SetLastApplied(lastApplied time.Time) { + h.Meta.LastApplied = lastApplied +} + +func foo() { + gofakeit.Password() +} diff --git a/internal/manifest/kinds/server/server.go b/internal/core/manifests/kinds/server/server.go similarity index 75% rename from internal/manifest/kinds/server/server.go rename to internal/core/manifests/kinds/server/server.go index 38ed89a..75da5e7 100644 --- a/internal/manifest/kinds/server/server.go +++ b/internal/core/manifests/kinds/server/server.go @@ -3,13 +3,13 @@ package server import ( "fmt" - "github.com/apiqube/cli/internal/manifest" - "github.com/apiqube/cli/internal/manifest/kinds" + "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifests/kinds" ) var ( - _ manifest.Manifest = (*Server)(nil) - _ manifest.Defaultable[*Server] = (*Server)(nil) + _ manifests.Manifest = (*Server)(nil) + _ manifests.Defaultable[*Server] = (*Server)(nil) ) type Server struct { @@ -42,7 +42,7 @@ func (s *Server) GetDependsOn() []string { } func (s *Server) Default() *Server { - s.Namespace = manifest.DefaultNamespace + s.Namespace = manifests.DefaultNamespace s.Spec.Headers = map[string]string{ "Content-Type": "application/json", } diff --git a/internal/manifest/kinds/service/service.go b/internal/core/manifests/kinds/service/service.go similarity index 85% rename from internal/manifest/kinds/service/service.go rename to internal/core/manifests/kinds/service/service.go index 8e90f75..0678362 100644 --- a/internal/manifest/kinds/service/service.go +++ b/internal/core/manifests/kinds/service/service.go @@ -3,13 +3,13 @@ package service import ( "fmt" - "github.com/apiqube/cli/internal/manifest" - "github.com/apiqube/cli/internal/manifest/kinds" + "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifests/kinds" ) var ( - _ manifest.Manifest = (*Service)(nil) - _ manifest.Defaultable[*Service] = (*Service)(nil) + _ manifests.Manifest = (*Service)(nil) + _ manifests.Defaultable[*Service] = (*Service)(nil) ) type Service struct { @@ -58,7 +58,7 @@ func (s *Service) GetDependsOn() []string { } func (s *Service) Default() *Service { - s.Namespace = manifest.DefaultNamespace + s.Namespace = manifests.DefaultNamespace return s } diff --git a/internal/manifest/kinds/tests/http.go b/internal/core/manifests/kinds/tests/http.go similarity index 87% rename from internal/manifest/kinds/tests/http.go rename to internal/core/manifests/kinds/tests/http.go index 3370972..47888f1 100644 --- a/internal/manifest/kinds/tests/http.go +++ b/internal/core/manifests/kinds/tests/http.go @@ -4,13 +4,13 @@ import ( "fmt" "time" - "github.com/apiqube/cli/internal/manifest" - "github.com/apiqube/cli/internal/manifest/kinds" + "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifests/kinds" ) var ( - _ manifest.Manifest = (*Http)(nil) - _ manifest.Defaultable[*Http] = (*Http)(nil) + _ manifests.Manifest = (*Http)(nil) + _ manifests.Defaultable[*Http] = (*Http)(nil) ) type Http struct { @@ -68,7 +68,7 @@ func (h *Http) GetDependsOn() []string { } func (h *Http) Default() *Http { - h.Namespace = manifest.DefaultNamespace + h.Namespace = manifests.DefaultNamespace return h } diff --git a/internal/core/manifests/kinds/values/values.go b/internal/core/manifests/kinds/values/values.go new file mode 100644 index 0000000..4cc8144 --- /dev/null +++ b/internal/core/manifests/kinds/values/values.go @@ -0,0 +1 @@ +package values diff --git a/internal/store/db.go b/internal/core/store/db.go similarity index 93% rename from internal/store/db.go rename to internal/core/store/db.go index bfe5c69..9a03ce6 100644 --- a/internal/store/db.go +++ b/internal/core/store/db.go @@ -8,7 +8,7 @@ import ( "sync" "github.com/adrg/xdg" - "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifests" "github.com/apiqube/cli/internal/ui" parcer "github.com/apiqube/cli/internal/yaml" "github.com/dgraph-io/badger/v4" @@ -101,7 +101,7 @@ func LoadManifestList() ([]string, error) { return manifestList, err } -func SaveManifests(mans ...manifest.Manifest) error { +func SaveManifests(mans ...manifests.Manifest) error { if !IsEnabled() { return nil } @@ -129,12 +129,12 @@ func SaveManifests(mans ...manifest.Manifest) error { }) } -func LoadManifests(ids ...string) ([]manifest.Manifest, error) { +func LoadManifests(ids ...string) ([]manifests.Manifest, error) { if !IsEnabled() { return nil, nil } - var results []manifest.Manifest + var results []manifests.Manifest var rErr error err := instance.db.View(func(txn *badger.Txn) error { @@ -151,7 +151,7 @@ func LoadManifests(ids ...string) ([]manifest.Manifest, error) { continue } - var mans []manifest.Manifest + var mans []manifests.Manifest if err = item.Value(func(data []byte) error { if mans, err = parcer.ParseManifests(data); err != nil { diff --git a/internal/yaml/loader.go b/internal/core/yaml/loader.go similarity index 84% rename from internal/yaml/loader.go rename to internal/core/yaml/loader.go index e55ff78..d6b30ad 100644 --- a/internal/yaml/loader.go +++ b/internal/core/yaml/loader.go @@ -8,18 +8,18 @@ import ( "github.com/apiqube/cli/internal/ui" - "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifests" ) -func LoadManifestsFromDir(dir string) ([]manifest.Manifest, error) { +func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { files, err := os.ReadDir(dir) if err != nil { return nil, err } manifestsSet := make(map[string]struct{}) - var parsedManifests []manifest.Manifest - var result []manifest.Manifest + var parsedManifests []manifests.Manifest + var result []manifests.Manifest var counter int var content []byte diff --git a/internal/yaml/parse.go b/internal/core/yaml/parse.go similarity index 65% rename from internal/yaml/parse.go rename to internal/core/yaml/parse.go index eed29ab..c1c92c2 100644 --- a/internal/yaml/parse.go +++ b/internal/core/yaml/parse.go @@ -4,11 +4,11 @@ import ( "bytes" "fmt" - "github.com/apiqube/cli/internal/manifest" - "github.com/apiqube/cli/internal/manifest/kinds/load" - "github.com/apiqube/cli/internal/manifest/kinds/server" - "github.com/apiqube/cli/internal/manifest/kinds/service" - "github.com/apiqube/cli/internal/manifest/kinds/tests" + "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/manifests/kinds/load" + "github.com/apiqube/cli/internal/manifests/kinds/server" + "github.com/apiqube/cli/internal/manifests/kinds/service" + "github.com/apiqube/cli/internal/manifests/kinds/tests" "github.com/apiqube/cli/internal/ui" "gopkg.in/yaml.v3" ) @@ -17,9 +17,9 @@ type RawManifest struct { Kind string `yaml:"kind"` } -func ParseManifests(data []byte) ([]manifest.Manifest, error) { +func ParseManifests(data []byte) ([]manifests.Manifest, error) { docs := bytes.Split(data, []byte("\n---")) - var results []manifest.Manifest + var results []manifests.Manifest for _, doc := range docs { doc = bytes.TrimSpace(doc) @@ -32,31 +32,31 @@ func ParseManifests(data []byte) ([]manifest.Manifest, error) { return nil, fmt.Errorf("failed to decode raw s: %w", err) } - var m manifest.Manifest + var m manifests.Manifest switch raw.Kind { - case manifest.ServerManifestKind: + case manifests.ServerManifestKind: var s server.Server if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } m = &s - case manifest.ServiceManifestKind: + case manifests.ServiceManifestKind: var s service.Service if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } m = &s - case manifest.HttpTestManifestKind: + case manifests.HttpTestManifestKind: var h tests.Http if err := yaml.Unmarshal(doc, h.Default()); err != nil { return nil, err } m = &h - case manifest.HttpLoadTestManifestKind: + case manifests.HttpLoadTestManifestKind: var h load.Http if err := yaml.Unmarshal(doc, h.Default()); err != nil { return nil, err diff --git a/internal/yaml/saver.go b/internal/core/yaml/saver.go similarity index 78% rename from internal/yaml/saver.go rename to internal/core/yaml/saver.go index 40fd872..5fa4968 100644 --- a/internal/yaml/saver.go +++ b/internal/core/yaml/saver.go @@ -7,14 +7,14 @@ import ( "path/filepath" "github.com/adrg/xdg" - "github.com/apiqube/cli/internal/manifest" + "github.com/apiqube/cli/internal/manifests" "gopkg.in/yaml.v3" ) -func SaveManifestsAsCombined(mans ...manifest.Manifest) error { +func SaveManifestsAsCombined(mans ...manifests.Manifest) error { fileName := fmt.Sprintf("/combined-%s.yaml", mans[0].GetNamespace()) - filePath, err := xdg.DataFile(manifest.CombinedManifestsDirPath + fileName) + filePath, err := xdg.DataFile(manifests.CombinedManifestsDirPath + fileName) if err != nil { panic(err) } diff --git a/internal/manifest/interface.go b/internal/manifest/interface.go deleted file mode 100644 index 02fe2fd..0000000 --- a/internal/manifest/interface.go +++ /dev/null @@ -1,27 +0,0 @@ -package manifest - -const ( - CombinedManifestsDirPath = "qube/manifests/combined" - ExecutionPlansDirPath = "qube/plans/" -) - -const ( - DefaultNamespace = "default" - - ServerManifestKind = "Server" - ServiceManifestKind = "Service" - HttpTestManifestKind = "HttpTest" - HttpLoadTestManifestKind = "HttpLoadTest" -) - -type Manifest interface { - GetID() string - GetKind() string - GetName() string - GetNamespace() string - GetDependsOn() []string -} - -type Defaultable[T Manifest] interface { - Default() T -} diff --git a/internal/manifest/kinds/base.go b/internal/manifest/kinds/base.go deleted file mode 100644 index 5ca9937..0000000 --- a/internal/manifest/kinds/base.go +++ /dev/null @@ -1,13 +0,0 @@ -package kinds - -type Metadata struct { - Name string `yaml:"name" valid:"required,alpha"` - Namespace string `yaml:"namespace" valid:"required,alpha"` -} - -type BaseManifest struct { - Version uint8 `yaml:"version" valid:"required,numeric"` - Kind string `yaml:"kind" valid:"required,alpha,in(Server|Service|HttpTest|HttpLoadTest)"` - Metadata `yaml:"metadata"` - DependsOn []string `yaml:"dependsOn,omitempty"` -} diff --git a/internal/manifest/kinds/load/http.go b/internal/manifest/kinds/load/http.go deleted file mode 100644 index 0cb205f..0000000 --- a/internal/manifest/kinds/load/http.go +++ /dev/null @@ -1,74 +0,0 @@ -package load - -import ( - "fmt" - "time" - - "github.com/apiqube/cli/internal/manifest" - "github.com/apiqube/cli/internal/manifest/kinds" -) - -var ( - _ manifest.Manifest = (*Http)(nil) - _ manifest.Defaultable[*Http] = (*Http)(nil) -) - -type Http struct { - kinds.BaseManifest `yaml:",inline"` - - Spec struct { - Server string `yaml:"server,omitempty"` - Cases []HttpCase `yaml:"cases" valid:"required,length(1|100)"` - } `yaml:"spec" valid:"required"` -} - -type HttpCase struct { - Name string `yaml:"name" valid:"required"` - Method string `yaml:"method" valid:"required,uppercase,in(GET|POST|PUT|DELETE)"` - Endpoint string `yaml:"endpoint,omitempty"` - Url string `yaml:"url,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - Body map[string]interface{} `yaml:"body,omitempty"` - Expected *HttpExpect `yaml:"expected,omitempty"` - Extract *HttpExtractRule `yaml:"extract,omitempty"` - Timeout time.Duration `yaml:"timeout,omitempty"` - Async bool `yaml:"async,omitempty"` - Repeats int `yaml:"repeats,omitempty"` -} - -type HttpExpect struct { - Code int `yaml:"code" valid:"required,range(0|599)"` - Message string `yaml:"message,omitempty"` - Data map[string]interface{} `yaml:"data,omitempty"` -} - -type HttpExtractRule struct { - Path string `yaml:"path,omitempty"` - Value string `yaml:"value,omitempty"` -} - -func (h *Http) GetID() string { - return fmt.Sprintf("%s.%s.%s", h.Namespace, h.Kind, h.Name) -} - -func (h *Http) GetKind() string { - return h.Kind -} - -func (h *Http) GetName() string { - return h.Name -} - -func (h *Http) GetNamespace() string { - return h.Namespace -} - -func (h *Http) GetDependsOn() []string { - return h.DependsOn -} - -func (h *Http) Default() *Http { - h.Namespace = manifest.DefaultNamespace - - return h -} diff --git a/main.go b/main.go deleted file mode 100644 index f313c19..0000000 --- a/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "github.com/apiqube/cli/cmd" - -func main() { - cmd.Execute() -} From 39fd2f4a5ab513fa74b8838656cb2a135e523b11 Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 18:32:38 +0200 Subject: [PATCH 10/13] feat(manifests): all manifests updated, added new Plan manifest --- cmd/cli/apply.go | 9 +- cmd/cli/root.go | 3 +- cmd/main.go | 3 +- examples/values/Values.yaml | 13 ++ .../core/manifests/depends/dependencies.go | 5 +- internal/core/manifests/interface.go | 33 ++-- internal/core/manifests/kinds/base.go | 20 +- internal/core/manifests/kinds/helpers.go | 33 ++++ .../manifests/kinds/internal/plan/plan.go | 89 +++++++++ internal/core/manifests/kinds/load/http.go | 173 ------------------ internal/core/manifests/kinds/meta.go | 102 +++++++++++ .../core/manifests/kinds/server/server.go | 51 ------ .../core/manifests/kinds/servers/server.go | 73 ++++++++ .../core/manifests/kinds/service/service.go | 64 ------- .../core/manifests/kinds/services/base.go | 16 ++ .../core/manifests/kinds/services/service.go | 78 ++++++++ .../core/manifests/kinds/tests/api/http.go | 87 +++++++++ internal/core/manifests/kinds/tests/base.go | 46 +++++ internal/core/manifests/kinds/tests/http.go | 74 -------- .../core/manifests/kinds/tests/load/http.go | 111 +++++++++++ .../core/manifests/kinds/values/values.go | 77 ++++++++ .../core/{yaml => manifests/loader}/loader.go | 10 +- .../core/{yaml => manifests/parsing}/parse.go | 18 +- internal/core/store/db.go | 89 +++------ internal/core/store/helpers.go | 11 ++ internal/core/store/independ.go | 69 +++++++ internal/core/yaml/saver.go | 47 ----- plugins/http/http_plugin.go | 44 ----- plugins/interface.go | 32 ---- {internal/ui => ui}/console.go | 0 {internal/ui => ui}/elements.go | 0 {internal/ui => ui}/model.go | 0 {internal/ui => ui}/styles.go | 0 {internal/ui => ui}/ui.go | 0 34 files changed, 878 insertions(+), 602 deletions(-) create mode 100644 examples/values/Values.yaml create mode 100644 internal/core/manifests/kinds/helpers.go create mode 100644 internal/core/manifests/kinds/internal/plan/plan.go delete mode 100644 internal/core/manifests/kinds/load/http.go create mode 100644 internal/core/manifests/kinds/meta.go delete mode 100644 internal/core/manifests/kinds/server/server.go create mode 100644 internal/core/manifests/kinds/servers/server.go delete mode 100644 internal/core/manifests/kinds/service/service.go create mode 100644 internal/core/manifests/kinds/services/base.go create mode 100644 internal/core/manifests/kinds/services/service.go create mode 100644 internal/core/manifests/kinds/tests/api/http.go create mode 100644 internal/core/manifests/kinds/tests/base.go delete mode 100644 internal/core/manifests/kinds/tests/http.go create mode 100644 internal/core/manifests/kinds/tests/load/http.go rename internal/core/{yaml => manifests/loader}/loader.go (83%) rename internal/core/{yaml => manifests/parsing}/parse.go (75%) create mode 100644 internal/core/store/helpers.go create mode 100644 internal/core/store/independ.go delete mode 100644 internal/core/yaml/saver.go delete mode 100644 plugins/http/http_plugin.go delete mode 100644 plugins/interface.go rename {internal/ui => ui}/console.go (100%) rename {internal/ui => ui}/elements.go (100%) rename {internal/ui => ui}/model.go (100%) rename {internal/ui => ui}/styles.go (100%) rename {internal/ui => ui}/ui.go (100%) diff --git a/cmd/cli/apply.go b/cmd/cli/apply.go index 2c825de..f10195d 100644 --- a/cmd/cli/apply.go +++ b/cmd/cli/apply.go @@ -2,8 +2,9 @@ package cli import ( "github.com/apiqube/cli/internal/core/manifests/depends" - "github.com/apiqube/cli/internal/core/yaml" - "github.com/apiqube/cli/internal/ui" + "github.com/apiqube/cli/internal/core/manifests/loader" + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/ui" "github.com/spf13/cobra" ) @@ -27,7 +28,7 @@ var applyCmd = &cobra.Command{ ui.Printf("Applying manifests from: %s", file) ui.Spinner(true, "Loading manifests") - mans, err := yaml.LoadManifestsFromDir(file) + mans, err := loader.LoadManifestsFromDir(file) if err != nil { ui.Spinner(false) ui.Errorf("Failed to load manifests: %s", err.Error()) @@ -49,7 +50,7 @@ var applyCmd = &cobra.Command{ ui.Print("Execution plan generated successfully") ui.Spinner(true, "Saving manifests...") - if err := yaml.SaveManifestsAsCombined(mans...); err != nil { + if err := store.SaveManifests(mans...); err != nil { ui.Error("Failed to save manifests: " + err.Error()) return } diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 4acf3e9..082d4d0 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -2,10 +2,11 @@ package cli import ( "fmt" + "time" + "github.com/apiqube/cli/internal/core/store" "github.com/apiqube/cli/internal/ui" "github.com/spf13/cobra" - "time" ) var rootCmd = &cobra.Command{ diff --git a/cmd/main.go b/cmd/main.go index 3759a88..6c8bbc9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,11 @@ package main import ( + "time" + "github.com/apiqube/cli/internal/core/store" "github.com/apiqube/cli/internal/ui" "github.com/dgraph-io/badger/v4/badger/cmd" - "time" ) func main() { diff --git a/examples/values/Values.yaml b/examples/values/Values.yaml new file mode 100644 index 0000000..5c8c7b1 --- /dev/null +++ b/examples/values/Values.yaml @@ -0,0 +1,13 @@ +version: 1 + +kind: Values +metadata: + name: values + +spec: + users: + username: ["Max", "Carl", "John", "Alex"] + email: + - "email_1@gmail.com" + - "email_2@mail.com" + - "email_3@dog.io" \ No newline at end of file diff --git a/internal/core/manifests/depends/dependencies.go b/internal/core/manifests/depends/dependencies.go index fab4170..539d59a 100644 --- a/internal/core/manifests/depends/dependencies.go +++ b/internal/core/manifests/depends/dependencies.go @@ -3,10 +3,11 @@ package depends import ( "container/heap" "fmt" - "github.com/apiqube/cli/internal/collections" "strings" - "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/internal/core/collections" + + "github.com/apiqube/cli/internal/core/manifests" ) var priorityOrder = map[string]int{ diff --git a/internal/core/manifests/interface.go b/internal/core/manifests/interface.go index 3908a47..7081cff 100644 --- a/internal/core/manifests/interface.go +++ b/internal/core/manifests/interface.go @@ -1,22 +1,24 @@ package manifests import ( - "github.com/apiqube/cli/internal/manifests/kinds" "time" ) -const ( - CombinedManifestsDirPath = "qube/manifests/combined" - ExecutionPlansDirPath = "qube/plans/" -) - const ( DefaultNamespace = "default" - ServerManifestKind = "Server" - ServiceManifestKind = "Service" - HttpTestManifestKind = "HttpTest" - HttpLoadTestManifestKind = "HttpLoadTest" + PlanManifestKind = "Plan" + ValuesManifestLind = "Values" + ServerManifestKind = "Server" + ServiceManifestKind = "Service" + HttpTestManifestKind = "HttpTest" + HttpLoadTestManifestKind = "HttpLoadTest" + GRPCTestManifestKind = "GRPCTest" + GRPCLoadTestManifestKind = "GRPCLoadTest" + WSTestManifestKind = "WSTest" + WSLoadTestManifestKind = "WSLoadTest" + GRAPHQLTestManifestKind = "GraphQLTest" + GRAPHQLLoadTestManifestKind = "GraphQLLoadTest" ) type Manifest interface { @@ -24,10 +26,15 @@ type Manifest interface { GetKind() string GetName() string GetNamespace() string +} + +type Dependencies interface { GetDependsOn() []string } -var _ kinds.Meta +type MetaTable interface { + GetMeta() Meta +} type Meta interface { GetHash() string @@ -56,8 +63,8 @@ type Meta interface { SetLastApplied(lastApplied time.Time) } -type Defaultable[T Manifest] interface { - Default() T +type Defaultable interface { + Default() } type Prepare interface { diff --git a/internal/core/manifests/kinds/base.go b/internal/core/manifests/kinds/base.go index 6ac64ad..a9cd96f 100644 --- a/internal/core/manifests/kinds/base.go +++ b/internal/core/manifests/kinds/base.go @@ -1,26 +1,16 @@ package kinds -import "time" - type Metadata struct { Name string `yaml:"name" json:"name" valid:"required,alpha"` Namespace string `yaml:"namespace" json:"namespace" valid:"required,alpha"` } type BaseManifest struct { - Version uint8 `yaml:"version" json:"version" valid:"required,numeric"` - Kind string `yaml:"kind" json:"kind" valid:"required,alpha,in(Server|Service|HttpTest|HttpLoadTest)"` - Metadata `yaml:"metadata" json:"metadata"` - DependsOn []string `yaml:"dependsOn,omitempty" json:"dependsOn,omitempty"` + Version uint8 `yaml:"version" json:"version" valid:"required,numeric"` + Kind string `yaml:"kind" json:"kind" valid:"required,alpha,in(Server|Service|HttpTest|HttpLoadTest)"` + Metadata `yaml:"metadata" json:"metadata"` } -type Meta struct { - Hash string `yaml:"-" json:"hash"` - Version uint8 `yaml:"-" json:"version"` - CreatedAt time.Time `yaml:"-" json:"createdAt"` - CreatedBy string `yaml:"-" json:"createdBy"` - UpdatedAt time.Time `yaml:"-" json:"updatedAt"` - UpdatedBy string `yaml:"-" json:"updatedBy"` - UsedBy string `yaml:"-" json:"usedBy"` - LastApplied time.Time `yaml:"-" json:"lastApplied"` +type Dependencies struct { + DependsOn []string `yaml:"dependsOn" json:"dependsOn"` } diff --git a/internal/core/manifests/kinds/helpers.go b/internal/core/manifests/kinds/helpers.go new file mode 100644 index 0000000..a277ecc --- /dev/null +++ b/internal/core/manifests/kinds/helpers.go @@ -0,0 +1,33 @@ +package kinds + +import ( + "encoding/json" + "fmt" + + "github.com/apiqube/cli/internal/core/manifests" + "gopkg.in/yaml.v3" +) + +func FormManifestID(namespace, kind, name string) string { + return fmt.Sprintf("%s.%s.%s", namespace, kind, name) +} + +func BaseMarshalYAML(m manifests.Defaultable) ([]byte, error) { + m.Default() + return yaml.Marshal(m) +} + +func BaseMarshalJSON(m manifests.Defaultable) ([]byte, error) { + m.Default() + return json.MarshalIndent(m, "", " ") +} + +func BaseUnmarshalYAML(bytes []byte, m manifests.Defaultable) error { + m.Default() + return yaml.Unmarshal(bytes, m) +} + +func BaseUnmarshalJSON(bytes []byte, m manifests.Defaultable) error { + m.Default() + return json.Unmarshal(bytes, m) +} diff --git a/internal/core/manifests/kinds/internal/plan/plan.go b/internal/core/manifests/kinds/internal/plan/plan.go new file mode 100644 index 0000000..bac48f2 --- /dev/null +++ b/internal/core/manifests/kinds/internal/plan/plan.go @@ -0,0 +1,89 @@ +package plan + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Plan)(nil) + _ manifests.MetaTable = (*Plan)(nil) + _ manifests.Defaultable = (*Plan)(nil) + _ manifests.Prepare = (*Plan)(nil) + _ manifests.Marshaler = (*Plan)(nil) + _ manifests.Unmarshaler = (*Plan)(nil) +) + +type Plan struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + Stages Stages `yaml:"stages" json:"stages"` + Hooks Hooks `yaml:"hooks" json:"hooks"` + } `yaml:"spec" json:"spec"` + + Meta kinds.Meta `yaml:"meta" json:"meta"` +} + +type Stages struct { + Stages []Stage `yaml:",inline" json:",inline"` +} + +type Stage struct { + Name string `yaml:"name" json:"name"` + Manifests []string `yaml:"manifests" json:"manifests" ` + Parallel bool `yaml:"parallel" json:"parallel"` +} + +type Hooks struct { + OnSuccess []string `yaml:"onSuccess" json:"onSuccess"` + OnFailure []string `yaml:"onFailure" json:"onFailure"` +} + +func (p *Plan) GetID() string { + return kinds.FormManifestID(p.Namespace, p.Kind, p.Name) +} + +func (p *Plan) GetKind() string { + return p.Kind +} + +func (p *Plan) GetName() string { + return p.Name +} + +func (p *Plan) GetNamespace() string { + return p.Namespace +} + +func (p *Plan) GetMeta() manifests.Meta { + return p.Meta +} + +func (p *Plan) Default() { + p.Namespace = manifests.DefaultNamespace + p.Kind = manifests.PlanManifestKind + p.Meta = kinds.DefaultMeta +} + +func (p *Plan) Prepare() { + if p.Namespace == "" { + p.Namespace = manifests.DefaultNamespace + } +} + +func (p *Plan) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(p) +} + +func (p *Plan) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(p) +} + +func (p *Plan) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, p) +} + +func (p *Plan) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, p) +} diff --git a/internal/core/manifests/kinds/load/http.go b/internal/core/manifests/kinds/load/http.go deleted file mode 100644 index 78f5603..0000000 --- a/internal/core/manifests/kinds/load/http.go +++ /dev/null @@ -1,173 +0,0 @@ -package load - -import ( - "encoding/json" - "fmt" - "github.com/brianvoe/gofakeit/v7" - "gopkg.in/yaml.v3" - "math" - "time" - - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" -) - -var ( - _ manifests.Manifest = (*Http)(nil) - _ manifests.Defaultable[*Http] = (*Http)(nil) - _ manifests.Marshaler = (*Http)(nil) - _ manifests.Unmarshaler = (*Http)(nil) - _ manifests.Meta = (*Http)(nil) -) - -type Http struct { - kinds.BaseManifest `yaml:",inline" json:",inline"` - - Spec struct { - Server string `yaml:"server,omitempty" json:"server,omitempty"` - Cases []HttpCase `yaml:"cases" json:"cases" valid:"required,length(1|100)"` - } `yaml:"spec" json:"spec" valid:"required"` - - Meta kinds.Meta `yaml:"-" json:"meta"` -} - -type HttpCase struct { - Name string `yaml:"name" json:"name" valid:"required"` - Method string `yaml:"method" json:"method" valid:"required,uppercase,in(GET|POST|PUT|DELETE)"` - Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` - Url string `yaml:"url,omitempty" json:"url,omitempty"` - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` - Body map[string]interface{} `yaml:"body,omitempty" json:"body,omitempty"` - Expected *HttpExpect `yaml:"expected,omitempty" json:"expected,omitempty"` - Extract *HttpExtractRule `yaml:"extract,omitempty" json:"extract,omitempty"` - Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` - Async bool `yaml:"async,omitempty" json:"async,omitempty"` - Repeats int `yaml:"repeats,omitempty" json:"repeats,omitempty"` -} - -type HttpExpect struct { - Code int `yaml:"code" json:"code" valid:"required,range(0|599)"` - Message string `yaml:"message,omitempty" json:"message,omitempty"` - Data map[string]interface{} `yaml:"data,omitempty" json:"data,omitempty"` -} - -type HttpExtractRule struct { - Path string `yaml:"path,omitempty" json:"path,omitempty"` - Value string `yaml:"value,omitempty" json:"value,omitempty"` -} - -func (h *Http) GetID() string { - return fmt.Sprintf("%s.%s.%s", h.Namespace, h.Kind, h.Name) -} - -func (h *Http) GetKind() string { - return h.Kind -} - -func (h *Http) GetName() string { - return h.Name -} - -func (h *Http) GetNamespace() string { - return h.Namespace -} - -func (h *Http) GetDependsOn() []string { - return h.DependsOn -} - -func (h *Http) Default() *Http { - h.Namespace = manifests.DefaultNamespace - - return h -} - -func (h *Http) MarshalJSON() ([]byte, error) { - return json.Marshal(h) -} - -func (h *Http) UnmarshalJSON(bytes []byte) error { - return json.Unmarshal(bytes, h) -} - -func (h *Http) MarshalYAML() ([]byte, error) { - return yaml.Marshal(h) -} - -func (h *Http) UnmarshalYAML(bytes []byte) error { - return yaml.Unmarshal(bytes, h) -} - -func (h *Http) GetHash() string { - return h.Meta.Hash -} - -func (h *Http) SetHash(hash string) { - h.Meta.Hash = hash -} - -func (h *Http) GetVersion() uint8 { - return h.Meta.Version -} - -func (h *Http) SetVersion(version uint8) { - h.Meta.Version = version -} - -func (h *Http) IncVersion() { - if h.Meta.Version < math.MaxUint8 { - h.Meta.Version++ - } -} - -func (h *Http) GetCreatedAt() time.Time { - return h.Meta.CreatedAt -} - -func (h *Http) SetCreatedAt(createdAt time.Time) { - h.Meta.CreatedAt = createdAt -} - -func (h *Http) GetCreatedBy() string { - return h.Meta.CreatedBy -} - -func (h *Http) SetCreatedBy(createdBy string) { - h.Meta.CreatedBy = createdBy -} - -func (h *Http) GetUpdatedAt() time.Time { - return h.Meta.UpdatedAt -} - -func (h *Http) SetUpdatedAt(updatedAt time.Time) { - h.Meta.UpdatedAt = updatedAt -} - -func (h *Http) GetUpdatedBy() string { - return h.Meta.CreatedBy -} - -func (h *Http) SetUpdatedBy(updatedBy string) { - h.Meta.UpdatedBy = updatedBy -} - -func (h *Http) GetUsedBy() string { - return h.Meta.UsedBy -} - -func (h *Http) SetUsedBy(usedBy string) { - h.Meta.UsedBy = usedBy -} - -func (h *Http) GetLastApplied() time.Time { - return h.Meta.LastApplied -} - -func (h *Http) SetLastApplied(lastApplied time.Time) { - h.Meta.LastApplied = lastApplied -} - -func foo() { - gofakeit.Password() -} diff --git a/internal/core/manifests/kinds/meta.go b/internal/core/manifests/kinds/meta.go new file mode 100644 index 0000000..2c758eb --- /dev/null +++ b/internal/core/manifests/kinds/meta.go @@ -0,0 +1,102 @@ +package kinds + +import ( + "math" + "time" + + "github.com/apiqube/cli/internal/core/manifests" +) + +var DefaultMeta = Meta{ + Hash: "", + Version: 1, + CreatedAt: time.Now(), + CreatedBy: "qube", + UpdatedAt: time.Now(), + UpdatedBy: "qube", + UsedBy: "qube", + LastApplied: time.Now(), +} + +var _ manifests.Meta = (*Meta)(nil) + +type Meta struct { + Hash string `yaml:"-" json:"hash"` + Version uint8 `yaml:"-" json:"version"` + CreatedAt time.Time `yaml:"-" json:"createdAt"` + CreatedBy string `yaml:"-" json:"createdBy"` + UpdatedAt time.Time `yaml:"-" json:"updatedAt"` + UpdatedBy string `yaml:"-" json:"updatedBy"` + UsedBy string `yaml:"-" json:"usedBy"` + LastApplied time.Time `yaml:"-" json:"lastApplied"` +} + +func (m Meta) GetHash() string { + return m.Hash +} + +func (m Meta) SetHash(hash string) { + m.Hash = hash +} + +func (m Meta) GetVersion() uint8 { + return m.Version +} + +func (m Meta) SetVersion(version uint8) { + m.Version = version +} + +func (m Meta) IncVersion() { + if m.Version < math.MaxUint8 { + m.Version++ + } +} + +func (m Meta) GetCreatedAt() time.Time { + return m.CreatedAt +} + +func (m Meta) SetCreatedAt(createdAt time.Time) { + m.CreatedAt = createdAt +} + +func (m Meta) GetCreatedBy() string { + return m.CreatedBy +} + +func (m Meta) SetCreatedBy(createdBy string) { + m.CreatedBy = createdBy +} + +func (m Meta) GetUpdatedAt() time.Time { + return m.UpdatedAt +} + +func (m Meta) SetUpdatedAt(updatedAt time.Time) { + m.UpdatedAt = updatedAt +} + +func (m Meta) GetUpdatedBy() string { + return m.UpdatedBy +} + +func (m Meta) SetUpdatedBy(updatedBy string) { + m.UpdatedBy = updatedBy +} + +func (m Meta) GetUsedBy() string { + return m.UsedBy +} + +func (m Meta) SetUsedBy(usedBy string) { + m.UsedBy = usedBy +} + +func (m Meta) GetLastApplied() time.Time { + return m.LastApplied +} + +func (m Meta) SetLastApplied(lastApplied time.Time) { + m.LastApplied = lastApplied +} diff --git a/internal/core/manifests/kinds/server/server.go b/internal/core/manifests/kinds/server/server.go deleted file mode 100644 index 75da5e7..0000000 --- a/internal/core/manifests/kinds/server/server.go +++ /dev/null @@ -1,51 +0,0 @@ -package server - -import ( - "fmt" - - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" -) - -var ( - _ manifests.Manifest = (*Server)(nil) - _ manifests.Defaultable[*Server] = (*Server)(nil) -) - -type Server struct { - kinds.BaseManifest `yaml:",inline"` - - Spec struct { - BaseUrl string `yaml:"baseUrl" valid:"required,url"` - Headers map[string]string `yaml:"headers,omitempty"` - } `yaml:"spec" valid:"required"` -} - -func (s *Server) GetID() string { - return fmt.Sprintf("%s.%s.%s", s.Namespace, s.Kind, s.Name) -} - -func (s *Server) GetKind() string { - return s.Kind -} - -func (s *Server) GetName() string { - return s.Name -} - -func (s *Server) GetNamespace() string { - return s.Namespace -} - -func (s *Server) GetDependsOn() []string { - return s.DependsOn -} - -func (s *Server) Default() *Server { - s.Namespace = manifests.DefaultNamespace - s.Spec.Headers = map[string]string{ - "Content-Type": "application/json", - } - - return s -} diff --git a/internal/core/manifests/kinds/servers/server.go b/internal/core/manifests/kinds/servers/server.go new file mode 100644 index 0000000..a7f837c --- /dev/null +++ b/internal/core/manifests/kinds/servers/server.go @@ -0,0 +1,73 @@ +package servers + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Server)(nil) + _ manifests.MetaTable = (*Server)(nil) + _ manifests.Defaultable = (*Server)(nil) + _ manifests.Prepare = (*Server)(nil) + _ manifests.Marshaler = (*Server)(nil) + _ manifests.Unmarshaler = (*Server)(nil) +) + +type Server struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + BaseUrl string `yaml:"baseUrl" json:"baseUrl" valid:"required,url"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers" valid:"-"` + } `yaml:"spec" json:"spec" valid:"required"` + + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +func (s *Server) GetID() string { + return kinds.FormManifestID(s.Namespace, s.Kind, s.Name) +} + +func (s *Server) GetKind() string { + return s.Kind +} + +func (s *Server) GetName() string { + return s.Name +} + +func (s *Server) GetNamespace() string { + return s.Namespace +} + +func (s *Server) GetMeta() manifests.Meta { + return s.Meta +} + +func (s *Server) Default() { + s.Namespace = manifests.DefaultNamespace + s.Meta = kinds.DefaultMeta +} + +func (s *Server) Prepare() { + if s.Namespace == "" { + s.Namespace = manifests.DefaultNamespace + } +} + +func (s *Server) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(s) +} + +func (s *Server) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(s) +} + +func (s *Server) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, s) +} + +func (s *Server) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, s) +} diff --git a/internal/core/manifests/kinds/service/service.go b/internal/core/manifests/kinds/service/service.go deleted file mode 100644 index 0678362..0000000 --- a/internal/core/manifests/kinds/service/service.go +++ /dev/null @@ -1,64 +0,0 @@ -package service - -import ( - "fmt" - - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" -) - -var ( - _ manifests.Manifest = (*Service)(nil) - _ manifests.Defaultable[*Service] = (*Service)(nil) -) - -type Service struct { - kinds.BaseManifest `yaml:",inline"` - - Spec struct { - Containers []Container `yaml:"containers" valid:"required,length(1|50)"` - } `yaml:"spec" valid:"required"` -} - -type Container struct { - Name string `yaml:"name" valid:"required"` - ContainerName string `yaml:"containerName,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty"` - Image string `yaml:"image,omitempty"` - Ports []string `yaml:"ports,omitempty"` - Env map[string]string `yaml:"env,omitempty"` - Command string `yaml:"command,omitempty"` - Depends *ContainerDepend `yaml:"depends,omitempty"` - Replicas int `yaml:"replicas,omitempty" valid:"length(0|25)"` - HealthPath string `yaml:"healthPath,omitempty"` -} - -type ContainerDepend struct { - Depends []string `yaml:"depends,omitempty" valid:"required,length(1|25)"` -} - -func (s *Service) GetID() string { - return fmt.Sprintf("%s.%s.%s", s.Namespace, s.Kind, s.Name) -} - -func (s *Service) GetKind() string { - return s.Kind -} - -func (s *Service) GetName() string { - return s.Name -} - -func (s *Service) GetNamespace() string { - return s.Namespace -} - -func (s *Service) GetDependsOn() []string { - return s.DependsOn -} - -func (s *Service) Default() *Service { - s.Namespace = manifests.DefaultNamespace - - return s -} diff --git a/internal/core/manifests/kinds/services/base.go b/internal/core/manifests/kinds/services/base.go new file mode 100644 index 0000000..bfbe70b --- /dev/null +++ b/internal/core/manifests/kinds/services/base.go @@ -0,0 +1,16 @@ +package services + +import "github.com/apiqube/cli/internal/core/manifests/kinds" + +type Container struct { + Name string `yaml:"name" valid:"required"` + ContainerName string `yaml:"containerName,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty"` + Image string `yaml:"image,omitempty"` + Ports []string `yaml:"ports,omitempty"` + Env map[string]string `yaml:"env,omitempty"` + Command string `yaml:"command,omitempty"` + Replicas int `yaml:"replicas,omitempty" valid:"length(0|25)"` + HealthPath string `yaml:"healthPath,omitempty"` + kinds.Dependencies `yaml:",inline,omitempty" json:"dependencies,omitempty"` +} diff --git a/internal/core/manifests/kinds/services/service.go b/internal/core/manifests/kinds/services/service.go new file mode 100644 index 0000000..70f1900 --- /dev/null +++ b/internal/core/manifests/kinds/services/service.go @@ -0,0 +1,78 @@ +package services + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Service)(nil) + _ manifests.Dependencies = (*Service)(nil) + _ manifests.MetaTable = (*Service)(nil) + _ manifests.Defaultable = (*Service)(nil) + _ manifests.Prepare = (*Service)(nil) + _ manifests.Marshaler = (*Service)(nil) + _ manifests.Unmarshaler = (*Service)(nil) +) + +type Service struct { + kinds.BaseManifest `yaml:",inline"` + + Spec struct { + Containers []Container `yaml:"containers" valid:"required,length(1|50)"` + } `yaml:"spec" valid:"required"` + + kinds.Dependencies `yaml:",inline" json:",inline"` + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +func (s *Service) GetID() string { + return kinds.FormManifestID(s.Namespace, s.Kind, s.Name) +} + +func (s *Service) GetKind() string { + return s.Kind +} + +func (s *Service) GetName() string { + return s.Name +} + +func (s *Service) GetNamespace() string { + return s.Namespace +} + +func (s *Service) GetDependsOn() []string { + return s.DependsOn +} + +func (s *Service) GetMeta() manifests.Meta { + return s.Meta +} + +func (s *Service) Default() { + s.Namespace = manifests.DefaultNamespace + s.Meta = kinds.DefaultMeta +} + +func (s *Service) Prepare() { + if s.Namespace == "" { + s.Namespace = manifests.DefaultNamespace + } +} + +func (s *Service) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(s) +} + +func (s *Service) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(s) +} + +func (s *Service) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, s) +} + +func (s *Service) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, s) +} diff --git a/internal/core/manifests/kinds/tests/api/http.go b/internal/core/manifests/kinds/tests/api/http.go new file mode 100644 index 0000000..26619e4 --- /dev/null +++ b/internal/core/manifests/kinds/tests/api/http.go @@ -0,0 +1,87 @@ +package api + +import ( + "fmt" + + "github.com/apiqube/cli/internal/core/manifests/kinds/tests" + + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Http)(nil) + _ manifests.Dependencies = (*Http)(nil) + _ manifests.MetaTable = (*Http)(nil) + _ manifests.Defaultable = (*Http)(nil) + _ manifests.Prepare = (*Http)(nil) + _ manifests.Marshaler = (*Http)(nil) + _ manifests.Unmarshaler = (*Http)(nil) +) + +type Http struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + Server string `yaml:"server,omitempty" json:"server,omitempty"` + Cases []HttpCase `yaml:"cases" valid:"required,length(1|100)" json:"cases"` + } `yaml:"spec" json:"spec" valid:"required"` + + kinds.Dependencies `yaml:",inline" json:",inline"` + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +type HttpCase struct { + tests.HttpCase `yaml:",inline" json:",inline"` +} + +func (h *Http) GetID() string { + return fmt.Sprintf("%s.%s.%s", h.Namespace, h.Kind, h.Name) +} + +func (h *Http) GetKind() string { + return h.Kind +} + +func (h *Http) GetName() string { + return h.Name +} + +func (h *Http) GetNamespace() string { + return h.Namespace +} + +func (h *Http) GetDependsOn() []string { + return h.DependsOn +} + +func (h *Http) Default() { + h.Namespace = manifests.DefaultNamespace + h.Meta = kinds.DefaultMeta +} + +func (h *Http) GetMeta() manifests.Meta { + return h.Meta +} + +func (h *Http) Prepare() { + if h.Namespace == "" { + h.Namespace = manifests.DefaultNamespace + } +} + +func (h *Http) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(h) +} + +func (h *Http) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(h) +} + +func (h *Http) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, h) +} + +func (h *Http) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, h) +} diff --git a/internal/core/manifests/kinds/tests/base.go b/internal/core/manifests/kinds/tests/base.go new file mode 100644 index 0000000..a054958 --- /dev/null +++ b/internal/core/manifests/kinds/tests/base.go @@ -0,0 +1,46 @@ +package tests + +import ( + "time" +) + +type HttpCase struct { + Name string `yaml:"name" json:"name" valid:"required"` + Method string `yaml:"method" json:"method" valid:"required,uppercase,in(GET|POST|PUT|DELETE)"` + Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty"` + Url string `yaml:"url,omitempty" json:"url,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + Body map[string]any `yaml:"body,omitempty" json:"body,omitempty"` + Assert Assert `yaml:"assert,omitempty" json:"assert,omitempty"` + Save Save `yaml:"save,omitempty" json:"save,omitempty"` + Pass Pass `yaml:"pass,omitempty" json:"pass,omitempty"` + Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` + Parallel bool `yaml:"async,omitempty" json:"async,omitempty"` +} + +type Assert struct { + Assertions []*AssertElement `yaml:",inline,omitempty" json:",inline,omitempty"` +} + +type AssertElement struct { + Target string `yaml:"target,omitempty" json:"target,omitempty"` + Equals any `yaml:"equals,omitempty" json:"equals,omitempty"` + Contains string `yaml:"contains,omitempty" json:"contains,omitempty"` + Exists bool `yaml:"exists,omitempty" json:"exists,omitempty"` + Template string `yaml:"template,omitempty" json:"template,omitempty"` +} + +type Save struct { + Json map[string]string `yaml:"json,omitempty" json:"json,omitempty"` + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` + Status bool `yaml:"status,omitempty" json:"status,omitempty"` + Body bool `yaml:"body,omitempty" json:"body,omitempty"` + All bool `yaml:"all,omitempty" json:"all,omitempty"` + Group string `yaml:"group,omitempty" json:"group,omitempty"` +} + +type Pass struct { + From string `yaml:"from" json:"from"` + Map map[string]string `yaml:"map,omitempty" json:"map,omitempty"` + Inline bool `yaml:"inline,omitempty" json:"inline,omitempty"` +} diff --git a/internal/core/manifests/kinds/tests/http.go b/internal/core/manifests/kinds/tests/http.go deleted file mode 100644 index 47888f1..0000000 --- a/internal/core/manifests/kinds/tests/http.go +++ /dev/null @@ -1,74 +0,0 @@ -package tests - -import ( - "fmt" - "time" - - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds" -) - -var ( - _ manifests.Manifest = (*Http)(nil) - _ manifests.Defaultable[*Http] = (*Http)(nil) -) - -type Http struct { - kinds.BaseManifest `yaml:",inline"` - - Spec struct { - Server string `yaml:"server,omitempty"` - Cases []HttpCase `yaml:"cases" valid:"required,length(1|100)"` - } `yaml:"spec" valid:"required"` -} - -type HttpCase struct { - Name string `yaml:"name" valid:"required"` - Method string `yaml:"method" valid:"required,uppercase,in(GET|POST|PUT|DELETE)"` - Endpoint string `yaml:"endpoint,omitempty"` - Url string `yaml:"url,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - Body map[string]interface{} `yaml:"body,omitempty"` - Expected *HttpExpect `yaml:"expected,omitempty"` - Extract *HttpExtractRule `yaml:"extract,omitempty"` - Timeout time.Duration `yaml:"timeout,omitempty"` - Async bool `yaml:"async,omitempty"` - Repeats int `yaml:"repeats,omitempty"` -} - -type HttpExpect struct { - Code int `yaml:"code" valid:"required,range(0|599)"` - Message string `yaml:"message,omitempty"` - Data map[string]interface{} `yaml:"data,omitempty"` -} - -type HttpExtractRule struct { - Path string `yaml:"path,omitempty"` - Value string `yaml:"value,omitempty"` -} - -func (h *Http) GetID() string { - return fmt.Sprintf("%s.%s.%s", h.Namespace, h.Kind, h.Name) -} - -func (h *Http) GetKind() string { - return h.Kind -} - -func (h *Http) GetName() string { - return h.Name -} - -func (h *Http) GetNamespace() string { - return h.Namespace -} - -func (h *Http) GetDependsOn() []string { - return h.DependsOn -} - -func (h *Http) Default() *Http { - h.Namespace = manifests.DefaultNamespace - - return h -} diff --git a/internal/core/manifests/kinds/tests/load/http.go b/internal/core/manifests/kinds/tests/load/http.go new file mode 100644 index 0000000..03b61b5 --- /dev/null +++ b/internal/core/manifests/kinds/tests/load/http.go @@ -0,0 +1,111 @@ +package load + +import ( + "time" + + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests" +) + +var ( + _ manifests.Manifest = (*Http)(nil) + _ manifests.Dependencies = (*Http)(nil) + _ manifests.MetaTable = (*Http)(nil) + _ manifests.Defaultable = (*Http)(nil) + _ manifests.Prepare = (*Http)(nil) + _ manifests.Marshaler = (*Http)(nil) + _ manifests.Unmarshaler = (*Http)(nil) +) + +type Http struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + Server string `yaml:"server,omitempty" json:"server,omitempty"` + Cases []HttpCase `yaml:"cases" json:"cases" valid:"required,length(1|100)"` + } `yaml:"spec" json:"spec" valid:"required"` + + kinds.Dependencies `yaml:",inline" json:",inline"` + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +type HttpCase struct { + tests.HttpCase `yaml:",inline" json:",inline" valid:"in(constant|ramp|wave|step)"` + + Type string `yaml:"type,omitempty" json:"type,omitempty"` + Repeats int `yaml:"repeats,omitempty" json:"repeats,omitempty"` + Agents int `yaml:"agents,omitempty" json:"agents,omitempty"` + RPS int `yaml:"rps,omitempty" json:"rps,omitempty"` + Ramp *RampConfig `yaml:"ramp,omitempty" json:"ramp,omitempty"` + Wave *WaveConfig `yaml:"wave,omitempty" json:"wave,omitempty"` + Step *StepConfig `yaml:"step,omitempty" json:"step,omitempty"` + Duration time.Duration `yaml:"duration,omitempty" json:"duration,omitempty"` + SaveEvery int `yaml:"saveEvery,omitempty" json:"saveEvery,omitempty"` +} + +type WaveConfig struct { + Low int `yaml:"low,omitempty" json:"low,omitempty"` + High int `yaml:"high,omitempty" json:"high,omitempty"` + Delta int `yaml:"delta,omitempty" json:"delta,omitempty"` +} + +type RampConfig struct { + Start int `yaml:"start,omitempty" json:"start,omitempty"` + End int `yaml:"end,omitempty" json:"end,omitempty"` +} + +type StepConfig struct { + Pause time.Duration `yaml:"pause,omitempty" json:"pause,omitempty"` +} + +func (h *Http) GetID() string { + return kinds.FormManifestID(h.Namespace, h.Kind, h.Name) +} + +func (h *Http) GetKind() string { + return h.Kind +} + +func (h *Http) GetName() string { + return h.Name +} + +func (h *Http) GetNamespace() string { + return h.Namespace +} + +func (h *Http) GetDependsOn() []string { + return h.DependsOn +} + +func (h *Http) GetMeta() manifests.Meta { + return h.Meta +} + +func (h *Http) Default() { + h.Namespace = manifests.DefaultNamespace + h.Meta = kinds.DefaultMeta +} + +func (h *Http) Prepare() { + if h.Namespace == "" { + h.Namespace = manifests.DefaultNamespace + } +} + +func (h *Http) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(h) +} + +func (h *Http) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(h) +} + +func (h *Http) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, h) +} + +func (h *Http) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, h) +} diff --git a/internal/core/manifests/kinds/values/values.go b/internal/core/manifests/kinds/values/values.go index 4cc8144..292536d 100644 --- a/internal/core/manifests/kinds/values/values.go +++ b/internal/core/manifests/kinds/values/values.go @@ -1 +1,78 @@ package values + +import ( + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds" +) + +var ( + _ manifests.Manifest = (*Values)(nil) + _ manifests.Defaultable[*Values] = (*Values)(nil) + _ manifests.Marshaler = (*Values)(nil) + _ manifests.Unmarshaler = (*Values)(nil) + _ manifests.MetaTable = (*Values)(nil) + _ manifests.Prepare = (*Values)(nil) +) + +type Values struct { + kinds.BaseManifest `yaml:",inline" json:",inline"` + + Spec struct { + Content `yaml:",inline" json:",inline"` + } `yaml:"spec" valid:"required"` + + Meta kinds.Meta `yaml:"-" json:"meta"` +} + +type Content struct { + Values map[string]any `yaml:",inline" json:",inline"` +} + +func (v *Values) GetID() string { + return kinds.FormManifestID(v.Namespace, v.Kind, v.Name) +} + +func (v *Values) GetKind() string { + return v.Kind +} + +func (v *Values) GetName() string { + return v.Name +} + +func (v *Values) GetNamespace() string { + return v.Namespace +} + +func (v *Values) GetMeta() manifests.Meta { + return v.Meta +} + +func (v *Values) Default() *Values { + v.Namespace = manifests.DefaultNamespace + v.Meta = kinds.DefaultMeta + + return v +} + +func (v *Values) Prepare() { + if v.Namespace == "" { + v.Namespace = manifests.DefaultNamespace + } +} + +func (v *Values) MarshalYAML() ([]byte, error) { + return kinds.BaseMarshalYAML(v) +} + +func (v *Values) MarshalJSON() ([]byte, error) { + return kinds.BaseMarshalJSON(v) +} + +func (v *Values) UnmarshalYAML(bytes []byte) error { + return kinds.BaseUnmarshalYAML(bytes, v) +} + +func (v *Values) UnmarshalJSON(bytes []byte) error { + return kinds.BaseUnmarshalJSON(bytes, v) +} diff --git a/internal/core/yaml/loader.go b/internal/core/manifests/loader/loader.go similarity index 83% rename from internal/core/yaml/loader.go rename to internal/core/manifests/loader/loader.go index d6b30ad..e3b4ee6 100644 --- a/internal/core/yaml/loader.go +++ b/internal/core/manifests/loader/loader.go @@ -1,4 +1,4 @@ -package yaml +package loader import ( "fmt" @@ -6,9 +6,11 @@ import ( "path/filepath" "strings" - "github.com/apiqube/cli/internal/ui" + "github.com/apiqube/cli/internal/core/manifests/parsing" - "github.com/apiqube/cli/internal/manifests" + "github.com/apiqube/cli/ui" + + "github.com/apiqube/cli/internal/core/manifests" ) func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { @@ -34,7 +36,7 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { return nil, err } - parsedManifests, err = ParseManifests(content) + parsedManifests, err = parsing.ParseManifests(content) if err != nil { return nil, fmt.Errorf("in file %s: %w", file.Name(), err) } diff --git a/internal/core/yaml/parse.go b/internal/core/manifests/parsing/parse.go similarity index 75% rename from internal/core/yaml/parse.go rename to internal/core/manifests/parsing/parse.go index c1c92c2..c217bf7 100644 --- a/internal/core/yaml/parse.go +++ b/internal/core/manifests/parsing/parse.go @@ -1,15 +1,15 @@ -package yaml +package parsing import ( "bytes" "fmt" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/manifests/kinds/load" - "github.com/apiqube/cli/internal/manifests/kinds/server" - "github.com/apiqube/cli/internal/manifests/kinds/service" - "github.com/apiqube/cli/internal/manifests/kinds/tests" - "github.com/apiqube/cli/internal/ui" + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/internal/core/manifests/kinds/load" + "github.com/apiqube/cli/internal/core/manifests/kinds/servers" + "github.com/apiqube/cli/internal/core/manifests/kinds/services" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests" + "github.com/apiqube/cli/ui" "gopkg.in/yaml.v3" ) @@ -36,14 +36,14 @@ func ParseManifests(data []byte) ([]manifests.Manifest, error) { switch raw.Kind { case manifests.ServerManifestKind: - var s server.Server + var s servers.Server if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } m = &s case manifests.ServiceManifestKind: - var s service.Service + var s services.Service if err := yaml.Unmarshal(doc, s.Default()); err != nil { return nil, err } diff --git a/internal/core/store/db.go b/internal/core/store/db.go index 9a03ce6..7fa0413 100644 --- a/internal/core/store/db.go +++ b/internal/core/store/db.go @@ -5,12 +5,10 @@ import ( "fmt" "os" "strings" - "sync" "github.com/adrg/xdg" - "github.com/apiqube/cli/internal/manifests" - "github.com/apiqube/cli/internal/ui" - parcer "github.com/apiqube/cli/internal/yaml" + "github.com/apiqube/cli/internal/core/manifests" + parcer "github.com/apiqube/cli/internal/core/yaml" "github.com/dgraph-io/badger/v4" "gopkg.in/yaml.v3" ) @@ -19,64 +17,35 @@ const ( BadgerDatabaseDirPath = "qube/storage" ) -var ( +const ( manifestListKeyPrefix = "manifest_list:" ) -var ( - instance *Storage - once sync.Once -) - type Storage struct { - db *badger.DB - enabled bool - initialized bool + db *badger.DB } -func Init() { - once.Do(func() { - path, err := xdg.DataFile(BadgerDatabaseDirPath) - if err != nil { - ui.Errorf("Failed to open database: %v", err) - return - } - - if err = os.MkdirAll(path, os.ModePerm); err != nil { - ui.Errorf("Failed to create database: %v", err) - return - } - - db, err := badger.Open(badger.DefaultOptions(path).WithLogger(nil)) - if err != nil { - ui.Errorf("Failed to open database: %v", err) - return - } +func NewStorage() (*Storage, error) { + path, err := xdg.DataFile(BadgerDatabaseDirPath) + if err != nil { + return nil, fmt.Errorf("error getting data file path: %v", err) + } - instance = &Storage{ - db: db, - enabled: true, - initialized: true, - } - }) -} + if err = os.MkdirAll(path, os.ModePerm); err != nil { + return nil, fmt.Errorf("error creating data file path: %v", err) + } -func Stop() { - if instance != nil && instance.initialized { - instance.enabled = false - instance.initialized = false - if err := instance.db.Close(); err != nil { - ui.Errorf("Failed to close database: %v", err) - } - instance = nil + db, err := badger.Open(badger.DefaultOptions(path).WithLogger(nil)) + if err != nil { + return nil, fmt.Errorf("error opening database: %v", err) } -} -func IsEnabled() bool { - return instance != nil && instance.enabled + return &Storage{ + db: db, + }, nil } -func LoadManifestList() ([]string, error) { +func (s *Storage) LoadManifestList() ([]string, error) { if !IsEnabled() { return nil, nil } @@ -101,11 +70,7 @@ func LoadManifestList() ([]string, error) { return manifestList, err } -func SaveManifests(mans ...manifests.Manifest) error { - if !IsEnabled() { - return nil - } - +func (s *Storage) SaveManifests(mans ...manifests.Manifest) error { return instance.db.Update(func(txn *badger.Txn) error { var data []byte var err error @@ -129,11 +94,7 @@ func SaveManifests(mans ...manifests.Manifest) error { }) } -func LoadManifests(ids ...string) ([]manifests.Manifest, error) { - if !IsEnabled() { - return nil, nil - } - +func (s *Storage) LoadManifests(ids ...string) ([]manifests.Manifest, error) { var results []manifests.Manifest var rErr error @@ -170,11 +131,3 @@ func LoadManifests(ids ...string) ([]manifests.Manifest, error) { return results, errors.Join(rErr, err) } - -func genManifestKey(id string) []byte { - return []byte(id) -} - -func genManifestListKey(id string) []byte { - return []byte(fmt.Sprintf("%s%s", manifestListKeyPrefix, id)) -} diff --git a/internal/core/store/helpers.go b/internal/core/store/helpers.go new file mode 100644 index 0000000..9f0ecd7 --- /dev/null +++ b/internal/core/store/helpers.go @@ -0,0 +1,11 @@ +package store + +import "fmt" + +func genManifestKey(id string) []byte { + return []byte(id) +} + +func genManifestListKey(id string) []byte { + return []byte(fmt.Sprintf("%s%s", manifestListKeyPrefix, id)) +} diff --git a/internal/core/store/independ.go b/internal/core/store/independ.go new file mode 100644 index 0000000..0b28446 --- /dev/null +++ b/internal/core/store/independ.go @@ -0,0 +1,69 @@ +package store + +import ( + "sync" + + "github.com/apiqube/cli/internal/core/manifests" + "github.com/apiqube/cli/ui" +) + +var ( + instance *Storage + once sync.Once + enabled, initialized bool +) + +func Init() { + once.Do(func() { + db, err := NewStorage() + if err != nil { + ui.Errorf("Error initializing storage: %v", err) + } + + instance = db + enabled = true + initialized = true + }) +} + +func Stop() { + if instance != nil && initialized { + enabled = false + initialized = false + if err := instance.db.Close(); err != nil { + ui.Errorf("Failed to close database: %v", err) + } + instance = nil + } +} + +func IsEnabled() bool { + return instance != nil && enabled +} + +func LoadManifestList() ([]string, error) { + if !IsEnabled() { + ui.Errorf("Database instance not ready") + return nil, nil + } + + return instance.LoadManifestList() +} + +func SaveManifests(mans ...manifests.Manifest) error { + if !IsEnabled() { + ui.Errorf("Database instance not ready") + return nil + } + + return instance.SaveManifests(mans...) +} + +func LoadManifests(ids ...string) ([]manifests.Manifest, error) { + if !IsEnabled() { + ui.Errorf("Database instance not ready") + return nil, nil + } + + return instance.LoadManifests(ids...) +} diff --git a/internal/core/yaml/saver.go b/internal/core/yaml/saver.go deleted file mode 100644 index 5fa4968..0000000 --- a/internal/core/yaml/saver.go +++ /dev/null @@ -1,47 +0,0 @@ -package yaml - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - - "github.com/adrg/xdg" - "github.com/apiqube/cli/internal/manifests" - "gopkg.in/yaml.v3" -) - -func SaveManifestsAsCombined(mans ...manifests.Manifest) error { - fileName := fmt.Sprintf("/combined-%s.yaml", mans[0].GetNamespace()) - - filePath, err := xdg.DataFile(manifests.CombinedManifestsDirPath + fileName) - if err != nil { - panic(err) - } - - if err = os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil { - return err - } - - var buf bytes.Buffer - var data []byte - - for i, manifest := range mans { - data, err = yaml.Marshal(manifest) - if err != nil { - return fmt.Errorf("failed to marshal manifest %d: %w", i, err) - } - - if i > 0 { - buf.WriteString("---\n") - } - - buf.Write(data) - } - - if err = os.WriteFile(filePath, buf.Bytes(), 0o644); err != nil { - return fmt.Errorf("failed to write file: %w", err) - } - - return nil -} diff --git a/plugins/http/http_plugin.go b/plugins/http/http_plugin.go deleted file mode 100644 index 2304f7d..0000000 --- a/plugins/http/http_plugin.go +++ /dev/null @@ -1,44 +0,0 @@ -package http - -import ( - "bytes" - "fmt" - "net/http" - - "github.com/apiqube/cli/core/plan" - "github.com/apiqube/cli/plugins" -) - -func init() { - plugins.Register(Plugin{}) -} - -type Plugin struct{} - -func (p Plugin) Name() string { - return "http" -} - -func (p Plugin) Execute(step plan.StepConfig, ctx interface{}) (plugins.PluginResult, error) { - req, _ := http.NewRequest(step.Method, step.URL, bytes.NewBuffer([]byte(step.Body))) - for k, v := range step.Headers { - req.Header.Set(k, v) - } - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return plugins.PluginResult{Success: false}, err - } - - defer func() { - if err = resp.Body.Close(); err != nil { - fmt.Printf("Error closing response body %v\n", err) - } - }() - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return plugins.PluginResult{Success: true}, nil - } - - return plugins.PluginResult{Success: false}, nil -} diff --git a/plugins/interface.go b/plugins/interface.go deleted file mode 100644 index 145a1f0..0000000 --- a/plugins/interface.go +++ /dev/null @@ -1,32 +0,0 @@ -package plugins - -import ( - "fmt" - - "github.com/apiqube/cli/core/plan" -) - -type Plugin interface { - Name() string - Execute(step plan.StepConfig, ctx interface{}) (PluginResult, error) -} - -type PluginResult struct { - Success bool - Code int - Message string -} - -var registry = map[string]Plugin{} - -func Register(p Plugin) { - registry[p.Name()] = p -} - -func GetPlugin(name string) (Plugin, error) { - p, ok := registry[name] - if !ok { - return nil, fmt.Errorf("plugin '%s' not found", name) - } - return p, nil -} diff --git a/internal/ui/console.go b/ui/console.go similarity index 100% rename from internal/ui/console.go rename to ui/console.go diff --git a/internal/ui/elements.go b/ui/elements.go similarity index 100% rename from internal/ui/elements.go rename to ui/elements.go diff --git a/internal/ui/model.go b/ui/model.go similarity index 100% rename from internal/ui/model.go rename to ui/model.go diff --git a/internal/ui/styles.go b/ui/styles.go similarity index 100% rename from internal/ui/styles.go rename to ui/styles.go diff --git a/internal/ui/ui.go b/ui/ui.go similarity index 100% rename from internal/ui/ui.go rename to ui/ui.go From 07ce6fc71a98e4e02540eeb0c32c3634e612521d Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 18:33:27 +0200 Subject: [PATCH 11/13] fix(manifests): Values manifest fixed --- internal/core/manifests/kinds/values/values.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/core/manifests/kinds/values/values.go b/internal/core/manifests/kinds/values/values.go index 292536d..56a4c47 100644 --- a/internal/core/manifests/kinds/values/values.go +++ b/internal/core/manifests/kinds/values/values.go @@ -6,12 +6,12 @@ import ( ) var ( - _ manifests.Manifest = (*Values)(nil) - _ manifests.Defaultable[*Values] = (*Values)(nil) - _ manifests.Marshaler = (*Values)(nil) - _ manifests.Unmarshaler = (*Values)(nil) - _ manifests.MetaTable = (*Values)(nil) - _ manifests.Prepare = (*Values)(nil) + _ manifests.Manifest = (*Values)(nil) + _ manifests.Defaultable = (*Values)(nil) + _ manifests.Marshaler = (*Values)(nil) + _ manifests.Unmarshaler = (*Values)(nil) + _ manifests.MetaTable = (*Values)(nil) + _ manifests.Prepare = (*Values)(nil) ) type Values struct { @@ -48,11 +48,9 @@ func (v *Values) GetMeta() manifests.Meta { return v.Meta } -func (v *Values) Default() *Values { +func (v *Values) Default() { v.Namespace = manifests.DefaultNamespace v.Meta = kinds.DefaultMeta - - return v } func (v *Values) Prepare() { From 9bdafa3c5e3c80825f9388e677ff6b3b8b17667b Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 20:14:26 +0200 Subject: [PATCH 12/13] feat(manifest): added hashing for manifests --- Taskfile.yml | 2 +- cmd/cli/apply.go | 5 +- cmd/cli/root.go | 15 --- cmd/main.go | 13 +-- examples/{simple => combined}/combined.yaml | 0 examples/simple/http_test.yaml | 5 - internal/core/collections/priority_queue.go | 27 ++++- .../core/manifests/depends/dependencies.go | 16 +-- internal/core/manifests/hash/hash.go | 25 ++++ internal/core/manifests/loader/loader.go | 110 +++++++++++++++--- internal/core/manifests/parsing/parse.go | 18 +-- internal/core/store/db.go | 60 +++++++++- internal/core/store/helpers.go | 4 + internal/core/store/independ.go | 41 ++++++- 14 files changed, 262 insertions(+), 79 deletions(-) rename examples/{simple => combined}/combined.yaml (100%) create mode 100644 internal/core/manifests/hash/hash.go diff --git a/Taskfile.yml b/Taskfile.yml index ae6f44e..df13914 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -3,7 +3,7 @@ version: '3' vars: BINARY_NAME: qube BUILD_DIR: C:/Users/admin/go/bin - MAIN: . + MAIN: ./cmd VERSION: sh: git describe --tags --abbrev=0 2>/dev/null || echo "dev" diff --git a/cmd/cli/apply.go b/cmd/cli/apply.go index f10195d..089d016 100644 --- a/cmd/cli/apply.go +++ b/cmd/cli/apply.go @@ -36,7 +36,6 @@ var applyCmd = &cobra.Command{ } ui.Spinner(false) - ui.Printf("Loaded %d manifests", len(mans)) var result *depends.GraphResult if result, err = depends.BuildGraphWithPriority(mans); err != nil { @@ -44,7 +43,9 @@ var applyCmd = &cobra.Command{ return } - _ = result + for i, order := range result.ExecutionOrder { + ui.Printf("#Order %d %s", i+1, order) + } ui.Spinner(false) ui.Print("Execution plan generated successfully") diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 082d4d0..0c9fce7 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -1,27 +1,12 @@ package cli import ( - "fmt" - "time" - - "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/internal/ui" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "qube", Short: "ApiQube is a powerful test manager for apps and APIs", - PreRun: func(cmd *cobra.Command, args []string) { - fmt.Println("START !!!") - ui.Init() - store.Init() - }, - PostRun: func(cmd *cobra.Command, args []string) { - store.Stop() - ui.StopWithTimeout(time.Millisecond * 250) - fmt.Println("FINISH !!!") - }, } func Execute() { diff --git a/cmd/main.go b/cmd/main.go index 6c8bbc9..9d4ef7c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,19 +1,16 @@ package main import ( - "time" - + "github.com/apiqube/cli/cmd/cli" "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/internal/ui" - "github.com/dgraph-io/badger/v4/badger/cmd" + "github.com/apiqube/cli/ui" + "time" ) func main() { ui.Init() - defer ui.StopWithTimeout(time.Microsecond * 250) - store.Init() defer store.Stop() - - cmd.Execute() + cli.Execute() + ui.StopWithTimeout(time.Second) } diff --git a/examples/simple/combined.yaml b/examples/combined/combined.yaml similarity index 100% rename from examples/simple/combined.yaml rename to examples/combined/combined.yaml diff --git a/examples/simple/http_test.yaml b/examples/simple/http_test.yaml index caa961d..6fafea5 100644 --- a/examples/simple/http_test.yaml +++ b/examples/simple/http_test.yaml @@ -1,10 +1,7 @@ version: 1 - kind: HttpTest - metadata: name: simple-http-test - spec: server: simple-server cases: @@ -33,13 +30,11 @@ spec: password: "example_password" expected: code: 200 - - name: user-fetch method: GET endpoint: /users/{id} expected: code: 404 message: "User not found" - dependsOn: - default.Service.simple-service diff --git a/internal/core/collections/priority_queue.go b/internal/core/collections/priority_queue.go index 140c556..d919091 100644 --- a/internal/core/collections/priority_queue.go +++ b/internal/core/collections/priority_queue.go @@ -1,6 +1,9 @@ package collections -import "container/heap" +import ( + "container/heap" + "fmt" +) type PriorityQueue[T any] struct { nodes []T @@ -8,10 +11,15 @@ type PriorityQueue[T any] struct { } func NewPriorityQueue[T any](less func(a, b T) bool) *PriorityQueue[T] { - return &PriorityQueue[T]{less: less} + return &PriorityQueue[T]{ + nodes: make([]T, 0), + less: less, + } } -func (pq *PriorityQueue[T]) Len() int { return len(pq.nodes) } +func (pq *PriorityQueue[T]) Len() int { + return len(pq.nodes) +} func (pq *PriorityQueue[T]) Less(i, j int) bool { return pq.less(pq.nodes[i], pq.nodes[j]) @@ -22,13 +30,22 @@ func (pq *PriorityQueue[T]) Swap(i, j int) { } func (pq *PriorityQueue[T]) Push(x any) { - pq.nodes = append(pq.nodes, x) + item, ok := x.(T) + if !ok { + panic(fmt.Sprintf("invalid type: expected %T, got %T", *new(T), x)) + } + pq.nodes = append(pq.nodes, item) heap.Fix(pq, len(pq.nodes)-1) } func (pq *PriorityQueue[T]) Pop() any { + if len(pq.nodes) == 0 { + return nil + } item := pq.nodes[0] pq.nodes = pq.nodes[1:] - heap.Fix(pq, 0) + if len(pq.nodes) > 0 { + heap.Fix(pq, 0) + } return item } diff --git a/internal/core/manifests/depends/dependencies.go b/internal/core/manifests/depends/dependencies.go index 539d59a..017d8a1 100644 --- a/internal/core/manifests/depends/dependencies.go +++ b/internal/core/manifests/depends/dependencies.go @@ -45,14 +45,16 @@ func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) { } } - for _, node := range mans { - id := node.GetID() - for _, depID := range node.GetDependsOn() { - if depID == id { - return nil, fmt.Errorf("dependency error: %s manifest cannot depend on itself", id) + for _, man := range mans { + if dep, has := man.(manifests.Dependencies); has { + id := man.GetID() + for _, depID := range dep.GetDependsOn() { + if depID == id { + return nil, fmt.Errorf("dependency error: %s manifest cannot depend on itself", id) + } + graph[depID] = append(graph[depID], id) + inDegree[id]++ } - graph[depID] = append(graph[depID], id) - inDegree[id]++ } } diff --git a/internal/core/manifests/hash/hash.go b/internal/core/manifests/hash/hash.go new file mode 100644 index 0000000..e0c66b7 --- /dev/null +++ b/internal/core/manifests/hash/hash.go @@ -0,0 +1,25 @@ +package hash + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" +) + +func CalculateHashWithPath(filePath string, content []byte) (string, error) { + fileInfo, err := os.Stat(filePath) + if err != nil { + return "", err + } + + modTime := fileInfo.ModTime().UnixNano() + + hasher := sha256.New() + hasher.Write(content) + hasher.Write([]byte(fmt.Sprintf("%d", modTime))) + + hash := hex.EncodeToString(hasher.Sum(nil)) + + return hash, nil +} diff --git a/internal/core/manifests/loader/loader.go b/internal/core/manifests/loader/loader.go index e3b4ee6..ccea21b 100644 --- a/internal/core/manifests/loader/loader.go +++ b/internal/core/manifests/loader/loader.go @@ -2,11 +2,13 @@ package loader import ( "fmt" + "github.com/apiqube/cli/internal/core/manifests/hash" + "github.com/apiqube/cli/internal/core/manifests/parsing" + "github.com/apiqube/cli/internal/core/store" "os" "path/filepath" "strings" - - "github.com/apiqube/cli/internal/core/manifests/parsing" + "time" "github.com/apiqube/cli/ui" @@ -19,44 +21,114 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { return nil, err } - manifestsSet := make(map[string]struct{}) - var parsedManifests []manifests.Manifest - var result []manifests.Manifest - var counter int + existingHashes, err := store.LoadManifestHashes() + if err != nil { + return nil, fmt.Errorf("failed to load hashes: %w", err) + } + + hashCache := make(map[string]bool) + for _, h := range existingHashes { + hashCache[h] = true + } - var content []byte + var ( + newManifests []manifests.Manifest + existingIDs []string + manifestsSet = make(map[string]struct{}) + processedHashes = make(map[string]bool) + exsitis bool + ) for _, file := range files { if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { continue } - content, err = os.ReadFile(filepath.Join(dir, file.Name())) + var content []byte + + filePath := filepath.Join(dir, file.Name()) + + content, err = os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("error reading file %s: %w", filePath, err) + } + + var fileHash string + fileHash, err = hash.CalculateHashWithPath(filePath, content) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to calculate h for %s: %w", filePath, err) } - parsedManifests, err = parsing.ParseManifests(content) + if hashCache[fileHash] { + ui.Infof("Manifest file %s unchanged (%s) - using cache", file.Name(), shortHash(fileHash)) + exsitis = true + } + + var parsedManifests []manifests.Manifest + + parsedManifests, err = parsing.ParseYamlManifests(content) if err != nil { return nil, fmt.Errorf("in file %s: %w", file.Name(), err) } for _, m := range parsedManifests { - if _, ok := manifestsSet[m.GetID()]; ok { - ui.Infof("Manifest: %s loaded", m.GetID()) + if !exsitis { + manifestID := m.GetID() + + if _, ok := manifestsSet[manifestID]; ok { + ui.Warningf("Manifest: %s (from %s) already processed", manifestID, file.Name()) + continue + } + + if meta, ok := m.(manifests.MetaTable); ok { + meta.GetMeta().SetHash(fileHash) + now := time.Now() + meta.GetMeta().SetCreatedAt(now) + meta.GetMeta().SetUpdatedAt(now) + } + + manifestsSet[manifestID] = struct{}{} + newManifests = append(newManifests, m) + processedHashes[fileHash] = true + + ui.Successf("New manifest added: %s (h: %s)", manifestID, shortHash(fileHash)) } else { - ui.Infof("Manifest: %s cached", m.GetID()) - manifestsSet[m.GetID()] = struct{}{} - result = append(result, m) + existingIDs = append(existingIDs, m.GetID()) } + } + } + + for h := range processedHashes { + if err = store.SaveManifestHash(h); err != nil { + ui.Errorf("Failed to save manifest h: %s", err.Error()) + } + } - counter++ + var existingManifests []manifests.Manifest + if len(existingHashes) > 0 { + existingManifests, err = store.LoadManifests(existingIDs...) + if err != nil { + ui.Warningf("Failed to load existing manifests: %v", err) + } else { + newManifests = append(existingManifests, newManifests...) } } - if len(result) == 0 { - return nil, fmt.Errorf("manifests not found in %s", dir) + if len(newManifests) == 0 { + ui.Infof("New manifests not found") } - return result, nil + ui.Infof("Loaded %d manifests (%d new, %d from cache)", + len(newManifests), + len(newManifests)-len(existingManifests), + len(existingManifests)) + + return newManifests, nil +} + +func shortHash(fullHash string) string { + if len(fullHash) > 12 { + return fullHash[:12] + "..." + } + return fullHash } diff --git a/internal/core/manifests/parsing/parse.go b/internal/core/manifests/parsing/parse.go index c217bf7..aeea1e5 100644 --- a/internal/core/manifests/parsing/parse.go +++ b/internal/core/manifests/parsing/parse.go @@ -3,12 +3,12 @@ package parsing import ( "bytes" "fmt" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" "github.com/apiqube/cli/internal/core/manifests" - "github.com/apiqube/cli/internal/core/manifests/kinds/load" "github.com/apiqube/cli/internal/core/manifests/kinds/servers" "github.com/apiqube/cli/internal/core/manifests/kinds/services" - "github.com/apiqube/cli/internal/core/manifests/kinds/tests" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/load" "github.com/apiqube/cli/ui" "gopkg.in/yaml.v3" ) @@ -17,7 +17,7 @@ type RawManifest struct { Kind string `yaml:"kind"` } -func ParseManifests(data []byte) ([]manifests.Manifest, error) { +func ParseYamlManifests(data []byte) ([]manifests.Manifest, error) { docs := bytes.Split(data, []byte("\n---")) var results []manifests.Manifest @@ -37,34 +37,34 @@ func ParseManifests(data []byte) ([]manifests.Manifest, error) { switch raw.Kind { case manifests.ServerManifestKind: var s servers.Server - if err := yaml.Unmarshal(doc, s.Default()); err != nil { + if err := s.UnmarshalYAML(doc); err != nil { return nil, err } m = &s case manifests.ServiceManifestKind: var s services.Service - if err := yaml.Unmarshal(doc, s.Default()); err != nil { + if err := s.UnmarshalYAML(doc); err != nil { return nil, err } m = &s case manifests.HttpTestManifestKind: - var h tests.Http - if err := yaml.Unmarshal(doc, h.Default()); err != nil { + var h api.Http + if err := h.UnmarshalYAML(doc); err != nil { return nil, err } m = &h case manifests.HttpLoadTestManifestKind: var h load.Http - if err := yaml.Unmarshal(doc, h.Default()); err != nil { + if err := h.UnmarshalYAML(doc); err != nil { return nil, err } m = &h default: - ui.Errorf("Unknown s kind %s", raw.Kind) + ui.Errorf("Unknown manifest kind %s", raw.Kind) } results = append(results, m) diff --git a/internal/core/store/db.go b/internal/core/store/db.go index 7fa0413..b9c90ab 100644 --- a/internal/core/store/db.go +++ b/internal/core/store/db.go @@ -3,12 +3,12 @@ package store import ( "errors" "fmt" + "github.com/apiqube/cli/internal/core/manifests/parsing" "os" "strings" "github.com/adrg/xdg" "github.com/apiqube/cli/internal/core/manifests" - parcer "github.com/apiqube/cli/internal/core/yaml" "github.com/dgraph-io/badger/v4" "gopkg.in/yaml.v3" ) @@ -19,6 +19,7 @@ const ( const ( manifestListKeyPrefix = "manifest_list:" + manifestHashKeyPrefix = "manifest_hash:" ) type Storage struct { @@ -115,7 +116,7 @@ func (s *Storage) LoadManifests(ids ...string) ([]manifests.Manifest, error) { var mans []manifests.Manifest if err = item.Value(func(data []byte) error { - if mans, err = parcer.ParseManifests(data); err != nil { + if mans, err = parsing.ParseYamlManifests(data); err != nil { return err } @@ -131,3 +132,58 @@ func (s *Storage) LoadManifests(ids ...string) ([]manifests.Manifest, error) { return results, errors.Join(rErr, err) } + +func (s *Storage) CheckManifestHash(hash string) (bool, error) { + var result = true + var err error + + err = instance.db.View(func(txn *badger.Txn) error { + _, err = txn.Get(genManifestHashKey(hash)) + if errors.Is(err, badger.ErrKeyNotFound) { + result = false + return nil + } else if err != nil { + return fmt.Errorf("error getting manifest hash: %v", err) + } + + return err + }) + + return result, err +} + +func (s *Storage) LoadManifestHashes() ([]string, error) { + var results []string + var rErr error + + err := instance.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.Prefix = []byte(manifestHashKeyPrefix) + + it := txn.NewIterator(opts) + defer it.Close() + + for it.Rewind(); it.Valid(); it.Next() { + key := it.Item().Key() + results = append(results, strings.TrimPrefix(string(key), manifestHashKeyPrefix)) + } + + return nil + }) + + return results, errors.Join(rErr, err) +} + +func (s *Storage) SaveManifestHash(hash string) error { + var err error + + err = instance.db.Update(func(txn *badger.Txn) error { + return txn.Set(genManifestHashKey(hash), []byte(hash)) + }) + + if err != nil { + return fmt.Errorf("error saving manifest hash: %v", err) + } + + return nil +} diff --git a/internal/core/store/helpers.go b/internal/core/store/helpers.go index 9f0ecd7..86289b4 100644 --- a/internal/core/store/helpers.go +++ b/internal/core/store/helpers.go @@ -9,3 +9,7 @@ func genManifestKey(id string) []byte { func genManifestListKey(id string) []byte { return []byte(fmt.Sprintf("%s%s", manifestListKeyPrefix, id)) } + +func genManifestHashKey(hash string) []byte { + return []byte(fmt.Sprintf("%s%s", manifestHashKeyPrefix, hash)) +} diff --git a/internal/core/store/independ.go b/internal/core/store/independ.go index 0b28446..c3865a8 100644 --- a/internal/core/store/independ.go +++ b/internal/core/store/independ.go @@ -42,8 +42,7 @@ func IsEnabled() bool { } func LoadManifestList() ([]string, error) { - if !IsEnabled() { - ui.Errorf("Database instance not ready") + if !isEnabled() { return nil, nil } @@ -51,8 +50,7 @@ func LoadManifestList() ([]string, error) { } func SaveManifests(mans ...manifests.Manifest) error { - if !IsEnabled() { - ui.Errorf("Database instance not ready") + if !isEnabled() { return nil } @@ -60,10 +58,41 @@ func SaveManifests(mans ...manifests.Manifest) error { } func LoadManifests(ids ...string) ([]manifests.Manifest, error) { - if !IsEnabled() { - ui.Errorf("Database instance not ready") + if !isEnabled() { return nil, nil } return instance.LoadManifests(ids...) } + +func CheckManifestHash(hash string) (bool, error) { + if !isEnabled() { + return false, nil + } + + return instance.CheckManifestHash(hash) +} + +func LoadManifestHashes() ([]string, error) { + if !isEnabled() { + return nil, nil + } + + return instance.LoadManifestHashes() +} + +func SaveManifestHash(hash string) error { + if !isEnabled() { + return nil + } + + return instance.SaveManifestHash(hash) +} + +func isEnabled() bool { + if !IsEnabled() { + ui.Errorf("Database instance not ready") + return false + } + return true +} From 098bd77bb0bc6858990189a2a13852ed71384b9e Mon Sep 17 00:00:00 2001 From: Nofre Date: Fri, 16 May 2025 20:50:13 +0200 Subject: [PATCH 13/13] chore: tiding, formatting, fixing, linting --- cmd/main.go | 9 +++- core/executor/executor.go | 25 --------- core/plan/plan.go | 54 ------------------- examples/simple/http_test_second.yaml | 5 -- examples/simple/service.yaml | 11 +++- go.mod | 1 - go.sum | 2 - internal/core/manifests/hash/hash.go | 4 +- .../manifests/kinds/internal/plan/plan.go | 2 +- internal/core/manifests/kinds/meta.go | 36 ++++++------- .../core/manifests/kinds/servers/server.go | 2 +- .../core/manifests/kinds/services/service.go | 2 +- .../core/manifests/kinds/tests/api/http.go | 2 +- .../core/manifests/kinds/tests/load/http.go | 2 +- .../core/manifests/kinds/values/values.go | 2 +- internal/core/manifests/loader/loader.go | 46 +++++++++------- internal/core/manifests/parsing/parse.go | 1 + internal/core/store/db.go | 12 ++--- 18 files changed, 76 insertions(+), 142 deletions(-) delete mode 100644 core/executor/executor.go delete mode 100644 core/plan/plan.go diff --git a/cmd/main.go b/cmd/main.go index 9d4ef7c..0f35813 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,16 +1,21 @@ package main import ( + "time" + "github.com/apiqube/cli/cmd/cli" "github.com/apiqube/cli/internal/core/store" "github.com/apiqube/cli/ui" - "time" ) func main() { ui.Init() + defer ui.Stop() + store.Init() defer store.Stop() + cli.Execute() - ui.StopWithTimeout(time.Second) + + time.Sleep(time.Second) } diff --git a/core/executor/executor.go b/core/executor/executor.go deleted file mode 100644 index fe6ed2f..0000000 --- a/core/executor/executor.go +++ /dev/null @@ -1,25 +0,0 @@ -package executor - -import ( - "fmt" - - "github.com/apiqube/cli/core/plan" - "github.com/apiqube/cli/plugins" -) - -func ExecutePlan(plan *plan.ExecutionPlan) { - for _, step := range plan.Steps { - plugin, err := plugins.GetPlugin(step.Type) - if err != nil { - fmt.Printf("❌ Unknown plugin for step '%s'\n", step.Name) - continue - } - fmt.Printf("🔧 Executing step: %s\n", step.Name) - res, err := plugin.Execute(step, nil) - if err != nil || !res.Success { - fmt.Printf("❌ Step '%s' failed: %v\n", step.Name, err) - continue - } - fmt.Printf("✅ Step '%s' passed\n", step.Name) - } -} diff --git a/core/plan/plan.go b/core/plan/plan.go deleted file mode 100644 index 5025231..0000000 --- a/core/plan/plan.go +++ /dev/null @@ -1,54 +0,0 @@ -package plan - -import ( - "encoding/json" - "os" - "time" -) - -type StepConfig struct { - Name string `json:"name"` - Type string `json:"type"` - Method string `json:"method"` - URL string `json:"url"` - Body string `json:"body"` - Headers map[string]string `json:"headers"` -} - -type ExecutionPlan struct { - Name string `json:"name"` - Steps []StepConfig `json:"steps"` - Time time.Time `json:"time"` -} - -func BuildExecutionPlan(_ string) (*ExecutionPlan, error) { - return &ExecutionPlan{ - Name: "default-plan", - Time: time.Now(), - Steps: []StepConfig{{Name: "Example", Type: "http", Method: "GET", URL: "http://localhost"}}, - }, nil -} - -func SavePlan(plan *ExecutionPlan) error { - data, err := json.MarshalIndent(plan, "", " ") - if err != nil { - return err - } - - return os.WriteFile(".testman/plan.json", data, 0o644) -} - -func LoadPlan() (*ExecutionPlan, error) { - data, err := os.ReadFile(".apiqube/plan.json") - if err != nil { - return nil, err - } - - var plan ExecutionPlan - - if err = json.Unmarshal(data, &plan); err != nil { - return nil, err - } - - return &plan, nil -} diff --git a/examples/simple/http_test_second.yaml b/examples/simple/http_test_second.yaml index baf0c90..9024b98 100644 --- a/examples/simple/http_test_second.yaml +++ b/examples/simple/http_test_second.yaml @@ -1,11 +1,8 @@ version: 1 - kind: HttpTest - metadata: name: not-simple-http-test namespace: not-simple - spec: server: simple-server cases: @@ -17,13 +14,11 @@ spec: password: "example_password" expected: code: 200 - - name: user-fetch method: GET endpoint: /users/{id} expected: code: 404 message: "User not found" - dependsOn: - default.Service.simple-service diff --git a/examples/simple/service.yaml b/examples/simple/service.yaml index c681984..10d0028 100644 --- a/examples/simple/service.yaml +++ b/examples/simple/service.yaml @@ -13,14 +13,12 @@ spec: image: some_path ports: - 8080:8080 - - 50051:50051 env: run: run-command clean: clean-command db_url: postgres:5432 replicas: 3 healthPath: /health - - name: auth-service containerName: auth dockerfile: some_path @@ -32,6 +30,15 @@ spec: db_url: postgres:5432 replicas: 6 healthPath: /health + - name: lobby-service + containerName: lobby + dockerfile: some_path + image: some_path + ports: + - 8081:8081 + env: + db_url: redis_url + replicas: 2 dependsOn: - default.Server.simple-server \ No newline at end of file diff --git a/go.mod b/go.mod index ec769e7..f7dde9d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.3 require ( github.com/adrg/xdg v0.5.3 - github.com/brianvoe/gofakeit/v7 v7.2.1 github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 diff --git a/go.sum b/go.sum index be27707..bdf6b73 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/brianvoe/gofakeit/v7 v7.2.1 h1:AGojgaaCdgq4Adzrd2uWdbGNDyX6MWNhHdQBraNfOHI= -github.com/brianvoe/gofakeit/v7 v7.2.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= diff --git a/internal/core/manifests/hash/hash.go b/internal/core/manifests/hash/hash.go index e0c66b7..c58373d 100644 --- a/internal/core/manifests/hash/hash.go +++ b/internal/core/manifests/hash/hash.go @@ -17,7 +17,9 @@ func CalculateHashWithPath(filePath string, content []byte) (string, error) { hasher := sha256.New() hasher.Write(content) - hasher.Write([]byte(fmt.Sprintf("%d", modTime))) + if _, err = fmt.Fprintf(hasher, "%d", modTime); err != nil { + return "", fmt.Errorf("failed to calculate hash: %s", err.Error()) + } hash := hex.EncodeToString(hasher.Sum(nil)) diff --git a/internal/core/manifests/kinds/internal/plan/plan.go b/internal/core/manifests/kinds/internal/plan/plan.go index bac48f2..39cf70d 100644 --- a/internal/core/manifests/kinds/internal/plan/plan.go +++ b/internal/core/manifests/kinds/internal/plan/plan.go @@ -22,7 +22,7 @@ type Plan struct { Hooks Hooks `yaml:"hooks" json:"hooks"` } `yaml:"spec" json:"spec"` - Meta kinds.Meta `yaml:"meta" json:"meta"` + Meta *kinds.Meta `yaml:"meta" json:"meta"` } type Stages struct { diff --git a/internal/core/manifests/kinds/meta.go b/internal/core/manifests/kinds/meta.go index 2c758eb..44ad0e0 100644 --- a/internal/core/manifests/kinds/meta.go +++ b/internal/core/manifests/kinds/meta.go @@ -7,7 +7,7 @@ import ( "github.com/apiqube/cli/internal/core/manifests" ) -var DefaultMeta = Meta{ +var DefaultMeta = &Meta{ Hash: "", Version: 1, CreatedAt: time.Now(), @@ -31,72 +31,72 @@ type Meta struct { LastApplied time.Time `yaml:"-" json:"lastApplied"` } -func (m Meta) GetHash() string { +func (m *Meta) GetHash() string { return m.Hash } -func (m Meta) SetHash(hash string) { +func (m *Meta) SetHash(hash string) { m.Hash = hash } -func (m Meta) GetVersion() uint8 { +func (m *Meta) GetVersion() uint8 { return m.Version } -func (m Meta) SetVersion(version uint8) { +func (m *Meta) SetVersion(version uint8) { m.Version = version } -func (m Meta) IncVersion() { +func (m *Meta) IncVersion() { if m.Version < math.MaxUint8 { m.Version++ } } -func (m Meta) GetCreatedAt() time.Time { +func (m *Meta) GetCreatedAt() time.Time { return m.CreatedAt } -func (m Meta) SetCreatedAt(createdAt time.Time) { +func (m *Meta) SetCreatedAt(createdAt time.Time) { m.CreatedAt = createdAt } -func (m Meta) GetCreatedBy() string { +func (m *Meta) GetCreatedBy() string { return m.CreatedBy } -func (m Meta) SetCreatedBy(createdBy string) { +func (m *Meta) SetCreatedBy(createdBy string) { m.CreatedBy = createdBy } -func (m Meta) GetUpdatedAt() time.Time { +func (m *Meta) GetUpdatedAt() time.Time { return m.UpdatedAt } -func (m Meta) SetUpdatedAt(updatedAt time.Time) { +func (m *Meta) SetUpdatedAt(updatedAt time.Time) { m.UpdatedAt = updatedAt } -func (m Meta) GetUpdatedBy() string { +func (m *Meta) GetUpdatedBy() string { return m.UpdatedBy } -func (m Meta) SetUpdatedBy(updatedBy string) { +func (m *Meta) SetUpdatedBy(updatedBy string) { m.UpdatedBy = updatedBy } -func (m Meta) GetUsedBy() string { +func (m *Meta) GetUsedBy() string { return m.UsedBy } -func (m Meta) SetUsedBy(usedBy string) { +func (m *Meta) SetUsedBy(usedBy string) { m.UsedBy = usedBy } -func (m Meta) GetLastApplied() time.Time { +func (m *Meta) GetLastApplied() time.Time { return m.LastApplied } -func (m Meta) SetLastApplied(lastApplied time.Time) { +func (m *Meta) SetLastApplied(lastApplied time.Time) { m.LastApplied = lastApplied } diff --git a/internal/core/manifests/kinds/servers/server.go b/internal/core/manifests/kinds/servers/server.go index a7f837c..c7a5642 100644 --- a/internal/core/manifests/kinds/servers/server.go +++ b/internal/core/manifests/kinds/servers/server.go @@ -22,7 +22,7 @@ type Server struct { Headers map[string]string `yaml:"headers,omitempty" json:"headers" valid:"-"` } `yaml:"spec" json:"spec" valid:"required"` - Meta kinds.Meta `yaml:"-" json:"meta"` + Meta *kinds.Meta `yaml:"-" json:"meta"` } func (s *Server) GetID() string { diff --git a/internal/core/manifests/kinds/services/service.go b/internal/core/manifests/kinds/services/service.go index 70f1900..ef01844 100644 --- a/internal/core/manifests/kinds/services/service.go +++ b/internal/core/manifests/kinds/services/service.go @@ -23,7 +23,7 @@ type Service struct { } `yaml:"spec" valid:"required"` kinds.Dependencies `yaml:",inline" json:",inline"` - Meta kinds.Meta `yaml:"-" json:"meta"` + Meta *kinds.Meta `yaml:"-" json:"meta"` } func (s *Service) GetID() string { diff --git a/internal/core/manifests/kinds/tests/api/http.go b/internal/core/manifests/kinds/tests/api/http.go index 26619e4..0794d63 100644 --- a/internal/core/manifests/kinds/tests/api/http.go +++ b/internal/core/manifests/kinds/tests/api/http.go @@ -28,7 +28,7 @@ type Http struct { } `yaml:"spec" json:"spec" valid:"required"` kinds.Dependencies `yaml:",inline" json:",inline"` - Meta kinds.Meta `yaml:"-" json:"meta"` + Meta *kinds.Meta `yaml:"-" json:"meta"` } type HttpCase struct { diff --git a/internal/core/manifests/kinds/tests/load/http.go b/internal/core/manifests/kinds/tests/load/http.go index 03b61b5..2da156b 100644 --- a/internal/core/manifests/kinds/tests/load/http.go +++ b/internal/core/manifests/kinds/tests/load/http.go @@ -27,7 +27,7 @@ type Http struct { } `yaml:"spec" json:"spec" valid:"required"` kinds.Dependencies `yaml:",inline" json:",inline"` - Meta kinds.Meta `yaml:"-" json:"meta"` + Meta *kinds.Meta `yaml:"-" json:"meta"` } type HttpCase struct { diff --git a/internal/core/manifests/kinds/values/values.go b/internal/core/manifests/kinds/values/values.go index 56a4c47..778f4ee 100644 --- a/internal/core/manifests/kinds/values/values.go +++ b/internal/core/manifests/kinds/values/values.go @@ -21,7 +21,7 @@ type Values struct { Content `yaml:",inline" json:",inline"` } `yaml:"spec" valid:"required"` - Meta kinds.Meta `yaml:"-" json:"meta"` + Meta *kinds.Meta `yaml:"-" json:"meta"` } type Content struct { diff --git a/internal/core/manifests/loader/loader.go b/internal/core/manifests/loader/loader.go index ccea21b..2552995 100644 --- a/internal/core/manifests/loader/loader.go +++ b/internal/core/manifests/loader/loader.go @@ -2,14 +2,15 @@ package loader import ( "fmt" - "github.com/apiqube/cli/internal/core/manifests/hash" - "github.com/apiqube/cli/internal/core/manifests/parsing" - "github.com/apiqube/cli/internal/core/store" "os" "path/filepath" "strings" "time" + "github.com/apiqube/cli/internal/core/manifests/hash" + "github.com/apiqube/cli/internal/core/manifests/parsing" + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/ui" "github.com/apiqube/cli/internal/core/manifests" @@ -32,11 +33,12 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { } var ( - newManifests []manifests.Manifest - existingIDs []string - manifestsSet = make(map[string]struct{}) - processedHashes = make(map[string]bool) - exsitis bool + mans []manifests.Manifest + existingIDs []string + manifestsSet = make(map[string]struct{}) + processedHashes = make(map[string]bool) + exists bool + newCounter, existsCounter int ) for _, file := range files { @@ -61,7 +63,10 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { if hashCache[fileHash] { ui.Infof("Manifest file %s unchanged (%s) - using cache", file.Name(), shortHash(fileHash)) - exsitis = true + exists = true + existsCounter++ + } else { + newCounter++ } var parsedManifests []manifests.Manifest @@ -72,7 +77,7 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { } for _, m := range parsedManifests { - if !exsitis { + if !exists { manifestID := m.GetID() if _, ok := manifestsSet[manifestID]; ok { @@ -88,7 +93,7 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { } manifestsSet[manifestID] = struct{}{} - newManifests = append(newManifests, m) + mans = append(mans, m) processedHashes[fileHash] = true ui.Successf("New manifest added: %s (h: %s)", manifestID, shortHash(fileHash)) @@ -110,20 +115,21 @@ func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { if err != nil { ui.Warningf("Failed to load existing manifests: %v", err) } else { - newManifests = append(existingManifests, newManifests...) + mans = append(existingManifests, mans...) } } - if len(newManifests) == 0 { - ui.Infof("New manifests not found") + if newCounter == 0 { + ui.Infof("Loaded %d manifests, new manifests not found", len(mans)) + } else { + ui.Infof("Loaded %d manifests (%d new, %d from cache)", + len(mans), + newCounter, + existsCounter, + ) } - ui.Infof("Loaded %d manifests (%d new, %d from cache)", - len(newManifests), - len(newManifests)-len(existingManifests), - len(existingManifests)) - - return newManifests, nil + return mans, nil } func shortHash(fullHash string) string { diff --git a/internal/core/manifests/parsing/parse.go b/internal/core/manifests/parsing/parse.go index aeea1e5..550c5ba 100644 --- a/internal/core/manifests/parsing/parse.go +++ b/internal/core/manifests/parsing/parse.go @@ -3,6 +3,7 @@ package parsing import ( "bytes" "fmt" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/api" "github.com/apiqube/cli/internal/core/manifests" diff --git a/internal/core/store/db.go b/internal/core/store/db.go index b9c90ab..97713b0 100644 --- a/internal/core/store/db.go +++ b/internal/core/store/db.go @@ -3,10 +3,11 @@ package store import ( "errors" "fmt" - "github.com/apiqube/cli/internal/core/manifests/parsing" "os" "strings" + "github.com/apiqube/cli/internal/core/manifests/parsing" + "github.com/adrg/xdg" "github.com/apiqube/cli/internal/core/manifests" "github.com/dgraph-io/badger/v4" @@ -134,7 +135,7 @@ func (s *Storage) LoadManifests(ids ...string) ([]manifests.Manifest, error) { } func (s *Storage) CheckManifestHash(hash string) (bool, error) { - var result = true + result := true var err error err = instance.db.View(func(txn *badger.Txn) error { @@ -175,15 +176,14 @@ func (s *Storage) LoadManifestHashes() ([]string, error) { } func (s *Storage) SaveManifestHash(hash string) error { - var err error + var rErr error - err = instance.db.Update(func(txn *badger.Txn) error { + err := instance.db.Update(func(txn *badger.Txn) error { return txn.Set(genManifestHashKey(hash), []byte(hash)) }) - if err != nil { return fmt.Errorf("error saving manifest hash: %v", err) } - return nil + return errors.Join(rErr, err) }