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
93 changes: 85 additions & 8 deletions cmd/virtwork/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ import (
)

var (
version = ""
commit = ""
date = ""

clusterConnect = cluster.Connect

ErrNoWorkloads = errors.New("no workloads specified: use --workloads or --from-catalog")
version = ""
commit = ""
date = ""
clusterConnect = cluster.Connect
ErrNoEntries = errors.New("no catalog entries found")
ErrNoWorkloads = errors.New("no workloads specified: use --workloads or --from-catalog")
ErrValidationFailed = errors.New("catalog validation failed")
)

func main() {
Expand All @@ -55,7 +55,7 @@ realistic CPU, memory, database, network, and disk I/O metrics.`,

config.BindPersistentFlags(rootCmd)

rootCmd.AddCommand(newRunCmd(), newCleanupCmd(), newVersionCmd())
rootCmd.AddCommand(newRunCmd(), newCleanupCmd(), newValidateCmd(), newVersionCmd())
return rootCmd
}

Expand Down Expand Up @@ -115,6 +115,83 @@ func newCleanupCmd() *cobra.Command {
return cmd
}

func newValidateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate [entry-names...]",
Short: "Validate catalog entries",
Long: `Check that catalog entries conform to the expected schema and structure.
Validates YAML parsing, service file presence, storage constraints,
service port ranges, role-to-service alignment, and placeholder consistency.`,
RunE: validateE,
}

config.BindValidateFlags(cmd)

return cmd
}

func validateE(cmd *cobra.Command, args []string) error {
catalogDir, _ := cmd.Flags().GetString("catalog-dir")
if catalogDir == "" {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("resolving home directory: %w", err)
}
catalogDir = home + "/.virtwork/catalog"
}

entries, err := os.ReadDir(catalogDir)
if err != nil {
return fmt.Errorf("reading catalog directory %s: %w", catalogDir, err)
}

var names []string
if len(args) > 0 {
names = args
} else {
for _, d := range entries {
if d.IsDir() {
names = append(names, d.Name())
}
}
}

if len(names) == 0 {
return fmt.Errorf("%s: %w", catalogDir, ErrNoEntries)
}

var failed int
for _, name := range names {
entry, loadErr := workloads.LoadCatalogEntry(catalogDir, name)
if loadErr != nil {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "FAIL %s: %v\n", name, loadErr)
failed++
continue
}

errs, warnings := workloads.ValidatePlaceholders(entry)

for _, w := range warnings {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "WARN %s: %s\n", name, w)
}

if len(errs) > 0 {
for _, e := range errs {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "FAIL %s: %s\n", name, e)
}
failed++
continue
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "OK %s\n", name)
}

if failed > 0 {
return fmt.Errorf("%d of %d entries: %w", failed, len(names), ErrValidationFailed)
}
return nil
}

func initAuditor(cmd *cobra.Command, cfg *config.Config) (audit.Auditor, error) {
noAudit, _ := cmd.Flags().GetBool("no-audit")
if noAudit || !cfg.AuditEnabled {
Expand Down
39 changes: 38 additions & 1 deletion cmd/virtwork/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ func newRootCmd() *cobra.Command {
}
config.BindCleanupFlags(cleanupCmd)

rootCmd.AddCommand(runCmd, cleanupCmd)
validateCmd := &cobra.Command{
Use: "validate [entry-names...]",
Short: "Validate catalog entries",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
config.BindValidateFlags(validateCmd)

rootCmd.AddCommand(runCmd, cleanupCmd, validateCmd)
return rootCmd
}

Expand Down Expand Up @@ -310,3 +319,31 @@ var _ = Describe("Cleanup command flags", func() {
Expect(val).To(BeTrue())
})
})

var _ = Describe("Validate command flags", func() {
var rootCmd *cobra.Command

BeforeEach(func() {
rootCmd = newRootCmd()
})

It("should accept --catalog-dir flag", func() {
rootCmd.SetArgs([]string{"validate", "--catalog-dir", "/tmp/my-catalog"})
Expect(rootCmd.Execute()).To(Succeed())

validateCmd, _, _ := rootCmd.Find([]string{"validate"})
val, err := validateCmd.Flags().GetString("catalog-dir")
Expect(err).NotTo(HaveOccurred())
Expect(val).To(Equal("/tmp/my-catalog"))
})

It("should default catalog-dir to empty string", func() {
rootCmd.SetArgs([]string{"validate"})
Expect(rootCmd.Execute()).To(Succeed())

validateCmd, _, _ := rootCmd.Find([]string{"validate"})
val, err := validateCmd.Flags().GetString("catalog-dir")
Expect(err).NotTo(HaveOccurred())
Expect(val).To(BeEmpty())
})
})
108 changes: 108 additions & 0 deletions cmd/virtwork/validatee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2026 Red Hat
// SPDX-License-Identifier: Apache-2.0

package main

import (
"bytes"
"os"
"path/filepath"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("validateE", func() {
var catalogDir string

writeFile := func(dir, name, content string) {
Expect(os.MkdirAll(dir, 0o750)).To(Succeed())
Expect(os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600)).To(Succeed())
}

BeforeEach(func() {
var err error
catalogDir, err = os.MkdirTemp("", "virtwork-validate-*")
Expect(err).NotTo(HaveOccurred())
})

AfterEach(func() {
Expect(os.RemoveAll(catalogDir)).To(Succeed())
})

It("should succeed for a valid catalog entry", func() {
entryDir := filepath.Join(catalogDir, "good")
writeFile(entryDir, "workload.yaml", "description: test\n")
writeFile(entryDir, "workload.service", "[Service]\nExecStart=/bin/test\n")

var out bytes.Buffer
cmd := newValidateCmd()
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs([]string{"--catalog-dir", catalogDir})
Expect(cmd.Execute()).To(Succeed())
Expect(out.String()).To(ContainSubstring("good"))
Expect(out.String()).To(ContainSubstring("OK"))
})

It("should fail for a nonexistent catalog directory", func() {
cmd := newValidateCmd()
cmd.SetArgs([]string{"--catalog-dir", "/nonexistent/path"})
Expect(cmd.Execute()).NotTo(Succeed())
})

It("should validate only named entries when positional args given", func() {
entryDir := filepath.Join(catalogDir, "one")
writeFile(entryDir, "workload.yaml", "description: test\n")
writeFile(entryDir, "workload.service", "[Service]\nExecStart=/bin/test\n")
otherDir := filepath.Join(catalogDir, "two")
writeFile(otherDir, "workload.yaml", "description: test2\n")
writeFile(otherDir, "workload.service", "[Service]\nExecStart=/bin/test2\n")

var out bytes.Buffer
cmd := newValidateCmd()
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs([]string{"--catalog-dir", catalogDir, "one"})
Expect(cmd.Execute()).To(Succeed())
Expect(out.String()).To(ContainSubstring("one"))
Expect(out.String()).NotTo(ContainSubstring("two"))
})

It("should exit non-zero when an entry fails validation", func() {
entryDir := filepath.Join(catalogDir, "bad")
Expect(os.MkdirAll(entryDir, 0o750)).To(Succeed())

cmd := newValidateCmd()
cmd.SetArgs([]string{"--catalog-dir", catalogDir})
Expect(cmd.Execute()).NotTo(Succeed())
})

It("should report placeholder warnings without failing", func() {
entryDir := filepath.Join(catalogDir, "warn")
writeFile(
entryDir,
"workload.yaml",
"description: test\nparams:\n - key: unused\n type: string\n default: x\n",
)
writeFile(entryDir, "workload.service", "[Service]\nExecStart=/bin/test\n")

var out bytes.Buffer
cmd := newValidateCmd()
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs([]string{"--catalog-dir", catalogDir})
Expect(cmd.Execute()).To(Succeed())
Expect(out.String()).To(ContainSubstring("unused"))
})

It("should fail when a placeholder has no matching param", func() {
entryDir := filepath.Join(catalogDir, "typo")
writeFile(entryDir, "workload.yaml", "description: test\n")
writeFile(entryDir, "workload.service", "[Service]\nExecStart=/bin/test --x={{oops}}\n")

cmd := newValidateCmd()
cmd.SetArgs([]string{"--catalog-dir", catalogDir})
Expect(cmd.Execute()).NotTo(Succeed())
})
})
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ func BindCleanupFlags(cmd *cobra.Command) {
f.BoolP("yes", "y", false, "Skip confirmation prompt and proceed with cleanup")
}

func BindValidateFlags(cmd *cobra.Command) {
cmd.Flags().String("catalog-dir", "", "Path to catalog directory (default ~/.virtwork/catalog)")
}

// LoadConfig loads configuration from flags, environment variables, config file,
// and defaults using the Viper priority chain: flags > env > file > defaults.
func LoadConfig(cmd *cobra.Command) (*Config, error) {
Expand Down
Loading