Skip to content

Asset loading test runner #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
05c160c
Refactoring: extracting common code
ycombinator Nov 10, 2020
ebf3a50
Adding skeleton for asset loading test runner
ycombinator Nov 10, 2020
27e1a08
Refactoring: defining and implementing TestRunner interface
ycombinator Nov 10, 2020
2dfb9f8
More fleshing out
ycombinator Nov 12, 2020
e13e739
More fleshing out
ycombinator Nov 12, 2020
eefad58
Adding HOWTO
ycombinator Nov 12, 2020
459df7e
Refactoring: make ingest pipeline name method of DataStreamManifest
ycombinator Nov 16, 2020
af707f3
Adding assertions
ycombinator Nov 16, 2020
1f0e17f
More fleshing out
ycombinator Nov 17, 2020
33c2ca4
Better test result reporting
ycombinator Nov 17, 2020
faba5b5
Fix issue with kibana assets being ignored
ycombinator Nov 17, 2020
4157710
Add data stream to asset
ycombinator Nov 17, 2020
8a58f8e
Adding entry to README
ycombinator Nov 17, 2020
c525f0d
Making language consistent
ycombinator Nov 17, 2020
0b12387
Adding godoc comments
ycombinator Nov 17, 2020
8a8aa95
More godoc comments
ycombinator Nov 17, 2020
9c91335
Build packages before starting up stack
ycombinator Nov 17, 2020
f482e49
Adding license headers
ycombinator Nov 17, 2020
778b9cf
Using nil slices
ycombinator Dec 29, 2020
b3f752b
Move settings to stack package
ycombinator Dec 30, 2020
8af8e4b
Refactoring: merging packages API into Kibana client
ycombinator Jan 6, 2021
5aecd27
Use multierror to collect all errors
ycombinator Jan 14, 2021
4ec725f
Removing IsConfigRequired
ycombinator Jan 14, 2021
91fd2c8
Bumping up apache version to something very large
ycombinator Jan 14, 2021
c0014db
Adding comment for large version
ycombinator Jan 14, 2021
dfd0503
Fixing paths
ycombinator Jan 14, 2021
93cff73
Check errors length
ycombinator Jan 14, 2021
5d14f10
Fixing typo
ycombinator Jan 14, 2021
4760e8c
Initialize OLDPWD
ycombinator Jan 14, 2021
1bfb2c0
Check for existence of flag before trying to parse it
ycombinator Jan 15, 2021
3d11f91
Adding comment to explain check
ycombinator Jan 15, 2021
010d452
Setting large versions for all test packages
ycombinator Jan 15, 2021
cef5f22
Removing vestigial stack settings code
ycombinator Jan 15, 2021
133e50e
Adding lens asset
ycombinator Jan 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,19 @@ Use this command to run tests on a package. Currently, there are two types of te

These tests allow you to exercise any Ingest Node Pipelines defined by your packages.

For details on how to configure pipeline test for a package, see the [HOWTO guide](docs/howto/pipeline_testing.md).
For details on how to configure and run pipeline tests for a package, see the [HOWTO guide](docs/howto/pipeline_testing.md).

#### System Tests

These tests allow you to test a package's ability to ingest data end-to-end.

For details on how to configure amd run system tests, see the [HOWTO guide](docs/howto/system_testing.md).
For details on how to configure and run system tests for a package, see the [HOWTO guide](docs/howto/system_testing.md).

#### Asset Loading Tests

These tests ensure that all the Elasticsearch and Kibana assets defined by your package get loaded up as expected.

For details on how to run asset loading tests for a package, see the [HOWTO guide](docs/howto/asset_testing.md).

### `elastic-package version`

Expand Down
48 changes: 34 additions & 14 deletions cmd/testrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd

