Skip to content
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
41 changes: 41 additions & 0 deletions src/components/grafana/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as pulumi from '@pulumi/pulumi';
import { AMPConnection, GrafanaConnection } from './connections';
import { Grafana } from './grafana';

export class GrafanaBuilder {
private readonly name: string;
private readonly connectionBuilders: GrafanaConnection.ConnectionBuilder[] =
[];

constructor(name: string) {
this.name = name;
}

public addAmp(name: string, args: AMPConnection.Args): this {
this.connectionBuilders.push(opts => new AMPConnection(name, args, opts));

return this;
}

public addConnection(builder: GrafanaConnection.ConnectionBuilder): this {
this.connectionBuilders.push(builder);

return this;
}

public build(opts: pulumi.ComponentResourceOptions = {}): Grafana {
if (!this.connectionBuilders.length) {
throw new Error(
'At least one connection is required. Call addConnection() to add custom connection or use one of existing connection builders.',
);
}

return new Grafana(
this.name,
{
connectionBuilders: this.connectionBuilders,
},
opts,
);
}
}
112 changes: 112 additions & 0 deletions src/components/grafana/connections/amp-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import * as grafana from '@pulumiverse/grafana';
import { mergeWithDefaults } from '../../../shared/merge-with-defaults';
import { GrafanaConnection } from './connection';

const awsConfig = new pulumi.Config('aws');
const pluginName = 'grafana-amazonprometheus-datasource';

export namespace AMPConnection {
export type Args = GrafanaConnection.Args & {
endpoint: pulumi.Input<string>;
region?: string;
pluginVersion?: string;
};
}

const defaults = {
pluginVersion: 'latest',
region: awsConfig.require('region'),
};

export class AMPConnection extends GrafanaConnection {
public readonly name: string;
public readonly dataSource: grafana.oss.DataSource;
public readonly plugin: grafana.cloud.PluginInstallation;
public readonly rolePolicy: aws.iam.RolePolicy;

constructor(
name: string,
args: AMPConnection.Args,
opts: pulumi.ComponentResourceOptions = {},
) {
super('studion:grafana:AMPConnection', name, args, opts);

const argsWithDefaults = mergeWithDefaults(defaults, args);

this.name = name;

this.rolePolicy = this.createRolePolicy();
this.plugin = this.createPlugin(argsWithDefaults.pluginVersion);
this.dataSource = this.createDataSource(
argsWithDefaults.region,
argsWithDefaults.endpoint,
);

this.registerOutputs();
}

private createRolePolicy(): aws.iam.RolePolicy {
const policy = aws.iam.getPolicyDocumentOutput({
statements: [
{
effect: 'Allow',
actions: [
'aps:GetSeries',
'aps:GetLabels',
'aps:GetMetricMetadata',
'aps:QueryMetrics',
],
resources: ['*'],
},
],
});

return new aws.iam.RolePolicy(
`${this.name}-amp-policy`,
{
role: this.role.id,
policy: policy.json,
},
{ parent: this },
);
}

private createPlugin(
pluginVersion: string,
): grafana.cloud.PluginInstallation {
return new grafana.cloud.PluginInstallation(
`${this.name}-amp-plugin`,
{
stackSlug: this.getStackSlug(),
slug: pluginName,
version: pluginVersion,
},
{ parent: this },
);
}

private createDataSource(
region: string,
endpoint: AMPConnection.Args['endpoint'],
): grafana.oss.DataSource {
const dataSourceName = `${this.name}-amp-datasource`;

return new grafana.oss.DataSource(
dataSourceName,
{
name: dataSourceName,
type: pluginName,
url: endpoint,
jsonDataEncoded: pulumi.jsonStringify({
sigV4Auth: true,
sigV4AuthType: 'grafana_assume_role',
sigV4Region: region,
sigV4AssumeRoleArn: this.role.arn,
}),
},
{ dependsOn: [this.plugin], parent: this },
);
}
}
85 changes: 85 additions & 0 deletions src/components/grafana/connections/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import * as grafana from '@pulumiverse/grafana';
import { commonTags } from '../../../shared/common-tags';

const grafanaConfig = new pulumi.Config('grafana');

export namespace GrafanaConnection {
export type Args = {
awsAccountId: string;
};

export type ConnectionBuilder = (
opts: pulumi.ComponentResourceOptions,
) => GrafanaConnection;
}

export abstract class GrafanaConnection extends pulumi.ComponentResource {
public readonly name: string;
public readonly role: aws.iam.Role;
public abstract readonly dataSource: grafana.oss.DataSource;

constructor(
type: string,
name: string,
args: GrafanaConnection.Args,
opts: pulumi.ComponentResourceOptions = {},
) {
super(type, name, {}, opts);

this.name = name;

this.role = this.createIamRole(args.awsAccountId);

this.registerOutputs();
}

protected getStackSlug(): string {
const grafanaUrl = grafanaConfig.get('url') ?? process.env.GRAFANA_URL;

if (!grafanaUrl) {
throw new Error(
'Grafana URL is not configured. Set it via Pulumi config (grafana:url) or GRAFANA_URL env var.',
);
}

return new URL(grafanaUrl).hostname.split('.')[0];
}

private createIamRole(awsAccountId: string): aws.iam.Role {
const stackSlug = this.getStackSlug();
const grafanaStack = grafana.cloud.getStack({ slug: stackSlug });

const assumeRolePolicy = aws.iam.getPolicyDocumentOutput({
statements: [
{
effect: 'Allow',
principals: [
{
type: 'AWS',
identifiers: [`arn:aws:iam::${awsAccountId}:root`],
},
],
actions: ['sts:AssumeRole'],
conditions: [
{
test: 'StringEquals',
variable: 'sts:ExternalId',
values: [pulumi.output(grafanaStack).id],
},
],
},
],
});

return new aws.iam.Role(
`${this.name}-grafana-iam-role`,
{
assumeRolePolicy: assumeRolePolicy.json,
tags: commonTags,
},
{ parent: this },
);
}
}
2 changes: 2 additions & 0 deletions src/components/grafana/connections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { GrafanaConnection } from './connection';
export { AMPConnection } from './amp-connection';
29 changes: 29 additions & 0 deletions src/components/grafana/grafana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as pulumi from '@pulumi/pulumi';
import { GrafanaConnection } from './connections';

export namespace Grafana {
export type Args = {
connectionBuilders: GrafanaConnection.ConnectionBuilder[];
};
}

export class Grafana extends pulumi.ComponentResource {
public readonly name: string;
public readonly connections: GrafanaConnection[];

constructor(
name: string,
args: Grafana.Args,
opts: pulumi.ComponentResourceOptions = {},
) {
super('studion:grafana:Grafana', name, {}, opts);

this.name = name;

this.connections = args.connectionBuilders.map(build =>
build({ parent: this }),
);

this.registerOutputs();
}
}
3 changes: 3 additions & 0 deletions src/components/grafana/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * as dashboard from './dashboards';
export { GrafanaConnection, AMPConnection } from './connections';
export { Grafana } from './grafana';
export { GrafanaBuilder } from './builder';