github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/cmd/bacalhau/validate.go (about) 1 package bacalhau 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 9 "github.com/filecoin-project/bacalhau/pkg/model" 10 "github.com/filecoin-project/bacalhau/pkg/util/templates" 11 "github.com/invopop/jsonschema" 12 "github.com/spf13/cobra" 13 14 "k8s.io/kubectl/pkg/util/i18n" 15 "sigs.k8s.io/yaml" 16 17 "github.com/xeipuuv/gojsonschema" 18 19 "github.com/tidwall/sjson" 20 ) 21 22 var ( 23 validateLong = templates.LongDesc(i18n.T(` 24 Validate a job from a file 25 26 JSON and YAML formats are accepted. 27 `)) 28 29 //nolint:lll // Documentation 30 validateExample = templates.Examples(i18n.T(` 31 # Validate a job using the data in job.yaml 32 bacalhau validate ./job.yaml 33 34 # Validate a job using stdin 35 cat job.yaml | bacalhau validate 36 37 # Output the jsonschema for a bacalhau job 38 bacalhau validate --output-schema 39 `)) 40 ) 41 42 type ValidateOptions struct { 43 Filename string // Filename for job (can be .json or .yaml) 44 OutputFormat string // Output format (json or yaml) 45 OutputSchema bool // Output the schema to stdout 46 OutputDirectory string // Output directory for the job 47 } 48 49 func NewValidateOptions() *ValidateOptions { 50 return &ValidateOptions{ 51 Filename: "", 52 OutputFormat: "yaml", 53 OutputSchema: false, 54 OutputDirectory: "", 55 } 56 } 57 58 func newValidateCmd() *cobra.Command { 59 OV := NewValidateOptions() 60 61 validateCmd := &cobra.Command{ 62 Use: "validate", 63 Short: "validate a job using a json or yaml file.", 64 Long: validateLong, 65 Example: validateExample, 66 Args: cobra.MinimumNArgs(0), 67 RunE: func(cmd *cobra.Command, cmdArgs []string) error { //nolint:unparam // incorrect that cmd is unused. 68 return validate(cmd, cmdArgs, OV) 69 }, 70 } 71 72 validateCmd.PersistentFlags().BoolVar( 73 &OV.OutputSchema, "output-schema", OV.OutputSchema, 74 `Output the JSON schema for a Job to stdout then exit`, 75 ) 76 77 return validateCmd 78 } 79 80 func validate(cmd *cobra.Command, cmdArgs []string, OV *ValidateOptions) error { 81 j := &model.Job{} 82 jsonSchemaData, err := GenerateJobJSONSchema() 83 if err != nil { 84 return err 85 } 86 87 if OV.OutputSchema { 88 //nolint 89 cmd.Printf("%s", jsonSchemaData) 90 return nil 91 } 92 93 if len(cmdArgs) == 0 { 94 _ = cmd.Usage() 95 Fatal(cmd, "You must specify a filename or provide the content to be validated via stdin.", 1) 96 } 97 98 OV.Filename = cmdArgs[0] 99 var byteResult []byte 100 101 if OV.Filename == "" { 102 // Read from stdin 103 byteResult, err = io.ReadAll(cmd.InOrStdin()) 104 if err != nil { 105 Fatal(cmd, fmt.Sprintf("Error reading from stdin: %s", err), 1) 106 } 107 if byteResult == nil { 108 // Can you ever get here? 109 Fatal(cmd, "No filename provided.", 1) 110 } 111 } else { 112 var file *os.File 113 fileextension := filepath.Ext(OV.Filename) 114 file, err = os.Open(OV.Filename) 115 116 if err != nil { 117 Fatal(cmd, fmt.Sprintf("Error opening file (%s): %s", OV.Filename, err), 1) 118 } 119 120 byteResult, err = io.ReadAll(file) 121 122 if err != nil { 123 return err 124 } 125 126 if fileextension == ".json" || fileextension == ".yaml" || fileextension == ".yml" { 127 // Yaml can parse json 128 err = model.YAMLUnmarshalWithMax(byteResult, &j) 129 if err != nil { 130 Fatal(cmd, fmt.Sprintf("Error unmarshaling yaml from file (%s): %s", OV.Filename, err), 1) 131 } 132 } else { 133 Fatal(cmd, fmt.Sprintf("File extension (%s) not supported. The file must end in either .yaml, .yml or .json.", fileextension), 1) 134 } 135 } 136 137 // Convert the schema to JSON - this is required for the gojsonschema library 138 // Noop if you pass JSON through 139 fileContentsAsJSONBytes, err := yaml.YAMLToJSON(byteResult) 140 if err != nil { 141 Fatal(cmd, fmt.Sprintf("Error converting yaml to json: %s", err), 1) 142 } 143 144 // println(str) 145 schemaLoader := gojsonschema.NewStringLoader(string(jsonSchemaData)) 146 documentLoader := gojsonschema.NewStringLoader(string(fileContentsAsJSONBytes)) 147 148 result, err := gojsonschema.Validate(schemaLoader, documentLoader) 149 if err != nil { 150 Fatal(cmd, fmt.Sprintf("Error validating json: %s", err), 1) 151 } 152 153 if result.Valid() { 154 cmd.Println("The Job is valid") 155 } else { 156 msg := "The Job is not valid. See errors:\n" 157 for _, desc := range result.Errors() { 158 msg += fmt.Sprintf("- %s\n", desc) 159 } 160 Fatal(cmd, msg, 1) 161 } 162 return nil 163 } 164 165 func GenerateJobJSONSchema() ([]byte, error) { 166 s := jsonschema.Reflect(&model.Job{}) 167 // Find key in a json document in Golang 168 // https://stackoverflow.com/questions/52953282/how-to-find-a-key-in-a-json-document 169 170 jsonSchemaData, err := model.JSONMarshalIndentWithMax(s, 2) 171 if err != nil { 172 return nil, fmt.Errorf("error indenting %s", err) 173 } 174 175 // JSON String 176 jsonString := string(jsonSchemaData) 177 178 enumTypes := []struct { 179 Name string 180 Path string 181 Enums []string 182 }{ 183 {Name: "Engine", 184 Path: "$defs.Spec.properties.Engine", 185 Enums: model.EngineNames()}, 186 {Name: "Verifier", 187 Path: "$defs.Spec.properties.Verifier", 188 Enums: model.VerifierNames()}, 189 {Name: "Publisher", 190 Path: "$defs.Spec.properties.Publisher", 191 Enums: model.PublisherNames()}, 192 {Name: "StorageSource", 193 Path: "$defs.StorageSpec.properties.StorageSource", 194 Enums: model.StorageSourceNames()}, 195 } 196 for _, enumType := range enumTypes { 197 // Use sjson to find the enum type path in the JSON 198 jsonString, _ = sjson.Set(jsonString, enumType.Path+".type", "string") 199 200 jsonString, _ = sjson.Set(jsonString, enumType.Path+".enum", enumType.Enums) 201 } 202 203 return []byte(jsonString), nil 204 }