import (
"fmt"
"path/filepath"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -52,7 +53,6 @@ func setupTestCommand() *cobra.Command {

cmd.PersistentFlags().BoolP(cobraext.FailOnMissingFlagName, "m", false, cobraext.FailOnMissingFlagDescription)
cmd.PersistentFlags().BoolP(cobraext.GenerateTestResultFlagName, "g", false, cobraext.GenerateTestResultFlagDescription)
cmd.PersistentFlags().StringSliceP(cobraext.DataStreamsFlagName, "d", nil, cobraext.DataStreamsFlagDescription)
cmd.PersistentFlags().StringP(cobraext.ReportFormatFlagName, "", string(formats.ReportFormatHuman), cobraext.ReportFormatFlagDescription)
cmd.PersistentFlags().StringP(cobraext.ReportOutputFlagName, "", string(outputs.ReportOutputSTDOUT), cobraext.ReportOutputFlagDescription)
cmd.PersistentFlags().DurationP(cobraext.DeferCleanupFlagName, "", 0, cobraext.DeferCleanupFlagDescription)
Expand All @@ -68,6 +68,10 @@ func setupTestCommand() *cobra.Command {
RunE: action,
}

if runner.CanRunPerDataStream() {
testTypeCmd.Flags().StringSliceP(cobraext.DataStreamsFlagName, "d", nil, cobraext.DataStreamsFlagDescription)
}

cmd.AddCommand(testTypeCmd)
}

Expand All @@ -84,11 +88,6 @@ func testTypeCommandActionFactory(runner testrunner.TestRunner) cobraext.Command
return cobraext.FlagParsingError(err, cobraext.FailOnMissingFlagName)
}

dataStreams, err := cmd.Flags().GetStringSlice(cobraext.DataStreamsFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.DataStreamsFlagName)
}

generateTestResult, err := cmd.Flags().GetBool(cobraext.GenerateTestResultFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.GenerateTestResultFlagName)
Expand All @@ -112,16 +111,37 @@ func testTypeCommandActionFactory(runner testrunner.TestRunner) cobraext.Command
return errors.Wrap(err, "locating package root failed")
}

testFolders, err := testrunner.FindTestFolders(packageRootPath, testType, dataStreams)
if err != nil {
return errors.Wrap(err, "unable to determine test folder paths")
}
var testFolders []testrunner.TestFolder
if runner.CanRunPerDataStream() {
var dataStreams []string
// We check for the existence of the data streams flag before trying to
// parse it because if the root test command is run instead of one of the
// subcommands of test, the data streams flag will not be defined.
if cmd.Flags().Lookup(cobraext.DataStreamsFlagName) != nil {
dataStreams, err = cmd.Flags().GetStringSlice(cobraext.DataStreamsFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.DataStreamsFlagName)
}
}

testFolders, err = testrunner.FindTestFolders(packageRootPath, dataStreams, testType)
if err != nil {
return errors.Wrap(err, "unable to determine test folder paths")
}

