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 }