Skip to content

Commit 1a64d3f

Browse files
authored
feat: export repository as Terraform (#29)
* feat: export repository as Terraform - Add Export as Terraform context menu command on repository nodes - Consolidate upstream fetch into single shared helper in upstreamChecker.js - Updated so all three upstream consumers use shared helper - Upstream auth secrets use Terraform variable placeholders, never plaintext * fix: reconcile inferred-format upstream fetch with full canonical list - Inline indicator now falls back to getAllUpstreamData when inferred formats are incomplete - Canonicalize inferred format order to match shared cache keys - Add regression test for partial inferred-format undercounting * fix: consolidate upstream fetch and fix WebView/Terraform export consumers - Remove duplicate fetch logic from WebView and exporter - Add abort/cancellation support for concurrent exports * fix: merge conflict resolution with upstream hardening - Merge shared upstream helper architecture with cache validation hardening - Rewrite cache robustness tests against current helper API - Fix test harness module instance alignment for repositoryNode and terraformExporter - docs: add 2.1.0 changelog for Terraform export, upstream and license resolution reliability * chore: updated .gitignore
1 parent a418217 commit 1a64d3f

17 files changed

Lines changed: 2085 additions & 278 deletions

.gitignore

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@ node_modules/
33
*.vsix
44
.env
55
.DS_Store
6+
.src/
67
.npmrc
78

8-
# Local planning / agent reference docs
9-
/internal-docs/
10-
AGENTS*.md
11-
CLAUDE*.md
12-
IMPLEMENTATION*.md
13-
API_REFERENCE*.md
14-
ARCHITECTURE*.md
9+
# Local planning
10+
internal_docs/
1511

1612
# Local agent / MCP config
1713
.mcp.json

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
This release transforms the extension from a basic package explorer into a full package intelligence platform with security remediation, dependency health scanning, license compliance, upstream inspection, and cross-repository promotion workflows.
55

6+
#### Terraform Export
7+
- "Export as Terraform" context menu command on repository nodes generates a complete HCL configuration file using the Cloudsmith Terraform provider.
8+
- Exports the repository resource with all non-default settings including permissions, format-specific options, broadcast state, and storage region.
9+
- Exports all configured upstreams as `cloudsmith_repository_upstream` resources across all package formats, with correct `upstream_type` derived from the format endpoint.
10+
- Exports retention rules as `cloudsmith_repository_retention_rule` when configured.
11+
- All resource references use Terraform interpolation (`data.cloudsmith_namespace`, `cloudsmith_repository`) for portable, import-ready configurations.
12+
- Upstream auth secrets are never exported as plaintext. Sensitive values use Terraform variable placeholders with `sensitive = true`.
13+
- Generated HCL opens in a new editor tab for review before saving.
14+
615
#### Package Search
716
- Full-text package search across workspaces and repositories with Cloudsmith query syntax support.
817
- Guided multi-step search with filter presets for quarantined packages, policy violations, vulnerability violations, and license violations.

extension.js

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@ const { UpstreamDetailProvider } = require("./views/upstreamDetailProvider");
1818
const { PromotionProvider } = require("./views/promotionProvider");
1919
const { SearchQueryBuilder } = require("./util/searchQueryBuilder");
2020
const { formatApiError } = require("./util/errorFormatter");
21+
<<<<<<< HEAD
2122
const { LicenseClassifier } = require("./util/licenseClassifier");
23+
=======
24+
const { fetchRepositoryUpstreams, generateTerraformConfig } = require("./util/terraformExporter");
25+
<<<<<<< HEAD
26+
>>>>>>> 52ddc2b (feat: export repository as Terraform)
27+
=======
28+
const { SUPPORTED_UPSTREAM_FORMATS } = require("./util/upstreamFormats");
29+
>>>>>>> 50c8bac (fix: consolidate upstream fetch and fix WebView/Terraform export consumers)
2230
const recentPackages = require("./util/recentPackages");
2331

32+
let exportTerraformAbortController = null;
33+
2434
/**
2535
* Helper: unwrap a property that may be stored as:
2636
* - a raw string: "value"
@@ -228,11 +238,7 @@ const FILTER_PRESETS = [
228238
},
229239
];
230240

231-
const FORMAT_OPTIONS = [
232-
"alpine", "cargo", "composer", "conan", "conda", "cran", "dart", "deb",
233-
"docker", "go", "helm", "hex", "maven", "npm", "nuget", "python",
234-
"rpm", "ruby", "swift", "terraform", "raw",
235-
];
241+
const FORMAT_OPTIONS = SUPPORTED_UPSTREAM_FORMATS;
236242

237243
/**
238244
* Helper: get workspaces from cache or fetch fresh.
@@ -292,6 +298,17 @@ async function getWorkspaces(context) {
292298
return result;
293299
}
294300

301+
async function getPreferredTextDocumentLanguage() {
302+
const availableLanguages = new Set(await vscode.languages.getLanguages());
303+
if (availableLanguages.has("terraform")) {
304+
return "terraform";
305+
}
306+
if (availableLanguages.has("hcl")) {
307+
return "hcl";
308+
}
309+
return "plaintext";
310+
}
311+
295312
function buildRawSearchQuery(query) {
296313
return new SearchQueryBuilder().raw(query).build();
297314
}
@@ -1697,6 +1714,101 @@ async function activate(context) {
16971714
await upstreamDetailProvider.show(workspace, repoSlug, repoName);
16981715
}),
16991716

1717+
vscode.commands.registerCommand("cloudsmith-vsc.exportTerraform", async (item) => {
1718+
if (!item) {
1719+
vscode.window.showWarningMessage("No repository selected.");
1720+
return;
1721+
}
1722+
1723+
const workspace = item.workspace;
1724+
const repoSlug = item.slug || item.slug_perm;
1725+
const repoName = item.name;
1726+
1727+
if (!workspace || !repoSlug || !repoName) {
1728+
vscode.window.showWarningMessage("Could not determine repository details.");
1729+
return;
1730+
}
1731+
1732+
if (exportTerraformAbortController) {
1733+
exportTerraformAbortController.abort();
1734+
}
1735+
1736+
const abortController = new AbortController();
1737+
exportTerraformAbortController = abortController;
1738+
const cloudsmithAPI = new CloudsmithAPI(context);
1739+
1740+
await vscode.window.withProgress(
1741+
{
1742+
location: vscode.ProgressLocation.Notification,
1743+
title: "Generating Terraform configuration...",
1744+
},
1745+
async () => {
1746+
try {
1747+
const [repoResult, retentionResult, upstreamResult] = await Promise.all([
1748+
cloudsmithAPI.get(`repos/${workspace}/${repoSlug}`),
1749+
cloudsmithAPI.get(`repos/${workspace}/${repoSlug}/retention`),
1750+
fetchRepositoryUpstreams(context, workspace, repoSlug, {
1751+
signal: abortController.signal,
1752+
}),
1753+
]);
1754+
1755+
if (abortController.signal.aborted || upstreamResult === null) {
1756+
return;
1757+
}
1758+
1759+
if (typeof repoResult === "string") {
1760+
vscode.window.showErrorMessage(
1761+
`Could not export repository. ${formatApiError(repoResult)}`
1762+
);
1763+
return;
1764+
}
1765+
1766+
const upstreamLoadFailed = Boolean(upstreamResult && upstreamResult.error);
1767+
const retentionRules = (
1768+
typeof retentionResult === "string" ||
1769+
!retentionResult ||
1770+
typeof retentionResult !== "object"
1771+
)
1772+
? null
1773+
: retentionResult;
1774+
1775+
const hclContent = generateTerraformConfig({
1776+
repo: repoResult,
1777+
workspace,
1778+
upstreams: upstreamLoadFailed ? [] : upstreamResult.data,
1779+
retention: retentionRules,
1780+
exportedAt: new Date().toISOString(),
1781+
upstreamLoadFailed,
1782+
});
1783+
1784+
const doc = await vscode.workspace.openTextDocument({
1785+
content: hclContent,
1786+
language: await getPreferredTextDocumentLanguage(),
1787+
});
1788+
1789+
if (abortController.signal.aborted) {
1790+
return;
1791+
}
1792+
1793+
await vscode.window.showTextDocument(doc);
1794+
} catch (error) {
1795+
if (abortController.signal.aborted) {
1796+
return;
1797+
}
1798+
1799+
const message = error && error.message ? error.message : String(error);
1800+
vscode.window.showErrorMessage(
1801+
`Could not export repository. ${formatApiError(message)}`
1802+
);
1803+
} finally {
1804+
if (exportTerraformAbortController === abortController) {
1805+
exportTerraformAbortController = null;
1806+
}
1807+
}
1808+
}
1809+
);
1810+
}),
1811+
17001812
// Phase 9: Preview upstream resolution
17011813
vscode.commands.registerCommand("cloudsmith-vsc.previewUpstreamResolution", async (item) => {
17021814
const defaultWsSlug = getDefaultWorkspace();
@@ -1714,11 +1826,6 @@ async function activate(context) {
17141826
});
17151827
if (!pkgName) return;
17161828

1717-
const FORMAT_OPTIONS = [
1718-
"alpine", "cargo", "composer", "conan", "conda", "cran", "dart", "deb",
1719-
"docker", "go", "helm", "hex", "maven", "npm", "nuget", "python",
1720-
"rpm", "ruby", "swift", "terraform", "raw",
1721-
];
17221829
const formatPick = await vscode.window.showQuickPick(
17231830
FORMAT_OPTIONS.map(f => ({ label: f })),
17241831
{ placeHolder: "Select a package format" }
@@ -1987,4 +2094,5 @@ function deactivate() {}
19872094
module.exports = {
19882095
activate,
19892096
deactivate,
2097+
FORMAT_OPTIONS,
19902098
};

0 commit comments

Comments
 (0)