if failOnMissing && len(testFolders) == 0 {
if len(dataStreams) > 0 {
return fmt.Errorf("no %s tests found for %s data stream(s)", testType, strings.Join(dataStreams, ","))
if failOnMissing && len(testFolders) == 0 {
if len(dataStreams) > 0 {
return fmt.Errorf("no %s tests found for %s data stream(s)", testType, strings.Join(dataStreams, ","))
}
return fmt.Errorf("no %s tests found", testType)
}
} else {
_, pkg := filepath.Split(packageRootPath)
testFolders = []testrunner.TestFolder{
{
Package: pkg,
},
}
return fmt.Errorf("no %s tests found", testType)
}

deferCleanup, err := cmd.Flags().GetDuration(cobraext.DeferCleanupFlagName)
Expand Down
57 changes: 57 additions & 0 deletions docs/howto/asset_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# HOWTO: Writing asset loading tests for a package

## Introduction

Elastic Packages define assets to be loaded into Elasticsearch and Kibana. Asset loading tests exercise installing a package to ensure that its assets are loaded into Elasticsearch and Kibana as expected.

## Conceptual process

Conceptually, running an asset load test involves the following steps:

1. Build the package.
1. Deploy Elasticsearch, Kibana, and the Package Registry (all part of the Elastic Stack). This step takes time so it should typically be done once as a pre-requisite to running asset loading tests on multiple packages.
1. Install the package.
1. Use various Kibana and Elasticsearch APIs to assert that the package's assets were loaded into Kibana and Elasticsearch as expected.
1. Remove the package.

## Defining an asset loading test

As a package developer, you do not need to do any work to define an asset loading test for your package. All the necessary information is already present in the package's files.

## Running an asset loading test

First, you must build your package. This corresponds to step 1 as described in the [_Conceptual process_](#Conceptual-process) section.

Navigate to the package's root folder (or any sub-folder under it) and run the following command.

```
elastic-package build
```

Next, you must deploy Elasticsearch, Kibana, and the Package Registry. This corresponds to step 2 as described in the [_Conceptual process_](#Conceptual-process) section.

```
elastic-package stack up -d
```

For a complete listing of options available for this command, run `elastic-package stack up -h` or `elastic-package help stack up`.

Next, you must set environment variables needed for further `elastic-package` commands.

```
$(elastic-package stack shellinit)
```

Next, you must invoke the asset loading test runner. This corresponds to steps 3 through 5 as described in the [_Conceptual process_](#Conceptual-process) section.

Navigate to the package's root folder (or any sub-folder under it) and run the following command.

```
elastic-package test asset
```

Finally, when you are done running all asset loading tests, bring down the Elastic Stack. This corresponds to step 4 as described in the [_Conceptual process_](#Conceptual-process) section.

```
elastic-package stack down
```
4 changes: 4 additions & 0 deletions internal/kibana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ func (c *Client) put(resourcePath string, body []byte) (int, []byte, error) {
return c.sendRequest(http.MethodPut, resourcePath, body)
}

func (c *Client) delete(resourcePath string) (int, []byte, error) {
return c.sendRequest(http.MethodDelete, resourcePath, nil)
}

func (c *Client) sendRequest(method, resourcePath string, body []byte) (int, []byte, error) {
reqBody := bytes.NewReader(body)
base, err := url.Parse(c.host)
Expand Down
52 changes: 52 additions & 0 deletions internal/kibana/packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 kibana

import (
"encoding/json"
"fmt"

"github.com/pkg/errors"

"github.com/elastic/elastic-package/internal/packages"
)

// InstallPackage installs the given package in Fleet.
func (c *Client) InstallPackage(pkg packages.PackageManifest) ([]packages.Asset, error) {
path := fmt.Sprintf("%s/epm/packages/%s-%s", FleetAPI, pkg.Name, pkg.Version)
statusCode, respBody, err := c.post(path, nil)
if err != nil {
return nil, errors.Wrap(err, "could not install package")
}

return processResults("install", statusCode, respBody)
}

// RemovePackage removes the given package from Fleet.
func (c *Client) RemovePackage(pkg packages.PackageManifest) ([]packages.Asset, error) {
path := fmt.Sprintf("%s/epm/packages/%s-%s", FleetAPI, pkg.Name, pkg.Version)
statusCode, respBody, err := c.delete(path)
if err != nil {
return nil, errors.Wrap(err, "could not delete package")
}

return processResults("remove", statusCode, respBody)
}

func processResults(action string, statusCode int, respBody []byte) ([]packages.Asset, error) {
if statusCode != 200 {
return nil, fmt.Errorf("could not %s package; API status code = %d; response body = %s", action, statusCode, respBody)
}

var resp struct {
Assets []packages.Asset `json:"response"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, errors.Wrapf(err, "could not convert %s package (response) to JSON", action)
}

return resp.Assets, nil
}
168 changes: 168 additions & 0 deletions internal/packages/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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 packages

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"

"github.com/elastic/elastic-package/internal/multierror"
)

// AssetType represents the type of package asset.
type AssetType string

// Supported asset types.
const (
AssetTypeElasticsearchIndexTemplate AssetType = "index_template"
AssetTypeElasticsearchIngestPipeline AssetType = "ingest_pipeline"

AssetTypeKibanaSavedSearch AssetType = "search"
AssetTypeKibanaVisualization AssetType = "visualization"
AssetTypeKibanaDashboard AssetType = "dashboard"
AssetTypeKibanaMap AssetType = "map"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Lens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 133e50e.

AssetTypeKibanaLens AssetType = "lens"
)

// Asset represents a package asset to be loaded into Kibana or Elasticsearch.
type Asset struct {
ID string `json:"id"`
Type AssetType `json:"type"`
DataStream string
}

// LoadPackageAssets parses the package contents and returns a list of assets defined by the package.
func LoadPackageAssets(pkgRootPath string) ([]Asset, error) {
assets, err := loadKibanaAssets(pkgRootPath)
if err != nil {
return nil, errors.Wrap(err, "could not load kibana assets")
}

a, err := loadElasticsearchAssets(pkgRootPath)
if err != nil {
return a, errors.Wrap(err, "could not load elasticsearch assets")
}
assets = append(assets, a...)

return assets, nil
}

func loadKibanaAssets(pkgRootPath string) ([]Asset, error) {
kibanaAssetsFolderPath := filepath.Join(pkgRootPath, "kibana")

var (
errs multierror.Error

assetTypes = []AssetType{
AssetTypeKibanaDashboard,
AssetTypeKibanaVisualization,
AssetTypeKibanaSavedSearch,
AssetTypeKibanaMap,
AssetTypeKibanaLens,
}

assets []Asset
)

for _, assetType := range assetTypes {
a, err := loadFileBasedAssets(kibanaAssetsFolderPath, assetType)
if err != nil {
errs = append(errs, errors.Wrapf(err, "could not load kibana %s assets", assetType))
continue
}

assets = append(assets, a...)
}

if len(errs) > 0 {
return nil, errs
}

return assets, nil
}

func loadElasticsearchAssets(pkgRootPath string) ([]Asset, error) {
packageManifestPath := filepath.Join(pkgRootPath, PackageManifestFile)
pkgManifest, err := ReadPackageManifest(packageManifestPath)
if err != nil {
return nil, errors.Wrap(err, "reading package manifest file failed")
}

dataStreamManifestPaths, err := filepath.Glob(filepath.Join(pkgRootPath, "data_stream", "*", DataStreamManifestFile))
if err != nil {
return nil, errors.Wrap(err, "could not read data stream manifest file paths")
}

var assets []Asset
for _, dsManifestPath := range dataStreamManifestPaths {
dsManifest, err := ReadDataStreamManifest(dsManifestPath)
if err != nil {
return nil, errors.Wrap(err, "reading data stream manifest failed")
}

indexTemplateName := fmt.Sprintf("%s-%s.%s", dsManifest.Type, pkgManifest.Name, dsManifest.Name)
asset := Asset{
ID: indexTemplateName,
Type: AssetTypeElasticsearchIndexTemplate,
DataStream: dsManifest.Name,
}
assets = append(assets, asset)

if dsManifest.Type == dataStreamTypeLogs {
ingestPipelineName := dsManifest.GetPipelineNameOrDefault()
if ingestPipelineName == defaultPipelineName {
ingestPipelineName = fmt.Sprintf("%s-%s.%s-%s", dsManifest.Type, pkgManifest.Name, dsManifest.Name, pkgManifest.Version)
}
asset = Asset{
ID: ingestPipelineName,
Type: AssetTypeElasticsearchIngestPipeline,
DataStream: dsManifest.Name,
}
assets = append(assets, asset)
}
}

return assets, nil
}

func loadFileBasedAssets(kibanaAssetsFolderPath string, assetType AssetType) ([]Asset, error) {
assetsFolderPath := filepath.Join(kibanaAssetsFolderPath, string(assetType))
_, err := os.Stat(assetsFolderPath)
if err != nil && os.IsNotExist(err) {
// No assets folder defined; nothing to load
return nil, nil
}
if err != nil {
return nil, errors.Wrapf(err, "error finding kibana %s assets folder", assetType)
}

files, err := ioutil.ReadDir(assetsFolderPath)
if err != nil {
return nil, errors.Wrapf(err, "could not read %s files", assetType)
}

var assets []Asset
for _, f := range files {
if f.IsDir() {
continue
}

name := f.Name()
id := strings.TrimSuffix(name, ".json")

asset := Asset{
ID: id,
Type: assetType,
}
assets = append(assets, asset)
}

return assets, nil
}
Loading