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

Query a remote Elasticsearch for License and features #7946

Merged
merged 2 commits into from
Aug 29, 2018
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
11 changes: 11 additions & 0 deletions libbeat/common/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,14 @@ func RunWith(
}
}
}

// GetEnvOr return the value of the environment variable if the value is set, if its not set it will
// return the default value.
//
// Note: if the value is set but it is an empty string we will return the empty string.
func GetEnvOr(name, def string) string {
if env, ok := os.LookupEnv(name); ok {
return env
}
return def
}
Binary file added x-pack/beatless/beatless
Binary file not shown.
1 change: 1 addition & 0 deletions x-pack/beatless/data/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"uuid":"fd4356a8-16c9-4f5b-9261-e370617be071"}
43 changes: 43 additions & 0 deletions x-pack/beatless/licenser/1
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"build": {
"hash": "595516e",
"date": "2018-08-17T23:22:27.102119Z"
},
"license": {
"uid": "936183d8-f48c-4a3f-959a-a52aa2563279",
"type": "trial",
"mode": "trial",
"status": "active",
"expiry_date_in_millis": 1538060781728
},
"features": {
"graph": {
"available": false,
"enabled": true
},
"logstash": {
"available": false,
"enabled": true
},
"ml": {
"available": false,
"enabled": true
},
"monitoring": {
"available": true,
"enabled": true
},
"rollup": {
"available": true,
"enabled": true
},
"security": {
"available": false,
"enabled": true
},
"watcher": {
"available": false,
"enabled": true
}
}
}
29 changes: 29 additions & 0 deletions x-pack/beatless/licenser/callback_watcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 licenser

// CallbackWatcher defines an addhoc listener for events generated by the manager.
type CallbackWatcher struct {
New func(License)
Stopped func()
}

// OnNewLicense is called when a new license is set in the manager.
func (cb *CallbackWatcher) OnNewLicense(license License) {
if cb.New == nil {
return
}
cb.New(license)
}

// OnManagerStopped is called when the manager is stopped, watcher are expected to terminates any
// features that depends on a specific license.
func (cb *CallbackWatcher) OnManagerStopped() {
if cb.Stopped == nil {
return
}

cb.Stopped()
}
30 changes: 30 additions & 0 deletions x-pack/beatless/licenser/callback_watcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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 licenser

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCallbackWatcher(t *testing.T) {
t.Run("when no callback is set do not execute anything", func(t *testing.T) {
w := &CallbackWatcher{}
w.OnNewLicense(License{})
w.OnManagerStopped()
})

t.Run("proxy call to callback function", func(t *testing.T) {
c := 0
w := &CallbackWatcher{
New: func(license License) { c++ },
Stopped: func() { c++ },
}
w.OnNewLicense(License{})
w.OnManagerStopped()
assert.Equal(t, 2, c)
})
}
43 changes: 43 additions & 0 deletions x-pack/beatless/licenser/data/x-pack-trial-6.4.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"build": {
"hash": "595516e",
"date": "2018-08-17T23:22:27.102119Z"
},
"license": {
"uid": "936183d8-f48c-4a3f-959a-a52aa2563279",
"type": "trial",
"mode": "trial",
"status": "active",
"expiry_date_in_millis": 1538060781728
},
"features": {
"graph": {
"available": false,
"enabled": true
},
"logstash": {
"available": false,
"enabled": true
},
"ml": {
"available": false,
"enabled": true
},
"monitoring": {
"available": true,
"enabled": true
},
"rollup": {
"available": true,
"enabled": true
},
"security": {
"available": false,
"enabled": true
},
"watcher": {
"available": false,
"enabled": true
}
}
}
42 changes: 42 additions & 0 deletions x-pack/beatless/licenser/data/xpack-6.4.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"build": {
"hash": "053779d",
"date": "2018-07-20T05:25:16.206115Z"
},
"license": {
"uid": "936183d8-f48c-4a3f-959a-a52aa2563279",
"type": "platinum",
"mode": "platinum",
"status": "active"
},
"features": {
"graph": {
"available": false,
"enabled": true
},
"logstash": {
"available": false,
"enabled": true
},
"ml": {
"available": false,
"enabled": true
},
"monitoring": {
"available": true,
"enabled": true
},
"rollup": {
"available": true,
"enabled": true
},
"security": {
"available": false,
"enabled": true
},
"watcher": {
"available": false,
"enabled": true
}
}
}
147 changes: 147 additions & 0 deletions x-pack/beatless/licenser/elastic_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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 licenser

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"

