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  }