Skip to content

Commit 74e5ca6

Browse files
jpograndbanck
andcommitted
Plan Apply Log (#1487)
* Plan and Apply Log Download Button This buttons to a run in the Run View that has a plan or an apply to view the raw plan or apply log inside the editor. * Fix button label typo * Mark execution-details as nullable * Improve run list error message * Add run status to hover * Round relative year values * Inline stripAnsi The strip-ansi package is only available as ESM * Don't open project filter without login --------- Co-authored-by: Daniel Banck <[email protected]>
1 parent 82cb48d commit 74e5ca6

File tree

11 files changed

+352
-27
lines changed

11 files changed

+352
-27
lines changed

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,16 @@
486486
"title": "View Run",
487487
"icon": "$(globe)"
488488
},
489+
{
490+
"command": "terraform.cloud.run.plan.downloadLog",
491+
"title": "View Raw Plan Log",
492+
"icon": "$(book)"
493+
},
494+
{
495+
"command": "terraform.cloud.run.apply.downloadLog",
496+
"title": "View Raw Apply Log",
497+
"icon": "$(output)"
498+
},
489499
{
490500
"command": "terraform.cloud.workspaces.filterByProject",
491501
"title": "Filter by Project",
@@ -541,6 +551,14 @@
541551
{
542552
"command": "terraform.cloud.run.viewInBrowser",
543553
"when": "false"
554+
},
555+
{
556+
"command": "terraform.cloud.run.plan.downloadLog",
557+
"when": "false"
558+
},
559+
{
560+
"command": "terraform.cloud.run.apply.downloadLog",
561+
"when": "false"
544562
}
545563
],
546564
"view/title": [
@@ -593,6 +611,16 @@
593611
"command": "terraform.cloud.run.viewInBrowser",
594612
"when": "view == terraform.cloud.runs && viewItem != empty",
595613
"group": "inline"
614+
},
615+
{
616+
"command": "terraform.cloud.run.plan.downloadLog",
617+
"when": "view == terraform.cloud.runs && viewItem =~ /hasPlan/",
618+
"group": "inline"
619+
},
620+
{
621+
"command": "terraform.cloud.run.apply.downloadLog",
622+
"when": "view == terraform.cloud.runs && viewItem =~ /hasApply/",
623+
"group": "inline"
596624
}
597625
]
598626
},
@@ -650,7 +678,7 @@
650678
},
651679
{
652680
"view": "terraform.cloud.workspaces",
653-
"contents": "No organizations found for this token. Please create a new Terraform Cloud organization to get started:\n[Create or select a organization](command:terraform.cloud.organization.picker)",
681+
"contents": "No organizations found for this token. Please create a new Terraform Cloud organization to get started:\n[Create or select an organization](command:terraform.cloud.organization.picker)",
654682
"when": "terraform.cloud.signed-in && !terraform.cloud.organizationsExist && !terraform.cloud.organizationsChosen"
655683
},
656684
{
@@ -710,7 +738,7 @@
710738
"@types/glob": "^8.1.0",
711739
"@types/jest": "^29.5.1",
712740
"@types/mocha": "^10.0.1",
713-
"@types/node": "^16.11.7",
741+
"@types/node": "^16.18.36",
714742
"@types/vscode": "~1.75.1",
715743
"@types/webpack-env": "^1.18.0",
716744
"@types/which": "^3.0.0",

src/features/terraformCloud.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../providers/tfc/organizationPicker';
1717
import { APIQuickPick } from '../providers/tfc/uiHelpers';
1818
import { TerraformCloudWebUrl } from '../terraformCloud';
19+
import { PlanLogContentProvider } from '../providers/tfc/contentProvider';
1920

2021
export class TerraformCloudFeature implements vscode.Disposable {
2122
private statusBar: OrganizationStatusBar;
@@ -31,6 +32,9 @@ export class TerraformCloudFeature implements vscode.Disposable {
3132
this.reporter,
3233
outputChannel,
3334
);
35+
this.context.subscriptions.push(
36+
vscode.workspace.registerTextDocumentContentProvider('vscode-terraform', new PlanLogContentProvider()),
37+
);
3438
this.statusBar = new OrganizationStatusBar(context);
3539

3640
authProvider.onDidChangeSessions(async (event) => {

src/providers/tfc/contentProvider.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import axios from 'axios';
7+
import * as vscode from 'vscode';
8+
9+
import { apiClient } from '../../terraformCloud';
10+
import stripAnsi from './helpers';
11+
12+
export class PlanLogContentProvider implements vscode.TextDocumentContentProvider {
13+
onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
14+
onDidChange = this.onDidChangeEmitter.event;
15+
16+
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
17+
try {
18+
const logUrl = await this.getLogUrl(uri);
19+
if (!logUrl) {
20+
throw new Error('Unable to parse log Url');
21+
}
22+
23+
const result = await axios.get<string>(logUrl, { headers: { Accept: 'text/plain' } });
24+
return this.parseLog(result.data);
25+
} catch (error) {
26+
if (error instanceof Error) {
27+
await vscode.window.showErrorMessage('Failed to load log:', error.message);
28+
} else if (typeof error === 'string') {
29+
await vscode.window.showErrorMessage('Failed to load log:', error);
30+
}
31+
32+
console.error(error);
33+
return '';
34+
}
35+
}
36+
37+
private parseLog(text: string) {
38+
text = stripAnsi(text); // strip ansi escape codes
39+
text = text.replace('', '').replace('', ''); // remove control characters
40+
41+
return text;
42+
}
43+
44+
private async getLogUrl(uri: vscode.Uri) {
45+
const id = uri.path.replace('/', '');
46+
47+
switch (uri.authority) {
48+
case 'plan':
49+
return await this.getPlanLogUrl(id);
50+
case 'apply':
51+
return await this.getApplyLogUrl(id);
52+
}
53+
}
54+
55+
private async getPlanLogUrl(id: string) {
56+
const plan = await apiClient.getPlan({
57+
params: {
58+
plan_id: id,
59+
},
60+
});
61+
62+
return plan.data.attributes['log-read-url'];
63+
}
64+
65+
private async getApplyLogUrl(id: string) {
66+
const apply = await apiClient.getApply({
67+
params: {
68+
apply_id: id,
69+
},
70+
});
71+
72+
return apply.data.attributes['log-read-url'];
73+
}
74+
}

src/providers/tfc/helpers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,19 @@ export function RelativeTimeFormat(d: Date): string {
161161
const months = Math.round(diffSeconds / SECONDS_IN_MONTH);
162162
return rtf.format(-months, 'month');
163163
}
164-
const years = diffSeconds / SECONDS_IN_YEAR;
164+
const years = Math.round(diffSeconds / SECONDS_IN_YEAR);
165165
return rtf.format(-years, 'year');
166166
}
167+
168+
// See https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js#L2
169+
const ansiRegex = new RegExp(
170+
[
171+
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
172+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
173+
].join('|'),
174+
'g',
175+
);
176+
177+
export default function stripAnsi(text: string) {
178+
return text.replace(ansiRegex, '');
179+
}

0 commit comments

Comments
 (0)