Repository: bacalhau-project/bacalhau (language: python)

Issue:
Move `bacalhau validate` to `bacalhau job validate`
Remove the legacy command from the CLI and direct users to use the new command via a helpful error message. e.g.
```
This command has moved! Please use `job validate` to validate jobs.
```


Discussion / hints:

Fix patch:
diff --git a/cmd/cli/job/root.go b/cmd/cli/job/root.go
index 1ce8cb4a3e..c783f18cb1 100644
--- a/cmd/cli/job/root.go
+++ b/cmd/cli/job/root.go
@@ -22,5 +22,6 @@ func NewCmd() *cobra.Command {
 	cmd.AddCommand(NewRunCmd())
 	cmd.AddCommand(NewStopCmd())
 	cmd.AddCommand(NewGetCmd())
+	cmd.AddCommand(NewValidateCmd())
 	return cmd
 }
diff --git a/cmd/cli/job/run.go b/cmd/cli/job/run.go
index 98b1037b15..d0594e48d7 100644
--- a/cmd/cli/job/run.go
+++ b/cmd/cli/job/run.go
@@ -1,10 +1,7 @@
 package job
 
 import (
-	"errors"
 	"fmt"
-	"io"
-	"os"
 
 	"github.com/spf13/cobra"
 	"k8s.io/kubectl/pkg/util/i18n"
@@ -96,28 +93,9 @@ func (o *RunOptions) run(cmd *cobra.Command, args []string, api client.API) erro
 	ctx := cmd.Context()
 
 	// read the job spec from stdin or file
-	var err error
-	var byteResult []byte
-	if len(args) == 0 {
-		byteResult, err = util.ReadFromStdinIfAvailable(cmd)
-		if err != nil {
-			return fmt.Errorf("unknown error reading from file or stdin: %w", err)
-		}
-	} else {
-		var fileContent *os.File
-		fileContent, err = os.Open(args[0])
-		if err != nil {
-			return fmt.Errorf("error opening file: %w", err)
-		}
-		defer fileContent.Close()
-
-		byteResult, err = io.ReadAll(fileContent)
-		if err != nil {
-			return fmt.Errorf("error reading file: %w", err)
-		}
-	}
-	if len(byteResult) == 0 {
-		return errors.New(userstrings.JobSpecBad)
+	jobBytes, err := util.ReadJobFromUser(cmd, args)
+	if err != nil {
+		return err
 	}
 
 	if !o.NoTemplate {
@@ -128,13 +106,13 @@ func (o *RunOptions) run(cmd *cobra.Command, args []string, api client.API) erro
 		if err != nil {
 			return fmt.Errorf("failed to create template parser: %w", err)
 		}
-		byteResult, err = parser.ParseBytes(byteResult)
+		jobBytes, err = parser.ParseBytes(jobBytes)
 		if err != nil {
 			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
 		}
 	}
 
-	j, err := marshaller.UnmarshalJob(byteResult)
+	j, err := marshaller.UnmarshalJob(jobBytes)
 	if err != nil {
 		return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
 	}
diff --git a/cmd/cli/job/validate.go b/cmd/cli/job/validate.go
new file mode 100644
index 0000000000..6304df641c
--- /dev/null
+++ b/cmd/cli/job/validate.go
@@ -0,0 +1,81 @@
+package job
+
+import (
+	"github.com/spf13/cobra"
+	"k8s.io/kubectl/pkg/util/i18n"
+
+	"github.com/bacalhau-project/bacalhau/cmd/util"
+	"github.com/bacalhau-project/bacalhau/pkg/lib/marshaller"
+	"github.com/bacalhau-project/bacalhau/pkg/userstrings"
+	"github.com/bacalhau-project/bacalhau/pkg/util/templates"
+)
+
+var (
+	validateLong = templates.LongDesc(i18n.T(`
+		Validate a job from a file
+		JSON and YAML formats are accepted.
+		Job Specification: https://docs.bacalhau.org/setting-up/jobs/job
+`))
+
+	//nolint:lll // Documentation
+	validateExample = templates.Examples(i18n.T(`
+		# Validate a job using the data in job.yaml
+		bacalhau job validate ./job.yaml
+`))
+)
+
+const JobSpecLink = "https://docs.bacalhau.org/setting-up/jobs/job"
+
+func NewValidateCmd() *cobra.Command {
+	validateCmd := &cobra.Command{
+		Use:     "validate",
+		Short:   "validate a job using a json or yaml file.",
+		Long:    validateLong,
+		Example: validateExample,
+		Args:    cobra.MinimumNArgs(1),
+		// so we don't print the usage when a job is invalid, just print the validation errors
+		// --help will still show usage
+		SilenceUsage: true,
+		// so we don't print the error twice
+		SilenceErrors: true,
+		RunE: func(cmd *cobra.Command, cmdArgs []string) error {
+			err := run(cmd, cmdArgs)
+			if err != nil {
+				cmd.Println("Error: " + userstrings.JobSpecBad)
+				cmd.Println("Job Specification: " + JobSpecLink)
+				cmd.Println()
+				return err
+			}
+			cmd.Println("OK")
+			return nil
+		},
+	}
+	return validateCmd
+}
+
+func run(cmd *cobra.Command, args []string) error {
+	// read the job spec from stdin or file
+	jobBytes, err := util.ReadJobFromUser(cmd, args)
+	if err != nil {
+		return err
+	}
+
+	j, err := marshaller.UnmarshalJob(jobBytes)
+	if err != nil {
+		return err
+	}
+
+	// Normalize and validate the job spec
+	j.Normalize()
+	if err := j.ValidateSubmission(); err != nil {
+		return err
+	}
+
+	if warnings := j.SanitizeSubmission(); len(warnings) > 0 {
+		for _, w := range warnings {
+			cmd.Println(w)
+		}
+	}
+
+	return nil
+}
diff --git a/cmd/cli/validate/validate.go b/cmd/cli/validate/validate.go
index e6eb5c8047..16a708686c 100644
--- a/cmd/cli/validate/validate.go
+++ b/cmd/cli/validate/validate.go
@@ -1,200 +1,14 @@
 package validate
 
 import (
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-
-	"github.com/bacalhau-project/bacalhau/pkg/lib/marshaller"
-	"github.com/invopop/jsonschema"
 	"github.com/spf13/cobra"
-
-	"github.com/bacalhau-project/bacalhau/pkg/model"
-	"github.com/bacalhau-project/bacalhau/pkg/util/templates"
-
-	"k8s.io/kubectl/pkg/util/i18n"
-	"sigs.k8s.io/yaml"
-
-	"github.com/xeipuuv/gojsonschema"
-
-	"github.com/tidwall/sjson"
-)
-
-var (
-	validateLong = templates.LongDesc(i18n.T(`
-		Validate a job from a file
-
-		JSON and YAML formats are accepted.
-`))
-
-	//nolint:lll // Documentation
-	validateExample = templates.Examples(i18n.T(`
-		# Validate a job using the data in job.yaml
-		bacalhau validate ./job.yaml
-
-		# Validate a job using stdin
-		cat job.yaml | bacalhau validate
-
-		# Output the jsonschema for a bacalhau job
-		bacalhau validate --output-schema
-`))
 )
 
-type ValidateOptions struct {
-	Filename        string // Filename for job (can be .json or .yaml)
-	OutputFormat    string // Output format (json or yaml)
-	OutputSchema    bool   // Output the schema to stdout
-	OutputDirectory string // Output directory for the job
-}
-
-func NewValidateOptions() *ValidateOptions {
-	return &ValidateOptions{
-		Filename:        "",
-		OutputFormat:    "yaml",
-		OutputSchema:    false,
-		OutputDirectory: "",
-	}
-}
-
 func NewCmd() *cobra.Command {
-	OV := NewValidateOptions()
-
-	validateCmd := &cobra.Command{
-		Use:     "validate",
-		Short:   "validate a job using a json or yaml file.",
-		Long:    validateLong,
-		Example: validateExample,
-		Args:    cobra.MinimumNArgs(0),
-		RunE: func(cmd *cobra.Command, cmdArgs []string) error {
-			return validate(cmd, cmdArgs, OV)
-		},
-	}
-
-	validateCmd.PersistentFlags().BoolVar(
-		&OV.OutputSchema, "output-schema", OV.OutputSchema,
-		`Output the JSON schema for a Job to stdout then exit`,
-	)
-
-	return validateCmd
-}
-
-func validate(cmd *cobra.Command, cmdArgs []string, OV *ValidateOptions) error {
-	j := &model.Job{}
-	jsonSchemaData, err := GenerateJobJSONSchema()
-	if err != nil {
-		return err
-	}
-
-	if OV.OutputSchema {
-		//nolint
-		cmd.Printf("%s", jsonSchemaData)
-		return nil
+	return &cobra.Command{
+		Use:        "validate",
+		Short:      "DEPRECATED: validate a job using a json or yaml file.",
+		Deprecated: "This command has moved! Please use `job validate` to validate jobs.",
+		RunE:       func(cmd *cobra.Command, cmdArgs []string) error { return nil },
 	}
-
-	if len(cmdArgs) == 0 {
-		_ = cmd.Usage()
-		return fmt.Errorf("you must specify a filename or provide the content to be validated via stdin")
-	}
-
-	OV.Filename = cmdArgs[0]
-	var byteResult []byte
-
-	if OV.Filename == "" {
-		// Read from stdin
-		byteResult, err = io.ReadAll(cmd.InOrStdin())
-		if err != nil {
-			return fmt.Errorf("error reading from stdin: %w", err)
-		}
-		if byteResult == nil {
-			// Can you ever get here?
-			return fmt.Errorf("no filename provided")
-		}
-	} else {
-		fileextension := filepath.Ext(OV.Filename)
-		file, err := os.Open(OV.Filename)
-		if err != nil {
-			return fmt.Errorf("error opening file (%s): %w", OV.Filename, err)
-		}
-
-		byteResult, err = io.ReadAll(file)
-		if err != nil {
-			return err
-		}
-
-		if fileextension == ".json" || fileextension == ".yaml" || fileextension == ".yml" {
-			// Yaml can parse json
-			err = marshaller.YAMLUnmarshalWithMax(byteResult, &j)
-			if err != nil {
-				return fmt.Errorf("error unmarshaling yaml from file (%s): %w", OV.Filename, err)
-			}
-		} else {
-			return fmt.Errorf("file extension (%s) not supported. The file must end in either .yaml, .yml or .json", fileextension)
-		}
-	}
-
-	// Convert the schema to JSON - this is required for the gojsonschema library
-	// Noop if you pass JSON through
-	fileContentsAsJSONBytes, err := yaml.YAMLToJSON(byteResult)
-	if err != nil {
-		return fmt.Errorf("error converting yaml to json: %w", err)
-	}
-
-	// println(str)
-	schemaLoader := gojsonschema.NewStringLoader(string(jsonSchemaData))
-	documentLoader := gojsonschema.NewStringLoader(string(fileContentsAsJSONBytes))
-
-	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
-	if err != nil {
-		return fmt.Errorf("error validating json: %w", err)
-	}
-
-	if result.Valid() {
-		cmd.Println("The Job is valid")
-	} else {
-		msg := "The Job is not valid. See errors:\n"
-		for _, desc := range result.Errors() {
-			msg += fmt.Sprintf("- %s\n", desc)
-		}
-		return fmt.Errorf(msg)
-	}
-	return nil
-}
-
-func GenerateJobJSONSchema() ([]byte, error) {
-	s := jsonschema.Reflect(&model.Job{})
-	// Find key in a json document in Golang
-	// https://stackoverflow.com/questions/52953282/how-to-find-a-key-in-a-json-document
-
-	jsonSchemaData, err := marshaller.JSONMarshalIndentWithMax(s, 2)
-	if err != nil {
-		return nil, fmt.Errorf("error indenting %s", err)
-	}
-
-	// JSON String
-	jsonString := string(jsonSchemaData)
-
-	enumTypes := []struct {
-		Name  string
-		Path  string
-		Enums []string
-	}{
-		{Name: "Engine",
-			Path:  "$defs.Spec.properties.Engine",
-			Enums: model.EngineNames()},
-		{Name: "Publisher",
-			Path:  "$defs.Spec.properties.Publisher",
-			Enums: model.PublisherNames()},
-		{Name: "StorageSource",
-			Path:  "$defs.StorageSpec.properties.StorageSource",
-			Enums: model.StorageSourceNames()},
-	}
-	for _, enumType := range enumTypes {
-		// Use sjson to find the enum type path in the JSON
-		jsonString, _ = sjson.Set(jsonString, enumType.Path+".type", "string")
-
-		jsonString, _ = sjson.Set(jsonString, enumType.Path+".enum", enumType.Enums)
-	}
-
-	return []byte(jsonString), nil
 }
diff --git a/cmd/util/reader.go b/cmd/util/reader.go
index 76fa8acd58..dd1a9a95e9 100644
--- a/cmd/util/reader.go
+++ b/cmd/util/reader.go
@@ -6,12 +6,54 @@ import (
 	"os"
 
 	"github.com/spf13/cobra"
+
+	"github.com/bacalhau-project/bacalhau/pkg/userstrings"
 )
 
+func ReadJobFromUser(cmd *cobra.Command, args []string) ([]byte, error) {
+	// read the job spec from stdin or file
+	var err error
+	var jobBytes []byte
+	if len(args) == 0 {
+		jobBytes, err = ReadFromStdinIfAvailable(cmd)
+		if err != nil {
+			return nil, fmt.Errorf("reading job from stdin: %w", err)
+		}
+	} else {
+		jobBytes, err = ReadFromFile(args[0])
+		if err != nil {
+			return nil, err
+		}
+	}
+	if len(jobBytes) == 0 {
+		return nil, fmt.Errorf("%s: no content provided", userstrings.JobSpecBad)
+	}
+	return jobBytes, nil
+}
+
 func ReadFromStdinIfAvailable(cmd *cobra.Command) ([]byte, error) {
 	// write to stderr since stdout is reserved for details of jobs.
-	if _, err := io.WriteString(os.Stderr, "Reading from /dev/stdin; send Ctrl-d to stop."); err != nil {
-		return nil, fmt.Errorf("unable to write to stderr")
+	if _, err := io.WriteString(cmd.ErrOrStderr(), "Reading from /dev/stdin; send Ctrl-d to stop."); err != nil {
+		return nil, fmt.Errorf("unable to write to stderr: %w", err)
+	}
+	result, err := io.ReadAll(cmd.InOrStdin())
+	if err != nil {
+		return nil, fmt.Errorf("reading job from stdin: %w", err)
+	}
+	return result, nil
+}
+
+func ReadFromFile(path string) ([]byte, error) {
+	var fileContent *os.File
+	fileContent, err := os.Open(path)
+	if err != nil {
+		return nil, fmt.Errorf("opening job file (%q): %w", path, err)
+	}
+	defer fileContent.Close()
+
+	result, err := io.ReadAll(fileContent)
+	if err != nil {
+		return nil, fmt.Errorf("reading job file (%q): %w", path, err)
 	}
-	return io.ReadAll(cmd.InOrStdin())
+	return result, nil
 }
diff --git a/go.mod b/go.mod
index a6a1e717b0..b2b6c66195 100644
--- a/go.mod
+++ b/go.mod
@@ -27,7 +27,6 @@ require (
 	github.com/hashicorp/go-plugin v1.6.0
 	github.com/hashicorp/go-retryablehttp v0.7.5
 	github.com/imdario/mergo v0.3.16
-	github.com/invopop/jsonschema v0.12.0
 	github.com/ipfs/boxo v0.18.0
 	github.com/ipfs/go-cid v0.4.1
 	github.com/ipfs/go-ipld-format v0.6.0
@@ -61,9 +60,7 @@ require (
 	github.com/swaggo/swag v1.16.2
 	github.com/tetratelabs/wazero v1.6.0
 	github.com/theckman/yacspin v0.13.12
-	github.com/tidwall/sjson v1.2.5
 	github.com/vincent-petithory/dataurl v1.0.0
-	github.com/xeipuuv/gojsonschema v1.2.0
 	go.etcd.io/bbolt v1.3.8
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0
 	go.opentelemetry.io/otel v1.26.0
@@ -106,8 +103,6 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 // indirect
-	github.com/bahlo/generic-list-go v0.2.0 // indirect
-	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/distribution/reference v0.5.0 // indirect
 	github.com/docker/go-connections v0.4.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -152,7 +147,6 @@ require (
 	github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v1.2.2 // indirect
-	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
 	github.com/yashtewari/glob-intersection v0.2.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/mod v0.15.0 // indirect
@@ -258,9 +252,6 @@ require (
 	github.com/spf13/cast v1.5.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/subosito/gotenv v1.4.2 // indirect
-	github.com/tidwall/gjson v1.14.4 // indirect
-	github.com/tidwall/match v1.1.1 // indirect
-	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
 	github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect
 	github.com/whyrusleeping/cbor-gen v0.0.0-20240109153615-66e95c3e8a87 // indirect
diff --git a/go.sum b/go.sum
index 8d4daad422..88ecb575c7 100644
--- a/go.sum
+++ b/go.sum
@@ -108,8 +108,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 h1:F7tQr61zYnTaeY50Rn4jwfVQbtcq
 github.com/aws/aws-sdk-go-v2/service/sts v1.28.0/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4=
 github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
 github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
-github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
-github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
 github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -121,8 +119,6 @@ github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwN
 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
 github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
-github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
-github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
 github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
 github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b h1:6+ZFm0flnudZzdSE0JxlhR2hKnGPcNB35BjQf4RYQDY=
@@ -397,8 +393,6 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
 github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
-github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
 github.com/ipfs-shipyard/nopfs v0.0.12 h1:mvwaoefDF5VI9jyvgWCmaoTJIJFAfrbyQV5fJz35hlk=
 github.com/ipfs-shipyard/nopfs v0.0.12/go.mod h1:mQyd0BElYI2gB/kq/Oue97obP4B3os4eBmgfPZ+hnrE=
 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7UynTbtdlt+w08ggb1UGLGaGjp1mMaZhoTZSctpn5Ak=
@@ -855,15 +849,6 @@ github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzl
 github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
 github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
 github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
-github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
-github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
-github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
-github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
-github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
 github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ=
 github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM=
@@ -890,15 +875,10 @@ github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdz
 github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
 github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 h1:E9S12nwJwEOXe2d6gT6qxdvqMnNq+VnSsKPgm2ZZNds=
 github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI=
-github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
-github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
 github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/go.work.sum b/go.work.sum
index 583dbd8861..869ce7d6f0 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -2855,6 +2855,14 @@ github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e h1:
 github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e/go.mod h1:XDKHRm5ThF8YJjx001LtgelzsoaEcvnA7lVWz9EeX3g=
 github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
 github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
 github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -2905,6 +2913,8 @@ github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyh
 github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc=
 github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
 github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
diff --git a/pkg/lib/marshaller/utils.go b/pkg/lib/marshaller/utils.go
index a90839d1aa..47bf1037c5 100644
--- a/pkg/lib/marshaller/utils.go
+++ b/pkg/lib/marshaller/utils.go
@@ -158,7 +158,7 @@ func validateRawJob(jobSpec map[string]interface{}) error {
 	}
 	// any fields that remain are considered unknown
 	for key := range jobSpec {
-		mErr = errors.Join(mErr, fmt.Errorf("unknown field: '%s' in 'Job' is ignored", key))
+		mErr = errors.Join(mErr, fmt.Errorf("unknown field: '%s' in 'Job'", key))
 	}
 	return mErr
 }


Test patch:
diff --git a/cmd/cli/validate/validate_test.go b/cmd/cli/validate/validate_test.go
deleted file mode 100644
index 7e12d600c4..0000000000
--- a/cmd/cli/validate/validate_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-//go:build unit || !integration
-
-package validate_test
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-	"github.com/stretchr/testify/suite"
-
-	cmdtesting "github.com/bacalhau-project/bacalhau/cmd/testing"
-	"github.com/bacalhau-project/bacalhau/testdata"
-)
-
-type ValidateSuite struct {
-	cmdtesting.BaseSuite
-}
-
-func TestValidateSuite(t *testing.T) {
-	suite.Run(t, new(ValidateSuite))
-}
-
-func (s *ValidateSuite) TestValidate() {
-	tests := map[string]struct {
-		testFile *testdata.FixtureLegacy
-		valid    bool
-	}{
-		"validJobFile":   {testFile: testdata.YamlJobNoop, valid: true},
-		"InvalidJobFile": {testFile: testdata.YamlJobNoopInvalid, valid: false},
-	}
-	for name, test := range tests {
-		s.Run(name, func() {
-			_, out, err := s.ExecuteTestCobraCommand("validate",
-				test.testFile.AsTempFile(s.T(), fmt.Sprintf("%s.*.yaml", name)),
-			)
-
-			if test.valid {
-				require.NoError(s.T(), err)
-				require.Contains(s.T(), out, "The Job is valid", fmt.Sprintf("%s: Jobspec Invalid", name))
-			} else {
-				require.Error(s.T(), err)
-
-				fatalError := string(out)
-				require.Contains(s.T(), fatalError, "The Job is not valid.", fmt.Sprintf("%s: Jobspec Invalid returning valid", name))
-				require.Contains(s.T(), fatalError, "APIVersion is required", fmt.Sprintf("%s: Jobspec Invalid returning valid", name))
-			}
-		})
-
-	}
-}
diff --git a/pkg/lib/marshaller/utils_test.go b/pkg/lib/marshaller/utils_test.go
index 6ed7f27569..5c239bbf78 100644
--- a/pkg/lib/marshaller/utils_test.go
+++ b/pkg/lib/marshaller/utils_test.go
@@ -69,7 +69,7 @@ func TestUnmarshalJob(t *testing.T) {
 		j, err := UnmarshalJob([]byte(jobWithUnknownFields))
 		assert.Nil(t, j)
 		assert.Error(t, err)
-		assert.ErrorContains(t, err, "unknown field: 'Selector' in 'Job' is ignored")
+		assert.ErrorContains(t, err, "unknown field: 'Selector' in 'Job'")
 	})
 	t.Run("job with un-settable field", func(t *testing.T) {
 		var jobWithUnknownFields = `