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

Issue:
Move `bacalhau create` to `bacalhau job run`
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 run` to create jobs.
```


Discussion / hints:

Fix patch:
diff --git a/cmd/cli/create/create.go b/cmd/cli/create/create.go
index 8c3c59b843..1a5973ee05 100644
--- a/cmd/cli/create/create.go
+++ b/cmd/cli/create/create.go
@@ -1,258 +1,18 @@
 package create
 
 import (
-	"fmt"
-	"io"
-	"os"
-	"reflect"
-	"strings"
-	"time"
-
-	"github.com/ipld/go-ipld-prime/codec/json"
 	"github.com/spf13/cobra"
-	"k8s.io/kubectl/pkg/util/i18n"
-	"sigs.k8s.io/yaml"
-
-	"github.com/bacalhau-project/bacalhau/cmd/util"
-	"github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags"
-	"github.com/bacalhau-project/bacalhau/cmd/util/hook"
-	"github.com/bacalhau-project/bacalhau/cmd/util/printer"
-	"github.com/bacalhau-project/bacalhau/pkg/config/types"
-	legacy_job "github.com/bacalhau-project/bacalhau/pkg/legacyjob"
-	"github.com/bacalhau-project/bacalhau/pkg/lib/marshaller"
-	"github.com/bacalhau-project/bacalhau/pkg/model"
-	clientv1 "github.com/bacalhau-project/bacalhau/pkg/publicapi/client"
-	clientv2 "github.com/bacalhau-project/bacalhau/pkg/publicapi/client/v2"
-	"github.com/bacalhau-project/bacalhau/pkg/userstrings"
-	"github.com/bacalhau-project/bacalhau/pkg/util/templates"
 )
 
-var (
-	createLong = templates.LongDesc(i18n.T(`
-		Create a job from a file or from stdin.
-
-		JSON and YAML formats are accepted.
-	`))
-	//nolint:lll // Documentation
-	createExample = templates.Examples(i18n.T(`
-		# Create a job using the data in job.yaml
-		bacalhau create ./job.yaml
-
-		# Create a new job from an already executed job
-		bacalhau describe 6e51df50 | bacalhau create `))
-)
-
-type CreateOptions struct {
-	Filename        string                                // Filename for job (can be .json or .yaml)
-	Concurrency     int                                   // Number of concurrent jobs to run
-	RunTimeSettings *cliflags.RunTimeSettingsWithDownload // Run time settings for execution (e.g. wait, get, etc after submission)
-	DownloadFlags   *cliflags.DownloaderSettings          // Settings for running Download
-}
-
-func NewCreateOptions() *CreateOptions {
-	return &CreateOptions{
-		Filename:        "",
-		Concurrency:     1,
-		DownloadFlags:   cliflags.NewDefaultDownloaderSettings(),
-		RunTimeSettings: cliflags.DefaultRunTimeSettingsWithDownload(),
-	}
-}
-
 func NewCmd() *cobra.Command {
-	OC := NewCreateOptions()
-
 	createCmd := &cobra.Command{
-		Use:      "create",
-		Short:    "Create a job using a json or yaml file.",
-		Long:     createLong,
-		Example:  createExample,
-		Args:     cobra.MinimumNArgs(0),
-		PreRunE:  hook.RemoteCmdPreRunHooks,
-		PostRunE: hook.RemoteCmdPostRunHooks,
+		Use:        "create",
+		Short:      "DEPRECATED: Create a job using a json or yaml file.",
+		Deprecated: "This command has moved! Please use `job run` to create jobs.",
+		Args:       cobra.MinimumNArgs(0),
 		RunE: func(cmd *cobra.Command, cmdArgs []string) error {
-			// initialize a new or open an existing repo merging any config file(s) it contains into cfg.
-			cfg, err := util.SetupRepoConfig(cmd)
-			if err != nil {
-				return fmt.Errorf("failed to setup repo: %w", err)
-			}
-			// create a v1 api client
-			apiV1, err := util.GetAPIClient(cfg)
-			if err != nil {
-				return fmt.Errorf("failed to create v1 api client: %w", err)
-			}
-			// create a v2 api client
-			apiV2, err := util.GetAPIClientV2(cmd, cfg)
-			if err != nil {
-				return fmt.Errorf("failed to create v2 api client: %w", err)
-			}
-			return create(cmd, cmdArgs, apiV1, apiV2, cfg, OC)
+			return nil
 		},
 	}
-
-	createCmd.Flags().AddFlagSet(cliflags.NewDownloadFlags(OC.DownloadFlags))
-	createCmd.Flags().AddFlagSet(cliflags.NewRunTimeSettingsFlagsWithDownload(OC.RunTimeSettings))
-
 	return createCmd
 }