"github.com/pkg/errors"

"github.com/elastic/beats/libbeat/logp"
"github.com/elastic/beats/libbeat/outputs/elasticsearch"
)

const xPackURL = "/_xpack"

// params defaults query parameters to send to the '_xpack' endpoint by default we only need
// machine parseable data.
var params = map[string]string{
"human": "false",
}

var stateLookup = map[string]State{
"inactive": Inactive,
"active": Active,
}

var licenseLookup = map[string]LicenseType{
"oss": OSS,
"trial": Trial,
"basic": Basic,
"gold": Gold,
"platinum": Platinum,
}

// UnmarshalJSON takes a bytes array and convert it to the appropriate license type.
func (t *LicenseType) UnmarshalJSON(b []byte) error {
if len(b) <= 2 {
return fmt.Errorf("invalid string for license type, received: '%s'", string(b))
}
s := string(b[1 : len(b)-1])
if license, ok := licenseLookup[s]; ok {
*t = license
return nil
}

return fmt.Errorf("unknown license type, received: '%s'", s)
}

// UnmarshalJSON takes a bytes array and convert it to the appropriate state.
func (st *State) UnmarshalJSON(b []byte) error {
// we are only interested in the content between the quotes.
if len(b) <= 2 {
return fmt.Errorf("invalid string for state, received: '%s'", string(b))
}

s := string(b[1 : len(b)-1])
if state, ok := stateLookup[s]; ok {
*st = state
return nil
}
return fmt.Errorf("unknown state, received: '%s'", s)
}

// UnmarshalJSON takes a bytes array and transform the int64 to a golang time.
func (et *expiryTime) UnmarshalJSON(b []byte) error {
if len(b) < 0 {
return fmt.Errorf("invalid value for expiry time, received: '%s'", string(b))
}

ts, err := strconv.Atoi(string(b))
if err != nil {
return errors.Wrap(err, "could not parse value for expiry time")
}

*et = expiryTime(time.Unix(0, int64(time.Millisecond)*int64(ts)).UTC())
return nil
}

// ElasticFetcher wraps an elasticsearch clients to retrieve licensing information
// on a specific cluster.
type ElasticFetcher struct {
client *elasticsearch.Client
log *logp.Logger
}

// NewElasticFetcher creates a new Elastic Fetcher
func NewElasticFetcher(client *elasticsearch.Client) *ElasticFetcher {
return &ElasticFetcher{client: client, log: logp.NewLogger("elasticfetcher")}
}

// Fetch retrieves the license information from an Elasticsearch Client, it will call the `_xpack`
// end point and will return a parsed license. If the `_xpack` endpoint is unreacheable we will
// return the OSS License otherwise we return an error.
func (f *ElasticFetcher) Fetch() (*License, error) {
status, body, err := f.client.Request("GET", xPackURL, "", params, nil)
// When we are running an OSS release of elasticsearch the _xpack endpoint will return a 405,
// "Method Not Allowed", so we return the default OSS license.
if status == http.StatusMethodNotAllowed {
f.log.Debug("received 'Method Not allowed' (405) response from server, fallback to OSS license")
return OSSLicense, nil
}

if status == http.StatusUnauthorized {
return nil, errors.New("Unauthorized access, could not connect to the xpack endpoint, verify your credentials")
}

if status != http.StatusOK {
return nil, fmt.Errorf("could not retrieve license information, response code: %d", status)
}

if err != nil {
return nil, errors.Wrap(err, "could not retrieve the license information from the cluster")
}

license, err := f.parseJSON(body)
if err != nil {
f.log.Debugw("invalid response from server", "body", string(body))
return nil, errors.Wrap(err, "could not extract license information from the server response")
}

return license, nil
}

// Xpack Response, temporary struct to merge the features into the license struct.
type xpackResponse struct {
License License `json:"license"`
Features features `json:"features"`
}

func (f *ElasticFetcher) parseJSON(b []byte) (*License, error) {
info := &xpackResponse{}

if err := json.Unmarshal(b, info); err != nil {
return nil, err
}

license := info.License
license.Features = info.Features

return &license, nil
}
Loading