Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkpoint telemetry integration #326

Merged
merged 7 commits into from
Aug 21, 2020
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
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"