Skip to content

Commit 047ff96

Browse files
authored
Add setting to configure a directory with geoip databases (#1211)
1 parent 58bca9f commit 047ff96

File tree

11 files changed

+287
-3
lines changed

11 files changed

+287
-3
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ Use this command to add, remove, and manage multiple config profiles.
266266

267267
Individual user profiles appear in ~/.elastic-package/stack, and contain all the config files needed by the "stack" subcommand.
268268
Once a new profile is created, it can be specified with the -p flag, or the ELASTIC_PACKAGE_PROFILE environment variable.
269-
User profiles are not overwritten on upgrade of elastic-stack, and can be freely modified to allow for different stack configs.
269+
User profiles can be configured with a "config.yml" file in the profile directory.
270270

271271
### `elastic-package profiles create`
272272

@@ -474,6 +474,28 @@ Use this command to print the version of elastic-package that you have installed
474474

475475

476476

477+
## Elastic Package profiles
478+
479+
The `profiles` subcommand allows to work with different configurations. By default,
480+
`elastic-package` uses the "default" profile. Other profiles can be created with the
481+
`elastic-package profiles create` command. Once a profile is created, it will have its
482+
own directory inside the elastic-package data directory. Once you have more profiles,
483+
you can change the default with `elastic-package profiles use`.
484+
485+
You can find the profiles in your system with `elastic-package profiles list`.
486+
487+
You can delete profiles with `elastic-package profiles delete`.
488+
489+
Each profile can have a `config.yml` file that allows to persist configuration settings
490+
that apply only to commands using this profile. You can find a `config.yml.example` that
491+
you can copy to start.
492+
493+
The following settings are available per profile:
494+
495+
* `stack.geoip_dir` defines a directory with GeoIP databases that can be used by
496+
Elasticsearch in stacks managed by elastic-package. It is recommended to use
497+
an absolute path, out of the `.elastic-package` directory.
498+
477499
## Development
478500

479501
Even though the project is "go-gettable", there is the `Makefile` present, which can be used to build, format or vendor

cmd/profiles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func setupProfilesCommand() *cobraext.Command {
3131
3232
Individual user profiles appear in ~/.elastic-package/stack, and contain all the config files needed by the "stack" subcommand.
3333
Once a new profile is created, it can be specified with the -p flag, or the ELASTIC_PACKAGE_PROFILE environment variable.
34-
User profiles are not overwritten on upgrade of elastic-stack, and can be freely modified to allow for different stack configs.`
34+
User profiles can be configured with a "config.yml" file in the profile directory.`
3535

3636
profileCommand := &cobra.Command{
3737
Use: "profiles",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Directory containing GeoIP databases for stacks managed by elastic-agent.
2+
# stack.geoip_dir: "/path/to/geoip_dir/"

internal/profile/_testdata/config.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# An expected setting.
2+
stack.geoip_dir: "/home/foo/Documents/ingest-geoip"
3+
4+
# An empty string, should exist, but return empty.
5+
other.empty: ""
6+
7+
# A nested value, should work as "other.nested".
8+
other:
9+
nested: "foo"
10+
11+
# A number. Will be parsed as string.
12+
other.number: 42
13+
14+
# A float. Will be parsed as string.
15+
other.float: 0.12345
16+
17+
# A bool. Will be parsed as string.
18+
other.bool: false

internal/profile/config.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package profile
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"os"
11+
12+
"github.com/elastic/go-ucfg/yaml"
13+
14+
"github.com/elastic/elastic-package/internal/common"
15+
)
16+
17+
type config struct {
18+
settings common.MapStr
19+
}
20+
21+
func loadProfileConfig(path string) (config, error) {
22+
cfg, err := yaml.NewConfigWithFile(path)
23+
if errors.Is(err, os.ErrNotExist) {
24+
return config{}, nil
25+
}
26+
if err != nil {
27+
return config{}, fmt.Errorf("can't load profile configuration (%s): %w", path, err)
28+
}
29+
30+
settings := make(common.MapStr)
31+
err = cfg.Unpack(settings)
32+
if err != nil {
33+
return config{}, fmt.Errorf("can't unpack configuration: %w", err)
34+
}
35+
36+
return config{settings: settings}, nil
37+
}
38+
39+
func (c *config) get(name string) (string, bool) {
40+
raw, err := c.settings.GetValue(name)
41+
if err != nil {
42+
return "", false
43+
}
44+
switch v := raw.(type) {
45+
case string:
46+
return v, true
47+
default:
48+
return fmt.Sprintf("%v", v), true
49+
}
50+
}

internal/profile/config_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package profile
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestLoadProfileConfig(t *testing.T) {
15+
cases := []struct {
16+
name string
17+
expected string
18+
found bool
19+
}{
20+
{
21+
name: "stack.geoip_dir",
22+
expected: "/home/foo/Documents/ingest-geoip",
23+
found: true,
24+
},
25+
{
26+
name: "other.empty",
27+
expected: "",
28+
found: true,
29+
},
30+
{
31+
name: "other.nested",
32+
expected: "foo",
33+
found: true,
34+
},
35+
{
36+
name: "other.number",
37+
expected: "42",
38+
found: true,
39+
},
40+
{
41+
name: "other.float",
42+
expected: "0.12345",
43+
found: true,
44+
},
45+
{
46+
name: "other.bool",
47+
expected: "false",
48+
found: true,
49+
},
50+
{
51+
name: "not.present",
52+
found: false,
53+
},
54+
}
55+
56+
config, err := loadProfileConfig("_testdata/config.yml")
57+
require.NoError(t, err)
58+
59+
for _, c := range cases {
60+
t.Run(c.name, func(t *testing.T) {
61+
value, found := config.get(c.name)
62+
if assert.Equal(t, c.found, found) {
63+
assert.Equal(t, c.expected, value)
64+
}
65+
})
66+
}
67+
}

internal/profile/profile.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package profile
66

77
import (
8+
"embed"
89
"errors"
910
"fmt"
1011
"os"
@@ -21,16 +22,27 @@ const (
2122
// PackageProfileMetaFile is the filename of the profile metadata file
2223
PackageProfileMetaFile = "profile.json"
2324

25+
// PackageProfileConfigFile is the filename of the profile configuration file
26+
PackageProfileConfigFile = "config.yml"
27+
2428
// DefaultProfile is the name of the default profile.
2529
DefaultProfile = "default"
2630
)
2731

32+
//go:embed _static
33+
var static embed.FS
34+
2835
var (
36+
staticSource = resource.NewSourceFS(static)
2937
profileResources = []resource.Resource{
3038
&resource.File{
3139
Path: PackageProfileMetaFile,
3240
Content: profileMetadataContent,
3341
},
42+
&resource.File{
43+
Path: PackageProfileConfigFile + ".example",
44+
Content: staticSource.File("_static/config.yml.example"),
45+
},
3446
}
3547
)
3648

@@ -123,6 +135,8 @@ type Profile struct {
123135
// ProfilePath is the absolute path to the profile
124136
ProfilePath string
125137
ProfileName string
138+
139+
config config
126140
}
127141

128142
// Path returns an absolute path to the given file
@@ -131,6 +145,15 @@ func (profile Profile) Path(names ...string) string {
131145
return filepath.Join(elems...)
132146
}
133147

148+
// Config returns a configuration setting, or its default if setting not found
149+
func (profile Profile) Config(name string, def string) string {
150+
v, found := profile.config.get(name)
151+
if !found {
152+
return def
153+
}
154+
return v
155+
}
156+
134157
// ErrNotAProfile is returned in cases where we don't have a valid profile directory
135158
var ErrNotAProfile = errors.New("not a profile")
136159

@@ -211,9 +234,16 @@ func loadProfile(elasticPackagePath string, profileName string) (*Profile, error
211234
return nil, ErrNotAProfile
212235
}
213236

237+
configPath := filepath.Join(profilePath, PackageProfileConfigFile)
238+
config, err := loadProfileConfig(configPath)
239+
if err != nil {
240+
return nil, fmt.Errorf("error loading configuration for profile %q: %w", profileName, err)
241+
}
242+
214243
profile := Profile{
215244
ProfileName: profileName,
216245
ProfilePath: profilePath,
246+
config: config,
217247
}
218248

219249
return &profile, nil

internal/stack/_static/docker-compose-stack.yml.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ services:
1414
volumes:
1515
- "./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml"
1616
- "../certs/elasticsearch:/usr/share/elasticsearch/config/certs"
17-
- "./ingest-geoip:/usr/share/elasticsearch/config/ingest-geoip"
17+
- "{{ fact "geoip_dir" }}:/usr/share/elasticsearch/config/ingest-geoip"
1818
- "./service_tokens:/usr/share/elasticsearch/config/service_tokens"
1919
ports:
2020
- "127.0.0.1:9200:9200"

internal/stack/resources.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ func applyResources(profile *profile.Profile, stackVersion string) error {
117117

118118
"username": elasticsearchUsername,
119119
"password": elasticsearchPassword,
120+
121+
"geoip_dir": profile.Config("stack.geoip_dir", "./ingest-geoip"),
120122
})
121123

122124
os.MkdirAll(stackDir, 0755)

internal/stack/resources_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package stack
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"gopkg.in/yaml.v3"
16+
17+
"github.com/elastic/elastic-package/internal/profile"
18+
)
19+
20+
func TestApplyResourcesWithCustomGeoipDir(t *testing.T) {
21+
const expectedGeoipPath = "/some/path/ingest-geoip"
22+
const profileName = "custom_geoip"
23+
24+
elasticPackagePath := t.TempDir()
25+
profilesPath := filepath.Join(elasticPackagePath, "profiles")
26+
27+
os.Setenv("ELASTIC_PACKAGE_DATA_HOME", elasticPackagePath)
28+
29+
// Create profile.
30+
err := profile.CreateProfile(profile.Options{
31+
// PackagePath is actually the profiles path, what is a bit counterintuitive.
32+
PackagePath: profilesPath,
33+
Name: profileName,
34+
})
35+
require.NoError(t, err)
36+
37+
// Write configuration to the profile.
38+
configPath := filepath.Join(profilesPath, profileName, profile.PackageProfileConfigFile)
39+
config := fmt.Sprintf("stack.geoip_dir: %q", expectedGeoipPath)
40+
err = os.WriteFile(configPath, []byte(config), 0644)
41+
require.NoError(t, err)
42+
43+
p, err := profile.LoadProfile(profileName)
44+
require.NoError(t, err)
45+
t.Logf("Profile name: %s, path: %s", p.ProfileName, p.ProfilePath)
46+
47+
// Smoke test to check that we are actually loading the profile we want and it has the setting.
48+
v := p.Config("stack.geoip_dir", "")
49+
require.Equal(t, expectedGeoipPath, v)
50+
51+
// Now, apply resources and check that the variable has been used.
52+
err = applyResources(p, "8.6.1")
53+
require.NoError(t, err)
54+
55+
d, err := os.ReadFile(p.Path(profileStackPath, SnapshotFile))
56+
require.NoError(t, err)
57+
58+
var composeFile struct {
59+
Services struct {
60+
Elasticsearch struct {
61+
Volumes []string `yaml:"volumes"`
62+
} `yaml:"elasticsearch"`
63+
} `yaml:"services"`
64+
}
65+
err = yaml.Unmarshal(d, &composeFile)
66+
require.NoError(t, err)
67+
68+
volumes := composeFile.Services.Elasticsearch.Volumes
69+
expectedVolume := fmt.Sprintf("%s:/usr/share/elasticsearch/config/ingest-geoip", expectedGeoipPath)
70+
assert.Contains(t, volumes, expectedVolume)
71+
}

0 commit comments

Comments
 (0)