diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 76caf146961..bedd72c2d4c 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -27,6 +27,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - Remove version information from default ILM policy for improved upgrade experience on custom policies. {pull}14745[14745] - Running `setup` cmd respects `setup.ilm.overwrite` setting for improved support of custom policies. {pull}14741[14741] - Libbeat: Do not overwrite agent.*, ecs.version, and host.name. {pull}14407[14407] +- Libbeat: Cleanup the x-pack licenser code to use the new license endpoint and the new format. {pull}15091[15091] *Auditbeat* diff --git a/x-pack/libbeat/licenser/callback_watcher.go b/x-pack/libbeat/licenser/callback_watcher.go deleted file mode 100644 index 054ad6b22aa..00000000000 --- a/x-pack/libbeat/licenser/callback_watcher.go +++ /dev/null @@ -1,29 +0,0 @@ -// 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() -} diff --git a/x-pack/libbeat/licenser/callback_watcher_test.go b/x-pack/libbeat/licenser/callback_watcher_test.go deleted file mode 100644 index 4307f7abd9d..00000000000 --- a/x-pack/libbeat/licenser/callback_watcher_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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) - }) -} diff --git a/x-pack/libbeat/licenser/check_test.go b/x-pack/libbeat/licenser/check_test.go index 89ab84cad68..d5ad6d6306e 100644 --- a/x-pack/libbeat/licenser/check_test.go +++ b/x-pack/libbeat/licenser/check_test.go @@ -24,7 +24,7 @@ func testCheckTrial(t *testing.T) { t.Run("valid trial license", func(t *testing.T) { l := License{ - Mode: Trial, + Type: Trial, TrialExpiry: expiryTime(time.Now().Add(1 * time.Hour)), } assert.True(t, CheckTrial(log, l)) @@ -32,14 +32,14 @@ func testCheckTrial(t *testing.T) { t.Run("expired trial license", func(t *testing.T) { l := License{ - Mode: Trial, + Type: Trial, TrialExpiry: expiryTime(time.Now().Add(-1 * time.Hour)), } assert.False(t, CheckTrial(log, l)) }) t.Run("other license", func(t *testing.T) { - l := License{Mode: Basic} + l := License{Type: Basic} assert.False(t, CheckTrial(log, l)) }) } @@ -51,19 +51,19 @@ func testCheckLicenseCover(t *testing.T) { fn := CheckLicenseCover(license) t.Run("active", func(t *testing.T) { - l := License{Mode: license, Status: Active} + l := License{Type: license, Status: Active} assert.True(t, fn(log, l)) }) t.Run("inactive", func(t *testing.T) { - l := License{Mode: license, Status: Inactive} + l := License{Type: license, Status: Inactive} assert.False(t, fn(log, l)) }) } } func testValidate(t *testing.T) { - l := License{Mode: Basic, Status: Active} + l := License{Type: Basic, Status: Active} t.Run("when one of the check is valid", func(t *testing.T) { valid := Validate(logp.NewLogger(""), l, CheckLicenseCover(Platinum), CheckLicenseCover(Basic)) assert.True(t, valid) diff --git a/x-pack/libbeat/licenser/elastic_fetcher.go b/x-pack/libbeat/licenser/elastic_fetcher.go index 38d978fdee6..e22613546ae 100644 --- a/x-pack/libbeat/licenser/elastic_fetcher.go +++ b/x-pack/libbeat/licenser/elastic_fetcher.go @@ -14,12 +14,11 @@ import ( "github.com/pkg/errors" - "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/logp" "github.com/elastic/beats/libbeat/outputs/elasticsearch" ) -const xPackURL = "/_xpack" +const xPackURL = "/_license" // params defaults query parameters to send to the '_xpack' endpoint by default we only need // machine parseable data. @@ -129,8 +128,7 @@ func (f *ElasticFetcher) Fetch() (*License, error) { // Xpack Response, temporary struct to merge the features into the license struct. type xpackResponse struct { - License License `json:"license"` - Features features `json:"features"` + License License `json:"license"` } func (f *ElasticFetcher) parseJSON(b []byte) (*License, error) { @@ -141,7 +139,6 @@ func (f *ElasticFetcher) parseJSON(b []byte) (*License, error) { } license := info.License - license.Features = info.Features return &license, nil } @@ -193,20 +190,3 @@ func newESClientMux(clients []elasticsearch.Client) *esClientMux { return &esClientMux{idx: idx, clients: tmp} } - -// Create takes a raw configuration and will create a a license manager based on the elasticsearch -// output configuration, if no output is found we return an error. -func Create(cfg *common.ConfigNamespace, refreshDelay, graceDelay time.Duration) (*Manager, error) { - if !cfg.IsSet() || cfg.Name() != "elasticsearch" { - return nil, ErrNoElasticsearchConfig - } - - clients, err := elasticsearch.NewElasticsearchClients(cfg.Config()) - if err != nil { - return nil, err - } - clientsMux := newESClientMux(clients) - - manager := New(clientsMux, refreshDelay, graceDelay) - return manager, nil -} diff --git a/x-pack/libbeat/licenser/elastic_fetcher_integration_test.go b/x-pack/libbeat/licenser/elastic_fetcher_integration_test.go index f2ce9d233f1..99c722efc31 100644 --- a/x-pack/libbeat/licenser/elastic_fetcher_integration_test.go +++ b/x-pack/libbeat/licenser/elastic_fetcher_integration_test.go @@ -52,12 +52,4 @@ func TestElasticsearch(t *testing.T) { assert.Equal(t, Active, license.Status) assert.NotEmpty(t, license.UUID) - - assert.NotNil(t, license.Features.Graph) - assert.NotNil(t, license.Features.Logstash) - assert.NotNil(t, license.Features.ML) - assert.NotNil(t, license.Features.Monitoring) - assert.NotNil(t, license.Features.Rollup) - assert.NotNil(t, license.Features.Security) - assert.NotNil(t, license.Features.Watcher) } diff --git a/x-pack/libbeat/licenser/elastic_fetcher_test.go b/x-pack/libbeat/licenser/elastic_fetcher_test.go index 6d07eae30a4..1d60cb107f4 100644 --- a/x-pack/libbeat/licenser/elastic_fetcher_test.go +++ b/x-pack/libbeat/licenser/elastic_fetcher_test.go @@ -21,7 +21,7 @@ import ( func newServerClientPair(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *elasticsearch.Client) { mux := http.NewServeMux() - mux.Handle("/_xpack/", http.HandlerFunc(handler)) + mux.Handle("/_license/", http.HandlerFunc(handler)) server := httptest.NewServer(mux) @@ -135,29 +135,7 @@ func TestParseJSON(t *testing.T) { assert.True(t, len(license.UUID) > 0) assert.NotNil(t, license.Type) - assert.NotNil(t, license.Mode) assert.NotNil(t, license.Status) - - assert.False(t, license.Features.Graph.Available) - assert.True(t, license.Features.Graph.Enabled) - - assert.False(t, license.Features.Logstash.Available) - assert.True(t, license.Features.Logstash.Enabled) - - assert.False(t, license.Features.ML.Available) - assert.True(t, license.Features.ML.Enabled) - - assert.True(t, license.Features.Monitoring.Available) - assert.True(t, license.Features.Monitoring.Enabled) - - assert.True(t, license.Features.Rollup.Available) - assert.True(t, license.Features.Rollup.Enabled) - - assert.False(t, license.Features.Security.Available) - assert.True(t, license.Features.Security.Enabled) - - assert.False(t, license.Features.Watcher.Available) - assert.True(t, license.Features.Watcher.Enabled) }) return nil diff --git a/x-pack/libbeat/licenser/license.go b/x-pack/libbeat/licenser/license.go index 09a2d19597f..e1c64fb314b 100644 --- a/x-pack/libbeat/licenser/license.go +++ b/x-pack/libbeat/licenser/license.go @@ -13,13 +13,21 @@ import ( // // The x-pack endpoint returns the following JSON response. // -// "license": { -// "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", -// "type": "platinum", -// "mode": "platinum", -// "status": "active" -// }, -// +//{ +// "license" : { +// "status" : "active", +// "uid" : "cbff45e7-c553-41f7-ae4f-9205eabd80xx", +// "type" : "trial", +// "issue_date" : "2018-10-20T22:05:12.332Z", +// "issue_date_in_millis" : 1540073112332, +// "expiry_date" : "2018-11-19T22:05:12.332Z", +// "expiry_date_in_millis" : 1542665112332, +// "max_nodes" : 1000, +// "issued_to" : "test", +// "issuer" : "elasticsearch", +// "start_date_in_millis" : -1 +// } +// } // Definition: // type is the installed license. // mode is the license in operation. (effective license) @@ -27,43 +35,15 @@ import ( type License struct { UUID string `json:"uid"` Type LicenseType `json:"type"` - Mode LicenseType `json:"mode"` Status State `json:"status"` - Features features `json:"features"` TrialExpiry expiryTime `json:"expiry_date_in_millis,omitempty"` } -// Features defines the list of features exposed by the elasticsearch cluster. -type features struct { - Graph graph `json:"graph"` - Logstash logstash `json:"logstash"` - ML ml `json:"ml"` - Monitoring monitoring `json:"monitoring"` - Rollup rollup `json:"rollup"` - Security security `json:"security"` - Watcher watcher `json:"watcher"` -} - type expiryTime time.Time -// Base define the field common for every feature. -type Base struct { - Enabled bool `json:"enabled"` - Available bool `json:"available"` -} - -// Defines all the available features -type graph struct{ *Base } -type logstash struct{ *Base } -type ml struct{ *Base } -type monitoring struct{ *Base } -type rollup struct{ *Base } -type security struct{ *Base } -type watcher struct{ *Base } - -// Get return the current license +// Get returns the license type. func (l *License) Get() LicenseType { - return l.Mode + return l.Type } // Cover returns true if the provided license is included in the range of license. @@ -72,7 +52,7 @@ func (l *License) Get() LicenseType { // gold -> match gold and platinum // platinum -> match platinum only func (l *License) Cover(license LicenseType) bool { - if l.Mode >= license { + if l.Type >= license { return true } return false @@ -80,7 +60,7 @@ func (l *License) Cover(license LicenseType) bool { // Is returns true if the provided license is an exact match. func (l *License) Is(license LicenseType) bool { - return l.Mode == license + return l.Type == license } // IsActive returns true if the current license from the server is active. @@ -90,7 +70,7 @@ func (l *License) IsActive() bool { // IsTrial returns true if the remote cluster is in trial mode. func (l *License) IsTrial() bool { - return l.Mode == Trial + return l.Type == Trial } // IsTrialExpired returns false if the we are not in trial mode and when we are in trial mode @@ -112,6 +92,5 @@ func (l *License) IsTrialExpired() bool { func (l *License) EqualTo(other *License) bool { return l.UUID == other.UUID && l.Type == other.Type && - l.Mode == other.Mode && l.Status == other.Status } diff --git a/x-pack/libbeat/licenser/license_test.go b/x-pack/libbeat/licenser/license_test.go index 174d762851b..d8c8882c2fb 100644 --- a/x-pack/libbeat/licenser/license_test.go +++ b/x-pack/libbeat/licenser/license_test.go @@ -28,7 +28,7 @@ func TestLicenseGet(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - l := License{Mode: test.t} + l := License{Type: test.t} assert.Equal(t, test.t, l.Get()) }) } @@ -69,7 +69,7 @@ func TestLicenseIs(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - l := License{Mode: test.t} + l := License{Type: test.t} assert.Equal(t, test.expected, l.Cover(test.query)) }) } @@ -110,7 +110,7 @@ func TestLicenseIsStrict(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - l := License{Mode: test.t} + l := License{Type: test.t} assert.Equal(t, test.expected, l.Is(test.query)) }) } @@ -149,12 +149,12 @@ func TestIsTrial(t *testing.T) { }{ { name: "is a trial license", - l: License{Mode: Trial}, + l: License{Type: Trial}, expected: true, }, { name: "is not a trial license", - l: License{Mode: Basic}, + l: License{Type: Basic}, expected: false, }, } @@ -174,17 +174,17 @@ func TestIsTrialExpired(t *testing.T) { }{ { name: "trial is expired", - l: License{Mode: Trial, TrialExpiry: expiryTime(time.Now().Add(-2 * time.Hour))}, + l: License{Type: Trial, TrialExpiry: expiryTime(time.Now().Add(-2 * time.Hour))}, expected: true, }, { name: "trial is not expired", - l: License{Mode: Trial, TrialExpiry: expiryTime(time.Now().Add(2 * time.Minute))}, + l: License{Type: Trial, TrialExpiry: expiryTime(time.Now().Add(2 * time.Minute))}, expected: false, }, { name: "license is not on trial", - l: License{Mode: Basic, TrialExpiry: expiryTime(time.Now().Add(2 * time.Minute))}, + l: License{Type: Basic, TrialExpiry: expiryTime(time.Now().Add(2 * time.Minute))}, expected: false, }, } diff --git a/x-pack/libbeat/licenser/manager.go b/x-pack/libbeat/licenser/manager.go deleted file mode 100644 index 29495311c54..00000000000 --- a/x-pack/libbeat/licenser/manager.go +++ /dev/null @@ -1,354 +0,0 @@ -// 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 ( - "context" - "errors" - "fmt" - "math/rand" - "sync" - "time" - - "github.com/gofrs/uuid" - - "github.com/elastic/beats/libbeat/common/backoff" - "github.com/elastic/beats/libbeat/logp" -) - -func mustUUIDV4() uuid.UUID { - uuid, err := uuid.NewV4() - if err != nil { - panic(err) - } - return uuid -} - -// OSSLicense default license to use. -var ( - OSSLicense = &License{ - UUID: mustUUIDV4().String(), - Type: OSS, - Mode: OSS, - Status: Active, - Features: features{ - Graph: graph{}, - Logstash: logstash{}, - ML: ml{}, - Monitoring: monitoring{}, - Rollup: rollup{}, - Security: security{}, - Watcher: watcher{}, - }, - } -) - -// Watcher allows a type to receive a new event when a new license is received. -type Watcher interface { - OnNewLicense(license License) - OnManagerStopped() -} - -// Fetcher interface implements the mechanism to retrieve a License. Currently we only -// support license coming from the '/_xpack' rest api. -type Fetcher interface { - Fetch() (*License, error) -} - -// Errors returned by the manager. -var ( - ErrWatcherAlreadyExist = errors.New("watcher already exist") - ErrWatcherDoesntExist = errors.New("watcher doesn't exist") - - ErrManagerStopped = errors.New("license manager is stopped") - ErrNoLicenseFound = errors.New("no license found") - - ErrNoElasticsearchConfig = errors.New("no elasticsearch output configuration found, verify your configuration") -) - -// Backoff values when the remote cluster is not responding. -var ( - maxBackoff = 60 * time.Second - initBackoff = 1 * time.Second - jitterCap = 1000 // 1000 milliseconds -) - -// Manager keeps tracks of license management, it uses a fetcher usually the ElasticFetcher to -// retrieve a licence from a specific cluster. -// -// Starting the manager will start a go routine to periodically query the license fetcher. -// if an error occur on the fetcher we will retry until we successfully -// receive a new license. During that period we start a grace counter, we assume the license is -// still valid during the grace period, when this period expire we will keep retrying but the previous -// license will be invalidated and we will fallback to the OSS license. -// -// Retrieving the current license: -// - Call the `Get()` on the manager instance. -// - Or register a `Watcher` with the manager to receive the new license and acts on it, you will -// also receive an event when the Manager is stopped. -// -// -// Notes: -// - When the manager is started no license is set by default. -// - When a license is invalidated, we fallback to the OSS License and the watchers get notified. -// - Adding a watcher will automatically send the current license to the newly added watcher if -// available. -type Manager struct { - done chan struct{} - sync.RWMutex - wg sync.WaitGroup - fetcher Fetcher - duration time.Duration - gracePeriod time.Duration - license *License - watchers map[Watcher]Watcher - log *logp.Logger -} - -// New takes an elasticsearch client and wraps it into a fetcher, the fetch will handle the JSON -// and response code from the cluster. -func New(client esclient, duration time.Duration, gracePeriod time.Duration) *Manager { - fetcher := NewElasticFetcher(client) - return NewWithFetcher(fetcher, duration, gracePeriod) -} - -// NewWithFetcher takes a fetcher and return a license manager. -func NewWithFetcher(fetcher Fetcher, duration time.Duration, gracePeriod time.Duration) *Manager { - m := &Manager{ - fetcher: fetcher, - duration: duration, - log: logp.NewLogger("license-manager"), - done: make(chan struct{}), - gracePeriod: gracePeriod, - watchers: make(map[Watcher]Watcher), - } - - return m -} - -// AddWatcher register a new watcher to receive events when the license is retrieved or when the manager -// is closed. -func (m *Manager) AddWatcher(watcher Watcher) error { - m.Lock() - defer m.Unlock() - - if _, ok := m.watchers[watcher]; ok { - return ErrWatcherAlreadyExist - } - - m.watchers[watcher] = watcher - - // when we register a new watchers send the current license unless we did not retrieve it. - if m.license != nil { - watcher.OnNewLicense(*m.license) - } - return nil -} - -// RemoveWatcher removes the watcher if it exist or return an error. -func (m *Manager) RemoveWatcher(watcher Watcher) error { - m.Lock() - defer m.Unlock() - if _, ok := m.watchers[watcher]; ok { - delete(m.watchers, watcher) - return nil - } - return ErrWatcherDoesntExist -} - -// Get return the current active license, it can return an error if the manager is stopped or when -// there is no license in the manager, Instead of querying the Manager it is easier to register a -// watcher to listen to license change. -func (m *Manager) Get() (*License, error) { - m.Lock() - defer m.Unlock() - - select { - case <-m.done: - return nil, ErrManagerStopped - default: - if m.license == nil { - return nil, ErrNoLicenseFound - } - return m.license, nil - } -} - -// Start starts the License manager, the manager will start a go routine to periodically -// retrieve the license from the fetcher. -func (m *Manager) Start() { - // First update should be in sync at startup to ensure a - // consistent state. - m.log.Info("License manager started, retrieving initial license") - m.wg.Add(1) - go m.worker() -} - -// Stop terminates the license manager, the go routine will be stopped and the cached license will -// be removed and no more checks can be done on the manager. -func (m *Manager) Stop() { - select { - case <-m.done: - m.log.Error("License manager already stopped") - default: - } - - defer m.log.Info("License manager stopped") - defer m.notify(func(w Watcher) { - w.OnManagerStopped() - }) - - // stop the periodic check license and wait for it to complete - close(m.done) - m.wg.Wait() - - // invalidate current license - m.Lock() - defer m.Unlock() - m.license = nil -} - -func (m *Manager) notify(op func(Watcher)) { - m.RLock() - defer m.RUnlock() - - if len(m.watchers) == 0 { - m.log.Debugf("No watchers configured") - return - } - - m.log.Debugf("Notifying %d watchers", len(m.watchers)) - for _, w := range m.watchers { - op(w) - } -} - -func (m *Manager) worker() { - defer m.wg.Done() - m.log.Debugf("Starting periodic license check, refresh: %s grace: %s ", m.duration, m.gracePeriod) - defer m.log.Debug("Periodic license check is stopped") - - jitter := rand.Intn(jitterCap) - - // Add some jitter to space requests from a large fleet of beats. - select { - case <-time.After(time.Duration(jitter) * time.Millisecond): - } - - // eager initial check. - m.update() - - // periodically checks license. - for { - select { - case <-m.done: - return - case <-time.After(m.duration): - m.log.Debug("License is too old, updating, grace period: %s", m.gracePeriod) - m.update() - } - } -} - -func (m *Manager) update() { - backoff := backoff.NewEqualJitterBackoff(m.done, initBackoff, maxBackoff) - startedAt := time.Now() - for { - select { - case <-m.done: - return - default: - license, err := m.fetcher.Fetch() - if err != nil { - m.log.Infof("Cannot retrieve license, retrying later, error: %+v", err) - - // check if the license is still in the grace period. - // permit some operations if the license could not be checked - // right away. This is to smooth any networks problems. - if grace := time.Now().Sub(startedAt); grace > m.gracePeriod { - m.log.Info("Grace period expired, invalidating license") - m.invalidate() - } else { - m.log.Debugf("License is too old, grace time remaining: %s", m.gracePeriod-grace) - } - - backoff.Wait() - continue - } - - // we have a valid license, notify watchers and sleep until next check. - m.log.Infow( - "Valid license retrieved", - "license mode", - license.Get(), - "type", - license.Type, - "status", - license.Status, - ) - m.saveAndNotify(license) - return - } - } -} - -func (m *Manager) saveAndNotify(license *License) { - if !m.save(license) { - return - } - - l := *license - m.notify(func(w Watcher) { - w.OnNewLicense(l) - }) -} - -func (m *Manager) save(license *License) bool { - m.Lock() - defer m.Unlock() - - // License didn't change no need to notify watchers. - if m.license != nil && m.license.EqualTo(license) { - return false - } - defer m.log.Debug("License information updated") - - m.license = license - return true -} - -func (m *Manager) invalidate() { - defer m.log.Debug("Invalidate cached license, fallback to OSS") - m.saveAndNotify(OSSLicense) -} - -// WaitForLicense transforms the async manager into a sync check, this is useful if you want -// to block you application until you have received an initial license from the cluster, the manager -// is not affected and will stay asynchronous. -func WaitForLicense(ctx context.Context, log *logp.Logger, manager *Manager, checks ...CheckFunc) (err error) { - log.Info("Waiting on synchronous license check") - received := make(chan struct{}) - callback := CallbackWatcher{New: func(license License) { - log.Debug("Validating license") - if !Validate(log, license, checks...) { - err = errors.New("invalid license") - } - close(received) - log.Infof("License is valid, mode: %s", license.Get()) - }} - - if err := manager.AddWatcher(&callback); err != nil { - return err - } - defer manager.RemoveWatcher(&callback) - - select { - case <-ctx.Done(): - return fmt.Errorf("license check was interrupted") - case <-received: - } - - return err -} diff --git a/x-pack/libbeat/licenser/manager_test.go b/x-pack/libbeat/licenser/manager_test.go deleted file mode 100644 index a18e56bedca..00000000000 --- a/x-pack/libbeat/licenser/manager_test.go +++ /dev/null @@ -1,345 +0,0 @@ -// 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 ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/elastic/beats/libbeat/logp" -) - -type message struct { - license *License - err error -} - -type mockFetcher struct { - sync.Mutex - bus chan message - last *message -} - -func newMockFetcher() *mockFetcher { - return &mockFetcher{bus: make(chan message, 1)} -} - -func (m *mockFetcher) Fetch() (*License, error) { - m.Lock() - defer m.Unlock() - for { - select { - case message := <-m.bus: - m.last = &message - - // assume other calls to receive the same value, - // until we change it. - return message.license, message.err - default: - if m.last != nil { - return m.last.license, m.last.err - } - continue - } - } -} - -func (m *mockFetcher) Insert(license *License, err error) { - m.bus <- message{license: license, err: err} -} - -func (m *mockFetcher) Close() { - close(m.bus) -} - -func TestRetrieveLicense(t *testing.T) { - i := &License{ - UUID: mustUUIDV4().String(), - Type: Basic, - Mode: Basic, - Status: Active, - } - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - t.Run("return an error if the manager is stopped", func(t *testing.T) { - m := NewWithFetcher(mock, time.Duration(2*time.Second), time.Duration(1*time.Second)) - m.Start() - m.Stop() - - _, err := m.Get() - - assert.Error(t, ErrManagerStopped, err) - }) - - t.Run("at startup when no license is retrieved return an error", func(t *testing.T) { - mck := newMockFetcher() - mck.Insert(nil, errors.New("not found")) - defer mck.Close() - - m := NewWithFetcher(mck, time.Duration(2*time.Second), time.Duration(1*time.Second)) - m.Start() - defer m.Stop() - _, err := m.Get() - - assert.Error(t, ErrNoLicenseFound, err) - }) - - t.Run("at startup", func(t *testing.T) { - m := NewWithFetcher(mock, time.Duration(2*time.Second), time.Duration(1*time.Second)) - m.Start() - defer m.Stop() - - // Lets us find the first license. - time.Sleep(1 * time.Second) - _, err := m.Get() - - assert.NoError(t, err) - }) - - t.Run("periodically", func(t *testing.T) { - period := time.Duration(1) - m := NewWithFetcher(mock, period, time.Duration(5*time.Second)) - - m.Start() - defer m.Stop() - - // Lets us find the first license. - time.Sleep(1 * time.Second) - - l, err := m.Get() - if !assert.NoError(t, err) { - return - } - if !assert.True(t, l.Is(Basic)) { - return - } - - i := &License{ - UUID: mustUUIDV4().String(), - Type: Platinum, - Mode: Platinum, - Status: Active, - } - mock.Insert(i, nil) - - select { - case <-time.After(time.Duration(1 * time.Second)): - l, err := m.Get() - if !assert.NoError(t, err) { - return - } - assert.True(t, l.Is(Platinum)) - } - }) -} - -func TestWatcher(t *testing.T) { - i := &License{ - UUID: mustUUIDV4().String(), - Type: Basic, - Mode: Basic, - Status: Active, - } - - t.Run("watcher must be uniquely registered", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(2*time.Second), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - w := CallbackWatcher{New: func(license License) {}} - - err := m.AddWatcher(&w) - if assert.NoError(t, err) { - return - } - defer m.RemoveWatcher(&w) - - err = m.AddWatcher(&w) - assert.Error(t, ErrWatcherAlreadyExist, err) - }) - - t.Run("cannot remove non existing watcher", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(2*time.Second), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - w := CallbackWatcher{New: func(license License) {}} - - err := m.RemoveWatcher(&w) - - assert.Error(t, ErrWatcherDoesntExist, err) - }) - - t.Run("adding a watcher trigger a a new license callback", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(2*time.Second), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - chanLicense := make(chan License) - defer close(chanLicense) - - w := CallbackWatcher{ - New: func(license License) { - chanLicense <- license - }, - } - - m.AddWatcher(&w) - defer m.RemoveWatcher(&w) - - select { - case license := <-chanLicense: - assert.Equal(t, Basic, license.Get()) - } - }) - - t.Run("periodically trigger a new license callback when the license change", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(1), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - chanLicense := make(chan License) - defer close(chanLicense) - - w := CallbackWatcher{ - New: func(license License) { - chanLicense <- license - }, - } - - m.AddWatcher(&w) - defer m.RemoveWatcher(&w) - - c := 0 - for { - select { - case license := <-chanLicense: - if c == 0 { - assert.Equal(t, Basic, license.Get()) - mock.Insert(&License{ - UUID: mustUUIDV4().String(), - Type: Platinum, - Mode: Platinum, - Status: Active, - }, nil) - c++ - continue - } - assert.Equal(t, Platinum, license.Get()) - return - } - } - }) - - t.Run("trigger OnManagerStopped when the manager is stopped", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(1), time.Duration(1*time.Second)) - m.Start() - - var wg sync.WaitGroup - - wg.Add(1) - w := CallbackWatcher{ - Stopped: func() { - wg.Done() - }, - } - - m.AddWatcher(&w) - defer m.RemoveWatcher(&w) - - m.Stop() - - wg.Wait() - }) -} - -func TestWaitForLicense(t *testing.T) { - i := &License{ - UUID: mustUUIDV4().String(), - Type: Basic, - Mode: Basic, - Status: Active, - } - - t.Run("when license is available and valid", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(1), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - err := WaitForLicense(context.Background(), logp.NewLogger(""), m, CheckBasic) - assert.NoError(t, err) - }) - - t.Run("when license is available and not valid", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(1), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - err := WaitForLicense(context.Background(), logp.NewLogger(""), m, CheckLicenseCover(Platinum)) - assert.Error(t, err) - }) - - t.Run("when license is not available we can still interrupt", func(t *testing.T) { - mock := newMockFetcher() - mock.Insert(i, nil) - defer mock.Close() - - m := NewWithFetcher(mock, time.Duration(1), time.Duration(1*time.Second)) - - m.Start() - defer m.Stop() - - ctx, cancel := context.WithCancel(context.Background()) - executed := make(chan struct{}) - go func() { - err := WaitForLicense(ctx, logp.NewLogger(""), m, CheckLicenseCover(Platinum)) - assert.Error(t, err) - close(executed) - }() - cancel() - <-executed - }) -} diff --git a/x-pack/libbeat/licenser/oss_license.go b/x-pack/libbeat/licenser/oss_license.go new file mode 100644 index 00000000000..abae2050c9b --- /dev/null +++ b/x-pack/libbeat/licenser/oss_license.go @@ -0,0 +1,24 @@ +// 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 "github.com/gofrs/uuid" + +func mustUUIDV4() uuid.UUID { + uuid, err := uuid.NewV4() + if err != nil { + panic(err) + } + return uuid +} + +// OSSLicense default license to use. +var ( + OSSLicense = &License{ + UUID: mustUUIDV4().String(), + Type: OSS, + Status: Active, + } +) diff --git a/x-pack/libbeat/licenser/testdata/xpack-6.4.0.json b/x-pack/libbeat/licenser/testdata/xpack-6.4.0.json deleted file mode 100644 index 116247ae632..00000000000 --- a/x-pack/libbeat/licenser/testdata/xpack-6.4.0.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "build": { - "hash": "053779d", - "date": "2018-07-20T05:25:16.206115Z" - }, - "license": { - "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", - "type": "platinum", - "mode": "platinum", - "status": "active", - "expiry_date_in_millis": 1588291199999 - }, - "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 - } - } -} diff --git a/x-pack/libbeat/licenser/testdata/xpack-trial-6.4.0.json b/x-pack/libbeat/licenser/testdata/xpack-trial-6.4.0.json deleted file mode 100644 index 36368420891..00000000000 --- a/x-pack/libbeat/licenser/testdata/xpack-trial-6.4.0.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "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": 1588291199999 - }, - "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 - } - } -} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-basic.json b/x-pack/libbeat/licenser/testdata/xpack-with-basic.json new file mode 100644 index 00000000000..12c7c63dbd8 --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-basic.json @@ -0,0 +1,17 @@ +{ + "build": { + "hash": "053779d", + "date": "2018-07-20T05:25:16.206115Z" + }, + "license": { + "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", + "type": "basic", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" + } +} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-cloud-license.json b/x-pack/libbeat/licenser/testdata/xpack-with-cloud-license.json new file mode 100644 index 00000000000..f53ceecd0ad --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-cloud-license.json @@ -0,0 +1,71 @@ +{ + "license": { + "uid": "743f4bee-e69c-419a-a54e-083611ab5e68", + "type": "enterprise", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1590000000000, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "signature": "AAAA...123", + "cluster_licenses": [ + { + "license": { + "uid": "bf610b02-470e-d453-0b75-d2f7dfead89f", + "type": "enterprise", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1590000000000, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "signature": "AAAA...xyz" + } + }, + { + "license": { + "uid": "304d04fe-c2d2-8774-cd34-7a71a4cc8c4d", + "type": "platinum", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1590000000000, + "max_nodes": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "signature": "AAAA...uvw" + } + } + ] + }, + "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 + } + } +} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-enterprise.json b/x-pack/libbeat/licenser/testdata/xpack-with-enterprise.json index 670d221ab90..4a08071c2ba 100644 --- a/x-pack/libbeat/licenser/testdata/xpack-with-enterprise.json +++ b/x-pack/libbeat/licenser/testdata/xpack-with-enterprise.json @@ -6,38 +6,12 @@ "license": { "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", "type": "enterprise", - "mode": "enterprise", - "status": "active", - "expiry_date_in_millis": 1588291199999 - }, - "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 - } + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" } } diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-gold.json b/x-pack/libbeat/licenser/testdata/xpack-with-gold.json new file mode 100644 index 00000000000..bf2c7498567 --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-gold.json @@ -0,0 +1,17 @@ +{ + "build": { + "hash": "053779d", + "date": "2018-07-20T05:25:16.206115Z" + }, + "license": { + "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", + "type": "gold", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" + } +} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-platinum.json b/x-pack/libbeat/licenser/testdata/xpack-with-platinum.json new file mode 100644 index 00000000000..617010c53ad --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-platinum.json @@ -0,0 +1,17 @@ +{ + "build": { + "hash": "053779d", + "date": "2018-07-20T05:25:16.206115Z" + }, + "license": { + "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", + "type": "platinum", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" + } +} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-relax-license-uuid.json b/x-pack/libbeat/licenser/testdata/xpack-with-relax-license-uuid.json index 2208b470a81..a7a0f7cd0db 100644 --- a/x-pack/libbeat/licenser/testdata/xpack-with-relax-license-uuid.json +++ b/x-pack/libbeat/licenser/testdata/xpack-with-relax-license-uuid.json @@ -9,35 +9,5 @@ "mode": "platinum", "status": "active", "expiry_date_in_millis": 1588291199999 - }, - "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 - } } } diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-standard.json b/x-pack/libbeat/licenser/testdata/xpack-with-standard.json new file mode 100644 index 00000000000..1bd7a10776b --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-standard.json @@ -0,0 +1,17 @@ +{ + "build": { + "hash": "053779d", + "date": "2018-07-20T05:25:16.206115Z" + }, + "license": { + "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", + "type": "standard", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" + } +} diff --git a/x-pack/libbeat/licenser/testdata/xpack-with-trial.json b/x-pack/libbeat/licenser/testdata/xpack-with-trial.json new file mode 100644 index 00000000000..5f6d8a0b7f9 --- /dev/null +++ b/x-pack/libbeat/licenser/testdata/xpack-with-trial.json @@ -0,0 +1,17 @@ +{ + "build": { + "hash": "053779d", + "date": "2018-07-20T05:25:16.206115Z" + }, + "license": { + "uid": "936183d8-f48c-4a3f-959a-a52aa2563279", + "type": "trial", + "issue_date_in_millis": 1576000000000, + "start_date_in_millis": 1576000000000, + "expiry_date_in_millis": 1588291199999, + "max_resource_units": 100, + "issued_to": "Elastic - INTERNAL", + "issuer": "Fabio Busatto", + "type": "enterprise" + } +}