diff --git a/Taskfile.yml b/Taskfile.yml index d42e7dc..df13914 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,8 +2,8 @@ version: '3' vars: BINARY_NAME: qube - BUILD_DIR: ./bin - MAIN: . + BUILD_DIR: C:/Users/admin/go/bin + MAIN: ./cmd 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/cli/apply.go similarity index 50% rename from cmd/apply.go rename to cmd/cli/apply.go index 038fb61..089d016 100644 --- a/cmd/apply.go +++ b/cmd/cli/apply.go @@ -1,12 +1,10 @@ -package cmd +package cli import ( - "slices" - "time" - - "github.com/apiqube/cli/internal/manifests/depends" - "github.com/apiqube/cli/internal/ui" - "github.com/apiqube/cli/internal/yaml" + "github.com/apiqube/cli/internal/core/manifests/depends" + "github.com/apiqube/cli/internal/core/manifests/loader" + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/ui" "github.com/spf13/cobra" ) @@ -16,57 +14,49 @@ func init() { } var applyCmd = &cobra.Command{ - Use: "apply", - Short: "Apply resources from manifest file", - RunE: func(cmd *cobra.Command, args []string) error { - ui.Init() - defer func() { - time.Sleep(time.Millisecond * 100) - ui.Stop() - }() - + Use: "apply", + Short: "Apply resources from manifest file", + SilenceErrors: true, + SilenceUsage: true, + Run: func(cmd *cobra.Command, args []string) { 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) 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()) - return err + 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()) + var result *depends.GraphResult + if result, err = depends.BuildGraphWithPriority(mans); err != nil { + ui.Errorf("Failed to generate plan: %s", err.Error()) + return } - ui.Spinner(true, "Generating execution plan") - - if err = depends.GeneratePlan("./examples/simple", mans); err != nil { - ui.Errorf("Failed to generate plan: %s", err.Error()) - return err + for i, order := range result.ExecutionOrder { + ui.Printf("#Order %d %s", i+1, order) } ui.Spinner(false) ui.Print("Execution plan generated successfully") ui.Spinner(true, "Saving manifests...") - if err := yaml.SaveManifests(file, mans...); err != nil { + if err := store.SaveManifests(mans...); err != nil { ui.Error("Failed to save manifests: " + err.Error()) - return err + return } ui.Spinner(false) ui.Println("Manifests applied successfully") - - return nil }, } diff --git a/cmd/root.go b/cmd/cli/root.go similarity index 81% rename from cmd/root.go rename to cmd/cli/root.go index 5f77738..0c9fce7 100644 --- a/cmd/root.go +++ b/cmd/cli/root.go @@ -1,6 +1,8 @@ -package cmd +package cli -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" +) var rootCmd = &cobra.Command{ Use: "qube", diff --git a/cmd/version.go b/cmd/cli/version.go similarity index 61% rename from cmd/version.go rename to cmd/cli/version.go index 5685857..38acbc7 100644 --- a/cmd/version.go +++ b/cmd/cli/version.go @@ -1,4 +1,4 @@ -package cmd +package cli import ( "fmt" @@ -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) }, diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..0f35813 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "time" + + "github.com/apiqube/cli/cmd/cli" + "github.com/apiqube/cli/internal/core/store" + "github.com/apiqube/cli/ui" +) + +func main() { + ui.Init() + defer ui.Stop() + + store.Init() + defer store.Stop() + + cli.Execute() + + 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/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/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/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/examples/simple/http_test_second.yaml b/examples/simple/http_test_second.yaml new file mode 100644 index 0000000..9024b98 --- /dev/null +++ b/examples/simple/http_test_second.yaml @@ -0,0 +1,24 @@ +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/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/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/go.mod b/go.mod index c6e8442..f7dde9d 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,29 @@ module github.com/apiqube/cli go 1.24.3 require ( - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 + github.com/adrg/xdg v0.5.3 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/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 @@ -28,7 +36,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 8f7db3f..bdf6b73 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ -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/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/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= @@ -15,10 +17,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= @@ -33,27 +60,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/core/collections/priority_queue.go b/internal/core/collections/priority_queue.go new file mode 100644 index 0000000..d919091 --- /dev/null +++ b/internal/core/collections/priority_queue.go @@ -0,0 +1,51 @@ +package collections + +import ( + "container/heap" + "fmt" +) + +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]{ + nodes: make([]T, 0), + 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) { + 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:] + 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 new file mode 100644 index 0000000..017d8a1 --- /dev/null +++ b/internal/core/manifests/depends/dependencies.go @@ -0,0 +1,116 @@ +package depends + +import ( + "container/heap" + "fmt" + "strings" + + "github.com/apiqube/cli/internal/core/collections" + + "github.com/apiqube/cli/internal/core/manifests" +) + +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 + Priority int +} + +func BuildGraphWithPriority(mans []manifests.Manifest) (*GraphResult, error) { + graph := make(map[string][]string) + inDegree := make(map[string]int) + idToNode := make(map[string]manifests.Manifest) + nodePriority := make(map[string]int) + + for _, node := range mans { + id := node.GetID() + idToNode[id] = node + inDegree[id] = 0 + + parts := strings.Split(id, ".") + if len(parts) >= 2 { + kind := parts[1] + nodePriority[id] = getPriority(kind) + } + } + + 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]++ + } + } + } + + priorityQueue := collections.NewPriorityQueue[*Node](func(a, b *Node) bool { + return a.Priority > b.Priority + }) + + for id, degree := range inDegree { + if degree == 0 { + heap.Push(priorityQueue, &Node{ + ID: id, + Priority: nodePriority[id], + }) + } + } + + 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(mans) { + cyclicNodes := findCyclicNodes(inDegree) + return nil, fmt.Errorf("dependency error: сyclic dependency: %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/core/manifests/hash/hash.go b/internal/core/manifests/hash/hash.go new file mode 100644 index 0000000..c58373d --- /dev/null +++ b/internal/core/manifests/hash/hash.go @@ -0,0 +1,27 @@ +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) + 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)) + + return hash, nil +} diff --git a/internal/core/manifests/interface.go b/internal/core/manifests/interface.go new file mode 100644 index 0000000..7081cff --- /dev/null +++ b/internal/core/manifests/interface.go @@ -0,0 +1,82 @@ +package manifests + +import ( + "time" +) + +const ( + DefaultNamespace = "default" + + 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 { + GetID() string + GetKind() string + GetName() string + GetNamespace() string +} + +type Dependencies interface { + GetDependsOn() []string +} + +type MetaTable interface { + GetMeta() 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 interface { + Default() +} + +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..a9cd96f --- /dev/null +++ b/internal/core/manifests/kinds/base.go @@ -0,0 +1,16 @@ +package kinds + +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"` +} + +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..39cf70d --- /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/meta.go b/internal/core/manifests/kinds/meta.go new file mode 100644 index 0000000..44ad0e0 --- /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/servers/server.go b/internal/core/manifests/kinds/servers/server.go new file mode 100644 index 0000000..c7a5642 --- /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/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..ef01844 --- /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..0794d63 --- /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/load/http.go b/internal/core/manifests/kinds/tests/load/http.go new file mode 100644 index 0000000..2da156b --- /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 new file mode 100644 index 0000000..778f4ee --- /dev/null +++ b/internal/core/manifests/kinds/values/values.go @@ -0,0 +1,76 @@ +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)(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() { + v.Namespace = manifests.DefaultNamespace + v.Meta = kinds.DefaultMeta +} + +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/manifests/loader/loader.go b/internal/core/manifests/loader/loader.go new file mode 100644 index 0000000..2552995 --- /dev/null +++ b/internal/core/manifests/loader/loader.go @@ -0,0 +1,140 @@ +package loader + +import ( + "fmt" + "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" +) + +func LoadManifestsFromDir(dir string) ([]manifests.Manifest, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + 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 ( + mans []manifests.Manifest + existingIDs []string + manifestsSet = make(map[string]struct{}) + processedHashes = make(map[string]bool) + exists bool + newCounter, existsCounter int + ) + + for _, file := range files { + if file.IsDir() || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { + continue + } + + 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, fmt.Errorf("failed to calculate h for %s: %w", filePath, err) + } + + if hashCache[fileHash] { + ui.Infof("Manifest file %s unchanged (%s) - using cache", file.Name(), shortHash(fileHash)) + exists = true + existsCounter++ + } else { + newCounter++ + } + + 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 !exists { + 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{}{} + mans = append(mans, m) + processedHashes[fileHash] = true + + ui.Successf("New manifest added: %s (h: %s)", manifestID, shortHash(fileHash)) + } else { + 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()) + } + } + + 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 { + mans = append(existingManifests, mans...) + } + } + + 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, + ) + } + + return mans, 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 new file mode 100644 index 0000000..550c5ba --- /dev/null +++ b/internal/core/manifests/parsing/parse.go @@ -0,0 +1,75 @@ +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/servers" + "github.com/apiqube/cli/internal/core/manifests/kinds/services" + "github.com/apiqube/cli/internal/core/manifests/kinds/tests/load" + "github.com/apiqube/cli/ui" + "gopkg.in/yaml.v3" +) + +type RawManifest struct { + Kind string `yaml:"kind"` +} + +func ParseYamlManifests(data []byte) ([]manifests.Manifest, error) { + docs := bytes.Split(data, []byte("\n---")) + var results []manifests.Manifest + + for _, doc := range docs { + doc = bytes.TrimSpace(doc) + if len(doc) == 0 { + continue + } + + var raw RawManifest + if err := yaml.Unmarshal(doc, &raw); err != nil { + return nil, fmt.Errorf("failed to decode raw s: %w", err) + } + + var m manifests.Manifest + + switch raw.Kind { + case manifests.ServerManifestKind: + var s servers.Server + if err := s.UnmarshalYAML(doc); err != nil { + return nil, err + } + m = &s + + case manifests.ServiceManifestKind: + var s services.Service + if err := s.UnmarshalYAML(doc); err != nil { + return nil, err + } + m = &s + + case manifests.HttpTestManifestKind: + 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 := h.UnmarshalYAML(doc); err != nil { + return nil, err + } + m = &h + + default: + ui.Errorf("Unknown manifest kind %s", raw.Kind) + } + + results = append(results, m) + } + + return results, nil +} diff --git a/internal/core/store/db.go b/internal/core/store/db.go new file mode 100644 index 0000000..97713b0 --- /dev/null +++ b/internal/core/store/db.go @@ -0,0 +1,189 @@ +package store + +import ( + "errors" + "fmt" + "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" + "gopkg.in/yaml.v3" +) + +const ( + BadgerDatabaseDirPath = "qube/storage" +) + +const ( + manifestListKeyPrefix = "manifest_list:" + manifestHashKeyPrefix = "manifest_hash:" +) + +type Storage struct { + db *badger.DB +} + +func NewStorage() (*Storage, error) { + path, err := xdg.DataFile(BadgerDatabaseDirPath) + if err != nil { + return nil, fmt.Errorf("error getting data file path: %v", err) + } + + if err = os.MkdirAll(path, os.ModePerm); err != nil { + return nil, fmt.Errorf("error creating data file path: %v", err) + } + + db, err := badger.Open(badger.DefaultOptions(path).WithLogger(nil)) + if err != nil { + return nil, fmt.Errorf("error opening database: %v", err) + } + + return &Storage{ + db: db, + }, nil +} + +func (s *Storage) 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 (s *Storage) SaveManifests(mans ...manifests.Manifest) error { + 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 (s *Storage) LoadManifests(ids ...string) ([]manifests.Manifest, error) { + var results []manifests.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 []manifests.Manifest + + if err = item.Value(func(data []byte) error { + if mans, err = parsing.ParseYamlManifests(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 (s *Storage) CheckManifestHash(hash string) (bool, error) { + 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 rErr 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 errors.Join(rErr, err) +} diff --git a/internal/core/store/helpers.go b/internal/core/store/helpers.go new file mode 100644 index 0000000..86289b4 --- /dev/null +++ b/internal/core/store/helpers.go @@ -0,0 +1,15 @@ +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)) +} + +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 new file mode 100644 index 0000000..c3865a8 --- /dev/null +++ b/internal/core/store/independ.go @@ -0,0 +1,98 @@ +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() { + return nil, nil + } + + return instance.LoadManifestList() +} + +func SaveManifests(mans ...manifests.Manifest) error { + if !isEnabled() { + return nil + } + + return instance.SaveManifests(mans...) +} + +func LoadManifests(ids ...string) ([]manifests.Manifest, error) { + 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 +} 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/manifests/depends/plan.go b/internal/manifests/depends/plan.go deleted file mode 100644 index 05283c1..0000000 --- a/internal/manifests/depends/plan.go +++ /dev/null @@ -1,43 +0,0 @@ -package depends - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/apiqube/cli/internal/manifests" -) - -type ExecutionPlan struct { - Order []string `json:"order"` -} - -func GeneratePlan(outPath string, manifests []manifests.Manifest) error { - graph, _, err := BuildDependencyGraph(manifests) - if err != nil { - return err - } - - order, err := TopoSort(graph) - if err != nil { - return err - } - - return SaveExecutionPlan(outPath, order) -} - -func SaveExecutionPlan(path string, order []string) error { - plan := ExecutionPlan{Order: order} - - data, err := json.MarshalIndent(plan, "", " ") - if err != nil { - 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) - } - - return os.WriteFile(filepath.Join(path, "execution-plan.json"), data, 0o644) -} diff --git a/internal/manifests/depends/sort.go b/internal/manifests/depends/sort.go deleted file mode 100644 index 8733062..0000000 --- a/internal/manifests/depends/sort.go +++ /dev/null @@ -1,60 +0,0 @@ -package depends - -import ( - "fmt" - - "github.com/apiqube/cli/internal/manifests" -) - -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 []manifests.Manifest, order []string) ([]manifests.Manifest, error) { - idMap := make(map[string]manifests.Manifest) - for _, m := range mans { - idMap[m.GetID()] = m - } - - sorted := make([]manifests.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 -} diff --git a/internal/manifests/interface.go b/internal/manifests/interface.go deleted file mode 100644 index ef01d3a..0000000 --- a/internal/manifests/interface.go +++ /dev/null @@ -1,22 +0,0 @@ -package manifests - -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/manifests/kinds/base.go b/internal/manifests/kinds/base.go deleted file mode 100644 index 5ca9937..0000000 --- a/internal/manifests/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/manifests/kinds/load/http.go b/internal/manifests/kinds/load/http.go deleted file mode 100644 index 3825656..0000000 --- a/internal/manifests/kinds/load/http.go +++ /dev/null @@ -1,74 +0,0 @@ -package load - -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/manifests/kinds/server/server.go b/internal/manifests/kinds/server/server.go deleted file mode 100644 index 75da5e7..0000000 --- a/internal/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/manifests/kinds/service/service.go b/internal/manifests/kinds/service/service.go deleted file mode 100644 index 0678362..0000000 --- a/internal/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/manifests/kinds/tests/http.go b/internal/manifests/kinds/tests/http.go deleted file mode 100644 index 47888f1..0000000 --- a/internal/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/manifests/validate.go b/internal/manifests/validate.go deleted file mode 100644 index e4eae33..0000000 --- a/internal/manifests/validate.go +++ /dev/null @@ -1,11 +0,0 @@ -package manifests - -import "github.com/asaskevich/govalidator" - -func ValidateManifest(manifest any) error { - if ok, err := govalidator.ValidateStruct(manifest); !ok && err != nil { - return err - } - - return nil -} diff --git a/internal/yaml/loader.go b/internal/yaml/loader.go deleted file mode 100644 index 8dc7edd..0000000 --- a/internal/yaml/loader.go +++ /dev/null @@ -1,42 +0,0 @@ -package yaml - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/apiqube/cli/internal/manifests" -) - -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 content []byte - - for _, file := range files { - if file.IsDir() || file.Name() == "combined.yaml" || (!strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml")) { - continue - } - - content, err = os.ReadFile(filepath.Join(dir, file.Name())) - if err != nil { - return nil, err - } - - manifest, err = ParseManifest(content) - if err != nil { - return nil, fmt.Errorf("in file %s: %w", file.Name(), err) - } - - mans = append(mans, manifest) - } - - return mans, nil -} diff --git a/internal/yaml/parse.go b/internal/yaml/parse.go deleted file mode 100644 index deef425..0000000 --- a/internal/yaml/parse.go +++ /dev/null @@ -1,63 +0,0 @@ -package yaml - -import ( - "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" - "gopkg.in/yaml.v3" -) - -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 - - switch raw.Kind { - case manifests.ServerManifestKind: - var m server.Server - - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } - - manifest = &m - case manifests.ServiceManifestKind: - var m service.Service - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } - - manifest = &m - case manifests.HttpTestManifestKind: - var m tests.Http - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } - - manifest = &m - case manifests.HttpLoadTestManifestKind: - var m load.Http - - if err := yaml.Unmarshal(data, m.Default()); err != nil { - return nil, err - } - - manifest = &m - - default: - return nil, fmt.Errorf("unsupported kind: %s", raw.Kind) - } - - return manifest, nil -} diff --git a/internal/yaml/saver.go b/internal/yaml/saver.go deleted file mode 100644 index 1026c33..0000000 --- a/internal/yaml/saver.go +++ /dev/null @@ -1,39 +0,0 @@ -package yaml - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - - "github.com/apiqube/cli/internal/manifests" - "gopkg.in/yaml.v3" -) - -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) - } - - var buf bytes.Buffer - - for i, manifest := range manifests { - 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) - } - - outputPath := filepath.Join(dir, "combined.yaml") - if err := os.WriteFile(outputPath, buf.Bytes(), 0o644); err != nil { - return fmt.Errorf("failed to write file: %w", err) - } - - return nil -} 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() -} 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 92% rename from internal/ui/styles.go rename to ui/styles.go index 6932e8e..f30793b 100644 --- a/internal/ui/styles.go +++ b/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/ui/ui.go b/ui/ui.go similarity index 94% rename from internal/ui/ui.go rename to ui/ui.go index 09c0874..3850a27 100644 --- a/internal/ui/ui.go +++ b/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 }