-
-//nolint:funlen,gocyclo
-func create(
-	cmd *cobra.Command,
-	cmdArgs []string,
-	apiV1 *clientv1.APIClient,
-	apiV2 clientv2.API,
-	cfg types.BacalhauConfig,
-	OC *CreateOptions,
-) error { //nolint:funlen,gocyclo
-	ctx := cmd.Context()
-
-	// Custom unmarshaller
-	// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field?rq=1
-	var jwi model.JobWithInfo
-	var j *model.Job
-	var byteResult []byte
-	var rawMap map[string]interface{}
-
-	j, err := model.NewJobWithSaneProductionDefaults()
-	if err != nil {
-		return err
-	}
-
-	if len(cmdArgs) == 0 {
-		byteResult, err = util.ReadFromStdinIfAvailable(cmd)
-		if err != nil {
-			return fmt.Errorf("unknown error reading from file or stdin: %w", err)
-		}
-	} else {
-		OC.Filename = cmdArgs[0]
-
-		var fileContent *os.File
-		fileContent, err = os.Open(OC.Filename)
-
-		if err != nil {
-			return fmt.Errorf("error opening file: %w", err)
-		}
-
-		byteResult, err = io.ReadAll(fileContent)
-		if err != nil {
-			return fmt.Errorf("error reading file: %w", err)
-		}
-	}
-
-	// Do a first pass for parsing to see if it's a Job or JobWithInfo
-	err = marshaller.YAMLUnmarshalWithMax(byteResult, &rawMap)
-	if err != nil {
-		return fmt.Errorf("error parsing file: %w", err)
-	}
-
-	// If it's a JobWithInfo, we need to convert it to a Job
-	if _, isJobWithInfo := rawMap["Job"]; isJobWithInfo {
-		err = marshaller.YAMLUnmarshalWithMax(byteResult, &jwi)
-		if err != nil {
-			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-		}
-		byteResult, err = marshaller.YAMLMarshalWithMax(jwi.Job)
-		if err != nil {
-			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-		}
-	} else if _, isTask := rawMap["with"]; isTask {
-		// Else it might be a IPVM Task in JSON format
-		var task *model.Task
-		task, err = model.UnmarshalIPLD[model.Task](byteResult, json.Decode, model.UCANTaskSchema)
-		if err != nil {
-			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-		}
-
-		job, err := model.NewJobWithSaneProductionDefaults()
-		if err != nil {
-			// TODO this is a bit extreme, maybe just ensure the above call doesn't return an error? the mergo package is a bit pointless there.
-			panic(err)
-		}
-
-		spec, err := task.ToSpec()
-		if err != nil {
-			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-		}
-
-		job.Spec = *spec
-		byteResult, err = marshaller.YAMLMarshalWithMax(job)
-		if err != nil {
-			return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-		}
-	}
-
-	if len(byteResult) == 0 {
-		// TODO better error
-		return fmt.Errorf("%s: job is empty", userstrings.JobSpecBad)
-	}
-
-	// Turns out the yaml parser supports both yaml & json (because json is a subset of yaml)
-	// so we can just use that
-	err = marshaller.YAMLUnmarshalWithMax(byteResult, &j)
-	if err != nil {
-		return fmt.Errorf("%s: %w", userstrings.JobSpecBad, err)
-	}
-
-	// See if the job spec is empty
-	if j == nil || reflect.DeepEqual(j.Spec, &model.Job{}) {
-		// TODO better error
-		return fmt.Errorf("%s: job is empty", userstrings.JobSpecBad)
-	}
-
-	// Warn on fields with data that will be ignored
-	var unusedFieldList []string
-	if j.Metadata.ClientID != "" {
-		unusedFieldList = append(unusedFieldList, "ClientID")
-		j.Metadata.ClientID = ""
-	}
-	if !reflect.DeepEqual(j.Metadata.CreatedAt, time.Time{}) {
-		unusedFieldList = append(unusedFieldList, "CreatedAt")
-		j.Metadata.CreatedAt = time.Time{}
-	}
-	if j.Metadata.ID != "" {
-		unusedFieldList = append(unusedFieldList, "ID")
-		j.Metadata.ID = ""
-	}
-	if j.Metadata.Requester.RequesterNodeID != "" {
-		unusedFieldList = append(unusedFieldList, "RequesterNodeID")
-		j.Metadata.Requester.RequesterNodeID = ""
-	}
-	if len(j.Metadata.Requester.RequesterPublicKey) != 0 {
-		unusedFieldList = append(unusedFieldList, "RequesterPublicKey")
-		j.Metadata.Requester.RequesterPublicKey = nil
-	}
-
-	if !model.IsValidPublisher(j.Spec.PublisherSpec.Type) {
-		j.Spec.PublisherSpec = model.PublisherSpec{
-			//nolint:staticcheck // TODO: remove this when we have a proper publisher
-			Type: j.Spec.Publisher,
-		}
-	}
-
-	// Warn on fields with data that will be ignored
-	if len(unusedFieldList) > 0 {
-		cmd.Printf("WARNING: The following fields have data in them and will be ignored on creation: %s\n", strings.Join(unusedFieldList, ", "))
-	}
-
-	err = legacy_job.VerifyJob(ctx, j)
-	if err != nil {
-		return fmt.Errorf("error verifying job: %w", err)
-	}
-	if OC.RunTimeSettings.DryRun {
-		// Converting job to yaml
-		var yamlBytes []byte
-		yamlBytes, err = yaml.Marshal(j)
-		if err != nil {
-			return fmt.Errorf("error converting job to yaml: %w", err)
-		}
-		cmd.Print(string(yamlBytes))
-		return nil
-	}
-
-	executingJob, err := apiV1.Submit(ctx, j)
-	if err != nil {
-		return fmt.Errorf("submitting job for execution: %w", err)
-	}
-
-	return printer.PrintJobExecutionLegacy(ctx, executingJob, cmd, OC.DownloadFlags, OC.RunTimeSettings, apiV1, apiV2, cfg)
-}


