Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cmd/cli/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ The same command runs locally and in CI. The e2e.yaml file is the source of trut
ork e2e
ork e2e -f e2e.yaml
ork e2e -f e2e.yaml --keep-cluster
ork e2e -f e2e.yaml --cluster my-existing-context`,
ork e2e -f e2e.yaml --cluster my-existing-context
ork e2e -f e2e.yaml --version v1.2.3 --values values.yaml`,
RunE: func(cmd *cobra.Command, args []string) error {
file, _ := cmd.Flags().GetString("file")
keepCluster, _ := cmd.Flags().GetBool("keep-cluster")
useCurrentCtx, _ := cmd.Flags().GetBool("use-current")
clusterCtx, _ := cmd.Flags().GetString("cluster")
version, _ := cmd.Flags().GetString("version")
valuesFiles, _ := cmd.Flags().GetStringSlice("values")
helmArgRaw, _ := cmd.Flags().GetStringSlice("helm-arg")
var helmArgs []string
for _, arg := range helmArgRaw {
helmArgs = append(helmArgs, "--set", arg)
}

runner, err := e2e.New(file, clusterCtx, useCurrentCtx, keepCluster)
runner, err := e2e.New(file, clusterCtx, useCurrentCtx, keepCluster, version, valuesFiles, helmArgs...)
if err != nil {
return err
}
Expand All @@ -43,6 +51,9 @@ func init() {
e2eCmd.Flags().Bool("keep-cluster", false, "Keep the kind cluster after the test completes")
e2eCmd.Flags().Bool("use-current", false, "Use the current kubectl context, skip cluster creation")
e2eCmd.Flags().String("cluster", "", "Use an existing kubectl context instead of creating a cluster")
e2eCmd.Flags().String("version", "", "Orkestra version to install (e.g., v1.2.3)")
e2eCmd.Flags().StringSlice("values", []string{}, "Helm values files to pass to Orkestra installation")
e2eCmd.Flags().StringSlice("helm-arg", []string{}, "Additional Helm --set arguments (e.g., key=value)")

// Shadow global flags
e2eCmd.Flags().Bool("debug", false, "")
Expand Down
8 changes: 6 additions & 2 deletions cmd/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,15 @@ func initProject(name, pack string, refresh bool) error {

fmt.Printf("\n%s\n\n", green(projectPrint))

dir := p.Name
if first != "" {
dir = dir + "/" + first
}
fmt.Println("To run the first example:")
if !isCurrentDirectory(name) {
fmt.Printf(" cd %s/%s/%s\n", name, pack, first)
fmt.Printf(" cd %s/%s\n", name, dir)
} else {
fmt.Printf(" cd %s/%s\n", pack, first)
fmt.Printf(" cd %s\n", dir)
}

if p.isBeginnerPack() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/init_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import (
// ──────────────────────────────────────────────────────────────────────────────

func extractEmbeddedPack(root, pack string) error {
p, ok := Packs[pack]
p, ok := GetPack(pack)
if !ok {
return fmt.Errorf("unknown pack %q — run `ork init --list` to see available packs", pack)
}
srcPath := p.Path

targetDir := filepath.Join(root, pack)
targetDir := filepath.Join(root, filepath.Base(p.Path))

if err := fs.WalkDir(examples.FS, srcPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
Expand Down
28 changes: 16 additions & 12 deletions cmd/cli/init_packs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

package cli

import (
"path/filepath"

"github.com/orkspace/orkestra/examples"
)

// packPaths maps CLI pack names to their paths inside the embedded FS.
// Most packs are top-level directories; rollback is nested under use-cases.
type Pack struct {
Expand Down Expand Up @@ -36,21 +42,19 @@ var Packs = map[string]Pack{
Description: "Full-stack, cross-CRD, external gates, once-secrets.",
Path: "use-cases",
},
// "rollback": {
// Name: "rollback",
// Description: "Zero-config and configurable failure recovery",
// Path: "use-cases/rollback",
// },
// "developer": {
// Name: "developer",
// Description: "Local to production in minutes — deploy your app without writing operator code.",
// Path: "developer",
// },
}

func GetPack(name string) (Pack, bool) {
p, ok := Packs[name]
return p, ok
if p, ok := Packs[name]; ok {
return p, true
}
// Sub-path fallback: any valid directory in the embedded FS works as a pack.
// ork init my-project --pack use-cases/multi-tenancy extracts into my-project/multi-tenancy.
if f, err := examples.FS.Open(name); err == nil {
f.Close()
return Pack{Name: filepath.Base(name), Path: name}, true
}
return Pack{}, false
}

func ListPacks() []Pack {
Expand Down
2 changes: 1 addition & 1 deletion cmd/cli/registry_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ var registryPushCmd = &cobra.Command{
}
} else {
fmt.Printf("\nRunning E2E gate (%s)...\n", registry.FileE2E)
runner, err := e2e.New(e2eFile, "", false, false)
runner, err := e2e.New(e2eFile, "", false, false, "", nil)
if err != nil {
return fmt.Errorf("e2e gate: %w\n\nUse --force or --no-e2e to skip", err)
}
Expand Down
41 changes: 41 additions & 0 deletions cmd/cli/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//go:build !runtime && !gateway

package cli

import (
"fmt"

"github.com/orkspace/orkestra/pkg/devserver"
"github.com/spf13/cobra"
)

var startCmd = &cobra.Command{
Use: "start",
Short: "Start an Orkestra background service",
}

var startDevServerCmd = &cobra.Command{
Use: "dev-server",
Short: "Start the mock dev server for external: examples",
Long: `Starts a lightweight mock HTTP server on :9999 that handles all endpoints
used by external:, full-stack, and feature-flag examples — no real services needed.

Press Ctrl+C to stop.`,
RunE: func(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")

if err := devserver.Start(port); err != nil {
return fmt.Errorf("starting dev server: %w", err)
}

fmt.Println("Dev server running. Press Ctrl+C to stop.")
<-cmd.Context().Done()
return nil
},
}

func init() {
rootCmd.AddCommand(startCmd)
startCmd.AddCommand(startDevServerCmd)
startDevServerCmd.Flags().Int("port", devserver.Port, "Port to listen on")
}
116 changes: 75 additions & 41 deletions cmd/cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"sort"
"strings"

"path/filepath"

"github.com/orkspace/orkestra/pkg/e2e"
"github.com/orkspace/orkestra/pkg/katalog"
"github.com/orkspace/orkestra/pkg/konfig"
orktypes "github.com/orkspace/orkestra/pkg/types"
Expand Down Expand Up @@ -179,70 +182,93 @@ func validateE2EFile(path string) error {
return fmt.Errorf("reading %s: %w", path, err)
}

var e2e orktypes.E2E
if err := yaml.Unmarshal(data, &e2e); err != nil {
var doc orktypes.E2E
if err := yaml.Unmarshal(data, &doc); err != nil {
return fmt.Errorf("parsing %s: %w", path, err)
}

baseDir := filepath.Dir(path)
isAggregator := len(doc.Imports) > 0 && doc.Spec.Katalog == "" && doc.Spec.Init == nil

var errs []string

if e2e.Metadata.Name == "" {
if doc.Metadata.Name == "" {
errs = append(errs, "metadata.name is required")
}
if e2e.Spec.Katalog == "" && e2e.Spec.Init == nil {
errs = append(errs, "spec.katalog is required (or spec.init for example packs)")
}
if e2e.Spec.CRD == "" && e2e.Spec.Init == nil {
errs = append(errs, "spec.crd is required (or spec.init for example packs)")
}
if e2e.Spec.CR == "" && e2e.Spec.Init == nil {
errs = append(errs, "spec.cr is required (or spec.init for example packs)")
}
if len(e2e.Spec.Expect) == 0 {
errs = append(errs, "spec.expect must contain at least one expectation")
}
for i, exp := range e2e.Spec.Expect {
if exp.Name == "" {
errs = append(errs, fmt.Sprintf("spec.expect[%d].name is required", i))
if !isAggregator {
if doc.Spec.Katalog == "" && doc.Spec.Init == nil {
errs = append(errs, "spec.katalog is required (or spec.init for example packs, or imports)")
}
if doc.Spec.CRD == "" && doc.Spec.Init == nil {
errs = append(errs, "spec.crd is required (or spec.init for example packs, or imports)")
}
if doc.Spec.CR == "" && doc.Spec.Init == nil {
errs = append(errs, "spec.cr is required (or spec.init for example packs, or imports)")
}
if exp.After != "cr-applied" && exp.After != "cr-deleted" {
errs = append(errs, fmt.Sprintf("spec.expect[%d].after must be cr-applied or cr-deleted (got %q)", i, exp.After))
if len(doc.Spec.Expect) == 0 {
errs = append(errs, "spec.expect must contain at least one expectation")
}
if len(exp.Resources) == 0 && len(exp.Commands) == 0 {
errs = append(errs, fmt.Sprintf("spec.expect[%d] (%q): must have at least one resource or command check", i, exp.Name))
for i, exp := range doc.Spec.Expect {
if exp.Name == "" {
errs = append(errs, fmt.Sprintf("spec.expect[%d].name is required", i))
}
if exp.After != "cr-applied" && exp.After != "cr-deleted" {
errs = append(errs, fmt.Sprintf("spec.expect[%d].after must be cr-applied or cr-deleted (got %q)", i, exp.After))
}
if len(exp.Resources) == 0 && len(exp.Commands) == 0 {
errs = append(errs, fmt.Sprintf("spec.expect[%d] (%q): must have at least one resource or command check", i, exp.Name))
}
}
}

if len(errs) > 0 {
// Validate imports (collect per-import errors for display).
importErrs := e2e.ValidateImports(baseDir, doc.Imports)

if len(errs) > 0 || len(importErrs) > 0 {
for _, e := range errs {
fmt.Printf(" %s %s\n", failureMark(), e)
}
for _, ie := range importErrs {
fmt.Printf(" %s import: %s\n", failureMark(), ie)
}
fmt.Println()
return fmt.Errorf("%d validation error(s) in %s", len(errs), path)
return fmt.Errorf("%d validation error(s) in %s", len(errs)+len(importErrs), path)
}

icon := healthIcon("ready")
fmt.Printf("%s %s\n", icon, bold(e2e.Metadata.Name))
if e2e.Metadata.Description != "" {
fmt.Printf(" %s\n", gray(e2e.Metadata.Description))
fmt.Printf("%s %s\n", icon, bold(doc.Metadata.Name))
if doc.Metadata.Description != "" {
fmt.Printf(" %s\n", gray(doc.Metadata.Description))
}
fmt.Printf(" %s\n",
gray(fmt.Sprintf("katalog : %s\n crd : %s\n cr : %s",
e2e.Spec.Katalog, e2e.Spec.CRD, e2e.Spec.CR)),
)
if s := e2e.Spec.Setup; s != nil {
if len(s.Apply) > 0 {
fmt.Printf(" %s\n", gray("setup.apply : "+strings.Join(s.Apply, ", ")))
}
if len(s.Helm) > 0 {
fmt.Printf(" %s\n", gray(fmt.Sprintf("setup.helm : %d chart(s)", len(s.Helm))))
if !isAggregator {
fmt.Printf(" %s\n",
gray(fmt.Sprintf("katalog : %s\n crd : %s\n cr : %s",
doc.Spec.Katalog, doc.Spec.CRD, doc.Spec.CR)),
)
if s := doc.Spec.Setup; s != nil {
if len(s.Apply) > 0 {
fmt.Printf(" %s\n", gray("setup.apply : "+strings.Join(s.Apply, ", ")))
}
if len(s.Helm) > 0 {
fmt.Printf(" %s\n", gray(fmt.Sprintf("setup.helm : %d chart(s)", len(s.Helm))))
}
if len(s.Wait) > 0 {
fmt.Printf(" %s\n", gray(fmt.Sprintf("setup.wait : %d resource(s)", len(s.Wait))))
}
}
if len(s.Wait) > 0 {
fmt.Printf(" %s\n", gray(fmt.Sprintf("setup.wait : %d resource(s)", len(s.Wait))))
}
if len(doc.Imports) > 0 {
fmt.Printf(" %s\n", gray(fmt.Sprintf("imports : %d file(s)", len(doc.Imports))))
for _, imp := range doc.Imports {
label := imp.Path
if imp.FreshCluster {
label += " (fresh cluster)"
}
fmt.Printf(" %s %s\n", healthIcon("ready"), gray(label))
}
}
fmt.Println()
for _, exp := range e2e.Spec.Expect {
for _, exp := range doc.Spec.Expect {
to := exp.Timeout
if to == "" {
to = "60s"
Expand All @@ -252,7 +278,15 @@ func validateE2EFile(path string) error {
}
fmt.Println()
fmt.Println(strings.Repeat("─", 60))
fmt.Printf("%d expectation(s) valid\n", len(e2e.Spec.Expect))
if isAggregator {
fmt.Printf("%d import(s) valid\n", len(doc.Imports))
} else {
fmt.Printf("%d expectation(s) valid", len(doc.Spec.Expect))
if len(doc.Imports) > 0 {
fmt.Printf(", %d import(s) valid", len(doc.Imports))
}
fmt.Println()
}

return nil
}
Expand Down
Loading
Loading