Skip to content

Commit

Permalink
Initial implementation of server licensing service
Browse files Browse the repository at this point in the history
  • Loading branch information
eliperelman committed Jul 12, 2019
1 parent 7f94130 commit f78bdb4
Show file tree
Hide file tree
Showing 10 changed files with 565 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/core/server/licensing/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export const SERVICE_NAME = 'licensing';
export const DEFAULT_POLLING_FREQUENCY = 30001; // 30 seconds
export enum LICENSE_STATUS {
Unavailable = 'UNAVAILABLE',
Invalid = 'INVALID',
Expired = 'EXPIRED',
Valid = 'VALID',
}
export enum LICENSE_TYPE {
basic = 10,
standard = 20,
gold = 30,
platinum = 40,
trial = 50,
}
21 changes: 21 additions & 0 deletions src/core/server/licensing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export * from './types';
export { LicensingService } from './licensing_service';
51 changes: 51 additions & 0 deletions src/core/server/licensing/license_feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { LicensingServiceSetup } from './licensing_service_setup';
import { LicenseFeatureSerializer } from './types';

export class LicenseFeature {
private serializable: LicenseFeatureSerializer = service => ({
name: this.name,
isAvailable: this.isAvailable,
isEnabled: this.isEnabled,
});

constructor(
public name: string,
private feature: any = {},
private service: LicensingServiceSetup
) {}

get isAvailable() {
return !!this.feature.available;
}

get isEnabled() {
return !!this.feature.enabled;
}

onObject(serializable: LicenseFeatureSerializer) {
this.serializable = serializable;
}

toObject() {
return this.serializable(this.service);
}
}
36 changes: 36 additions & 0 deletions src/core/server/licensing/licensing_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Env } from '../config';
import { LicensingConfigType } from './types';

export class LicensingConfig {
public enabled: boolean;
public clusterSource: string;
public pollingFrequency: number;

/**
* @internal
*/
constructor(rawConfig: LicensingConfigType, env: Env) {
this.enabled = rawConfig.enabled;
this.clusterSource = rawConfig.clusterSource;
this.pollingFrequency = rawConfig.pollingFrequency;
}
}
160 changes: 160 additions & 0 deletions src/core/server/licensing/licensing_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { BehaviorSubject, Observable, Subscription, from, interval } from 'rxjs';
import { first, map, switchMap, tap } from 'rxjs/operators';
import moment from 'moment';
import { CoreService } from '../../types';
import { Logger } from '../logging';
import { CoreContext } from '../core_context';
import { LicensingServiceSubject, LicensingConfigType, LicensingSetupDependencies } from './types';
import { SERVICE_NAME } from './constants';
import { LicensingConfig } from './licensing_config';
import { LicensingServiceSetup } from './licensing_service_setup';

/** @internal */
export class LicensingService
implements CoreService<LicensingServiceSubject, LicensingServiceSubject> {
private readonly config$: Observable<LicensingConfig>;
private readonly logger: Logger;
private poller$!: Observable<number>;
private pollerSubscription!: Subscription;
private service$!: LicensingServiceSubject;
private license: any;

constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get(SERVICE_NAME);
this.config$ = coreContext.configService
.atPath<LicensingConfigType>(SERVICE_NAME)
.pipe(map(rawConfig => new LicensingConfig(rawConfig, coreContext.env)));
}

private hasLicenseInfoChanged(newLicense: any) {
return (
newLicense.mode !== this.license.mode ||
newLicense.status !== this.license.status ||
newLicense.expiry_date_in_millis !== this.license.expiry_date_in_millis
);
}

private async fetchInfo(
{ http }: LicensingSetupDependencies,
clusterSource: string,
pollingFrequency: number
) {
this.logger.debug(
`Calling [${clusterSource}] Elasticsearch _xpack API. Polling frequency: ${pollingFrequency}`
);

const cluster = http.server.plugins.elasticsearch.getCluster(clusterSource);

try {
const response = await cluster.callWithInternalUser('transport.request', {
method: 'GET',
path: '/_xpack',
});
const newLicense = (response && response.license) || {};
const features = (response && response.features) || {};
const licenseInfoChanged = this.hasLicenseInfoChanged(newLicense);

if (licenseInfoChanged) {
const licenseInfo = [
`mode: ${newLicense.mode}`,
`status: ${newLicense.status}`,
'expiry_date_in_millis' in newLicense &&
`expiry date: ${moment(newLicense.expiry_date_in_millis, 'x').format()}`,
]
.filter(Boolean)
.join(' | ');

this.logger.info(
`Imported ${this.license ? 'changed ' : ''}license information` +
` from Elasticsearch for the [${clusterSource}] cluster: ${licenseInfo}`
);

return { license: false, error: null, features };
}

return { license: newLicense, error: null, features };
} catch (err) {
this.logger.warn(
`License information could not be obtained from Elasticsearch` +
` for the [${clusterSource}] cluster. ${err}`
);

return { license: null, error: err, features: {} };
}
}

private create(
{ clusterSource, pollingFrequency }: LicensingConfig,
deps: LicensingSetupDependencies
) {
if (this.service$) {
return this.service$;
}

const service$ = new BehaviorSubject<LicensingServiceSetup | null>(null);

this.poller$ = interval(pollingFrequency);
this.poller$.pipe(
switchMap(_ =>
from(this.fetchInfo(deps, clusterSource, pollingFrequency)).pipe(
tap(({ license, error, features }) => {
// If license is false, the license did not change and we don't need to push
// a new one
if (license !== false) {
service$.next(new LicensingServiceSetup(license, features, error, clusterSource));
}
})
)
)
);

this.pollerSubscription = this.poller$.subscribe();

deps.http.server.events.on('stop', () => {
this.stop();
});

return service$;
}

public async setup(deps: LicensingSetupDependencies) {
const config = await this.config$.pipe(first()).toPromise();
const service$ = this.create(config, deps);

this.service$ = service$;

return this.service$;
}

public async start(deps: LicensingSetupDependencies) {
const config = await this.config$.pipe(first()).toPromise();
const service$ = this.create(config, deps);

return service$;
}

public async stop() {
if (this.pollerSubscription) {
this.pollerSubscription.unsubscribe();
}
}
}
Loading

0 comments on commit f78bdb4

Please sign in to comment.