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

export class GrafanaBuilder {
private name: string;
private prometheusConfig?: Grafana.PrometheusConfig;
private tags?: Grafana.Args['tags'];

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

public withPrometheus(config: Grafana.PrometheusConfig): this {
this.prometheusConfig = config;

return this;
}

public withTags(tags: Grafana.Args['tags']): this {
this.tags = tags;

return this;
}

public build(opts: pulumi.ComponentResourceOptions = {}): Grafana {
return new Grafana(
this.name,
{
prometheus: this.prometheusConfig,
tags: this.tags,
},
opts,
);
}
}
137 changes: 137 additions & 0 deletions src/components/grafana/grafana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import * as grafana from '@pulumiverse/grafana';

// Fixed AWS account ID owned by Grafana Cloud, used to assume roles in customer accounts.
const GRAFANA_CLOUD_AWS_ACCOUNT_ID = '008923505280';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is exposed in GH, but the docs do not mention there is just one fixed account ID.
Remove the constant and make it a config option.


export namespace Grafana {
export type PrometheusConfig = {
prometheusEndpoint: pulumi.Input<string>;
region: pulumi.Input<string>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should region be optional and fallback to the current AWS region used by the stack?

};

export type Args = {
prometheus?: PrometheusConfig;
tags?: pulumi.Input<{
[key: string]: pulumi.Input<string>;
}>;
};
}

export class Grafana extends pulumi.ComponentResource {
prometheusDataSource?: grafana.oss.DataSource;

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

if (args.prometheus) {
const ampRole = this.createAmpRole(name, args.tags);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set ampRole on the instance, e.g. this.ampRole = ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we would have more data sources than just prometheus so I would recommend:

  • renaming this iam role to more generic name (grafanaIamRole or something)
  • separating creating that iam role and amp role policy to two methods
  • creating iam role outside this condition
  • creating amp role policy inside this condition (grouped with creating promtheus data source)

this.createPrometheusDataSource(name, args.prometheus, ampRole);
}

this.registerOutputs();
}

private getStackSlug(): string {
const grafanaUrl = process.env.GRAFANA_URL;

if (!grafanaUrl) {
throw new Error('GRAFANA_URL environment variable is not set.');
}
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stack config should be the first option, env is the fallback, e.g.

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

const url = grafanaConfig.get('url') ?? process.env.GRAFANA_URL; 

NOTE: This should apply to other props mentioned in PR description since they are provider options.


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

private createAmpRole(name: string, tags: Grafana.Args['tags']) {
const stackSlug = this.getStackSlug();
const grafanaStack = grafana.cloud.getStack({ slug: stackSlug });

const ampRole = new aws.iam.Role(
`${name}-amp-role`,
{
assumeRolePolicy: pulumi.jsonStringify({
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
AWS: `arn:aws:iam::${GRAFANA_CLOUD_AWS_ACCOUNT_ID}:root`,
},
Action: 'sts:AssumeRole',
Condition: {
StringEquals: {
'sts:ExternalId': pulumi.output(grafanaStack).id,
},
},
},
],
}),
tags,
},
{ parent: this },
);

new aws.iam.RolePolicy(
`${name}-amp-policy`,
{
role: ampRole.id,
policy: JSON.stringify({
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: [
'aps:GetSeries',
'aps:GetLabels',
'aps:GetMetricMetadata',
'aps:QueryMetrics',
],
Resource: '*',
},
],
}),
},
{ parent: this },
);

return ampRole;
}

private createPrometheusDataSource(
name: string,
config: Grafana.PrometheusConfig,
ampRole: aws.iam.Role,
) {
const stackSlug = this.getStackSlug();

const plugin = new grafana.cloud.PluginInstallation(
`${name}-prometheus-plugin`,
{
stackSlug,
slug: 'grafana-amazonprometheus-datasource',
version: 'latest',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this configurable and use the latest as a default?
It seems convenient to have the ability to pinpoint a specific version.

},
{ parent: this },
);

this.prometheusDataSource = new grafana.oss.DataSource(
`${name}-prometheus-datasource`,
{
type: 'grafana-amazonprometheus-datasource',
url: config.prometheusEndpoint,
jsonDataEncoded: pulumi.jsonStringify({
sigV4Auth: true,
sigV4AuthType: 'grafana_assume_role',
sigV4Region: config.region,
sigV4AssumeRoleArn: ampRole.arn,
}),
},
{ dependsOn: [plugin], parent: this },
);
}
}
2 changes: 2 additions & 0 deletions src/components/grafana/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * as dashboard from './dashboards';
export { Grafana } from './grafana';
export { GrafanaBuilder } from './builder';