github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/cmd/bacalhau/create.go (about)

     1  package bacalhau
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"reflect"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/filecoin-project/bacalhau/pkg/bacerrors"
    12  	"github.com/filecoin-project/bacalhau/pkg/downloader/util"
    13  	jobutils "github.com/filecoin-project/bacalhau/pkg/job"
    14  	"github.com/filecoin-project/bacalhau/pkg/model"
    15  	"github.com/filecoin-project/bacalhau/pkg/system"
    16  	"github.com/filecoin-project/bacalhau/pkg/userstrings"
    17  	"github.com/filecoin-project/bacalhau/pkg/util/templates"
    18  	"github.com/ipld/go-ipld-prime/codec/json"
    19  	"github.com/spf13/cobra"
    20  	"k8s.io/kubectl/pkg/util/i18n"
    21  	"sigs.k8s.io/yaml"
    22  )
    23  
    24  var (
    25  	createLong = templates.LongDesc(i18n.T(`
    26  		Create a job from a file or from stdin.
    27  
    28  		JSON and YAML formats are accepted.
    29  	`))
    30  	//nolint:lll // Documentation
    31  	createExample = templates.Examples(i18n.T(`
    32  		# Create a job using the data in job.yaml
    33  		bacalhau create ./job.yaml
    34  
    35  		# Create a new job from an already executed job
    36  		bacalhau describe 6e51df50 | bacalhau create -`))
    37  )
    38  
    39  type CreateOptions struct {
    40  	Filename        string                   // Filename for job (can be .json or .yaml)
    41  	Concurrency     int                      // Number of concurrent jobs to run
    42  	Confidence      int                      // Minimum number of nodes that must agree on a verification result
    43  	RunTimeSettings RunTimeSettings          // Run time settings for execution (e.g. wait, get, etc after submission)
    44  	DownloadFlags   model.DownloaderSettings // Settings for running Download
    45  	DryRun          bool
    46  }
    47  
    48  func NewCreateOptions() *CreateOptions {
    49  	return &CreateOptions{
    50  		Filename:        "",
    51  		Concurrency:     1,
    52  		Confidence:      0,
    53  		DownloadFlags:   *util.NewDownloadSettings(),
    54  		RunTimeSettings: *NewRunTimeSettings(),
    55  	}
    56  }
    57  
    58  func newCreateCmd() *cobra.Command {
    59  	OC := NewCreateOptions()
    60  
    61  	createCmd := &cobra.Command{
    62  		Use:     "create",
    63  		Short:   "Create a job using a json or yaml file.",
    64  		Long:    createLong,
    65  		Example: createExample,
    66  		Args:    cobra.MinimumNArgs(0),
    67  		PreRun:  applyPorcelainLogLevel,
    68  		RunE: func(cmd *cobra.Command, cmdArgs []string) error {
    69  			return create(cmd, cmdArgs, OC)
    70  		},
    71  	}
    72  
    73  	createCmd.Flags().AddFlagSet(NewIPFSDownloadFlags(&OC.DownloadFlags))
    74  	createCmd.Flags().AddFlagSet(NewRunTimeSettingsFlags(&OC.RunTimeSettings))
    75  	createCmd.PersistentFlags().BoolVar(
    76  		&OC.DryRun, "dry-run", OC.DryRun,
    77  		`Do not submit the job, but instead print out what will be submitted`,
    78  	)
    79  
    80  	return createCmd
    81  }
    82  
    83  func create(cmd *cobra.Command, cmdArgs []string, OC *CreateOptions) error { //nolint:funlen,gocyclo
    84  	ctx := cmd.Context()
    85  
    86  	cm := ctx.Value(systemManagerKey).(*system.CleanupManager)
    87  
    88  	// Custom unmarshaller
    89  	// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field?rq=1
    90  	var jwi model.JobWithInfo
    91  	var j *model.Job
    92  	var err error
    93  	var byteResult []byte
    94  	var rawMap map[string]interface{}
    95  
    96  	j, err = model.NewJobWithSaneProductionDefaults()
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	if len(cmdArgs) == 0 {
   102  		byteResult, err = ReadFromStdinIfAvailable(cmd, cmdArgs)
   103  		if err != nil {
   104  			Fatal(cmd, fmt.Sprintf("Unknown error reading from file or stdin: %s\n", err), 1)
   105  			return err
   106  		}
   107  	} else {
   108  		OC.Filename = cmdArgs[0]
   109  
   110  		var fileContent *os.File
   111  		fileContent, err = os.Open(OC.Filename)
   112  
   113  		if err != nil {
   114  			Fatal(cmd, fmt.Sprintf("Error opening file: %s", err), 1)
   115  			return err
   116  		}
   117  
   118  		byteResult, err = io.ReadAll(fileContent)
   119  		if err != nil {
   120  			Fatal(cmd, fmt.Sprintf("Error reading file: %s", err), 1)
   121  			return err
   122  		}
   123  	}
   124  
   125  	// Do a first pass for parsing to see if it's a Job or JobWithInfo
   126  	err = model.YAMLUnmarshalWithMax(byteResult, &rawMap)
   127  	if err != nil {
   128  		Fatal(cmd, fmt.Sprintf("Error parsing file: %s", err), 1)
   129  		return err
   130  	}
   131  
   132  	// If it's a JobWithInfo, we need to convert it to a Job
   133  	if _, isJobWithInfo := rawMap["Job"]; isJobWithInfo {
   134  		err = model.YAMLUnmarshalWithMax(byteResult, &jwi)
   135  		if err != nil {
   136  			Fatal(cmd, userstrings.JobSpecBad, 1)
   137  			return err
   138  		}
   139  		byteResult, err = model.YAMLMarshalWithMax(jwi.Job)
   140  		if err != nil {
   141  			Fatal(cmd, userstrings.JobSpecBad, 1)
   142  			return err
   143  		}
   144  	} else if _, isTask := rawMap["with"]; isTask {
   145  		// Else it might be a IPVM Task in JSON format
   146  		var task *model.Task
   147  		task, taskErr := model.UnmarshalIPLD[model.Task](byteResult, json.Decode, model.UCANTaskSchema)
   148  		if taskErr != nil {
   149  			Fatal(cmd, userstrings.JobSpecBad, 1)
   150  			return taskErr
   151  		}
   152  
   153  		job, taskErr := model.NewJobWithSaneProductionDefaults()
   154  		if taskErr != nil {
   155  			panic(taskErr)
   156  		}
   157  
   158  		spec, taskErr := task.ToSpec()
   159  		if taskErr != nil {
   160  			Fatal(cmd, userstrings.JobSpecBad, 1)
   161  			return taskErr
   162  		}
   163  
   164  		job.Spec = *spec
   165  		byteResult, taskErr = model.YAMLMarshalWithMax(job)
   166  		if taskErr != nil {
   167  			Fatal(cmd, userstrings.JobSpecBad, 1)
   168  			return taskErr
   169  		}
   170  	}
   171  
   172  	if len(byteResult) == 0 {
   173  		Fatal(cmd, userstrings.JobSpecBad, 1)
   174  		return err
   175  	}
   176  
   177  	// Turns out the yaml parser supports both yaml & json (because json is a subset of yaml)
   178  	// so we can just use that
   179  	err = model.YAMLUnmarshalWithMax(byteResult, &j)
   180  	if err != nil {
   181  		Fatal(cmd, userstrings.JobSpecBad, 1)
   182  		return err
   183  	}
   184  
   185  	// See if the job spec is empty
   186  	if j == nil || reflect.DeepEqual(j.Spec, &model.Job{}) {
   187  		Fatal(cmd, userstrings.JobSpecBad, 1)
   188  		return err
   189  	}
   190  
   191  	// Warn on fields with data that will be ignored
   192  	var unusedFieldList []string
   193  	if j.Metadata.ClientID != "" {
   194  		unusedFieldList = append(unusedFieldList, "ClientID")
   195  		j.Metadata.ClientID = ""
   196  	}
   197  	if !reflect.DeepEqual(j.Metadata.CreatedAt, time.Time{}) {
   198  		unusedFieldList = append(unusedFieldList, "CreatedAt")
   199  		j.Metadata.CreatedAt = time.Time{}
   200  	}
   201  	if !reflect.DeepEqual(j.Spec.ExecutionPlan, model.JobExecutionPlan{}) {
   202  		unusedFieldList = append(unusedFieldList, "Verification")
   203  		j.Spec.ExecutionPlan = model.JobExecutionPlan{}
   204  	}
   205  	if j.Metadata.ID != "" {
   206  		unusedFieldList = append(unusedFieldList, "ID")
   207  		j.Metadata.ID = ""
   208  	}
   209  	if j.Metadata.Requester.RequesterNodeID != "" {
   210  		unusedFieldList = append(unusedFieldList, "RequesterNodeID")
   211  		j.Metadata.Requester.RequesterNodeID = ""
   212  	}
   213  	if len(j.Metadata.Requester.RequesterPublicKey) != 0 {
   214  		unusedFieldList = append(unusedFieldList, "RequesterPublicKey")
   215  		j.Metadata.Requester.RequesterPublicKey = nil
   216  	}
   217  
   218  	// Warn on fields with data that will be ignored
   219  	if len(unusedFieldList) > 0 {
   220  		cmd.Printf("WARNING: The following fields have data in them and will be ignored on creation: %s\n", strings.Join(unusedFieldList, ", "))
   221  	}
   222  
   223  	err = jobutils.VerifyJob(ctx, j)
   224  	if err != nil {
   225  		if _, ok := err.(*bacerrors.ImageNotFound); ok {
   226  			Fatal(cmd, fmt.Sprintf("Docker image '%s' not found in the registry, or needs authorization.", j.Spec.Docker.Image), 1)
   227  			return err
   228  		} else {
   229  			Fatal(cmd, fmt.Sprintf("Error verifying job: %s", err), 1)
   230  			return err
   231  		}
   232  	}
   233  	if OC.DryRun {
   234  		// Converting job to yaml
   235  		var yamlBytes []byte
   236  		yamlBytes, err = yaml.Marshal(j)
   237  		if err != nil {
   238  			Fatal(cmd, fmt.Sprintf("Error converting job to yaml: %s", err), 1)
   239  			return err
   240  		}
   241  		cmd.Print(string(yamlBytes))
   242  		return nil
   243  	}
   244  
   245  	err = ExecuteJob(ctx,
   246  		cm,
   247  		cmd,
   248  		j,
   249  		OC.RunTimeSettings,
   250  		OC.DownloadFlags,
   251  	)
   252  
   253  	if err != nil {
   254  		Fatal(cmd, fmt.Sprintf("Error executing job: %s", err), 1)
   255  		return err
   256  	}
   257  
   258  	return nil
   259  }