diff --git a/code/go/internal/validator/common/helpers.go b/code/go/internal/validator/common/helpers.go new file mode 100644 index 000000000..420bc3499 --- /dev/null +++ b/code/go/internal/validator/common/helpers.go @@ -0,0 +1,45 @@ +// 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 common + +import ( + "os" + "strconv" +) + +// EnvVarWarningsAsErrors is the environment variable name used to enable warnings as errors +// this meachinsm will be removed once structured errors are supported https://github.com/elastic/package-spec/issues/342 +const EnvVarWarningsAsErrors = "PACKAGE_SPEC_WARNINGS_AS_ERRORS" + +// IsDefinedWarningsAsErrors checks whether or not warnings should be considered as errors, +// it checks the environment variable is defined and the value that it contains +func IsDefinedWarningsAsErrors() bool { + var err error + warningsAsErrors := false + warningsAsErrorsStr, found := os.LookupEnv(EnvVarWarningsAsErrors) + if found { + warningsAsErrors, err = strconv.ParseBool(warningsAsErrorsStr) + if err != nil { + return false + } + } + return warningsAsErrors +} + +// EnableWarningsAsErrors is a function to enable warnings as errors, setting environment variable as true +func EnableWarningsAsErrors() error { + if err := os.Setenv(EnvVarWarningsAsErrors, "true"); err != nil { + return err + } + return nil +} + +// DisableWarningsAsErrors is a function to disable warnings as errors, unsetting environment variable +func DisableWarningsAsErrors() error { + if err := os.Unsetenv(EnvVarWarningsAsErrors); err != nil { + return err + } + return nil +} diff --git a/code/go/internal/validator/common/helpers_test.go b/code/go/internal/validator/common/helpers_test.go new file mode 100644 index 000000000..1fe238cb1 --- /dev/null +++ b/code/go/internal/validator/common/helpers_test.go @@ -0,0 +1,48 @@ +// 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 common + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsDefinedWarningsAsErrors(t *testing.T) { + cases := []struct { + name string + envVarValue string + expected bool + }{ + {"true", "true", true}, + {"false", "false", false}, + {"other", "other", false}, + {"empty", "", false}, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if err := os.Setenv(EnvVarWarningsAsErrors, test.envVarValue); err != nil { + require.NoError(t, err) + } + value := IsDefinedWarningsAsErrors() + assert.Equal(t, test.expected, value) + + if err := DisableWarningsAsErrors(); err != nil { + require.NoError(t, err) + } + }) + } + + t.Run("undefined", func(t *testing.T) { + if err := DisableWarningsAsErrors(); err != nil { + require.NoError(t, err) + } + value := IsDefinedWarningsAsErrors() + assert.Equal(t, false, value) + }) +} diff --git a/code/go/internal/validator/folder_spec.go b/code/go/internal/validator/folder_spec.go index 76fc63c44..2f0d965a6 100644 --- a/code/go/internal/validator/folder_spec.go +++ b/code/go/internal/validator/folder_spec.go @@ -16,6 +16,7 @@ import ( ve "github.com/elastic/package-spec/code/go/internal/errors" "github.com/elastic/package-spec/code/go/internal/spectypes" + "github.com/elastic/package-spec/code/go/internal/validator/common" ) type validator struct { @@ -61,7 +62,11 @@ func (v *validator) Validate() ve.ValidationErrors { if v.pkg.Version.Major() > 0 && v.pkg.Version.Prerelease() == "" { errs = append(errs, errors.Errorf("spec for [%s] defines beta features which can't be enabled for packages with a stable semantic version", v.pkg.Path(v.folderPath))) } else { - log.Printf("Warning: package with non-stable semantic version and active beta features (enabled in [%s]) can't be released as stable version.", v.pkg.Path(v.folderPath)) + if common.IsDefinedWarningsAsErrors() { + errs = append(errs, errors.Errorf("Warning: package with non-stable semantic version and active beta features (enabled in [%s]) can't be released as stable version.", v.pkg.Path(v.folderPath))) + } else { + log.Printf("Warning: package with non-stable semantic version and active beta features (enabled in [%s]) can't be released as stable version.", v.pkg.Path(v.folderPath)) + } } default: errs = append(errs, errors.Errorf("unsupport release level, supported values: beta, ga")) diff --git a/code/go/internal/validator/semantic/validate_required_fields.go b/code/go/internal/validator/semantic/validate_required_fields.go index 756eb99dc..c1b0764e1 100644 --- a/code/go/internal/validator/semantic/validate_required_fields.go +++ b/code/go/internal/validator/semantic/validate_required_fields.go @@ -5,9 +5,10 @@ package semantic import ( + "github.com/pkg/errors" + ve "github.com/elastic/package-spec/code/go/internal/errors" "github.com/elastic/package-spec/code/go/internal/fspath" - "github.com/pkg/errors" ) // ValidateRequiredFields validates that required fields are present and have the expected diff --git a/code/go/internal/validator/semantic/validate_unique_fields.go b/code/go/internal/validator/semantic/validate_unique_fields.go index 551aa895e..c7b2249ec 100644 --- a/code/go/internal/validator/semantic/validate_unique_fields.go +++ b/code/go/internal/validator/semantic/validate_unique_fields.go @@ -8,9 +8,10 @@ import ( "sort" "strings" + "github.com/pkg/errors" + ve "github.com/elastic/package-spec/code/go/internal/errors" "github.com/elastic/package-spec/code/go/internal/fspath" - "github.com/pkg/errors" ) // ValidateUniqueFields verifies that any field is defined only once on each data stream. diff --git a/code/go/internal/validator/semantic/validate_visualizations_used_by_value.go b/code/go/internal/validator/semantic/validate_visualizations_used_by_value.go new file mode 100644 index 000000000..8cacc2e4d --- /dev/null +++ b/code/go/internal/validator/semantic/validate_visualizations_used_by_value.go @@ -0,0 +1,113 @@ +// 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" + "log" + "path" + + "github.com/pkg/errors" + + ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" + "github.com/elastic/package-spec/code/go/internal/pkgpath" + "github.com/elastic/package-spec/code/go/internal/validator/common" +) + +type reference struct { + ID string + Name string + Type string +} + +// ValidateVisualizationsUsedByValue warns if there are any Kibana +// Dashboard that defines visualizations by reference instead of value. +// That is, it warns if a Kibana dashbaord file, foo.json, +// defines some visualization using reference (containing an element of +// "visualization" type inside references key). +func ValidateVisualizationsUsedByValue(fsys fspath.FS) ve.ValidationErrors { + warningsAsErrors := common.IsDefinedWarningsAsErrors() + var errs ve.ValidationErrors + + filePaths := path.Join("kibana", "dashboard", "*.json") + objectFiles, err := pkgpath.Files(fsys, filePaths) + if err != nil { + errs = append(errs, errors.Wrap(err, "error finding Kibana Dashboard files")) + return errs + } + + for _, objectFile := range objectFiles { + filePath := objectFile.Path() + + objectReferences, err := objectFile.Values(`$.references`) + if err != nil { + // no references key in dashboard json + continue + } + + references, err := anyReference(objectReferences) + if err != nil { + errs = append(errs, errors.Wrapf(err, "error getting references in file: %s", fsys.Path(filePath))) + } + if len(references) > 0 { + s := fmt.Sprintf("%s (%s)", references[0].ID, references[0].Type) + for _, ref := range references[1:] { + s = fmt.Sprintf("%s, %s (%s)", s, ref.ID, ref.Type) + } + if warningsAsErrors { + errs = append(errs, errors.Errorf("Warning: references found in dashboard %s: %s", filePath, s)) + } else { + log.Printf("Warning: references found in dashboard %s: %s", filePath, s) + } + + } + } + + return errs +} + +func anyReference(val interface{}) ([]reference, error) { + allReferences, err := toReferenceSlice(val) + if err != nil { + return []reference{}, fmt.Errorf("unable to convert references: %w", err) + } + + if len(allReferences) == 0 { + return []reference{}, nil + } + + var references []reference + for _, reference := range allReferences { + switch reference.Type { + case "lens", "visualization": + references = append(references, reference) + } + } + return references, nil + +} + +func toReferenceSlice(val interface{}) ([]reference, error) { + vals, ok := val.([]interface{}) + if !ok { + return nil, errors.New("conversion error to array") + } + var refs []reference + for _, v := range vals { + r, ok := v.(map[string]interface{}) + if !ok { + return nil, errors.New("conversion error to reference element") + } + ref := reference{ + ID: r["id"].(string), + Type: r["type"].(string), + Name: r["name"].(string), + } + + refs = append(refs, ref) + } + return refs, nil +} diff --git a/code/go/internal/validator/semantic/validate_visualizations_used_by_value_test.go b/code/go/internal/validator/semantic/validate_visualizations_used_by_value_test.go new file mode 100644 index 000000000..ea65c0255 --- /dev/null +++ b/code/go/internal/validator/semantic/validate_visualizations_used_by_value_test.go @@ -0,0 +1,107 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToReferencesToSlice(t *testing.T) { + + var tests = []struct { + name string + references interface{} + expected []reference + }{ + { + "References", + []interface{}{ + map[string]interface{}{ + "id": "12345", + "name": "panel_0", + "type": "visualization", + }, + map[string]interface{}{ + "id": "9000", + "name": "panel_1", + "type": "other", + }, + }, + []reference{ + reference{ + ID: "12345", + Name: "panel_0", + Type: "visualization", + }, + reference{ + ID: "9000", + Name: "panel_1", + Type: "other", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + references, err := toReferenceSlice(test.references) + require.NoError(t, err) + assert.Equal(t, test.expected, references) + }) + } +} + +func TestAnyReference(t *testing.T) { + + var tests = []struct { + name string + references interface{} + expected []reference + }{ + { + "SomeReferences", + []interface{}{ + map[string]interface{}{ + "id": "12345", + "name": "panel_0", + "type": "visualization", + }, + map[string]interface{}{ + "id": "9000", + "name": "panel_1", + "type": "lens", + }, + map[string]interface{}{ + "id": "4", + "name": "panel_1", + "type": "map", + }, + map[string]interface{}{ + "id": "42", + "name": "panel_1", + "type": "index-pattern", + }, + }, + []reference{ + {"12345", "panel_0", "visualization"}, + {"9000", "panel_1", "lens"}, + }, + }, + { + "Empty", + []interface{}{}, + []reference{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ids, err := anyReference(test.references) + require.NoError(t, err) + assert.Equal(t, test.expected, ids) + }) + } +} diff --git a/code/go/internal/validator/spec.go b/code/go/internal/validator/spec.go index 69b024e36..db36a0d2b 100644 --- a/code/go/internal/validator/spec.go +++ b/code/go/internal/validator/spec.go @@ -81,6 +81,7 @@ func (s Spec) ValidatePackage(pkg Package) ve.ValidationErrors { //semantic.ValidateUniqueFields, semantic.ValidateDimensionFields, semantic.ValidateRequiredFields, + semantic.ValidateVisualizationsUsedByValue, } return rules.validate(&pkg) diff --git a/code/go/internal/validator/spec_test.go b/code/go/internal/validator/spec_test.go index 08d61c466..a5e36df7e 100644 --- a/code/go/internal/validator/spec_test.go +++ b/code/go/internal/validator/spec_test.go @@ -5,11 +5,12 @@ package validator import ( - "github.com/elastic/package-spec/code/go/internal/fspath" "testing" "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/require" + + "github.com/elastic/package-spec/code/go/internal/fspath" ) func TestNewSpec(t *testing.T) { @@ -60,6 +61,6 @@ func TestBetaFeatures_Package_GA(t *testing.T) { require.NoError(t, err) errs := s.ValidatePackage(*pkg) - require.Len(t,errs, 1) + require.Len(t, errs, 1) require.Equal(t, errs[0].Error(), "spec for [testdata/packages/features_beta/beta] defines beta features which can't be enabled for packages with a stable semantic version") -} \ No newline at end of file +} diff --git a/code/go/pkg/validator/validator_test.go b/code/go/pkg/validator/validator_test.go index ca47c2ed4..0d7af20e1 100644 --- a/code/go/pkg/validator/validator_test.go +++ b/code/go/pkg/validator/validator_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/validator/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,9 +29,6 @@ func TestValidateFile(t *testing.T) { "deploy_terraform": {}, "time_series": {}, "missing_data_stream": {}, - "custom_logs": {}, - "httpjson_input": {}, - "sql_input": {}, "icons_dark_mode": {}, "bad_additional_content": { "bad-bad", @@ -302,6 +300,57 @@ func TestValidateDuplicatedFields(t *testing.T) { } +func TestValidateWarnings(t *testing.T) { + tests := map[string][]string{ + "good": []string{}, + "custom_logs": []string{ + "package with non-stable semantic version and active beta features (enabled in [../../../../test/packages/custom_logs]) can't be released as stable version.", + }, + "httpjson_input": []string{ + "package with non-stable semantic version and active beta features (enabled in [../../../../test/packages/httpjson_input]) can't be released as stable version.", + }, + "sql_input": []string{ + "package with non-stable semantic version and active beta features (enabled in [../../../../test/packages/sql_input]) can't be released as stable version.", + }, + "visualizations_by_reference": []string{ + "references found in dashboard kibana/dashboard/visualizations_by_reference-82273ffe-6acc-4f2f-bbee-c1004abba63d.json: visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6 (visualization), visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b (lens)", + }, + } + if err := common.EnableWarningsAsErrors(); err != nil { + require.NoError(t, err) + } + defer common.DisableWarningsAsErrors() + + for pkgName, expectedWarnContains := range tests { + t.Run(pkgName, func(t *testing.T) { + warnPrefix := fmt.Sprintf("Warning: ") + + pkgRootPath := path.Join("..", "..", "..", "..", "test", "packages", pkgName) + errs := ValidateFromPath(pkgRootPath) + if len(expectedWarnContains) == 0 { + require.NoError(t, errs) + } else { + require.Error(t, errs) + vErrs, ok := errs.(errors.ValidationErrors) + if ok { + require.Len(t, errs, len(expectedWarnContains)) + var warnMessages []string + for _, vErr := range vErrs { + warnMessages = append(warnMessages, vErr.Error()) + } + + for _, expectedWarnMessage := range expectedWarnContains { + expectedWarn := warnPrefix + expectedWarnMessage + require.Contains(t, warnMessages, expectedWarn) + } + return + } + require.Equal(t, errs.Error(), expectedWarnContains[0]) + } + }) + } +} + func requireErrorMessage(t *testing.T, pkgName string, invalidItemsPerFolder map[string][]string, expectedErrorMessage string) { pkgRootPath := filepath.Join("..", "..", "..", "..", "test", "packages", pkgName) diff --git a/test/packages/visualizations_by_reference/changelog.yml b/test/packages/visualizations_by_reference/changelog.yml new file mode 100644 index 000000000..ba105f727 --- /dev/null +++ b/test/packages/visualizations_by_reference/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.1.2 + changes: + - description: initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/131 \ No newline at end of file diff --git a/test/packages/visualizations_by_reference/docs/README.md b/test/packages/visualizations_by_reference/docs/README.md new file mode 100644 index 000000000..1c9bf4968 --- /dev/null +++ b/test/packages/visualizations_by_reference/docs/README.md @@ -0,0 +1 @@ +Main \ No newline at end of file diff --git a/test/packages/visualizations_by_reference/kibana/dashboard/visualizations_by_reference-82273ffe-6acc-4f2f-bbee-c1004abba63d.json b/test/packages/visualizations_by_reference/kibana/dashboard/visualizations_by_reference-82273ffe-6acc-4f2f-bbee-c1004abba63d.json new file mode 100644 index 000000000..a44c7d494 --- /dev/null +++ b/test/packages/visualizations_by_reference/kibana/dashboard/visualizations_by_reference-82273ffe-6acc-4f2f-bbee-c1004abba63d.json @@ -0,0 +1,54 @@ +{ + "attributes": { + "description": "Dashboard", + "panelsJSON": [ + { + "embeddableConfig": { + "enhancements": {} + }, + "gridData": { + "h": 15, + "i": "2df03d80-37c4-4dea-9b5e-f3f9eefb319e", + "w": 24, + "x": 24, + "y": 0 + }, + "panelIndex": "2df03d80-37c4-4dea-9b5e-f3f9eefb319e", + "panelRefName": "panel_0", + "version": "7.5.2" + }, + { + "embeddableConfig": { + "enhancements": {} + }, + "gridData": { + "h": 15, + "i": "b09a478f-8a72-44d3-bbcc-1f2a546c74f0", + "w": 24, + "x": 0, + "y": 0 + }, + "panelIndex": "b09a478f-8a72-44d3-bbcc-1f2a546c74f0", + "panelRefName": "panel_1", + "version": "7.5.2" + } + ], + "timeRestore": false, + "title": "Dashboard by References", + "version": 1 + }, + "id": "visualizations_by_reference-82273ffe-6acc-4f2f-bbee-c1004abba63d", + "references": [ + { + "id": "visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b", + "name": "panel_1", + "type": "lens" + } + ], + "type": "dashboard" +} diff --git a/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6.json b/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6.json new file mode 100644 index 000000000..61fe9366d --- /dev/null +++ b/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6.json @@ -0,0 +1,5 @@ +{ + "id": "visualizations_by_reference-5e1a01ff-6f9a-41c1-b7ad-326472db42b6", + "references": [], + "type": "visualization" +} diff --git a/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b.json b/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b.json new file mode 100644 index 000000000..2cb91ce09 --- /dev/null +++ b/test/packages/visualizations_by_reference/kibana/visualization/visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b.json @@ -0,0 +1,5 @@ +{ + "id": "visualizations_by_reference-8287a5d5-1576-4f3a-83c4-444e9058439b", + "references": [], + "type": "visualization" +} diff --git a/test/packages/visualizations_by_reference/manifest.yml b/test/packages/visualizations_by_reference/manifest.yml new file mode 100644 index 000000000..909840ab7 --- /dev/null +++ b/test/packages/visualizations_by_reference/manifest.yml @@ -0,0 +1,28 @@ +format_version: 1.0.4 +name: visualizations_by_reference +title: BAD +description: This package is bad. +version: 0.1.2 +type: integration +release: beta +conditions: + kibana.version: '^7.9.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 + vars: + - name: hosts + type: text + title: Hosts + multi: true + required: true + show_user: true + default: + - http://127.0.0.1 +owner: + github: elastic/foobar diff --git a/versions/1/changelog.yml b/versions/1/changelog.yml index 82bad56b5..d4179cd02 100644 --- a/versions/1/changelog.yml +++ b/versions/1/changelog.yml @@ -4,15 +4,18 @@ ## - version: 1.14.1-next changes: - - description: Add support for icons in dark mode - type: enhancement - link: https://github.com/elastic/package-spec/pull/378 - description: Add definition for the license file in a package type: enhancement link: https://github.com/elastic/package-spec/pull/367 - description: Prepare for next patch release type: enhancement link: https://github.com/elastic/package-spec/pull/375 + - description: Add support for icons in dark mode + type: enhancement + link: https://github.com/elastic/package-spec/pull/378 + - description: Add warning when dashboards include visualizations by reference + type: enhancement + link: https://github.com/elastic/package-spec/pull/389 - version: 1.14.0 changes: - description: Add 'textarea' var type