diff --git a/AGENTS.md b/AGENTS.md index f8c685798..d80fdb18c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,33 @@ versions: ``` **Important**: Remove property references before the definitions they depend on. +**Important**: This is not needed for development files (files under `_dev` directories) + +#### Version Patch Comment Conventions + +Comments in version patches should: +- Be placed on the `path` line, not the `op` line +- Only be added for complex paths with array indices (e.g., `required/-`) +- Be omitted for simple, self-explanatory paths + +```yaml +# Good - comment on path line for array index +- op: add + path: "/properties/policy_templates/items/required/-" # re-add type as required + value: type + +# Good - no comment for simple path +- op: remove + path: "/properties/requires" + +# Bad - comment on op line +- op: remove # removes requires field + path: "/properties/requires" + +# Bad - redundant comment on simple path +- op: remove + path: "/properties/requires" # removes requires property +``` ## Test Packages @@ -150,6 +177,189 @@ tests := map[string]struct { **Note**: Use tabs for indentation in Go files. +## Compliance Testing + +Compliance tests are integration tests that verify package functionality in a real Kibana/Elasticsearch environment. They are located in `compliance/features/*.feature` files using Gherkin syntax. + +### Creating Compliance Tests + +Add a new feature file in `compliance/features/`: + +```gherkin +Feature: Basic package types support + Basic tests with minimal packages + + @3.3.0 + Scenario: Basic content package can be installed + Given the "basic_content" package is installed + And prebuilt detection rules are loaded + Then there is a dashboard "basic_content-dashboard-abc-1" + And there is a detection rule "12cea9e9-5766-474d-a9dc-34ef7c7677c7" +``` + +**Version tags**: Use `@X.Y.Z` to indicate the minimum spec version required for the test. Tests tagged with versions higher than the current version won't be executed until that version is released. + +**Common patterns**: +- Package installation: `Given the "package_name" package is installed` +- Policy creation: `And a policy is created with "package_name" package` +- Verification: `Then there is an index template "template_name" with pattern "pattern-*"` +- Dependencies: `And the required input/content packages are installed` + +## Semantic Validators + +Semantic validators implement custom validation logic beyond JSON schema constraints. They are located in `code/go/internal/validator/semantic/`. + +### Creating Semantic Validators + +**Key Patterns**: +* Use `pkgpath.Files()` to read manifests and `file.Values("$.jsonpath")` to query YAML. +* Reuse existing methods in the package if they are useful, even if they are + defined in the files of other semantic validators. + +Example structure: +```go +func ValidateMyRule(fsys fspath.FS) specerrors.ValidationErrors { + manifest, err := readManifest(fsys) + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredError(err, specerrors.UnassignedCode)} + } + + // Validation logic + return validateMyRule(*manifest) +} + +func readManifest(fsys fspath.FS) (*pkgpath.File, error) { + manifestPath := "manifest.yml" + f, err := pkgpath.Files(fsys, manifestPath) + if err != nil { + return nil, fmt.Errorf("can't locate manifest file: %w", err) + } + if len(f) != 1 { + return nil, fmt.Errorf("single manifest file expected") + } + return &f[0], nil +} + +func validateMyRule(manifest pkgpath.File) specerrors.ValidationErrors { + val, err := manifest.Values("$.my.field") + if err != nil || val == nil { + return nil + } + // Type assertions and validation... +} +``` + +### Best Practices + +1. **Split into helper functions**: Separate file reading/parsing from validation logic +2. **Use pkgpath patterns**: + - `pkgpath.Files(fsys, "manifest.yml")` - single file + - `pkgpath.Files(fsys, "data_stream/*/manifest.yml")` - glob pattern +3. **Query with JSONPath**: `file.Values("$.policy_templates[0].name")` +4. **Type assertions**: Query results are `interface{}`, assert to expected types +5. **Error handling**: Return structured errors with file paths and context +6. **Use descriptive variable names**: Avoid abbreviations in validators. Use full names like `packageName` (not `pkgName`), `dataStreamManifests` (not `dsManifests`), `templateIndex` (not `ptIdx`) for better readability. +7. **Use fsys.Path() for error messages**: When creating error messages, use `fsys.Path("relative/path")` to get the full package path, not just the relative path from a file object. This ensures error messages match the test framework's expectations. + ```go + // Good - uses fsys.Path() for full package path + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %s", fsys.Path("_dev/test/config.yml"), err) + + // Bad - uses config.Path() which is relative within fsys + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %s", config.Path(), err) + ``` + +### Testing Semantic Validators + +**Use unit tests with t.TempDir() for simple validation rules. For complex scenarios involving multiple files and directories, use test packages in test/packages/ tested via validator_test.go.** + +#### Simple Unit Test Pattern + +For validators that check a single file or simple structure: + +```go +func TestValidateMyRule(t *testing.T) { + tests := map[string]struct { + manifest string + expectError bool + errorContains string + }{ + "valid": { + manifest: `name: test +format_version: 3.6.0`, + expectError: false, + }, + "invalid": { + manifest: `name: test +format_version: 3.6.0 +invalid_field: value`, + expectError: true, + errorContains: "invalid_field", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + pkgRoot := t.TempDir() + err := os.WriteFile(filepath.Join(pkgRoot, "manifest.yml"), + []byte(tc.manifest), 0644) + require.NoError(t, err) + + fsys := fspath.DirFS(pkgRoot) + errs := ValidateMyRule(fsys) + + if tc.expectError { + require.NotEmpty(t, errs) + require.Contains(t, errs[0].Error(), tc.errorContains) + } else { + require.Empty(t, errs) + } + }) + } +} +``` + +#### Complex Test Package Pattern + +For validators that need complete package structures (multiple data streams, pipelines, kibana objects, etc.): + +1. Create test package in `test/packages/` using `elastic-package create package` +2. Add test case to `code/go/pkg/validator/validator_test.go`: + +```go +tests := map[string]struct { + invalidPkgFilePath string + expectedErrContains []string +}{ + "good_my_feature": {}, // Valid package + "bad_my_feature": { + "manifest.yml", + []string{`validation error message`}, + }, +} +``` + +**When to use each approach:** +- **Unit tests (t.TempDir)**: Single file validation, simple manifest checks, basic field validation +- **Test packages**: Full package validation, multiple data streams, cross-file dependencies, complex pipeline/kibana object scenarios + +### Registering Validators + +In `code/go/internal/validator/spec.go`: +```go +{ + Func: semantic.ValidateMyRule, + Type: spectypes.Integration, + Version: semver.MustParse("3.6.0"), +} +``` + +### Reference Examples + +- `validate_minimum_kibana_version.go` - Multiple validation functions, pkgpath patterns +- `validate_package_references.go` - Policy templates and data streams validation +- `validate_deprecated_replaced_by_test.go` - Unit testing with t.TempDir() + ## Testing Commands ```bash @@ -159,6 +369,9 @@ go test ./code/go/internal # Run specific test go test -v -run "TestValidateFile/my_test" ./code/go/pkg/validator/... +# Run semantic validator tests +go test -v ./code/go/internal/validator/semantic -run TestMyValidator + # Run all tests go test ./code/go/... @@ -167,6 +380,9 @@ make -C code/go update # Format Go files make -C code/go format + +# Linting, required +make check ``` ## Changelog Management diff --git a/code/go/internal/validator/semantic/validate_package_references.go b/code/go/internal/validator/semantic/validate_package_references.go new file mode 100644 index 000000000..12293ec96 --- /dev/null +++ b/code/go/internal/validator/semantic/validate_package_references.go @@ -0,0 +1,232 @@ +// 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 semantic + +import ( + "slices" + + "github.com/Masterminds/semver/v3" + + "github.com/elastic/package-spec/v3/code/go/internal/fspath" + "github.com/elastic/package-spec/v3/code/go/internal/pkgpath" + "github.com/elastic/package-spec/v3/code/go/pkg/specerrors" +) + +// ValidatePackageReferences checks that package references in policy templates and data streams +// are listed in the manifest's requires section and are of the correct type (input packages only). +func ValidatePackageReferences(fsys fspath.FS) specerrors.ValidationErrors { + manifest, err := readManifest(fsys) + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path("manifest.yml"), err)} + } + + // Build lists of required packages by type + requiredPackages, err := getRequiredPackagesByType(*manifest) + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path("manifest.yml"), err)} + } + + // Validate policy template input package references + errs := validatePolicyTemplatePackageReferences(fsys, *manifest, requiredPackages) + + // Validate data stream stream package references + errs = append(errs, validateDataStreamPackageReferences(fsys, requiredPackages)...) + + // Validate restrictions on version formats + errs = append(errs, validateFixedVersions(fsys, requiredPackages)...) + + return errs +} + +// requiredPackages contains lists of required packages organized by type. +type requiredPackages struct { + input []requiredPackage + content []requiredPackage +} + +// requiredPackage contains information about required packages. +type requiredPackage struct { + name string + version string +} + +func getRequiredPackagesByType(manifest pkgpath.File) (requiredPackages, error) { + packages := requiredPackages{} + + // Get input packages from requires.input + inputPackages, err := manifest.Values("$.requires.input") + if err == nil && inputPackages != nil { + packages.input = extractPackageNamesFromRequires(inputPackages) + } + + // Get content packages from requires.content + contentPackages, err := manifest.Values("$.requires.content") + if err == nil && contentPackages != nil { + packages.content = extractPackageNamesFromRequires(contentPackages) + } + + return packages, nil +} + +func extractPackageNamesFromRequires(packages interface{}) []requiredPackage { + pkgArray, ok := packages.([]interface{}) + if !ok { + return nil + } + + var result []requiredPackage + for _, pkg := range pkgArray { + pkgMap, ok := pkg.(map[string]interface{}) + if !ok { + continue + } + + name, ok := pkgMap["name"].(string) + if !ok { + continue + } + + version, _ := pkgMap["version"].(string) + + result = append(result, requiredPackage{name: name, version: version}) + } + return result +} + +func validatePolicyTemplatePackageReferences(fsys fspath.FS, manifest pkgpath.File, requiredPackages requiredPackages) specerrors.ValidationErrors { + var errs specerrors.ValidationErrors + + policyTemplates, err := manifest.Values("$.policy_templates") + if err != nil || policyTemplates == nil { + return nil + } + + policyTemplateArray, ok := policyTemplates.([]interface{}) + if !ok { + return nil + } + + for templateIndex, template := range policyTemplateArray { + templateMap, ok := template.(map[string]interface{}) + if !ok { + continue + } + + inputs, ok := templateMap["inputs"].([]interface{}) + if !ok { + continue + } + + for inputIndex, input := range inputs { + inputMap, ok := input.(map[string]interface{}) + if !ok { + continue + } + + packageName, ok := inputMap["package"].(string) + if !ok || packageName == "" { + continue + } + + equalName := func(p requiredPackage) bool { + return p.name == packageName + } + + // Check if it's a content package (not allowed) + if slices.ContainsFunc(requiredPackages.content, equalName) { + errs = append(errs, specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: policy_templates[%d].inputs[%d] references package \"%s\" which is a content package, only input packages allowed", + fsys.Path("manifest.yml"), templateIndex, inputIndex, packageName)) + continue + } + + // Check if it's in required input packages + if !slices.ContainsFunc(requiredPackages.input, equalName) { + errs = append(errs, specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: policy_templates[%d].inputs[%d] references package \"%s\" which is not listed in requires section", + fsys.Path("manifest.yml"), templateIndex, inputIndex, packageName)) + } + } + } + + return errs +} + +func validateDataStreamPackageReferences(fsys fspath.FS, requiredPackages requiredPackages) specerrors.ValidationErrors { + dataStreamManifests, err := pkgpath.Files(fsys, "data_stream/*/manifest.yml") + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredErrorf("error while searching for data stream manifests: %w", err)} + } + + var errs specerrors.ValidationErrors + for _, dataStreamManifest := range dataStreamManifests { + streams, err := dataStreamManifest.Values("$.streams") + if err != nil || streams == nil { + continue + } + + streamArray, ok := streams.([]interface{}) + if !ok { + continue + } + + for streamIndex, stream := range streamArray { + streamMap, ok := stream.(map[string]interface{}) + if !ok { + continue + } + + packageName, ok := streamMap["package"].(string) + if !ok || packageName == "" { + continue + } + + equalName := func(p requiredPackage) bool { + return p.name == packageName + } + + // Check if it's a content package (not allowed) + if slices.ContainsFunc(requiredPackages.content, equalName) { + errs = append(errs, specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: streams[%d] references package \"%s\" which is a content package, only input packages allowed", + dataStreamManifest.Path(), streamIndex, packageName)) + continue + } + + // Check if it's in required input packages + if !slices.ContainsFunc(requiredPackages.input, equalName) { + errs = append(errs, specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: streams[%d] references package \"%s\" which is not listed in manifest requires section", + dataStreamManifest.Path(), streamIndex, packageName)) + } + } + } + + return errs +} + +func validateFixedVersions(fsys fspath.FS, requiredPackages requiredPackages) specerrors.ValidationErrors { + var errs specerrors.ValidationErrors + + // Validate that input packages have fixed versions, and no constraints. + for i, p := range requiredPackages.input { + if p.version == "" { + // Version presence controlled by the schema. + continue + } + + _, err := semver.NewVersion(p.version) + if err != nil { + errs = append(errs, specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: field requires.input.%d.version: version \"%s\" for package \"%s\" must be a valid semantic version, constraints are not allowed", + fsys.Path("manifest.yml"), i, p.version, p.name)) + } + } + + return errs +} diff --git a/code/go/internal/validator/semantic/validate_package_references_test.go b/code/go/internal/validator/semantic/validate_package_references_test.go new file mode 100644 index 000000000..3d2292cbb --- /dev/null +++ b/code/go/internal/validator/semantic/validate_package_references_test.go @@ -0,0 +1,235 @@ +// 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 semantic + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/package-spec/v3/code/go/internal/fspath" +) + +func TestValidatePackageReferences(t *testing.T) { + t.Run("valid_policy_template_package_reference", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + input: + - name: filelog_otel + version: "1.0.0" +policy_templates: + - name: apache + inputs: + - package: filelog_otel + title: Collect logs + description: Collecting Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.Empty(t, errs, "expected no validation errors") + }) + + t.Run("invalid_policy_template_package_reference", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + input: + - name: filelog_otel + version: "1.0.0" +policy_templates: + - name: apache + inputs: + - package: missing_package + title: Collect logs + description: Collecting Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 1) + assert.ErrorContains(t, errs, `policy_templates[0].inputs[0] references package "missing_package" which is not listed in requires section`) + }) + + t.Run("package_reference_to_content_package", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + content: + - name: apache_otel + version: "^1.0.0" +policy_templates: + - name: apache + inputs: + - package: apache_otel + title: Collect logs + description: Collecting Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 1) + assert.ErrorContains(t, errs, `policy_templates[0].inputs[0] references package "apache_otel" which is a content package, only input packages allowed`) + }) + + t.Run("valid_datastream_package_reference", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + input: + - name: filelog_otel + version: "1.0.0" +`), 0o644) + require.NoError(t, err) + + err = os.MkdirAll(filepath.Join(d, "data_stream", "logs"), 0o755) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "manifest.yml"), []byte(` +title: Apache logs +type: logs +streams: + - package: filelog_otel + title: Apache Logs + description: Collect Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.Empty(t, errs, "expected no validation errors") + }) + + t.Run("invalid_datastream_package_reference", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + input: + - name: filelog_otel + version: "1.0.0" +`), 0o644) + require.NoError(t, err) + + err = os.MkdirAll(filepath.Join(d, "data_stream", "logs"), 0o755) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(d, "data_stream", "logs", "manifest.yml"), []byte(` +title: Apache logs +type: logs +streams: + - package: missing_package + title: Apache Logs + description: Collect Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 1) + assert.ErrorContains(t, errs, `streams[0] references package "missing_package" which is not listed in manifest requires section`) + }) + + t.Run("datastream_reference_to_content_package", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + content: + - name: security_rules + version: "^1.0.0" +`), 0o644) + require.NoError(t, err) + + err = os.MkdirAll(filepath.Join(d, "data_stream", "alerts"), 0o755) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(d, "data_stream", "alerts", "manifest.yml"), []byte(` +title: Security alerts +type: logs +streams: + - package: security_rules + title: Security Alerts + description: Collect security alerts +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 1) + assert.ErrorContains(t, errs, `streams[0] references package "security_rules" which is a content package, only input packages allowed`) + }) + + t.Run("no_requires_section", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +policy_templates: + - name: apache + inputs: + - package: some_package + title: Collect logs + description: Collecting Apache logs +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 1) + assert.ErrorContains(t, errs, `policy_templates[0].inputs[0] references package "some_package" which is not listed in requires section`) + }) + + t.Run("multiple_invalid_references", func(t *testing.T) { + d := t.TempDir() + + err := os.WriteFile(filepath.Join(d, "manifest.yml"), []byte(` +format_version: 3.6.0 +type: integration +requires: + input: + - name: valid_package + version: "1.0.0" +policy_templates: + - name: apache + inputs: + - package: missing_package_1 + title: First input + description: First description + - package: missing_package_2 + title: Second input + description: Second description +`), 0o644) + require.NoError(t, err) + + errs := ValidatePackageReferences(fspath.DirFS(d)) + require.NotEmpty(t, errs, "expected validation errors") + assert.Len(t, errs, 2) + assert.ErrorContains(t, errs, `policy_templates[0].inputs[0] references package "missing_package_1"`) + assert.ErrorContains(t, errs, `policy_templates[0].inputs[1] references package "missing_package_2"`) + }) +} diff --git a/code/go/internal/validator/semantic/validate_test_package_requirements.go b/code/go/internal/validator/semantic/validate_test_package_requirements.go new file mode 100644 index 000000000..ac8be78ef --- /dev/null +++ b/code/go/internal/validator/semantic/validate_test_package_requirements.go @@ -0,0 +1,218 @@ +// 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 semantic + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/elastic/package-spec/v3/code/go/internal/fspath" + "github.com/elastic/package-spec/v3/code/go/internal/pkgpath" + "github.com/elastic/package-spec/v3/code/go/pkg/specerrors" +) + +// ValidateTestPackageRequirements checks that package requirements in test configurations +// reference packages listed in the manifest and that versions satisfy constraints. +func ValidateTestPackageRequirements(fsys fspath.FS) specerrors.ValidationErrors { + manifest, err := readManifest(fsys) + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path("manifest.yml"), err)} + } + + // Build map of required packages with their version constraints + requiredPackages, err := getRequiredPackagesWithConstraints(*manifest) + if err != nil { + return specerrors.ValidationErrors{ + specerrors.NewStructuredErrorf("file \"%s\" is invalid: %w", fsys.Path("manifest.yml"), err)} + } + + var errs specerrors.ValidationErrors + + // Validate integration-level test configs + integrationTestErrs := validateIntegrationTestRequirements(fsys, requiredPackages) + errs = append(errs, integrationTestErrs...) + + // Validate data stream test configs + dataStreamTestErrs := validateDataStreamTestRequirements(fsys, requiredPackages) + errs = append(errs, dataStreamTestErrs...) + + return errs +} + +func getRequiredPackagesWithConstraints(manifest pkgpath.File) (map[string]string, error) { + requiredPackages := make(map[string]string) + + // Get input packages from requires.input + inputPackages, err := manifest.Values("$.requires.input") + if err == nil && inputPackages != nil { + if pkgArray, ok := inputPackages.([]interface{}); ok { + for i := 0; i < len(pkgArray); i++ { + name, err := manifest.Values(fmt.Sprintf("$.requires.input[%d].name", i)) + if err != nil || name == nil { + continue + } + version, err := manifest.Values(fmt.Sprintf("$.requires.input[%d].version", i)) + if err != nil || version == nil { + continue + } + if nameStr, ok := name.(string); ok { + if versionStr, ok := version.(string); ok { + requiredPackages[nameStr] = versionStr + } + } + } + } + } + + // Get content packages from requires.content + contentPackages, err := manifest.Values("$.requires.content") + if err == nil && contentPackages != nil { + if pkgArray, ok := contentPackages.([]interface{}); ok { + for i := 0; i < len(pkgArray); i++ { + name, err := manifest.Values(fmt.Sprintf("$.requires.content[%d].name", i)) + if err != nil || name == nil { + continue + } + version, err := manifest.Values(fmt.Sprintf("$.requires.content[%d].version", i)) + if err != nil || version == nil { + continue + } + if nameStr, ok := name.(string); ok { + if versionStr, ok := version.(string); ok { + requiredPackages[nameStr] = versionStr + } + } + } + } + } + + return requiredPackages, nil +} + +func validateIntegrationTestRequirements(fsys fspath.FS, requiredPackages map[string]string) specerrors.ValidationErrors { + testConfig, err := pkgpath.Files(fsys, "_dev/test/config.yml") + if err != nil || len(testConfig) == 0 { + return nil + } + + var errs specerrors.ValidationErrors + for _, config := range testConfig { + // Check each test type (system, policy, etc.) + testTypes := []string{"system", "policy", "pipeline", "static", "asset"} + for _, testType := range testTypes { + requires, err := config.Values(fmt.Sprintf("$.%s.requires", testType)) + if err != nil || requires == nil { + continue + } + + if reqArray, ok := requires.([]interface{}); ok { + for idx, req := range reqArray { + if reqMap, ok := req.(map[string]interface{}); ok { + pkgName, _ := reqMap["package"].(string) + version, _ := reqMap["version"].(string) + + if pkgName == "" || version == "" { + continue + } + + err := validateTestRequirement(fsys.Path("_dev/test/config.yml"), testType, idx, pkgName, version, requiredPackages) + if err != nil { + errs = append(errs, err) + } + } + } + } + } + } + + return errs +} + +func validateDataStreamTestRequirements(fsys fspath.FS, requiredPackages map[string]string) specerrors.ValidationErrors { + testConfigs, err := pkgpath.Files(fsys, "data_stream/*/_dev/test/*/config.yml") + if err != nil { + return nil + } + + var errs specerrors.ValidationErrors + for _, config := range testConfigs { + requires, err := config.Values("$.requires") + if err != nil || requires == nil { + continue + } + + if reqArray, ok := requires.([]interface{}); ok { + for idx, req := range reqArray { + if reqMap, ok := req.(map[string]interface{}); ok { + pkgName, _ := reqMap["package"].(string) + version, _ := reqMap["version"].(string) + + if pkgName == "" || version == "" { + continue + } + + err := validateTestRequirement(fsys.Path(config.Path()), "", idx, pkgName, version, requiredPackages) + if err != nil { + errs = append(errs, err) + } + } + } + } + } + + return errs +} + +func validateTestRequirement(configPath, testType string, idx int, pkgName, version string, requiredPackages map[string]string) *specerrors.StructuredError { + constraint, exists := requiredPackages[pkgName] + if !exists { + location := fmt.Sprintf("requires[%d]", idx) + if testType != "" { + location = fmt.Sprintf("%s.%s", testType, location) + } + return specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: %s references package \"%s\" which is not listed in manifest requires section", + configPath, location, pkgName) + } + + // Parse the test version + testVersion, err := semver.NewVersion(version) + if err != nil { + location := fmt.Sprintf("requires[%d]", idx) + if testType != "" { + location = fmt.Sprintf("%s.%s", testType, location) + } + return specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: %s has invalid version \"%s\": %w", + configPath, location, version, err) + } + + // Parse the constraint from manifest + c, err := semver.NewConstraint(constraint) + if err != nil { + location := fmt.Sprintf("requires[%d]", idx) + if testType != "" { + location = fmt.Sprintf("%s.%s", testType, location) + } + return specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: %s package \"%s\" has invalid constraint in manifest: %w", + configPath, location, pkgName, err) + } + + // Check if test version satisfies constraint + if !c.Check(testVersion) { + location := fmt.Sprintf("requires[%d]", idx) + if testType != "" { + location = fmt.Sprintf("%s.%s", testType, location) + } + return specerrors.NewStructuredErrorf( + "file \"%s\" is invalid: %s package \"%s\" version \"%s\" does not satisfy constraint \"%s\"", + configPath, location, pkgName, version, constraint) + } + + return nil +} diff --git a/code/go/internal/validator/semantic/validate_test_package_requirements_test.go b/code/go/internal/validator/semantic/validate_test_package_requirements_test.go new file mode 100644 index 000000000..7fd5a5a2d --- /dev/null +++ b/code/go/internal/validator/semantic/validate_test_package_requirements_test.go @@ -0,0 +1,172 @@ +// 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 semantic + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/package-spec/v3/code/go/internal/fspath" +) + +func TestValidateTestPackageRequirements(t *testing.T) { + tests := map[string]struct { + manifest string + testConfig string + testConfigPath string + expectError bool + errorContains string + }{ + "valid_integration_test_requirement": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: sql_input + version: ^1.2.0`, + testConfig: `system: + requires: + - package: sql_input + version: 1.2.5`, + testConfigPath: "_dev/test/config.yml", + expectError: false, + }, + "valid_datastream_test_requirement": { + manifest: `name: test +format_version: 3.6.0 +requires: + content: + - name: logs_package + version: ~1.0.0`, + testConfig: `requires: + - package: logs_package + version: 1.0.3`, + testConfigPath: "data_stream/example/_dev/test/system/config.yml", + expectError: false, + }, + "package_not_in_manifest": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: sql_input + version: ^1.2.0`, + testConfig: `policy: + requires: + - package: missing_package + version: 1.0.0`, + testConfigPath: "_dev/test/config.yml", + expectError: true, + errorContains: "missing_package\" which is not listed in manifest requires", + }, + "version_does_not_satisfy_constraint": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: sql_input + version: ^2.0.0`, + testConfig: `system: + requires: + - package: sql_input + version: 1.5.0`, + testConfigPath: "_dev/test/config.yml", + expectError: true, + errorContains: "version \"1.5.0\" does not satisfy constraint \"^2.0.0\"", + }, + "invalid_test_version": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: sql_input + version: ^1.0.0`, + testConfig: `system: + requires: + - package: sql_input + version: invalid`, + testConfigPath: "_dev/test/config.yml", + expectError: true, + errorContains: "invalid version", + }, + "multiple_test_types_with_requirements": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: pkg1 + version: ^1.0.0 + content: + - name: pkg2 + version: ~2.0.0`, + testConfig: `system: + requires: + - package: pkg1 + version: 1.5.0 +policy: + requires: + - package: pkg2 + version: 2.0.5`, + testConfigPath: "_dev/test/config.yml", + expectError: false, + }, + "no_requires_in_test": { + manifest: `name: test +format_version: 3.6.0 +requires: + input: + - name: sql_input + version: ^1.0.0`, + testConfig: `system: + skip: + reason: Test reason + link: https://example.com`, + testConfigPath: "_dev/test/config.yml", + expectError: false, + }, + "no_requires_in_manifest": { + manifest: `name: test +format_version: 3.6.0`, + testConfig: `system: + requires: + - package: sql_input + version: 1.0.0`, + testConfigPath: "_dev/test/config.yml", + expectError: true, + errorContains: "sql_input\" which is not listed in manifest requires", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + pkgRoot := t.TempDir() + + // Create manifest + err := os.WriteFile(filepath.Join(pkgRoot, "manifest.yml"), []byte(tc.manifest), 0644) + require.NoError(t, err) + + // Create test config in appropriate path + testConfigFullPath := filepath.Join(pkgRoot, tc.testConfigPath) + err = os.MkdirAll(filepath.Dir(testConfigFullPath), 0755) + require.NoError(t, err) + err = os.WriteFile(testConfigFullPath, []byte(tc.testConfig), 0644) + require.NoError(t, err) + + fsys := fspath.DirFS(pkgRoot) + errs := ValidateTestPackageRequirements(fsys) + + if tc.expectError { + require.NotEmpty(t, errs, "expected validation errors but got none") + assert.Contains(t, errs[0].Error(), tc.errorContains) + } else { + assert.Empty(t, errs, "expected no validation errors but got: %v", errs) + } + }) + } +} diff --git a/code/go/internal/validator/semantic/validate_version_integrity.go b/code/go/internal/validator/semantic/validate_version_integrity.go index c3fb6c275..5806571d9 100644 --- a/code/go/internal/validator/semantic/validate_version_integrity.go +++ b/code/go/internal/validator/semantic/validate_version_integrity.go @@ -47,17 +47,12 @@ func ValidateVersionIntegrity(fsys fspath.FS) specerrors.ValidationErrors { } func readManifestVersion(fsys fspath.FS) (string, error) { - manifestPath := "manifest.yml" - f, err := pkgpath.Files(fsys, manifestPath) + manifest, err := readManifest(fsys) if err != nil { - return "", fmt.Errorf("can't locate manifest file: %w", err) + return "", err } - if len(f) != 1 { - return "", errors.New("single manifest file expected") - } - - val, err := f[0].Values("$.version") + val, err := manifest.Values("$.version") if err != nil { return "", fmt.Errorf("can't read manifest version: %w", err) } diff --git a/code/go/internal/validator/spec.go b/code/go/internal/validator/spec.go index f074d8701..aa465cf39 100644 --- a/code/go/internal/validator/spec.go +++ b/code/go/internal/validator/spec.go @@ -232,6 +232,8 @@ func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules {fn: semantic.ValidatePipelineOnFailure, types: []string{"integration"}, since: semver.MustParse("3.6.0")}, {fn: semantic.ValidateIntegrationInputsDeprecation, types: []string{"integration"}, since: semver.MustParse("3.6.0")}, {fn: semantic.ValidateDeprecatedReplacedBy, since: semver.MustParse("3.6.0")}, + {fn: semantic.ValidatePackageReferences, types: []string{"integration"}, since: semver.MustParse("3.6.0")}, + {fn: semantic.ValidateTestPackageRequirements, types: []string{"integration"}, since: semver.MustParse("3.6.0")}, } var validationRules validationRules diff --git a/code/go/pkg/validator/validator_test.go b/code/go/pkg/validator/validator_test.go index 3d127006e..d9af3d782 100644 --- a/code/go/pkg/validator/validator_test.go +++ b/code/go/pkg/validator/validator_test.go @@ -30,31 +30,33 @@ func TestValidateFile(t *testing.T) { invalidPkgFilePath string expectedErrContains []string }{ - "good": {}, - "good_v2": {}, - "good_v3": {}, - "good_var_groups": {}, - "good_input": {}, - "good_input_otel": {}, - "good_input_dynamic_signal_type": {}, - "good_input_template_paths": {}, - "good_integration_template_paths": {}, - "good_content": {}, - "good_content_with_dev": {}, - "good_lookup_index": {}, - "good_alert_rule_templates": {}, - "deploy_custom_agent": {}, - "deploy_custom_agent_multi_services": {}, - "deploy_docker": {}, - "deploy_terraform": {}, - "missing_data_stream": {}, - "icons_dark_mode": {}, - "ignored_malformed": {}, - "custom_ilm_policy": {}, - "profiling_symbolizer": {}, - "logs_synthetic_mode": {}, - "kibana_configuration_links": {}, - "with_links": {}, + "good": {}, + "good_v2": {}, + "good_v3": {}, + "good_var_groups": {}, + "good_input": {}, + "good_input_otel": {}, + "good_input_dynamic_signal_type": {}, + "good_input_template_paths": {}, + "good_integration_template_paths": {}, + "good_content": {}, + "good_content_with_dev": {}, + "good_lookup_index": {}, + "good_alert_rule_templates": {}, + "good_requires": {}, + "good_package_reference_policy_template": {}, + "deploy_custom_agent": {}, + "deploy_custom_agent_multi_services": {}, + "deploy_docker": {}, + "deploy_terraform": {}, + "missing_data_stream": {}, + "icons_dark_mode": {}, + "ignored_malformed": {}, + "custom_ilm_policy": {}, + "profiling_symbolizer": {}, + "logs_synthetic_mode": {}, + "kibana_configuration_links": {}, + "with_links": {}, "bad_duration_vars": { "manifest.yml", []string{ @@ -238,6 +240,47 @@ func TestValidateFile(t *testing.T) { `field policy_templates.0.deployment_modes.agentless.resources.requests: Additional property disk is not allowed`, }, }, + "bad_requires": { + "manifest.yml", + []string{ + `field requires.content.0.name: Does not match pattern '^[a-z0-9_]+$'`, + `field requires.input.0: version is required`, + `field requires.input.1.version: version "^1.0.0" for package "filelog_otel" must be a valid semantic version, constraints are not allowed`, + }, + }, + "bad_requires_old_version": { + "manifest.yml", + []string{ + `field (root): Additional property requires is not allowed`, + }, + }, + "bad_package_field_old_version": { + "manifest.yml", + []string{ + `field policy_templates.0.inputs.0: type is required`, + `field policy_templates.0.inputs.0: Additional property package is not allowed`, + }, + }, + "bad_datastream_package_old_version": { + "data_stream/logs/manifest.yml", + []string{ + `field streams.0: input is required`, + `field streams.0: Additional property package is not allowed`, + }, + }, + "bad_package_not_in_requires": { + "manifest.yml", + []string{ + `policy_templates[0].inputs[0] references package "missing_package" which is not listed in requires section`, + }, + }, + "bad_test_requires": { + "_dev/test/config.yml", + []string{ + `policy.requires[0] references package "missing_package" which is not listed in manifest requires section`, + `system.requires[0] package "sql_input" version "1.5.0" does not satisfy constraint "2.0.0"`, + }, + }, "bad_input_dataset_vars": { "_dev/test/policy/test-vars.yml", []string{ diff --git a/compliance/features/package-dependencies.feature b/compliance/features/package-dependencies.feature new file mode 100644 index 000000000..bc9de25b6 --- /dev/null +++ b/compliance/features/package-dependencies.feature @@ -0,0 +1,7 @@ +Feature: Package dependencies support + Integration packages can declare dependencies on input and content packages + + @3.6.0 + Scenario: Integration package with dependencies installs required packages + Given the "good_requires" package is installed + Then the content packages "good_requires" require are installed diff --git a/spec/changelog.yml b/spec/changelog.yml index 8c829d5a6..a00919beb 100644 --- a/spec/changelog.yml +++ b/spec/changelog.yml @@ -37,6 +37,10 @@ - description: Add support for multiple template paths in input packages, integration inputs, and data streams. type: enhancement link: https://github.com/elastic/package-spec/pull/1089 + # Pending on https://github.com/elastic/kibana/issues/252938 + - description: Add support for package dependencies in integration packages via requires field with input and content package types. + type: enhancement + link: https://github.com/elastic/package-spec/pull/1071 - version: 3.5.7 changes: - description: Allow _dev directory for content-only packages; use _dev/shared for development files (e.g. dashboard YML sources). diff --git a/spec/integration/_dev/test/config.spec.yml b/spec/integration/_dev/test/config.spec.yml index 214d0728e..87fdf47d5 100644 --- a/spec/integration/_dev/test/config.spec.yml +++ b/spec/integration/_dev/test/config.spec.yml @@ -16,6 +16,11 @@ spec: default: false skip: $ref: "../../data_stream/_dev/test/config.spec.yml#/definitions/skip" + requires: + description: Package dependencies required for these tests with exact versions. + type: array + items: + $ref: "../../data_stream/_dev/test/config.spec.yml#/definitions/package_requirement" properties: system: description: Configuration for system tests diff --git a/spec/integration/data_stream/_dev/test/config.spec.yml b/spec/integration/data_stream/_dev/test/config.spec.yml index 4f7f900be..3dafdf2d9 100644 --- a/spec/integration/data_stream/_dev/test/config.spec.yml +++ b/spec/integration/data_stream/_dev/test/config.spec.yml @@ -20,6 +20,22 @@ spec: type: string example: https://github.com/elastic/integrations/issues/520 required: [ "reason", "link" ] + package_requirement: + description: A package requirement with an exact version. + type: object + additionalProperties: false + properties: + package: + description: Name of the required package. + type: string + pattern: '^[a-z0-9_]+$' + version: + description: Exact version of the package required. + type: string + pattern: '^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$' + required: + - package + - version vars: description: Variables defined for the test case. type: diff --git a/spec/integration/data_stream/_dev/test/policy/config.spec.yml b/spec/integration/data_stream/_dev/test/policy/config.spec.yml index ede72a4a1..5c6016e6c 100644 --- a/spec/integration/data_stream/_dev/test/policy/config.spec.yml +++ b/spec/integration/data_stream/_dev/test/policy/config.spec.yml @@ -24,3 +24,8 @@ spec: vars: description: Variables used to configure settings defined in the package manifest. $ref: "./../config.spec.yml#/definitions/vars" + requires: + description: Package dependencies required for this test with exact versions. + type: array + items: + $ref: "./../config.spec.yml#/definitions/package_requirement" diff --git a/spec/integration/data_stream/_dev/test/static/config.spec.yml b/spec/integration/data_stream/_dev/test/static/config.spec.yml index 50240859f..d95f224ea 100644 --- a/spec/integration/data_stream/_dev/test/static/config.spec.yml +++ b/spec/integration/data_stream/_dev/test/static/config.spec.yml @@ -7,3 +7,8 @@ spec: properties: skip: $ref: "./../config.spec.yml#/definitions/skip" + requires: + description: Package dependencies required for this test with exact versions. + type: array + items: + $ref: "./../config.spec.yml#/definitions/package_requirement" diff --git a/spec/integration/data_stream/_dev/test/system/config.spec.yml b/spec/integration/data_stream/_dev/test/system/config.spec.yml index 966a3bd3c..c5584c6eb 100644 --- a/spec/integration/data_stream/_dev/test/system/config.spec.yml +++ b/spec/integration/data_stream/_dev/test/system/config.spec.yml @@ -167,3 +167,8 @@ spec: vars: description: Variables used to configure settings defined in the data stream manifest. $ref: "./../config.spec.yml#/definitions/vars" + requires: + description: Package dependencies required for this test with exact versions. + type: array + items: + $ref: "./../config.spec.yml#/definitions/package_requirement" diff --git a/spec/integration/data_stream/manifest.spec.yml b/spec/integration/data_stream/manifest.spec.yml index 636dd3111..774bcedbe 100644 --- a/spec/integration/data_stream/manifest.spec.yml +++ b/spec/integration/data_stream/manifest.spec.yml @@ -575,6 +575,8 @@ spec: - file not: const: otelcol + package: + $ref: "../../integration/manifest.spec.yml#/definitions/package_reference" title: description: > Title of the stream. It should include the source of the data that is @@ -610,7 +612,9 @@ spec: required: - title - description - - input + oneOf: + - required: [input] + - required: [package] agent: $ref: "../../integration/manifest.spec.yml#/definitions/agent" elasticsearch: @@ -665,14 +669,21 @@ spec: versions: - before: 3.6.0 patch: + - op: remove + path: "/properties/streams/items/properties/package" + - op: remove + path: "/properties/streams/items/oneOf" + - op: add + path: "/properties/streams/items/required/-" # re-add input as required + value: input - op: remove # removes template_paths field for streams path: "/properties/streams/items/properties/template_paths" - op: remove # removes deprecated field for data stream path: "/properties/deprecated" - - op: remove # remove deprecated field for vars - path: /definitions/vars/items/properties/deprecated - op: remove - path: "/properties/streams/items/properties/var_groups" # removes var_groups from streams + path: "/definitions/vars/items/properties/deprecated" + - op: remove + path: "/properties/streams/items/properties/var_groups" - before: 3.5.0 patch: # Require >=3.5.0 to use the duration variable type. diff --git a/spec/integration/manifest.spec.yml b/spec/integration/manifest.spec.yml index 5f81dff85..73a427b82 100644 --- a/spec/integration/manifest.spec.yml +++ b/spec/integration/manifest.spec.yml @@ -536,6 +536,56 @@ spec: required: - since - description + package_reference: + description: > + Reference to an input package. When specified, configuration is inherited + from the referenced package. The package must be listed in the manifest's + requires section. + type: string + pattern: '^[a-z0-9_]+$' + examples: + - filelog_otel + - sql_input + package_dependency: + description: A package dependency with name and version. + type: object + additionalProperties: false + properties: + name: + description: Name of the required package. + type: string + pattern: '^[a-z0-9_]+$' + examples: + - sql_input + - elastic_agent + version: + description: > + Version of the required package. For input packages, it is recommended to + use specific versions (e.g., "1.0.0") for consistency. For content packages, + version constraints (e.g., "^1.0.0") can be used to allow compatible updates. + type: string + examples: + - "1.0.0" + - "^1.0.0" + - "~1.2.3" + required: + - name + - version + requires: + description: Dependencies that this package requires to function properly. + type: object + additionalProperties: false + properties: + input: + description: List of required input packages. + type: array + items: + $ref: "#/definitions/package_dependency" + content: + description: List of required content packages. + type: array + items: + $ref: "#/definitions/package_dependency" var_groups: description: > Defines mutually exclusive groups of variables. When an option is selected, @@ -721,6 +771,8 @@ spec: type: string not: const: otelcol + package: + $ref: "#/definitions/package_reference" title: description: Title of input. type: string @@ -779,9 +831,11 @@ spec: examples: - credential_type: [cloud_connectors] required: - - type - title - description + oneOf: + - required: [type] + - required: [package] multiple: type: boolean icons: @@ -824,6 +878,8 @@ spec: type: array items: type: string + requires: + $ref: "#/definitions/requires" deprecated: $ref: "#/definitions/deprecated" required: @@ -858,6 +914,21 @@ spec: versions: - before: 3.6.0 patch: + - op: remove + path: "/properties/requires" + - op: remove + path: "/definitions/requires" + - op: remove + path: "/definitions/package_dependency" + - op: remove + path: "/definitions/package_reference" + - op: remove + path: "/properties/policy_templates/items/properties/inputs/items/properties/package" + - op: remove + path: "/properties/policy_templates/items/properties/inputs/items/oneOf" + - op: add + path: "/properties/policy_templates/items/properties/inputs/items/required/-" # re-add type as required + value: type - op: remove # removes template_paths field for policy template inputs path: "/properties/policy_templates/items/properties/inputs/items/properties/template_paths" - op: remove # removes template_paths definition diff --git a/test/packages/bad_datastream_package_old_version/changelog.yml b/test/packages/bad_datastream_package_old_version/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/bad_datastream_package_old_version/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_datastream_package_old_version/data_stream/logs/fields/fields.yml b/test/packages/bad_datastream_package_old_version/data_stream/logs/fields/fields.yml new file mode 100644 index 000000000..7c798f453 --- /dev/null +++ b/test/packages/bad_datastream_package_old_version/data_stream/logs/fields/fields.yml @@ -0,0 +1,12 @@ +- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: Data stream namespace. +- name: '@timestamp' + type: date + description: Event timestamp. diff --git a/test/packages/bad_datastream_package_old_version/data_stream/logs/manifest.yml b/test/packages/bad_datastream_package_old_version/data_stream/logs/manifest.yml new file mode 100644 index 000000000..c3becbb3c --- /dev/null +++ b/test/packages/bad_datastream_package_old_version/data_stream/logs/manifest.yml @@ -0,0 +1,6 @@ +title: Apache access logs +type: logs +streams: + - package: filelog_otel + title: Apache Access Logs + description: Collect Apache access logs with input package diff --git a/test/packages/bad_datastream_package_old_version/docs/README.md b/test/packages/bad_datastream_package_old_version/docs/README.md new file mode 100644 index 000000000..a0681a3d4 --- /dev/null +++ b/test/packages/bad_datastream_package_old_version/docs/README.md @@ -0,0 +1,3 @@ +# Bad Data Stream Package Field on Old Version + +This is a test package with package field in data stream on an old format version. diff --git a/test/packages/bad_datastream_package_old_version/manifest.yml b/test/packages/bad_datastream_package_old_version/manifest.yml new file mode 100644 index 000000000..68b0cdecb --- /dev/null +++ b/test/packages/bad_datastream_package_old_version/manifest.yml @@ -0,0 +1,14 @@ +format_version: 3.5.0 +name: bad_datastream_package_old_version +title: Package with datastream package field on old version +description: This package uses package field in data stream on an old format version. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +owner: + github: elastic/foobar + type: community diff --git a/test/packages/bad_package_field_old_version/changelog.yml b/test/packages/bad_package_field_old_version/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/bad_package_field_old_version/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_package_field_old_version/docs/README.md b/test/packages/bad_package_field_old_version/docs/README.md new file mode 100644 index 000000000..b59b4946f --- /dev/null +++ b/test/packages/bad_package_field_old_version/docs/README.md @@ -0,0 +1,3 @@ +# Bad Package Field on Old Version + +This is a test package with package field on an old format version. diff --git a/test/packages/bad_package_field_old_version/manifest.yml b/test/packages/bad_package_field_old_version/manifest.yml new file mode 100644 index 000000000..0a4176221 --- /dev/null +++ b/test/packages/bad_package_field_old_version/manifest.yml @@ -0,0 +1,22 @@ +format_version: 3.5.0 +name: bad_package_field_old_version +title: Package with package field on old version +description: This package uses package field on an old format version. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +policy_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - package: filelog_otel + title: Collect logs from Apache instances + description: Collecting Apache access and error logs +owner: + github: elastic/foobar + type: community diff --git a/test/packages/bad_package_not_in_requires/changelog.yml b/test/packages/bad_package_not_in_requires/changelog.yml new file mode 100644 index 000000000..84f336f36 --- /dev/null +++ b/test/packages/bad_package_not_in_requires/changelog.yml @@ -0,0 +1,5 @@ +- version: "0.0.1" + changes: + - description: Initial version + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_package_not_in_requires/docs/README.md b/test/packages/bad_package_not_in_requires/docs/README.md new file mode 100644 index 000000000..78505d84f --- /dev/null +++ b/test/packages/bad_package_not_in_requires/docs/README.md @@ -0,0 +1,3 @@ +# Bad Package Not In Requires + +This package is used for testing validation of package references. diff --git a/test/packages/bad_package_not_in_requires/manifest.yml b/test/packages/bad_package_not_in_requires/manifest.yml new file mode 100644 index 000000000..c5ab99a86 --- /dev/null +++ b/test/packages/bad_package_not_in_requires/manifest.yml @@ -0,0 +1,26 @@ +format_version: 3.6.0 +name: bad_package_not_in_requires +title: Package reference not in requires +description: This package references a package in policy template that is not listed in requires. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: sql_input + version: "1.0.0" +policy_templates: + - name: example + title: Example + description: Example policy template + inputs: + - package: missing_package + title: Missing package reference + description: This references a package not in requires +owner: + github: elastic/ecosystem + type: elastic diff --git a/test/packages/bad_requires/changelog.yml b/test/packages/bad_requires/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/bad_requires/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_requires/docs/README.md b/test/packages/bad_requires/docs/README.md new file mode 100644 index 000000000..d791a8c08 --- /dev/null +++ b/test/packages/bad_requires/docs/README.md @@ -0,0 +1,3 @@ +# Bad Requires Package + +This is a test package with invalid requires field. diff --git a/test/packages/bad_requires/manifest.yml b/test/packages/bad_requires/manifest.yml new file mode 100644 index 000000000..51bb16114 --- /dev/null +++ b/test/packages/bad_requires/manifest.yml @@ -0,0 +1,42 @@ +format_version: 3.6.0 +name: bad_requires +title: Package with invalid requires +description: This integration package has invalid dependencies. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: sql_input + # Missing version field + - name: filelog_otel + version: "^1.0.0" + content: + - name: elastic-agent-invalid + version: "^1.0.0" +policy_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - type: apache/metrics + title: Collect metrics from Apache instances + description: Collecting Apache status metrics + multi: false + vars: + - name: hosts + type: url + url_allowed_schemes: ['http', 'https'] + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/foobar + type: community diff --git a/test/packages/bad_requires_old_version/changelog.yml b/test/packages/bad_requires_old_version/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/bad_requires_old_version/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_requires_old_version/docs/README.md b/test/packages/bad_requires_old_version/docs/README.md new file mode 100644 index 000000000..a758a41ff --- /dev/null +++ b/test/packages/bad_requires_old_version/docs/README.md @@ -0,0 +1,3 @@ +# Bad Requires Old Version Package + +This is a test package with requires field on an old format version. diff --git a/test/packages/bad_requires_old_version/manifest.yml b/test/packages/bad_requires_old_version/manifest.yml new file mode 100644 index 000000000..ecb9beb60 --- /dev/null +++ b/test/packages/bad_requires_old_version/manifest.yml @@ -0,0 +1,37 @@ +format_version: 3.5.0 +name: bad_requires_old_version +title: Package with requires on old version +description: This package uses requires field on an old format version. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: sql_input + version: "1.0.0" +policy_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - type: apache/metrics + title: Collect metrics from Apache instances + description: Collecting Apache status metrics + multi: false + vars: + - name: hosts + type: url + url_allowed_schemes: ['http', 'https'] + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/foobar + type: community diff --git a/test/packages/bad_test_requires/_dev/test/config.yml b/test/packages/bad_test_requires/_dev/test/config.yml new file mode 100644 index 000000000..f873218a8 --- /dev/null +++ b/test/packages/bad_test_requires/_dev/test/config.yml @@ -0,0 +1,8 @@ +policy: + requires: + - package: missing_package + version: 1.0.0 +system: + requires: + - package: sql_input + version: 1.5.0 diff --git a/test/packages/bad_test_requires/changelog.yml b/test/packages/bad_test_requires/changelog.yml new file mode 100644 index 000000000..84f336f36 --- /dev/null +++ b/test/packages/bad_test_requires/changelog.yml @@ -0,0 +1,5 @@ +- version: "0.0.1" + changes: + - description: Initial version + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/bad_test_requires/docs/README.md b/test/packages/bad_test_requires/docs/README.md new file mode 100644 index 000000000..87ef1bc52 --- /dev/null +++ b/test/packages/bad_test_requires/docs/README.md @@ -0,0 +1,3 @@ +# Bad Test Requires + +Test package for validating test requires (both missing packages and version mismatches). diff --git a/test/packages/bad_test_requires/manifest.yml b/test/packages/bad_test_requires/manifest.yml new file mode 100644 index 000000000..329fd1199 --- /dev/null +++ b/test/packages/bad_test_requires/manifest.yml @@ -0,0 +1,26 @@ +format_version: 3.6.0 +name: bad_test_requires +title: Test requires validation +description: Test config with invalid package requirements. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: sql_input + version: "2.0.0" +policy_templates: + - name: example + title: Example + description: Example policy template + inputs: + - type: logfile + title: Log file input + description: Collect logs from files +owner: + github: elastic/ecosystem + type: elastic diff --git a/test/packages/good_package_reference_policy_template/changelog.yml b/test/packages/good_package_reference_policy_template/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/good_package_reference_policy_template/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/good_package_reference_policy_template/data_stream/logs/agent/stream/stream.yml.hbs b/test/packages/good_package_reference_policy_template/data_stream/logs/agent/stream/stream.yml.hbs new file mode 100644 index 000000000..299dab52c --- /dev/null +++ b/test/packages/good_package_reference_policy_template/data_stream/logs/agent/stream/stream.yml.hbs @@ -0,0 +1,8 @@ +data_stream: + {{#if data_stream.dataset}} + dataset: {{data_stream.dataset}} + {{/if}} +paths: +{{#each paths}} + - {{this}} +{{/each}} diff --git a/test/packages/good_package_reference_policy_template/data_stream/logs/fields/fields.yml b/test/packages/good_package_reference_policy_template/data_stream/logs/fields/fields.yml new file mode 100644 index 000000000..7c798f453 --- /dev/null +++ b/test/packages/good_package_reference_policy_template/data_stream/logs/fields/fields.yml @@ -0,0 +1,12 @@ +- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: Data stream namespace. +- name: '@timestamp' + type: date + description: Event timestamp. diff --git a/test/packages/good_package_reference_policy_template/data_stream/logs/manifest.yml b/test/packages/good_package_reference_policy_template/data_stream/logs/manifest.yml new file mode 100644 index 000000000..0b3de6603 --- /dev/null +++ b/test/packages/good_package_reference_policy_template/data_stream/logs/manifest.yml @@ -0,0 +1,9 @@ +title: Apache access logs +type: logs +streams: + - package: filelog_otel + title: Apache Access Logs + description: Collect Apache access logs with input package + - input: logfile + title: Apache Error Logs + description: Collect Apache error logs diff --git a/test/packages/good_package_reference_policy_template/docs/README.md b/test/packages/good_package_reference_policy_template/docs/README.md new file mode 100644 index 000000000..457aab89e --- /dev/null +++ b/test/packages/good_package_reference_policy_template/docs/README.md @@ -0,0 +1,3 @@ +# Good Package Reference in Policy Template + +This is a test package to validate package references in policy template inputs. diff --git a/test/packages/good_package_reference_policy_template/manifest.yml b/test/packages/good_package_reference_policy_template/manifest.yml new file mode 100644 index 000000000..200534dd3 --- /dev/null +++ b/test/packages/good_package_reference_policy_template/manifest.yml @@ -0,0 +1,42 @@ +format_version: 3.6.0 +name: good_package_reference_policy_template +title: Package with package reference in policy template +description: This integration package uses package field in policy template inputs. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: filelog_otel + version: "1.0.0" + - name: sql_input + version: "2.0.0" +policy_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - package: filelog_otel + title: Collect logs from Apache instances + description: Collecting Apache access and error logs + - type: apache/metrics + title: Collect metrics from Apache instances + description: Collecting Apache status metrics + multi: false + vars: + - name: hosts + type: url + url_allowed_schemes: ['http', 'https'] + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/foobar + type: community diff --git a/test/packages/good_requires/_dev/test/config.yml b/test/packages/good_requires/_dev/test/config.yml new file mode 100644 index 000000000..be6b62e88 --- /dev/null +++ b/test/packages/good_requires/_dev/test/config.yml @@ -0,0 +1,8 @@ +system: + requires: + - package: sql_input + version: 1.0.0 +policy: + requires: + - package: log_input + version: 2.0.0 diff --git a/test/packages/good_requires/changelog.yml b/test/packages/good_requires/changelog.yml new file mode 100644 index 000000000..27e62eecd --- /dev/null +++ b/test/packages/good_requires/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.0.1 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/1 diff --git a/test/packages/good_requires/docs/README.md b/test/packages/good_requires/docs/README.md new file mode 100644 index 000000000..f15b7b541 --- /dev/null +++ b/test/packages/good_requires/docs/README.md @@ -0,0 +1,3 @@ +# Good Requires Package + +This is a test package to validate the requires field in integration packages. diff --git a/test/packages/good_requires/manifest.yml b/test/packages/good_requires/manifest.yml new file mode 100644 index 000000000..45804ee0b --- /dev/null +++ b/test/packages/good_requires/manifest.yml @@ -0,0 +1,44 @@ +format_version: 3.6.0 +name: good_requires +title: Package with requires +description: This integration package has dependencies defined. +version: 0.0.1 +type: integration +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.0.0' +requires: + input: + - name: sql_input + version: "1.0.0" + - name: log_input + version: "2.0.0" + content: + - name: elastic_agent + version: "^1.2.0" + - name: system + version: "~2.0.0" +policy_templates: + - name: apache + title: Apache logs and metrics + description: Collect logs and metrics from Apache instances + inputs: + - type: apache/metrics + title: Collect metrics from Apache instances + description: Collecting Apache status metrics + multi: false + vars: + - name: hosts + type: url + url_allowed_schemes: ['http', 'https'] + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/foobar + type: community