Skip to content
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

Add options for API key in clients #2317

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ The output of this command is intended to be evaluated by the current shell. For

Relevant environment variables are:

- ELASTIC_PACKAGE_ELASTICSEARCH_API_KEY
- ELASTIC_PACKAGE_ELASTICSEARCH_HOST
- ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME
- ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD
Expand Down Expand Up @@ -690,13 +691,15 @@ There are available some environment variables that could be used to change some

- To configure the Elastic stack to be used by `elastic-package`:
- `ELASTIC_PACKAGE_ELASTICSEARCH_HOST`: Host of the elasticsearch (e.g. https://127.0.0.1:9200)
- `ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME`: User name to connect to elasticsearch (e.g. elastic)
- `ELASTIC_PACKAGE_ELASTICSEARCH_API_KEY`: API key to connect to elasticsearch and kibana. When set it takes precedence over username and password.
- `ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME`: User name to connect to elasticsearch and kibana (e.g. elastic)
- `ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD`: Password of that user.
- `ELASTIC_PACKAGE_ELASTICSEARCH_KIBANA_HOST`: Kibana URL (e.g. https://127.0.0.1:5601)
- `ELASTIC_PACKAGE_ELASTICSEARCH_CA_CERT`: Path to the CA certificate to connect to the Elastic stack services.

- To configure an external metricstore while running benchmarks (more info at [system benchmarking docs](https://github.com/elastic/elastic-package/blob/main/docs/howto/system_benchmarking.md#setting-up-an-external-metricstore) or [rally benchmarking docs](https://github.com/elastic/elastic-package/blob/main/docs/howto/rally_benchmarking.md#setting-up-an-external-metricstore)):
- `ELASTIC_PACKAGE_ESMETRICSTORE_HOST`: Host of the elasticsearch (e.g. https://127.0.0.1:9200)
- `ELASTIC_PACKAGE_ESMETRICSTORE_API_KEY`: API key to connect to elasticsearch and kibana. When set it takes precedence over username and password.
- `ELASTIC_PACKAGE_ESMETRICSTORE_USERNAME`: Username to connect to elasticsearch (e.g. elastic)
- `ELASTIC_PACKAGE_ESMETRICSTORE_PASSWORD`: Password for the user.
- `ELASTIC_PACKAGE_ESMETRICSTORE_CA_CERT`: Path to the CA certificate to connect to the Elastic stack services.
Expand Down
4 changes: 3 additions & 1 deletion cmd/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,16 +655,18 @@ func systemCommandAction(cmd *cobra.Command, args []string) error {

func initializeESMetricsClient(ctx context.Context) (*elasticsearch.Client, error) {
address := os.Getenv(benchcommon.ESMetricstoreHostEnv)
apiKey := os.Getenv(benchcommon.ESMetricstoreAPIKeyEnv)
user := os.Getenv(benchcommon.ESMetricstoreUsernameEnv)
pass := os.Getenv(benchcommon.ESMetricstorePasswordEnv)
cacert := os.Getenv(benchcommon.ESMetricstoreCACertificateEnv)
if address == "" || user == "" || pass == "" {
if address == "" || ((user == "" || pass == "") && apiKey == "") {
logger.Debugf("can't initialize metricstore, missing environment configuration")
return nil, nil
}

esClient, err := stack.NewElasticsearchClient(
elasticsearch.OptionWithAddress(address),
elasticsearch.OptionWithAPIKey(apiKey),
elasticsearch.OptionWithUsername(user),
elasticsearch.OptionWithPassword(pass),
elasticsearch.OptionWithCertificateAuthority(cacert),
Expand Down
1 change: 1 addition & 0 deletions cmd/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The output of this command is intended to be evaluated by the current shell. For

Relevant environment variables are:

- ELASTIC_PACKAGE_ELASTICSEARCH_API_KEY
- ELASTIC_PACKAGE_ELASTICSEARCH_HOST
- ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME
- ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD
Expand Down
1 change: 1 addition & 0 deletions internal/benchrunner/runners/common/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package common
import "github.com/elastic/elastic-package/internal/environment"

var (
ESMetricstoreAPIKeyEnv = environment.WithElasticPackagePrefix("ESMETRICSTORE_API_KEY")
ESMetricstoreHostEnv = environment.WithElasticPackagePrefix("ESMETRICSTORE_HOST")
ESMetricstoreUsernameEnv = environment.WithElasticPackagePrefix("ESMETRICSTORE_USERNAME")
ESMetricstorePasswordEnv = environment.WithElasticPackagePrefix("ESMETRICSTORE_PASSWORD")
Expand Down
9 changes: 9 additions & 0 deletions internal/elasticsearch/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ClusterStateRequest = esapi.ClusterStateRequest
// clientOptions are used to configure a client.
type clientOptions struct {
address string
apiKey string
username string
password string

Expand All @@ -47,6 +48,13 @@ type clientOptions struct {

type ClientOption func(*clientOptions)

// OptionWithAPIKey sets the API key to be used by the client for authentication.
func OptionWithAPIKey(apiKey string) ClientOption {
return func(opts *clientOptions) {
opts.apiKey = apiKey
}
}

// OptionWithAddress sets the address to be used by the client.
func OptionWithAddress(address string) ClientOption {
return func(opts *clientOptions) {
Expand Down Expand Up @@ -109,6 +117,7 @@ func NewConfig(customOptions ...ClientOption) (elasticsearch.Config, error) {

config := elasticsearch.Config{
Addresses: []string{options.address},
APIKey: options.apiKey,
Username: options.username,
Password: options.password,
}
Expand Down
14 changes: 13 additions & 1 deletion internal/kibana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var ErrUndefinedHost = errors.New("missing kibana host")
// Client is responsible for exporting dashboards from Kibana.
type Client struct {
host string
apiKey string
username string
password string

Expand Down Expand Up @@ -94,6 +95,13 @@ func Address(address string) ClientOption {
}
}

// APIKey option sets the API key to be used by the client for authentication.
func APIKey(apiKey string) ClientOption {
return func(c *Client) {
c.apiKey = apiKey
}
}

// TLSSkipVerify option disables TLS verification.
func TLSSkipVerify() ClientOption {
return func(c *Client) {
Expand Down Expand Up @@ -182,7 +190,11 @@ func (c *Client) newRequest(ctx context.Context, method, resourcePath string, re
return nil, fmt.Errorf("could not create %v request to Kibana API resource: %s: %w", method, resourcePath, err)
}

req.SetBasicAuth(c.username, c.password)
if c.apiKey != "" {
req.Header.Set("Authorization", "ApiKey "+c.apiKey)
} else {
req.SetBasicAuth(c.username, c.password)
}
req.Header.Add("content-type", "application/json")
req.Header.Add("kbn-xsrf", install.DefaultStackVersion)

Expand Down
17 changes: 17 additions & 0 deletions internal/stack/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
func NewElasticsearchClient(customOptions ...elasticsearch.ClientOption) (*elasticsearch.Client, error) {
options := []elasticsearch.ClientOption{
elasticsearch.OptionWithAddress(os.Getenv(ElasticsearchHostEnv)),
elasticsearch.OptionWithAPIKey(os.Getenv(ElasticsearchAPIKeyEnv)),
elasticsearch.OptionWithPassword(os.Getenv(ElasticsearchPasswordEnv)),
elasticsearch.OptionWithUsername(os.Getenv(ElasticsearchUsernameEnv)),
elasticsearch.OptionWithCertificateAuthority(os.Getenv(CACertificateEnv)),
Expand Down Expand Up @@ -58,6 +59,10 @@ func NewElasticsearchClientFromProfile(profile *profile.Profile, customOptions .
elasticsearchHost = profileConfig.ElasticsearchHostPort
logger.Debugf("Connecting with Elasticsearch host from current profile (profile: %s, host: %q)", profile.ProfileName, elasticsearchHost)
}
elasticsearchAPIKey, found := os.LookupEnv(ElasticsearchAPIKeyEnv)
if !found {
elasticsearchAPIKey = profileConfig.ElasticsearchAPIKey
}
elasticsearchPassword, found := os.LookupEnv(ElasticsearchPasswordEnv)
if !found {
elasticsearchPassword = profileConfig.ElasticsearchPassword
Expand All @@ -73,6 +78,7 @@ func NewElasticsearchClientFromProfile(profile *profile.Profile, customOptions .

options := []elasticsearch.ClientOption{
elasticsearch.OptionWithAddress(elasticsearchHost),
elasticsearch.OptionWithAPIKey(elasticsearchAPIKey),
elasticsearch.OptionWithPassword(elasticsearchPassword),
elasticsearch.OptionWithUsername(elasticsearchUsername),
elasticsearch.OptionWithCertificateAuthority(caCertificate),
Expand All @@ -86,6 +92,7 @@ func NewElasticsearchClientFromProfile(profile *profile.Profile, customOptions .
func NewKibanaClient(customOptions ...kibana.ClientOption) (*kibana.Client, error) {
options := []kibana.ClientOption{
kibana.Address(os.Getenv(KibanaHostEnv)),
kibana.APIKey(os.Getenv(ElasticsearchAPIKeyEnv)),
kibana.Password(os.Getenv(ElasticsearchPasswordEnv)),
kibana.Username(os.Getenv(ElasticsearchUsernameEnv)),
kibana.CertificateAuthority(os.Getenv(CACertificateEnv)),
Expand Down Expand Up @@ -123,6 +130,10 @@ func NewKibanaClientFromProfile(profile *profile.Profile, customOptions ...kiban
kibanaHost = profileConfig.KibanaHostPort
logger.Debugf("Connecting with Kibana host from current profile (profile: %s, host: %q)", profile.ProfileName, kibanaHost)
}
elasticsearchAPIKey, found := os.LookupEnv(ElasticsearchAPIKeyEnv)
if !found {
elasticsearchAPIKey = profileConfig.ElasticsearchAPIKey
}
elasticsearchPassword, found := os.LookupEnv(ElasticsearchPasswordEnv)
if !found {
elasticsearchPassword = profileConfig.ElasticsearchPassword
Expand All @@ -138,6 +149,7 @@ func NewKibanaClientFromProfile(profile *profile.Profile, customOptions ...kiban

options := []kibana.ClientOption{
kibana.Address(kibanaHost),
kibana.APIKey(elasticsearchAPIKey),
kibana.Password(elasticsearchPassword),
kibana.Username(elasticsearchUsername),
kibana.CertificateAuthority(caCertificate),
Expand All @@ -158,5 +170,10 @@ func FindCACertificate(profile *profile.Profile) (string, error) {
caCertPath = profileConfig.CACertificatePath
}

// Avoid returning an empty certificate path, fallback to the default path.
if caCertPath == "" {
caCertPath = profile.Path(CACertificateFile)
}
Comment on lines +173 to +176
Copy link
Contributor

Choose a reason for hiding this comment

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

In case of Serverless projects that are not required to set the CA certificate path, would this cause errors ?

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't, this CA certificate is only additive. Leaving this empty mounts an empty file in the dockerfile.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok 👍

I was wondering if it could affect in case it is used with a Serverless project that was created out of elastic-package. And the user sets the environment variables and it does not set the environment variable related to the CA to run for instance elastic-package install.

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't found regressions in the serverless provider with these changes.


return caCertPath, nil
}
1 change: 1 addition & 0 deletions internal/stack/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
Provider string `json:"provider,omitempty"`
Parameters map[string]string `json:"parameters,omitempty"`

ElasticsearchAPIKey string `json:"elasticsearch_api_key,omitempty"`
ElasticsearchHost string `json:"elasticsearch_host,omitempty"`
ElasticsearchUsername string `json:"elasticsearch_username,omitempty"`
ElasticsearchPassword string `json:"elasticsearch_password,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions internal/stack/initconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

type InitConfig struct {
ElasticsearchAPIKey string
ElasticsearchHostPort string
ElasticsearchUsername string
ElasticsearchPassword string
Expand All @@ -23,6 +24,7 @@ func StackInitConfig(profile *profile.Profile) (*InitConfig, error) {
}

return &InitConfig{
ElasticsearchAPIKey: config.ElasticsearchAPIKey,
ElasticsearchHostPort: config.ElasticsearchHost,
ElasticsearchUsername: config.ElasticsearchUsername,
ElasticsearchPassword: config.ElasticsearchPassword,
Expand Down
71 changes: 41 additions & 30 deletions internal/stack/shellinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (

// Environment variables describing the stack.
var (
ElasticsearchAPIKeyEnv = environment.WithElasticPackagePrefix("ELASTICSEARCH_API_KEY")
ElasticsearchHostEnv = environment.WithElasticPackagePrefix("ELASTICSEARCH_HOST")
ElasticsearchUsernameEnv = environment.WithElasticPackagePrefix("ELASTICSEARCH_USERNAME")
ElasticsearchPasswordEnv = environment.WithElasticPackagePrefix("ELASTICSEARCH_PASSWORD")
ElasticsearchUsernameEnv = environment.WithElasticPackagePrefix("ELASTICSEARCH_USERNAME")
KibanaHostEnv = environment.WithElasticPackagePrefix("KIBANA_HOST")
CACertificateEnv = environment.WithElasticPackagePrefix("CA_CERT")
)
Expand All @@ -38,60 +39,70 @@ func ShellInit(elasticStackProfile *profile.Profile, shellType string) (string,
if err != nil {
return "", nil
}
return shellInitWithConfig(config, shellType)
}

// NOTE: to add new env vars, the template need to be adjusted
t, err := initTemplate(shellType)
func shellInitWithConfig(config *InitConfig, shellType string) (string, error) {
pattern, err := selectPattern(shellType)
if err != nil {
return "", fmt.Errorf("cannot get shell init template: %w", err)
}

return fmt.Sprintf(t,
ElasticsearchHostEnv, config.ElasticsearchHostPort,
ElasticsearchUsernameEnv, config.ElasticsearchUsername,
ElasticsearchPasswordEnv, config.ElasticsearchPassword,
KibanaHostEnv, config.KibanaHostPort,
CACertificateEnv, config.CACertificatePath,
), nil
template := genTemplate(pattern)
return template([]generatorEnvVar{
{ElasticsearchAPIKeyEnv, config.ElasticsearchAPIKey},
{ElasticsearchHostEnv, config.ElasticsearchHostPort},
{ElasticsearchUsernameEnv, config.ElasticsearchUsername},
{ElasticsearchPasswordEnv, config.ElasticsearchPassword},
{KibanaHostEnv, config.KibanaHostPort},
{CACertificateEnv, config.CACertificatePath},
}), nil
}

type generatorEnvVar struct {
name string
value string
}

const (
// shell init code for POSIX compliant shells.
// IEEE POSIX Shell and Tools portion of the IEEE POSIX specification (IEEE Standard 1003.1)
posixTemplate = `export %s=%s
export %s=%s
export %s=%s
export %s=%s
export %s=%s`
posixPattern = `export %s=%s`

// fish shell init code.
// fish shell is similar but not compliant to POSIX.
fishTemplate = `set -x %s %s;
set -x %s %s;
set -x %s %s;
set -x %s %s;
set -x %s %s;`
fishPattern = `set -x %s %s;`

// PowerShell init code.
// Output to be evaluated with `elastic-package stack shellinit | Invoke-Expression
powershellTemplate = `$Env:%s="%s";
$Env:%s="%s";
$Env:%s="%s";
$Env:%s="%s";
$Env:%s="%s";`
powershellPattern = `$Env:%s="%s";`
)

func genTemplate(pattern string) func([]generatorEnvVar) string {
return func(vars []generatorEnvVar) string {
var builder strings.Builder
for i, v := range vars {
fmt.Fprintf(&builder, pattern, v.name, v.value)
if i < len(vars)-1 {
builder.WriteString("\n")
}
}
return builder.String()
}
}

// availableShellTypes list all available values for s in initTemplate
var availableShellTypes = []string{"bash", "dash", "fish", "sh", "zsh", "pwsh", "powershell"}

// InitTemplate returns code templates for shell initialization
func initTemplate(s string) (string, error) {
// SelectPattern returns the patterns to generate list of environment variables for each shell.
func selectPattern(s string) (string, error) {
switch s {
case "bash", "dash", "sh", "zsh":
return posixTemplate, nil
return posixPattern, nil
case "fish":
return fishTemplate, nil
return fishPattern, nil
case "pwsh", "powershell":
return powershellTemplate, nil
return powershellPattern, nil
default:
return "", errors.New("shell type is unknown, should be one of " + strings.Join(availableShellTypes, ", "))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,47 @@ func TestCodeTemplate(t *testing.T) {
args args
want string
}{
{"bash code template", args{"bash"}, posixTemplate},
{"fish code template", args{"fish"}, fishTemplate},
{"sh code template", args{"sh"}, posixTemplate},
{"zsh code template", args{"zsh"}, posixTemplate},
{"bash code template", args{"bash"}, posixPattern},
{"fish code template", args{"fish"}, fishPattern},
{"sh code template", args{"sh"}, posixPattern},
{"zsh code template", args{"zsh"}, posixPattern},
{"pwsh code template", args{"pwsh"}, powershellPattern},
{"powershell code template", args{"powershell"}, powershellPattern},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := initTemplate(tt.args.s); got != tt.want {
if got, _ := selectPattern(tt.args.s); got != tt.want {
t.Errorf("CodeTemplate() = %v, want %v", got, tt.want)
}
})
}
}

func TestShellInit(t *testing.T) {
config := InitConfig{
ElasticsearchHostPort: "https://elastic.example.com:9200",
ElasticsearchUsername: "admin",
ElasticsearchPassword: "secret",
KibanaHostPort: "https://kibana.example.com:5601",
}

expected := strings.TrimSpace(`
export ELASTIC_PACKAGE_ELASTICSEARCH_API_KEY=
export ELASTIC_PACKAGE_ELASTICSEARCH_HOST=https://elastic.example.com:9200
export ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME=admin
export ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD=secret
export ELASTIC_PACKAGE_KIBANA_HOST=https://kibana.example.com:5601
export ELASTIC_PACKAGE_CA_CERT=
`)

result, err := shellInitWithConfig(&config, "bash")
require.NoError(t, err)

assert.Equal(t, expected, result)
}

func TestCodeTemplate_wrongInput(t *testing.T) {
_, err := initTemplate("invalid shell type")
_, err := selectPattern("invalid shell type")
assert.Error(t, err, "shell type is unknown, should be one of "+strings.Join(availableShellTypes, ", "))
}

Expand Down
4 changes: 3 additions & 1 deletion tools/readme/readme.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,15 @@ There are available some environment variables that could be used to change some

- To configure the Elastic stack to be used by `elastic-package`:
- `ELASTIC_PACKAGE_ELASTICSEARCH_HOST`: Host of the elasticsearch (e.g. https://127.0.0.1:9200)
- `ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME`: User name to connect to elasticsearch (e.g. elastic)
- `ELASTIC_PACKAGE_ELASTICSEARCH_API_KEY`: API key to connect to elasticsearch and kibana. When set it takes precedence over username and password.
- `ELASTIC_PACKAGE_ELASTICSEARCH_USERNAME`: User name to connect to elasticsearch and kibana (e.g. elastic)
- `ELASTIC_PACKAGE_ELASTICSEARCH_PASSWORD`: Password of that user.
- `ELASTIC_PACKAGE_ELASTICSEARCH_KIBANA_HOST`: Kibana URL (e.g. https://127.0.0.1:5601)
- `ELASTIC_PACKAGE_ELASTICSEARCH_CA_CERT`: Path to the CA certificate to connect to the Elastic stack services.

- To configure an external metricstore while running benchmarks (more info at [system benchmarking docs](https://github.com/elastic/elastic-package/blob/main/docs/howto/system_benchmarking.md#setting-up-an-external-metricstore) or [rally benchmarking docs](https://github.com/elastic/elastic-package/blob/main/docs/howto/rally_benchmarking.md#setting-up-an-external-metricstore)):
- `ELASTIC_PACKAGE_ESMETRICSTORE_HOST`: Host of the elasticsearch (e.g. https://127.0.0.1:9200)
- `ELASTIC_PACKAGE_ESMETRICSTORE_API_KEY`: API key to connect to elasticsearch and kibana. When set it takes precedence over username and password.
- `ELASTIC_PACKAGE_ESMETRICSTORE_USERNAME`: Username to connect to elasticsearch (e.g. elastic)
- `ELASTIC_PACKAGE_ESMETRICSTORE_PASSWORD`: Password for the user.
- `ELASTIC_PACKAGE_ESMETRICSTORE_CA_CERT`: Path to the CA certificate to connect to the Elastic stack services.
Expand Down