Test patch:
diff --git a/cmd/cli/create/create_test.go b/cmd/cli/create/create_test.go
deleted file mode 100644
index bfa8db28dd..0000000000
--- a/cmd/cli/create/create_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-//go:build unit || !integration
-
-package create_test
-
-import (
-	"context"
-	"os"
-	"sync"
-	"testing"
-
-	"github.com/spf13/cobra"
-	"github.com/stretchr/testify/require"
-	"github.com/stretchr/testify/suite"
-
-	cmdtesting "github.com/bacalhau-project/bacalhau/cmd/testing"
-	"github.com/bacalhau-project/bacalhau/pkg/docker"
-	s3helper "github.com/bacalhau-project/bacalhau/pkg/s3"
-	testutils "github.com/bacalhau-project/bacalhau/pkg/test/utils"
-	"github.com/bacalhau-project/bacalhau/testdata"
-)
-
-type CreateSuite struct {
-	cmdtesting.BaseSuite
-}
-
-func TestCreateSuite(t *testing.T) {
-	suite.Run(t, new(CreateSuite))
-}
-
-func (s *CreateSuite) TestCreateGenericSubmitBetter() {
-	tests := []struct {
-		Name    string
-		Fixture *testdata.FixtureLegacy
-	}{
-		{
-			Name:    "noop json",
-			Fixture: testdata.JsonJobNoop,
-		},
-		{
-			Name:    "noop yaml",
-			Fixture: testdata.YamlJobNoop,
-		},
-		{
-			Name:    "s3 yaml",
-			Fixture: testdata.YamlJobS3,
-		},
-		{
-			Name:    "url noop yaml",
-			Fixture: testdata.YamlJobNoopUrl,
-		},
-		//"TODO: re-enable wasm job which is currently broken as it relies on pulling data from the public IPFS network")
-		/*
-			{
-				Name:    "wasm task json",
-				FixtureLegacy: testdata.TaskWasmJson,
-			},
-		*/
-		{
-			Name:    "docker engine spec json",
-			Fixture: testdata.JsonJobDockerEngineSpec,
-		},
-		{
-			Name:    "docker engine spec yaml",
-			Fixture: testdata.YamlJobDockerEngineSpec,
-		},
-		{
-			Name:    "wasm engine spec json",
-			Fixture: testdata.JsonJobWasmEngineSpec,
-		},
-	}
-
-	// Let's do this once
-	canRunS3Test := s3helper.CanRunS3Test()
-
-	for _, tc := range tests {
-		s.Run(tc.Name, func() {
-			if tc.Fixture.RequiresS3() && !canRunS3Test {
-				// Skip the S3 tests if we have no AWS credentials installed
-				s.T().Skip("No valid AWS credentials found")
-			}
-
-			if tc.Fixture.RequiresDocker() {
-				docker.MustHaveDocker(s.T())
-			}
-
-			ctx := context.Background()
-			_, out, err := s.ExecuteTestCobraCommandWithStdinBytes(tc.Fixture.Data, "create")
-
-			require.NoError(s.T(), err, "Error submitting job")
-			testutils.GetJobFromTestOutputLegacy(ctx, s.T(), s.Client, out)
-		})
-	}
-}
-
-func (s *CreateSuite) TestCreateFromStdin() {
-	_, out, err := s.ExecuteTestCobraCommandWithStdinBytes(testdata.YamlJobNoop.Data, "create")
-
-	require.NoError(s.T(), err, "Error submitting job.")
-
-	// Now run describe on the ID we got back
-	job := testutils.GetJobFromTestOutputLegacy(context.Background(), s.T(), s.Client, out)
-	_, _, err = s.ExecuteTestCobraCommand("describe", job.Metadata.ID)
-
-	require.NoError(s.T(), err, "Error describing job.")
-}
-
-// cspell:ignore Dont
-func (s *CreateSuite) TestCreateDontPanicOnEmptyFile() {
-	type commandReturn struct {
-		c   *cobra.Command
-		out string
-		err error
-	}
-
-	commandChan := make(chan commandReturn, 1)
-
-	wg := sync.WaitGroup{}
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-		c, out, err := s.ExecuteTestCobraCommand("create", "./testdata/empty.yaml")
-
-		commandChan <- commandReturn{c: c, out: out, err: err}
-	}()
-	wg.Wait()
-
-	stdinErr := os.Stdin.Close()
-	require.NoError(s.T(), stdinErr, "Error closing stdin")
-
-	commandReturnValue := <-commandChan
-
-	require.Contains(s.T(), commandReturnValue.out, "the job provided is invalid", "Output message should error properly.")
-}