Skip to content

Commit 9c8276d

Browse files
authored
Allow to define more than one service deployer and choose the deployer via configuration setting (#2638)
This PR allows us to define more than one service deployer in the "_dev/deploy" folder (at package or data stream level). If there are more than one service deployer defined, it must be added to the test system configuration file a new setting deployer. This new setting helps us to choose which specific specific service deployer will be used by each test.
1 parent 7ceab55 commit 9c8276d

File tree

102 files changed

+6415
-63
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+6415
-63
lines changed

docs/howto/system_testing.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,45 @@ wget -qO- https://raw.github.com/elastic/elastic-package/main/script
387387
elastic-package test system --data-streams pod -v # start system tests for the "pod" data stream
388388
```
389389

390+
391+
### Defining more than one service deployer
392+
393+
Since `elastic-package` v0.113.0, it is allowed to define more than one service deployer in each `_dev/deploy` folder. And each system test
394+
configuration can choose which service deployer to use among them.
395+
For instance, a data stream could contain a definition for Docker Compose and a Terraform service deployers.
396+
397+
First, `elastic-package` looks for the corresponding `_dev/folder` to use. It will follow this order, and the first one that exists has
398+
preference:
399+
- Deploy folder at Data Stream level: `packages/<package_name>/data_stream/<data_stream_name>/_dev/deploy/`
400+
- Deploy folder at Package level: `packages/<package_name>/data_stream/<data_stream_name>/_dev/deploy/`
401+
402+
If there is more than one service deployer defined in the deploy folder found, the system test configuration files of the
403+
required tests must set the `deployer` field to choose which service deployer configure and start for that given test. If that setting
404+
is not defined and there are more than one service edployer, `elastic-package` will fail with an error since it is not supported
405+
to run several service deployers at the same time.
406+
407+
Example of system test configuration including `deployer` setting:
408+
409+
```yaml
410+
deployer: docker
411+
service: nginx
412+
vars: ~
413+
data_stream:
414+
vars:
415+
paths:
416+
- "{{SERVICE_LOGS_DIR}}/access.log*"
417+
```
418+
419+
In this example, `elastic-package` looks for a Docker Compose service deployer in the given `_dev/deploy` folder found previously.
420+
421+
Each service deployer folder keep the same format and files as defined in previous sections.
422+
423+
For instance, this allows to test one data stream using different inputs, each input with a different service deployer. One of them could be using
424+
the Docker Compose service deployer, and another input could be using terraform to create resources in AWS.
425+
426+
You can find an example of a package using this in this [test package](../../test/packages/parallel/nginx_multiple_services/).
427+
428+
390429
### Test case definition
391430

392431
Next, we must define at least one configuration for each data stream that we
@@ -421,7 +460,11 @@ for system tests.
421460
| agent.provisioning_script.language | string | | Programming language of the provisioning script. Default: `sh`. |
422461
| agent.provisioning_script.contents | string | | Code to run as a provisioning script to customize the system where the agent will be run. |
423462
| agent.user | string | | User that runs the Elastic Agent process. |
463+
| assert.hit_count | integer | | Exact number of documents to wait for being ingested. |
464+
| assert.min_count | integer | | Minimum number of documents to wait for being ingested. |
465+
| assert.fields_present | []string| | List of fields that must be present in the documents to stop waiting for new documents. |
424466
| data_stream.vars | dictionary | | Data stream level variables to set (i.e. declared in `package_root/data_stream/$data_stream/manifest.yml`). If not specified the defaults from the manifest are used. |
467+
| deployer | string| | Name of the service deployer to setup for this system test. Available values: docker, tf or k8s. |
425468
| ignore_service_error | boolean | no | If `true`, it will ignore any failures in the deployed test services. Defaults to `false`. |
426469
| input | string | yes | Input type to test (e.g. logfile, httpjson, etc). Defaults to the input used by the first stream in the data stream manifest. |
427470
| numeric_keyword_fields | []string | | List of fields to ignore during validation that are mapped as `keyword` in Elasticsearch, but their JSON data type is a number. |
@@ -434,9 +477,6 @@ for system tests.
434477
| skip_transform_validation | boolean | | Disable or enable the transforms validation performed in system tests. |
435478
| vars | dictionary | | Package level variables to set (i.e. declared in `$package_root/manifest.yml`). If not specified the defaults from the manifest are used. |
436479
| wait_for_data_timeout | duration | | Amount of time to wait for data to be present in Elasticsearch. Defaults to 10m. |
437-
| assert.hit_count | integer | | Exact number of documents to wait for being ingested. |
438-
| assert.min_count | integer | | Minimum number of documents to wait for being ingested. |
439-
| assert.fields_present | []string| | List of fields that must be present in the documents to stop waiting for new documents. |
440480
441481
For example, the `apache/access` data stream's `test-access-log-config.yml` is
442482
shown below.

internal/agentdeployer/factory.go

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11-
"path/filepath"
11+
"slices"
1212

1313
"github.com/elastic/elastic-package/internal/logger"
1414
"github.com/elastic/elastic-package/internal/profile"
15+
"github.com/elastic/elastic-package/internal/servicedeployer"
1516
)
1617

1718
const (
@@ -30,6 +31,8 @@ type FactoryOptions struct {
3031
StackVersion string
3132
PolicyName string
3233

34+
DeployerName string
35+
3336
PackageName string
3437
DataStream string
3538

@@ -81,27 +84,26 @@ func Factory(options FactoryOptions) (AgentDeployer, error) {
8184
}
8285

8386
func selectAgentDeployerType(options FactoryOptions) (string, error) {
84-
devDeployPath, err := FindDevDeployPath(options)
87+
devDeployPath, err := servicedeployer.FindDevDeployPath(servicedeployer.FactoryOptions{
88+
DataStreamRootPath: options.DataStreamRootPath,
89+
DevDeployDir: options.DevDeployDir,
90+
PackageRootPath: options.PackageRootPath,
91+
})
8592
if errors.Is(err, os.ErrNotExist) {
8693
return "default", nil
8794
}
8895
if err != nil {
8996
return "", fmt.Errorf("can't find \"%s\" directory: %w", options.DevDeployDir, err)
9097
}
9198

92-
agentDeployerNames, err := findAgentDeployers(devDeployPath)
93-
if errors.Is(err, os.ErrNotExist) || len(agentDeployerNames) == 0 {
99+
agentDeployerName, err := findAgentDeployer(devDeployPath, options.DeployerName)
100+
if errors.Is(err, os.ErrNotExist) || (err == nil && agentDeployerName == "") {
94101
logger.Debugf("Not agent deployer found, using default one")
95102
return "default", nil
96103
}
97104
if err != nil {
98105
return "", fmt.Errorf("failed to find agent deployer: %w", err)
99106
}
100-
if len(agentDeployerNames) != 1 {
101-
return "", fmt.Errorf("expected to find only one agent deployer in \"%s\"", devDeployPath)
102-
}
103-
agentDeployerName := agentDeployerNames[0]
104-
105107
// if package defines `_dev/deploy/docker` or `_dev/deploy/tf` folder to start their services,
106108
// it should be using the default agent deployer`
107109
if agentDeployerName == "docker" || agentDeployerName == "tf" {
@@ -111,43 +113,28 @@ func selectAgentDeployerType(options FactoryOptions) (string, error) {
111113
return agentDeployerName, nil
112114
}
113115

114-
// FindDevDeployPath function returns a path reference to the "_dev/deploy" directory.
115-
func FindDevDeployPath(options FactoryOptions) (string, error) {
116-
dataStreamDevDeployPath := filepath.Join(options.DataStreamRootPath, options.DevDeployDir)
117-
info, err := os.Stat(dataStreamDevDeployPath)
118-
if err == nil && info.IsDir() {
119-
return dataStreamDevDeployPath, nil
120-
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
121-
return "", fmt.Errorf("stat failed for data stream (path: %s): %w", dataStreamDevDeployPath, err)
116+
func findAgentDeployer(devDeployPath, expectedDeployer string) (string, error) {
117+
names, err := servicedeployer.FindAllServiceDeployers(devDeployPath)
118+
if err != nil {
119+
return "", fmt.Errorf("failed to find service deployers in \"%s\": %w", devDeployPath, err)
122120
}
121+
deployers := slices.DeleteFunc(names, func(name string) bool {
122+
return expectedDeployer != "" && name != expectedDeployer
123+
})
123124

124-
packageDevDeployPath := filepath.Join(options.PackageRootPath, options.DevDeployDir)
125-
info, err = os.Stat(packageDevDeployPath)
126-
if err == nil && info.IsDir() {
127-
return packageDevDeployPath, nil
128-
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
129-
return "", fmt.Errorf("stat failed for package (path: %s): %w", packageDevDeployPath, err)
125+
// If we have more than one agent deployer, we expect to find only one.
126+
if expectedDeployer != "" && len(deployers) != 1 {
127+
return "", fmt.Errorf("expected to find %q agent deployer in %q", expectedDeployer, devDeployPath)
130128
}
131129

132-
return "", fmt.Errorf("\"%s\" %w", options.DevDeployDir, os.ErrNotExist)
133-
}
134-
135-
func findAgentDeployers(devDeployPath string) ([]string, error) {
136-
fis, err := os.ReadDir(devDeployPath)
137-
if err != nil {
138-
return nil, fmt.Errorf("can't read directory (path: %s): %w", devDeployPath, err)
130+
// It is allowed to have no agent deployers
131+
if len(deployers) == 0 {
132+
return "", nil
139133
}
140134

141-
var folders []os.DirEntry
142-
for _, fi := range fis {
143-
if fi.IsDir() {
144-
folders = append(folders, fi)
145-
}
135+
if len(deployers) == 1 {
136+
return deployers[0], nil
146137
}
147138

148-
var names []string
149-
for _, folder := range folders {
150-
names = append(names, folder.Name())
151-
}
152-
return names, nil
139+
return "", fmt.Errorf("expected to find only one agent deployer in \"%s\" (found %d agent deployers)", devDeployPath, len(deployers))
153140
}

internal/servicedeployer/factory.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"slices"
1213

1314
"github.com/elastic/elastic-package/internal/profile"
1415
)
@@ -31,6 +32,8 @@ type FactoryOptions struct {
3132

3233
PolicyName string
3334

35+
DeployerName string
36+
3437
Variant string
3538

3639
RunTearDown bool
@@ -46,10 +49,15 @@ func Factory(options FactoryOptions) (ServiceDeployer, error) {
4649
return nil, fmt.Errorf("can't find \"%s\" directory: %w", options.DevDeployDir, err)
4750
}
4851

49-
serviceDeployerName, err := findServiceDeployer(devDeployPath)
52+
serviceDeployerName, err := findServiceDeployer(devDeployPath, options.DeployerName)
5053
if err != nil {
5154
return nil, fmt.Errorf("can't find any valid service deployer: %w", err)
5255
}
56+
// It's allowed to not define a service deployer in system tests
57+
// if deployerName is not defined in the test configuration.
58+
if serviceDeployerName == "" {
59+
return nil, nil
60+
}
5361

5462
serviceDeployerPath := filepath.Join(devDeployPath, serviceDeployerName)
5563

@@ -123,37 +131,62 @@ func Factory(options FactoryOptions) (ServiceDeployer, error) {
123131
// FindDevDeployPath function returns a path reference to the "_dev/deploy" directory.
124132
func FindDevDeployPath(options FactoryOptions) (string, error) {
125133
dataStreamDevDeployPath := filepath.Join(options.DataStreamRootPath, options.DevDeployDir)
126-
if _, err := os.Stat(dataStreamDevDeployPath); err == nil {
134+
info, err := os.Stat(dataStreamDevDeployPath)
135+
if err == nil && info.IsDir() {
127136
return dataStreamDevDeployPath, nil
128-
} else if !errors.Is(err, os.ErrNotExist) {
137+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
129138
return "", fmt.Errorf("stat failed for data stream (path: %s): %w", dataStreamDevDeployPath, err)
130139
}
131140

132141
packageDevDeployPath := filepath.Join(options.PackageRootPath, options.DevDeployDir)
133-
if _, err := os.Stat(packageDevDeployPath); err == nil {
142+
info, err = os.Stat(packageDevDeployPath)
143+
if err == nil && info.IsDir() {
134144
return packageDevDeployPath, nil
135-
} else if !errors.Is(err, os.ErrNotExist) {
145+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
136146
return "", fmt.Errorf("stat failed for package (path: %s): %w", packageDevDeployPath, err)
137147
}
138148

139149
return "", fmt.Errorf("\"%s\" %w", options.DevDeployDir, os.ErrNotExist)
140150
}
141151

142-
func findServiceDeployer(devDeployPath string) (string, error) {
152+
func FindAllServiceDeployers(devDeployPath string) ([]string, error) {
143153
fis, err := os.ReadDir(devDeployPath)
144154
if err != nil {
145-
return "", fmt.Errorf("can't read directory (path: %s): %w", devDeployPath, err)
155+
return nil, fmt.Errorf("can't read directory (path: %s): %w", devDeployPath, err)
146156
}
147157

148-
var folders []os.DirEntry
158+
var names []string
149159
for _, fi := range fis {
150160
if fi.IsDir() {
151-
folders = append(folders, fi)
161+
names = append(names, fi.Name())
152162
}
153163
}
154164

155-
if len(folders) != 1 {
156-
return "", fmt.Errorf("expected to find only one service deployer in \"%s\"", devDeployPath)
165+
return names, nil
166+
}
167+
168+
func findServiceDeployer(devDeployPath, expectedDeployer string) (string, error) {
169+
names, err := FindAllServiceDeployers(devDeployPath)
170+
if err != nil {
171+
return "", fmt.Errorf("failed to find service deployers in %q: %w", devDeployPath, err)
172+
}
173+
deployers := slices.DeleteFunc(names, func(name string) bool {
174+
return expectedDeployer != "" && name != expectedDeployer
175+
})
176+
177+
if len(deployers) == 1 {
178+
return deployers[0], nil
179+
}
180+
181+
if expectedDeployer != "" {
182+
return "", fmt.Errorf("expected to find %q service deployer in %q", expectedDeployer, devDeployPath)
157183
}
158-
return folders[0].Name(), nil
184+
185+
// If "_dev/deploy" directory exists, but it is empty. It does not have any service deployer,
186+
// package-spec does not disallow to be empty this folder.
187+
if len(deployers) == 0 {
188+
return "", nil
189+
}
190+
191+
return "", fmt.Errorf("expected to find only one service deployer in %q (found %d service deployers)", devDeployPath, len(deployers))
159192
}

internal/testrunner/runners/system/test_config.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path/filepath"
1212
"regexp"
13+
"slices"
1314
"strings"
1415
"time"
1516

@@ -24,7 +25,10 @@ import (
2425
"github.com/elastic/elastic-package/internal/testrunner"
2526
)
2627

27-
var systemTestConfigFilePattern = regexp.MustCompile(`^test-([a-z0-9_.-]+)-config.yml$`)
28+
var (
29+
systemTestConfigFilePattern = regexp.MustCompile(`^test-([a-z0-9_.-]+)-config.yml$`)
30+
allowedDeployerNames = []string{"docker", "k8s", "tf"}
31+
)
2832

2933
type testConfig struct {
3034
testrunner.SkippableConfig `config:",inline"`
@@ -37,6 +41,8 @@ type testConfig struct {
3741
WaitForDataTimeout time.Duration `config:"wait_for_data_timeout"`
3842
SkipIgnoredFields []string `config:"skip_ignored_fields"`
3943

44+
Deployer string `config:"deployer"` // Name of the service deployer to use for this test.
45+
4046
Vars common.MapStr `config:"vars"`
4147
DataStream struct {
4248
Vars common.MapStr `config:"vars"`
@@ -129,6 +135,11 @@ func newConfig(configFilePath string, svcInfo servicedeployer.ServiceInfo, servi
129135
c.Agent.PreStartScript.Language = agentdeployer.DefaultAgentProgrammingLanguage
130136
}
131137

138+
// Not included in package-spec validation for deployer name
139+
if c.Deployer != "" && !slices.Contains(allowedDeployerNames, c.Deployer) {
140+
return nil, fmt.Errorf("invalid deployer name %q in system test configuration file %q, allowed values are: %s", c.Deployer, configFilePath, strings.Join(allowedDeployerNames, ", "))
141+
}
142+
132143
return &c, nil
133144
}
134145

0 commit comments

Comments
 (0)