Skip to content

Commit

Permalink
Add options for API key in clients
Browse files Browse the repository at this point in the history
  • Loading branch information
jsoriano committed Jan 3, 2025
1 parent 2fc5051 commit 98a658e
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 37 deletions.
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)
}

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

0 comments on commit 98a658e

Please sign in to comment.