code/go/internal/validator/spec.go (198 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package validator import ( "errors" "fmt" "io/fs" "log" "regexp" "slices" "strings" "github.com/Masterminds/semver/v3" spec "github.com/elastic/package-spec/v3" "github.com/elastic/package-spec/v3/code/go/internal/fspath" "github.com/elastic/package-spec/v3/code/go/internal/loader" "github.com/elastic/package-spec/v3/code/go/internal/packages" "github.com/elastic/package-spec/v3/code/go/internal/spectypes" "github.com/elastic/package-spec/v3/code/go/internal/validator/semantic" "github.com/elastic/package-spec/v3/code/go/pkg/specerrors" ) // Spec represents a package specification type Spec struct { // version is the version requested, what is included in the package, possibly without prerelease tags. version semver.Version // specVersion is the version of the spec actually loaded, what can include prerelease tags. specVersion semver.Version // fs contains the filesystem of the spec. fs fs.FS } type validationRule func(pkg fspath.FS) specerrors.ValidationErrors type validationRules []validationRule // GASpecCheckVersion represents minimum version to start checking for unreleased version of the spec var GASpecCheckVersion = semver.MustParse("3.0.1") // NewSpec creates a new Spec for the given version func NewSpec(version semver.Version) (*Spec, error) { specVersion, err := spec.CheckVersion(version) if err != nil { return nil, fmt.Errorf("could not load specification for version [%s]: %w", version.String(), err) } // With more current versions this is reported as a filterable validation error for GA packages. if version.LessThan(GASpecCheckVersion) { if specVersion.Prerelease() != "" { log.Printf("Warning: package using an unreleased version of the spec (%s)", specVersion) } } s := Spec{ version, *specVersion, spec.FS(), } return &s, nil } // ValidatePackage validates the given Package against the Spec func (s Spec) ValidatePackage(pkg packages.Package) specerrors.ValidationErrors { var errs specerrors.ValidationErrors rootSpec, err := loader.LoadSpec(s.fs, s.version, pkg.Type) if err != nil { errs = append(errs, specerrors.NewStructuredErrorf("could not read root folder spec file: %w", err)) return errs } if !s.version.LessThan(GASpecCheckVersion) && pkg.IsGA() { if s.specVersion.Prerelease() != "" { err := specerrors.NewStructuredError( fmt.Errorf("file \"%s\": package with GA version (%s) is using an unreleased version of the spec (%s)", pkg.Path("manifest.yml"), pkg.Version, s.specVersion), specerrors.CodeNonGASpecOnGAPackage) errs = append(errs, err) } } // Syntactic validations validator := newValidator(rootSpec, &pkg) errs = append(errs, validator.Validate()...) // Semantic validations errs = append(errs, s.rules(pkg.Type, rootSpec).validate(&pkg)...) return processErrors(errs) } func substringInSlice(str string, list []string) bool { for _, substr := range list { if strings.Contains(str, substr) { return true } } return false } func processErrors(errs specerrors.ValidationErrors) specerrors.ValidationErrors { var processedErrs specerrors.ValidationErrors // Rename unclear error messages msgTransforms := []struct { matcher *regexp.Regexp new string }{ { matcher: regexp.MustCompile(`Must not validate the schema \(not\)`), new: "Must not be present", }, { matcher: regexp.MustCompile("secret is required"), new: "variable identified as possible secret, secret parameter required to be set to true or false", }, { matcher: regexp.MustCompile(`(field processors.[0-9]+.rename): if is required`), new: "%s: rename \"message\" to \"event.original\" processor requires if: 'ctx.event?.original == null'", }, { matcher: regexp.MustCompile(`(field processors.[0-9]+): remove is required`), new: "%s: rename \"message\" to \"event.original\" processor requires remove \"message\" processor", }, { matcher: regexp.MustCompile(`(processors.[0-9]+.remove.field): processors.[0-9]+.remove.field does not match: "message"`), new: "%s: rename \"message\" to \"event.original\" processor requires remove \"message\" processor", }, { matcher: regexp.MustCompile(`(processors.[0-9]+.remove.if): processors.[0-9]+.remove.if does not match: "ctx\.event\?\.original != null"`), new: "%s: rename \"message\" to \"event.original\" processor requires remove \"message\" processor with if: 'ctx.event?.original != null'", }, } // Filter out redundant errors redundant := []string{ "Must validate \"then\" as \"if\" was valid", "Must validate \"else\" as \"if\" was not valid", "Must validate all the schemas (allOf)", "Must validate at least one schema (anyOf)", "Must validate one and only one schema (oneOf)", "At least one of the items must match", } // Add error code to specific errors addErrorCode := []struct { matcher *regexp.Regexp code string }{ { matcher: regexp.MustCompile(`rename "message" to "event.original" processor`), code: specerrors.MessageRenameToEventOriginalValidation, }, } for _, e := range errs { for _, msg := range msgTransforms { if match := msg.matcher.FindStringSubmatch(e.Error()); len(match) > 1 { e = specerrors.NewStructuredError( errors.New(strings.Replace(e.Error(), match[0], fmt.Sprintf(msg.new, match[1]), 1)), specerrors.UnassignedCode) } else if msg.matcher.MatchString(e.Error()) { e = specerrors.NewStructuredError( errors.New(strings.Replace(e.Error(), msg.matcher.FindString(e.Error()), msg.new, 1)), specerrors.UnassignedCode) } } for _, transform := range addErrorCode { if transform.matcher.MatchString(e.Error()) { e = specerrors.NewStructuredError(e, transform.code) } } if substringInSlice(e.Error(), redundant) { continue } processedErrs = append(processedErrs, e) } return processedErrs } func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules { rulesDef := []struct { fn validationRule since *semver.Version until *semver.Version types []string }{ {fn: semantic.ValidateVersionIntegrity}, {fn: semantic.ValidateChangelogLinks}, {fn: semantic.ValidatePrerelease}, {fn: semantic.WarnOn(semantic.ValidateMinimumKibanaVersion), until: semver.MustParse("3.0.0")}, {fn: semantic.ValidateMinimumKibanaVersion, since: semver.MustParse("3.0.0")}, {fn: semantic.ValidateFieldGroups}, {fn: semantic.ValidateFieldsLimits(rootSpec.MaxFieldsPerDataStream()), types: []string{"integration", "input"}}, {fn: semantic.ValidateUniqueFields, since: semver.MustParse("2.0.0"), types: []string{"integration", "input"}}, {fn: semantic.ValidateDimensionFields, types: []string{"integration", "input"}}, {fn: semantic.ValidateDateFields, types: []string{"integration", "input"}}, {fn: semantic.ValidateRequiredFields, types: []string{"integration", "input"}}, {fn: semantic.ValidateExternalFieldsWithDevFolder, types: []string{"integration", "input"}}, {fn: semantic.WarnOn(semantic.ValidateVisualizationsUsedByValue), types: []string{"integration", "content"}, until: semver.MustParse("3.0.0")}, {fn: semantic.ValidateVisualizationsUsedByValue, types: []string{"integration", "content"}, since: semver.MustParse("3.0.0")}, {fn: semantic.ValidateILMPolicyPresent, since: semver.MustParse("2.0.0"), types: []string{"integration"}}, {fn: semantic.ValidateProfilingNonGA, types: []string{"integration"}}, {fn: semantic.ValidateKibanaObjectIDs, types: []string{"integration", "content"}}, {fn: semantic.ValidateRoutingRulesAndDataset, types: []string{"integration"}, since: semver.MustParse("2.9.0")}, {fn: semantic.ValidateKibanaNoDanglingObjectIDs, since: semver.MustParse("3.0.0")}, {fn: semantic.ValidateKibanaFilterPresent, since: semver.MustParse("3.0.0")}, {fn: semantic.ValidateKibanaNoLegacyVisualizations, types: []string{"integration", "content"}, since: semver.MustParse("3.0.0")}, {fn: semantic.ValidateDimensionsPresent, types: []string{"integration"}, since: semver.MustParse("3.0.1")}, {fn: semantic.ValidateCapabilitiesRequired, since: semver.MustParse("2.10.0")}, // capabilities definition was added in spec version 2.10.0 {fn: semantic.ValidateRequiredVarGroups}, } var validationRules validationRules for _, rule := range rulesDef { if rule.since != nil && s.version.LessThan(rule.since) { continue } if rule.until != nil && !s.version.LessThan(rule.until) { continue } if rule.types != nil && !slices.Contains(rule.types, pkgType) { continue } validationRules = append(validationRules, rule.fn) } return validationRules } func (vr validationRules) validate(fsys fspath.FS) specerrors.ValidationErrors { var errs specerrors.ValidationErrors for _, validationRule := range vr { err := validationRule(fsys) errs.Append(err) } return errs }