Add initial code

Signed-off-by: Tomas Dabasinskas <tomas@dabasinskas.net>
This commit is contained in:
Tomas Dabasinskas
2023-02-05 19:49:08 +02:00
parent 0c48cb2137
commit 9bb20d2b0d
13 changed files with 775 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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 {
TaskScheduleDefinition,
TaskScheduleDefinitionConfig,
} from '@backstage/backend-tasks';
/**
* Represents the configuration for the Backstage.
*/
export interface Config {
/**
* Configuration for the catalog.
*/
catalog?: {
/**
* Configuration for the providers.
*/
providers?: {
/**
* Puppet entity provider configuration. Uses "default" as default ID for the single config variant.
*/
puppet?: ProviderConfig | Record<string, ProviderConfig>;
};
};
}
/**
* Configuration of {@link PuppetDBEntityProvider}.
*/
interface ProviderConfig {
/**
* (Required) The host of PuppetDB API instance.
*/
host: string;
/**
* (Optional) PQL query to filter PuppetDB nodes.
*/
query?: string;
/**
* (Optional) Task schedule definition for the refresh.
*/
schedule?: TaskScheduleDefinition;
}
@@ -35,8 +35,14 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/types": "workspace:^",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"node-fetch": "^2.6.7",
"uuid": "^8.0.0",
"winston": "^3.2.1"
@@ -0,0 +1,24 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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.
*/
/**
* A Backstage catalog backend module that helps integrate towards PuppetDB
*
* @packageDocumentation
*/
export * from './providers';
export * from './puppet';
@@ -0,0 +1,236 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed 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 {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-backend';
import { Logger } from 'winston';
import {
PuppetDBEntityProviderConfig,
readProviderConfigs,
} from './PuppetDBEntityProviderConfig';
import { Config } from '@backstage/config';
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
import * as uuid from 'uuid';
import { ResourceTransformer, defaultResourceTransformer } from '../puppet';
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
Entity,
} from '@backstage/catalog-model/';
import { merge } from 'lodash';
import { readPuppetNodes } from '../puppet/read';
import { ENDPOINT_NODES } from '../puppet/constants';
/**
* Reads nodes from [PuppetDB](https://www.puppet.com/docs/puppet/6/puppetdb_overview.html)
* based on the provided query and registers them as Resource entities in the catalog.
*
* @public
*/
export class PuppetDBEntityProvider implements EntityProvider {
private readonly config: PuppetDBEntityProviderConfig;
private readonly logger: Logger;
private readonly scheduleFn: () => Promise<void>;
private readonly transformer: ResourceTransformer;
private connection?: EntityProviderConnection;
/**
* Creates instances of {@link PuppetDBEntityProvider} from a configuration.
*
* @param config - The configuration to read provider information from.
* @param deps - The dependencies for {@link PuppetDBEntityProvider}.
*
* @returns A list of {@link PuppetDBEntityProvider} instances.
*/
static fromConfig(
config: Config,
deps: {
logger: Logger;
schedule?: TaskRunner;
scheduler?: PluginTaskScheduler;
transformer?: ResourceTransformer;
},
): PuppetDBEntityProvider[] {
if (!deps.schedule && !deps.scheduler) {
throw new Error('Either schedule or scheduler must be provided.');
}
return readProviderConfigs(config).map(providerConfig => {
if (!deps.schedule && !providerConfig.schedule) {
throw new Error(
`No schedule provided neither via code nor config for puppet-provider:${providerConfig.id}.`,
);
}
const taskRunner =
deps.schedule ??
deps.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
const transformer = deps.transformer ?? defaultResourceTransformer;
return new PuppetDBEntityProvider(
providerConfig,
deps.logger,
taskRunner,
transformer,
);
});
}
/**
* Creates an instance of {@link PuppetDBEntityProvider}.
*
* @param config - Configuration of the provider.
* @param logger - The instance of a {@link Logger}.
* @param taskRunner - The instance of {@link TaskRunner}.
* @param transformer - A {@link ResourceTransformer} function.
*
* @private
*/
private constructor(
config: PuppetDBEntityProviderConfig,
logger: Logger,
taskRunner: TaskRunner,
transformer: ResourceTransformer,
) {
this.config = config;
this.logger = logger.child({
target: this.getProviderName(),
});
this.scheduleFn = this.createScheduleFn(taskRunner);
this.transformer = transformer;
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.scheduleFn();
}
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.getProviderName} */
getProviderName(): string {
return `puppetdb-provider:${this.config.id}`;
}
/**
* Creates a function that can be used to schedule a refresh of the catalog.
*
* @param taskRunner - The instance of {@link TaskRunner}.
*
* @private
*/
private createScheduleFn(taskRunner: TaskRunner): () => Promise<void> {
return async () => {
const taskId = `${this.getProviderName()}:refresh`;
return taskRunner.run({
id: taskId,
fn: async () => {
const logger = this.logger.child({
class: PuppetDBEntityProvider.prototype.constructor.name,
taskId,
taskInstanceId: uuid.v4(),
});
try {
await this.refresh(logger);
} catch (error) {
logger.error(`${this.getProviderName()} refresh failed`, error);
}
},
});
};
}
/**
* Refreshes the catalog by reading nodes from PuppetDB and registering them as Resource Entities.
*
* @param logger - The instance of a {@link Logger}.
*/
async refresh(logger: Logger) {
if (!this.connection) {
throw new Error('Not initialized');
}
const { markReadComplete } = trackProgress(logger);
const entities = await readPuppetNodes(this.config, {
logger,
transformer: this.transformer,
});
const { markCommitComplete } = markReadComplete(entities);
await this.connection.applyMutation({
type: 'full',
entities: [...entities].map(entity => ({
locationKey: this.getProviderName(),
entity: withLocations(this.config.host, entity),
})),
});
markCommitComplete(entities);
}
}
/**
* Ensures the entities have required annotation data.
*
* @param host - The host of the PuppetDB instance.
* @param entity - The entity to add the annotations to.
*
* @returns Entity with @{@link ANNOTATION_LOCATION} and @{@link ANNOTATION_ORIGIN_LOCATION} annotations.
*/
function withLocations(host: string, entity: Entity): Entity {
const location = new URL(host);
location.pathname = `${ENDPOINT_NODES}/${entity.metadata?.name}`;
return merge(
{
metadata: {
annotations: {
[ANNOTATION_LOCATION]: `url:${location.toString()}`,
[ANNOTATION_ORIGIN_LOCATION]: `url:${location.toString()}`,
},
},
},
entity,
) as Entity;
}
/**
* Tracks the progress of the PuppetDB read and commit operations.
*
* @param logger - The instance of a {@link Logger}.
*/
function trackProgress(logger: Logger) {
let timestamp = Date.now();
function markReadComplete(entities: Entity[]) {
const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1);
timestamp = Date.now();
logger.info(
`Read ${entities?.length ?? 0} in ${readDuration} seconds. Committing...`,
);
return { markCommitComplete };
}
function markCommitComplete(entities: Entity[]) {
const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1);
logger.info(
`Committed ${entities?.length ?? 0} in ${commitDuration} seconds.`,
);
}
return { markReadComplete };
}
@@ -0,0 +1,99 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed 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 {
readTaskScheduleDefinitionFromConfig,
TaskScheduleDefinition,
} from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import { DEFAULT_PROVIDER_ID } from './constants';
/**
* Configuration of {@link PuppetDBEntityProvider}.
*
* @public
*/
export type PuppetDBEntityProviderConfig = {
/**
* ID of the provider.
*/
id: string;
/**
* (Required) The host of PuppetDB API instance.
*/
host: string;
/**
* (Optional) PQL query to filter PuppetDB nodes.
*/
query?: string;
/**
* (Optional) Task schedule definition for the refresh.
*/
schedule?: TaskScheduleDefinition;
};
/**
* Reads the configuration of the PuppetDB Entity Providers.
*
* @param config - The application configuration.
*
* @returns PuppetDB Entity Provider configurations list.
*/
export function readProviderConfigs(
config: Config,
): PuppetDBEntityProviderConfig[] {
const providersConfig = config.getOptionalConfig(
'catalog.providers.puppetdb',
);
if (!providersConfig) {
return [];
}
if (providersConfig.has('host')) {
return [readProviderConfig(DEFAULT_PROVIDER_ID, providersConfig)];
}
return providersConfig.keys().map(id => {
return readProviderConfig(id, providersConfig.getConfig(id));
});
}
/**
* Reads the configuration for the PuppetDB Entity Provider.
*
* @param id - ID of the provider.
* @param config - The application configuration.
*
* @returns The PuppetDB Entity Provider configuration.
*/
function readProviderConfig(
id: string,
config: Config,
): PuppetDBEntityProviderConfig {
const host = config.getString('host').replace(/\/+$/, '');
const query = config.getOptionalString('query');
const schedule = config.has('schedule')
? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule'))
: undefined;
return {
id,
host,
query,
schedule,
};
}
@@ -0,0 +1,29 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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.
*/
/**
* Name of the default provider when a using a simple configuration.
*
* @public
*/
export const DEFAULT_PROVIDER_ID = 'default';
/**
* Default owner for entities created by the PuppetDB provider.
*
* @public
*/
export const DEFAULT_OWNER = 'unknown';
@@ -0,0 +1,19 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed 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 { PuppetDBEntityProvider } from './PuppetDBEntityProvider';
export type { PuppetDBEntityProviderConfig } from './PuppetDBEntityProviderConfig';
export { DEFAULT_PROVIDER_ID, DEFAULT_OWNER } from './constants';
@@ -0,0 +1,32 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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.
*/
/**
* Annotation for specifying the certificate name of a node in PuppetDB.
*
* @public
*/
export const ANNOTATION_PUPPET_CERTNAME = 'puppet.com/certname';
/**
* Path of PuppetDB FactSets endpoint.
*/
export const ENDPOINT_FACTSETS = '/pdb/query/v4/factsets';
/**
* Path of PuppetDB Nodes endpoint.
*/
export const ENDPOINT_NODES = '/pdb/query/v4/nodes';
@@ -0,0 +1,24 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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 { ANNOTATION_PUPPET_CERTNAME } from './constants';
export { defaultResourceTransformer } from './transformers';
export type {
PuppetNode,
PuppetFactSet,
PuppetFact,
ResourceTransformer,
} from './types';
@@ -0,0 +1,75 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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 { PuppetDBEntityProviderConfig } from '../providers';
import { PuppetNode, ResourceTransformer } from './types';
import { ResourceEntity } from '@backstage/catalog-model/';
import { defaultResourceTransformer } from './transformers';
import { ResponseError } from '@backstage/errors';
import { ENDPOINT_FACTSETS } from './constants';
import { Logger } from 'winston';
/**
* Reads nodes and their facts from PuppetDB.
*
* @param config - The provider configuration.
* @param opts - Additional options.
*/
export async function readPuppetNodes(
config: PuppetDBEntityProviderConfig,
opts?: {
transformer?: ResourceTransformer;
logger?: Logger;
},
): Promise<ResourceEntity[]> {
const transformFn = opts?.transformer ?? defaultResourceTransformer;
const url = new URL(config.host);
url.pathname = ENDPOINT_FACTSETS;
if (config.query) {
url.searchParams.set('query', config.query);
}
if (opts?.logger) {
opts.logger
.child({ url: url.toString() })
.debug('Reading nodes from PuppetDB');
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
const nodes = (await response.json()) as PuppetNode[];
const entities: ResourceEntity[] = [];
for (const node of nodes) {
const entity = await transformFn(node, config);
if (entity) {
entities.push(entity);
}
}
return entities;
}
@@ -0,0 +1,65 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed 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 { ResourceTransformer } from './types';
import { DEFAULT_NAMESPACE, ResourceEntity } from '@backstage/catalog-model';
import { DEFAULT_OWNER } from '../providers';
import { ANNOTATION_PUPPET_CERTNAME } from './constants';
/**
* A default implementation of the {@link ResourceTransformer}.
*
* @param node - The found PuppetDB node entry in its source format. This is the entry that you want to transform.
* @param _config - The configuration for the entity provider.
*
* @returns A `ResourceEntity`.
*
* @public
*/
export const defaultResourceTransformer: ResourceTransformer = async (
node,
_config,
) => {
const certName = node.certname.toLowerCase();
const type = node.facts?.data?.find(e => e.name === 'is_virtual')?.value
? 'virtual-machine'
: 'physical-server';
const kernel = node.facts?.data?.find(e => e.name === 'kernel')?.value;
const entity: ResourceEntity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'Resource',
metadata: {
name: certName,
annotations: {
[ANNOTATION_PUPPET_CERTNAME]: certName,
},
namespace: DEFAULT_NAMESPACE,
description: node.facts?.data
?.find(e => e.name === 'ipaddress')
?.value?.toString(),
tags: kernel ? [kernel.toString().toLowerCase()] : [],
},
spec: {
type: type,
owner: DEFAULT_OWNER,
dependsOn: [],
dependencyOf: [],
},
};
return entity;
};
@@ -0,0 +1,102 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed 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 { ResourceEntity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
import { PuppetDBEntityProviderConfig } from '../providers/PuppetDBEntityProviderConfig';
/**
* Customize the ingested Resource entity.
*
* @param node - The found PuppetDB node entry in its source format. This is the entry that you want to transform.
* @param config - The configuration for the entity provider.
*
* @returns A `ResourceEntity` or `undefined` if you want to ignore the found group for being ingested by the catalog.
*
* @public
*/
export type ResourceTransformer = (
node: PuppetNode,
config: PuppetDBEntityProviderConfig,
) => Promise<ResourceEntity | undefined>;
/**
* A node in PuppetDB.
*
* @public
*/
export type PuppetNode = {
/**
* The most recent time of fact submission from the associated certname.
*/
timestamp: string;
/**
* The certname associated with the factset.
*/
certname: string;
/**
* A hash of the factset's certname, environment, timestamp, facts, and producer_timestamp.
*/
hash: string;
/**
* The most recent time of fact submission for the relevant certname from the Puppet Server.
*/
producer_timestamp: string;
/**
* The certname of the Puppet Server that sent the factset to PuppetDB.
*/
producer: string;
/**
* The environment associated with the fact.
*/
environment: string;
/**
* The facts associated with the factset.
*/
facts: PuppetFactSet;
};
/**
* The set of all facts for a single certname in PuppetDB.
*
* @public
*/
export type PuppetFactSet = {
/**
* The array of facts.
*/
data: PuppetFact[];
/**
* The URL to retrieve more information about the facts.
*/
href: string;
};
/**
* A fact in PuppetDB.
*
* @public
*/
export type PuppetFact = {
/**
* The name of the fact.
*/
name: string;
/**
* The value of the fact, in JSON format.
*/
value: JsonValue;
};
+6
View File
@@ -5211,10 +5211,16 @@ __metadata:
resolution: "@backstage/plugin-catalog-backend-module-puppetdb-backend@workspace:plugins/catalog-backend-module-puppetdb"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
"@backstage/types": "workspace:^"
"@types/lodash": ^4.14.151
lodash: ^4.17.21
luxon: ^3.0.0
msw: ^0.49.0
node-fetch: ^2.6.7
uuid: ^8.0.0