Skip to content

Commit

Permalink
Checkpoint telemetry integration (#326)
Browse files Browse the repository at this point in the history
* checkpoint telemetry reporting for cdktf get command

* added date to report options and removed unwanted parameters

* changes and fixes after pr review and documentation for telemetry collection

* checkpoint is disabled for ci

* checkpoint is disabled for release next workflow

* change checkpoint disable environment variable name

* adding integration tests
  • Loading branch information
anubhavmishra authored Aug 21, 2020
1 parent 03694d9 commit ab46998
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ jobs:
terraform: ["0.12.29", "0.13.0"]
container:
image: hashicorp/jsii-terraform
env:
CHECKPOINT_DISABLE: "1"

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -42,6 +44,8 @@ jobs:
container:
image: hashicorp/jsii-terraform
needs: build
env:
CHECKPOINT_DISABLE: "1"

steps:
- uses: actions/checkout@v2
Expand All @@ -62,6 +66,8 @@ jobs:
matrix:
terraform: ["0.12.29", "0.13.0"]
needs: build
env:
CHECKPOINT_DISABLE: "1"

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ jobs:
runs-on: ubuntu-latest
container:
image: hashicorp/jsii-terraform
env:
CHECKPOINT_DISABLE: "1"

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release_next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ jobs:
runs-on: ubuntu-latest
container:
image: hashicorp/jsii-terraform
env:
CHECKPOINT_DISABLE: "1"

steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Choose a language:
* Using the CDK for Terraform [tokens](./docs/working-with-cdk-for-terraform/tokens.md).
* Using Terraform [data sources](./docs/working-with-cdk-for-terraform/data-sources.md).
* Synthesizing Terraform configuration using CDK for Terraform [synthesize](./docs/working-with-cdk-for-terraform/synthesizing-config.md) command.
* Project [telemetry](./docs/working-with-cdk-for-terraform/telemetry.md).

## Contributing and Feedback

Expand Down
11 changes: 11 additions & 0 deletions docs/working-with-cdk-for-terraform/telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Telemetry

CDK for Terraform CLI ([cdktf-cli](../../packages/cdktf-cli)) interacts with a HashiCorp service called [Checkpoint](https://checkpoint.hashicorp.com)
to report project metrics such as cdktf version, project language, provider name, platform name, and other details that help guide the project maintainers with
feature and roadmap decisions. The code that interacts with Checkpoint is part of CDK for Terraform CLI and can be read [here](../../packages/cdktf-cli/bin/lib/checkpoint.ts).

All HashiCorp projects including Terraform that is used by CDK for Terraform use Checkpoint.
Read more about project metrics [here](https://github.com/hashicorp/terraform-cdk/issues/325).

The information that is sent to Checkpoint is anonymous and cannot be used to identify the user or host. The use of Checkpoint is completely optional
and it can be disabled at any time by setting the `CHECKPOINT_DISABLE` environment variable to a non-empty value.
25 changes: 25 additions & 0 deletions packages/cdktf-cli/bin/cmds/helper/constructs-maker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GetProvider } from '../../../lib/get/providers';
import { GetModule } from '../../../lib/get/modules';
import { Language } from '../../../lib/get/base';
import { Report } from './telemetry';

export interface ConstructsOptions {
codeMakerOutput: string;
Expand All @@ -13,13 +14,37 @@ export class ConstructsMaker {
if (modules.length > 0) {
await new GetModule().get(Object.assign({}, { codeMakerOutput: constructsOptions.codeMakerOutput,
targetLanguage: constructsOptions.language, isModule: true }, { targetNames: modules }));
await moduleTelemetry(constructsOptions.language, modules);
}
}

public async getProviders(constructsOptions: ConstructsOptions, providers: string[]): Promise<void> {
if (providers.length > 0) {
await new GetProvider().get(Object.assign({}, { codeMakerOutput: constructsOptions.codeMakerOutput,
targetLanguage: constructsOptions.language }, { targetNames: providers }));
await providerTelemetry(constructsOptions.language, providers);
}
}
}

async function providerTelemetry(language: string, providers: string[]): Promise<void> {
for (const p of providers) {
const [fqname, version] = p.split('@');
const name = fqname.split('/').pop()
if (!name) { throw new Error(`Provider name should be properly set in ${p}`) }

const payload = { name: name, fullName: fqname, version: version, type: 'provider' };

await Report('get', language, new Date(), payload);
}
}

async function moduleTelemetry(language: string, modules: string[]): Promise<void> {
for (const module of modules) {
const [source, version] = module.split('@');

const payload = { source: source, version: version, type: 'module' };

await Report('get', language, new Date(), payload)
}
}
17 changes: 16 additions & 1 deletion packages/cdktf-cli/bin/cmds/helper/synth-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { shell } from '../../../lib/util';
import * as fs from 'fs-extra';
import * as path from 'path'
import { TerraformStackMetadata } from 'cdktf'
import { Report } from './telemetry';
import { performance } from 'perf_hooks';

interface SynthesizedStackMetadata {
"//"?: {[key: string]: TerraformStackMetadata };
Expand All @@ -15,6 +17,9 @@ interface SynthesizedStack {

export class SynthStack {
public static async synth(command: string, outdir: string): Promise<SynthesizedStack[]> {
// start performance timer
const startTime = performance.now();

await shell(command, [], {
shell: true,
env: {
Expand All @@ -28,6 +33,10 @@ export class SynthStack {
process.exit(1);
}

// end performance timer
const endTime = performance.now();
await this.synthTelemetry(command, (endTime - startTime));

const stacks: SynthesizedStack[] = [];

for (const file of await fs.readdir(outdir)) {
Expand Down Expand Up @@ -55,4 +64,10 @@ export class SynthStack {

return stacks
}
}

public static async synthTelemetry(command: string, totalTime: number): Promise<void> {
const payload = { command: command, totalTime: totalTime };

await Report('synth', '', new Date(), payload);
}
}
16 changes: 16 additions & 0 deletions packages/cdktf-cli/bin/cmds/helper/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReportParams, ReportRequest } from '../../../lib/checkpoint'
import { versionNumber } from '../version-check';
import { readConfigSync } from '../../../lib/config';

const product = "cdktf"
const config = readConfigSync()

export async function Report(command: string, language: string, dateTime: Date, payload: {}): Promise<void> {
if (language == '') {
if (config.language) {
language = config.language
}
}
const reportParams: ReportParams = { command: command, product: product, version: versionNumber(), dateTime: dateTime, payload: payload, language: language };
await ReportRequest(reportParams);
}
19 changes: 13 additions & 6 deletions packages/cdktf-cli/bin/cmds/ui/models/terraform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,7 @@ export class Terraform {
}

public async init(): Promise<void> {
// Read the cdktf version from the 'cdk.tf.json' file
// and set the user agent.
const version = await readCDKTFVersion(this.workdir)
if (version != "") {
process.env.TF_APPEND_USER_AGENT = "cdktf " + version + " (+https://github.com/hashicorp/terraform-cdk)";
}
await this.setUserAgent()
await exec(terraformBinaryName, ['init'], { cwd: this.workdir, env: process.env })
}

Expand All @@ -98,17 +93,20 @@ export class Terraform {
if (destroy) {
options.push('-destroy')
}
await this.setUserAgent()
await exec(terraformBinaryName, options, { cwd: this.workdir, env: process.env });
const jsonPlan = await exec(terraformBinaryName, ['show', '-json', planFile], { cwd: this.workdir, env: process.env });
return new TerraformPlan(planFile, JSON.parse(jsonPlan));
}

public async deploy(planFile: string, stdout: (chunk: Buffer) => any): Promise<void> {
const relativePlanFile = path.relative(this.workdir, planFile);
await this.setUserAgent()
await exec(terraformBinaryName, ['apply', '-auto-approve', ...this.stateFileOption, relativePlanFile], { cwd: this.workdir, env: process.env }, stdout);
}

public async destroy(stdout: (chunk: Buffer) => any): Promise<void> {
await this.setUserAgent()
await exec(terraformBinaryName, ['destroy', '-auto-approve', ...this.stateFileOption], { cwd: this.workdir, env: process.env }, stdout);
}

Expand All @@ -127,4 +125,13 @@ export class Terraform {
private get stateFileOption() {
return ['-state', path.join(process.cwd(), 'terraform.tfstate')]
}

public async setUserAgent(): Promise<void> {
// Read the cdktf version from the 'cdk.tf.json' file
// and set the user agent.
const version = await readCDKTFVersion(this.workdir)
if (version != "") {
process.env.TF_APPEND_USER_AGENT = "cdktf/" + version + " (+https://github.com/hashicorp/terraform-cdk)";
}
}
}
88 changes: 88 additions & 0 deletions packages/cdktf-cli/lib/checkpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import https = require('https');
import { format } from 'url';
import { v4 as uuidv4 } from 'uuid';
import * as os from 'os';
import { processLogger } from './logging';

const BASE_URL = `https://checkpoint-api.hashicorp.com/v1/`;

const VALID_STATUS_CODES = [200, 201];

export interface ReportParams {
dateTime?: Date;
arch?: string;
os?: string;
payload: {};
product: string;
runID?: string;
version?: string;
command?: string;
language?: string;
}

async function post(url: string, data: string) {
return new Promise<any>((ok, ko) => {
const req = https.request(format(url), {
headers: {
'Accept': 'application/json',
'Content-Length': data.length,
'User-Agent': 'HashiCorp/cdktf-cli'
},
method: 'POST'
}, res => {
if (res.statusCode) {
const statusCode = res.statusCode;
if (!VALID_STATUS_CODES.includes(statusCode)) {
return ko(new Error(res.statusMessage));
}
}
const data = new Array<Buffer>();
res.on('data', chunk => data.push(chunk));

res.once('error', err => ko(err));
res.once('end', () => {
return ok();
});
});

req.write(data);

req.end();
})
}

export async function ReportRequest(reportParams: ReportParams): Promise<void> {
// we won't report when checkpoint is disabled.
if (process.env.CHECKPOINT_DISABLE) {
return
}

if (!reportParams.runID) {
reportParams.runID = uuidv4();
}

if (!reportParams.dateTime) {
reportParams.dateTime = new Date();
}

if (!reportParams.arch) {
reportParams.arch = os.arch();
}

if (!reportParams.os) {
reportParams.os = os.platform();
}

const postData = JSON.stringify(reportParams);

try {
await post(`${BASE_URL}telemetry/${reportParams.product}`, postData)
} catch (e) {
// Log errors writing to checkpoint
processLogger(e.message)
}

}



4 changes: 3 additions & 1 deletion packages/cdktf-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@types/node": "^14.0.26",
"@types/readline-sync": "^1.4.3",
"@types/uuid": "^8.3.0",
"cdktf": "0.0.0",
"chalk": "^4.1.0",
"codemaker": "^0.22.0",
Expand All @@ -44,6 +45,7 @@
"readline-sync": "^1.4.10",
"semver": "^7.3.2",
"sscaff": "^1.2.0",
"uuid": "^8.3.0",
"yargs": "^15.1.0"
},
"eslintConfig": {
Expand Down Expand Up @@ -85,4 +87,4 @@
"ts-jest": "^25.4.0",
"typescript": "^3.9.7"
}
}
}
8 changes: 8 additions & 0 deletions test/test-checkpoint-service/expected/cdk.tf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"//": {
"metadata": {
"version": "stubbed",
"stackName": "hello-terra"
}
}
}
15 changes: 15 additions & 0 deletions test/test-checkpoint-service/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Construct } from 'constructs';
import { App, TerraformStack, Testing } from 'cdktf';

export class HelloTerra extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);

// define resources here

}
}

const app = Testing.stubVersion(new App({stackTraces: false}));
new HelloTerra(app, 'hello-terra');
app.synth();
31 changes: 31 additions & 0 deletions test/test-checkpoint-service/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/sh
set -e
scriptdir=$(cd $(dirname $0) && pwd)

cd $(mktemp -d)
mkdir test && cd test

# hidden files should be ignored
touch .foo
mkdir .bar

# unset CHECKPOINT_DISABLE
export CHECKPOINT_DISABLE=""

# initialize an empty project
cdktf init --template typescript --project-name="typescript-test" --project-description="typescript test app" --local

# put some code in it
cp ${scriptdir}/main.ts .

# build
yarn compile
yarn synth > /dev/null

# get rid of downloaded Terraform providers, no point in diffing them
rm -rf cdktf.out/.terraform

# show output
diff cdktf.out ${scriptdir}/expected

echo "PASS"

0 comments on commit ab46998

Please sign in to comment.