Skip to content
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
9 changes: 5 additions & 4 deletions packages/@cdktn/cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@
"constructs": "10.6.0",
"cross-spawn": "7.0.6",
"fs-extra": "8.1.0",
"https-proxy-agent": "5.0.1",
"indent-string": "4.0.0",
"minimatch": "5.1.9",
"node-fetch": "2.7.0",
"pkg-up": "3.1.0",
"semver": "7.7.4",
"sscaff": "1.2.274",
"strip-ansi": "6.0.1",
"undici": "8.6.0",
"xml-js": "1.6.11",
"xstate": "4.38.3",
"zod": "3.25.76"
Expand All @@ -70,11 +69,13 @@
"@types/fs-extra": "8.1.5",
"@types/minimatch": "5.1.2",
"@types/node": "20.19.1",
"@types/node-fetch": "2.6.13",
"@types/semver": "7.7.1",
"nock": "13.5.6",
"nock": "^14.0.0",
"prettier": "2.8.8",
"tsc-files": "1.1.4",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=22.19.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import path from "path";
import { xml2js, js2xml, Element } from "xml-js";
import * as fs from "fs-extra";
import * as semver from "semver";
import fetch, { RequestInit, Response } from "node-fetch";
import { fetch, ProxyAgent, RequestInit, Response } from "undici";
import * as z from "zod";

const proxy = process.env.http_proxy || process.env.HTTP_PROXY;
const dispatcher: ProxyAgent | undefined = proxy
? new ProxyAgent(proxy)
: undefined;

/**
* Number of attempts (initial try + retries) for a registry probe before we give up and treat the registry as
* unreachable. Transient failures (network errors / 408 / 429 / 5xx) are retried; definitive responses are not.
Expand Down Expand Up @@ -90,7 +95,7 @@ async function fetchWithRetry(

for (let attempt = 1; attempt <= REGISTRY_PROBE_ATTEMPTS; attempt++) {
try {
const response = await fetch(url, init);
const response = await fetch(url, { ...init, dispatcher });
if (!isTransientStatus(response.status)) {
return response;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0

import { HttpsProxyAgent } from "https-proxy-agent";
import fetch from "node-fetch";
import { fetch, ProxyAgent } from "undici";
import * as semver from "semver";
import { logger } from "@cdktn/commons";
import { ProviderConstraint } from "./dependency-manager";
import { versionMatchesConstraint } from "./version-constraints";

const proxy = process.env.http_proxy || process.env.HTTP_PROXY;
let agent: HttpsProxyAgent | undefined;
if (proxy) {
agent = new HttpsProxyAgent(proxy);
}
const dispatcher: ProxyAgent | undefined = proxy
? new ProxyAgent(proxy)
: undefined;

// uses https://github.com/hashicorp/cdktf-repository-manager/blob/main/provider.json
// However, it got cleared out. We'll need to host the existing list somewhere
Expand Down Expand Up @@ -44,7 +42,7 @@ async function fetchWrapped<T>(url: string): Promise<T> {
let response;
try {
response = await fetch(url, {
agent,
dispatcher,
headers: { "User-Agent": "OpenConstructs/cdktn-cli" },
});
} catch {
Expand Down
10 changes: 3 additions & 7 deletions packages/@cdktn/cli-core/src/lib/dependencies/registry-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";
import { fetch, ProxyAgent } from "undici";
import { ProviderConstraint } from "./dependency-manager";
import * as semver from "semver";
import { Errors } from "@cdktn/commons";
Expand All @@ -19,14 +18,11 @@ async function fetchVersions(
constraint: ProviderConstraint,
): Promise<VersionsReturnType["versions"] | null> {
const proxy = process.env.http_proxy || process.env.HTTP_PROXY;
let agent;
if (proxy) {
agent = new HttpsProxyAgent(proxy);
}
const dispatcher = proxy ? new ProxyAgent(proxy) : undefined;
const url = `https://registry.terraform.io/v1/providers/${constraint.namespace}/${constraint.name}/versions`;

const result = await fetch(url, {
agent,
dispatcher,
headers: { "User-Agent": "OpenConstructs/cdktn-cli" },
});
if (!result.ok) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { mkdtempSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import nock from "nock";
import {
MockAgent,
setGlobalDispatcher,
getGlobalDispatcher,
Dispatcher,
} from "undici";
import { Language } from "@cdktn/commons";
import { PackageManager } from "../../../lib/dependencies/package-manager";

Expand All @@ -13,18 +18,19 @@ const PYPI_HOST = "https://pypi.org";
const NUGET_HOST = "https://azuresearch-usnc.nuget.org";

describe("package-manager", () => {
beforeAll(() => {
nock.disableNetConnect();
});

afterAll(() => {
nock.cleanAll();
nock.enableNetConnect();
let originalDispatcher: Dispatcher;
let mockAgent: MockAgent;

beforeEach(() => {
originalDispatcher = getGlobalDispatcher();
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
});

afterEach(() => {
expect(nock.pendingMocks()).toEqual([]);
nock.cleanAll();
mockAgent.assertNoPendingInterceptors();
setGlobalDispatcher(originalDispatcher);
});

describe("JavaPackageManager.isNpmVersionAvailable", () => {
Expand All @@ -43,7 +49,10 @@ describe("package-manager", () => {
});

it("returns true when the .pom exists on Maven Central (HTTP 200)", async () => {
nock(MAVEN_HOST).get(pomPath("0.2.64")).reply(200);
mockAgent
.get(MAVEN_HOST)
.intercept({ path: pomPath("0.2.64"), method: "GET" })
.reply(200, "");

await expect(
manager.isNpmVersionAvailable(
Expand All @@ -54,7 +63,10 @@ describe("package-manager", () => {
});

it("returns false when the .pom is absent on Maven Central (HTTP 404)", async () => {
nock(MAVEN_HOST).get(pomPath("0.2.64")).reply(404);
mockAgent
.get(MAVEN_HOST)
.intercept({ path: pomPath("0.2.64"), method: "GET" })
.reply(404, "");

await expect(
manager.isNpmVersionAvailable(
Expand All @@ -66,7 +78,11 @@ describe("package-manager", () => {

it("throws (rather than reporting absent) when Maven Central stays unreachable", async () => {
// A transient 5xx is retried; after all attempts fail we must abort, not report the version as missing.
nock(MAVEN_HOST).get(pomPath("0.2.64")).times(3).reply(503);
mockAgent
.get(MAVEN_HOST)
.intercept({ path: pomPath("0.2.64"), method: "GET" })
.reply(503, "")
.times(3);

await expect(
manager.isNpmVersionAvailable(
Expand All @@ -92,8 +108,9 @@ describe("package-manager", () => {
});

it("returns true when GitHub returns the tag ref (HTTP 200)", async () => {
nock(GITHUB_HOST)
.get(refPath("0.2.64"))
mockAgent
.get(GITHUB_HOST)
.intercept({ path: refPath("0.2.64"), method: "GET" })
.reply(200, { ref: "refs/tags/random/v0.2.64" });

await expect(manager.isNpmVersionAvailable(pkg, "0.2.64")).resolves.toBe(
Expand All @@ -102,8 +119,9 @@ describe("package-manager", () => {
});

it("returns false when GitHub reports the tag missing (HTTP 404)", async () => {
nock(GITHUB_HOST)
.get(refPath("0.2.64"))
mockAgent
.get(GITHUB_HOST)
.intercept({ path: refPath("0.2.64"), method: "GET" })
.reply(404, { message: "Not Found" });

await expect(manager.isNpmVersionAvailable(pkg, "0.2.64")).resolves.toBe(
Expand All @@ -114,10 +132,11 @@ describe("package-manager", () => {
it("throws on a rate-limit response instead of reading it as absent", async () => {
// GitHub returns a JSON body on rate-limit, so the old missing-`ref` check mistook a 429 for "tag absent".
// A 429 is transient: retried, then aborts.
nock(GITHUB_HOST)
.get(refPath("0.2.64"))
.times(3)
.reply(429, { message: "API rate limit exceeded" });
mockAgent
.get(GITHUB_HOST)
.intercept({ path: refPath("0.2.64"), method: "GET" })
.reply(429, { message: "API rate limit exceeded" })
.times(3);

await expect(
manager.isNpmVersionAvailable(pkg, "0.2.64"),
Expand All @@ -139,8 +158,9 @@ describe("package-manager", () => {
});

it("returns true when PyPI has the version (info present)", async () => {
nock(PYPI_HOST)
.get(jsonPath("0.2.64"))
mockAgent
.get(PYPI_HOST)
.intercept({ path: jsonPath("0.2.64"), method: "GET" })
.reply(200, { info: { version: "0.2.64" } });

await expect(manager.isNpmVersionAvailable(pkg, "0.2.64")).resolves.toBe(
Expand All @@ -149,15 +169,22 @@ describe("package-manager", () => {
});

it("returns false when PyPI reports the version missing (HTTP 404)", async () => {
nock(PYPI_HOST).get(jsonPath("0.2.64")).reply(404, {});
mockAgent
.get(PYPI_HOST)
.intercept({ path: jsonPath("0.2.64"), method: "GET" })
.reply(404, {});

await expect(manager.isNpmVersionAvailable(pkg, "0.2.64")).resolves.toBe(
false,
);
});

it("throws when PyPI stays unreachable rather than reporting absent", async () => {
nock(PYPI_HOST).get(jsonPath("0.2.64")).times(3).reply(502);
mockAgent
.get(PYPI_HOST)
.intercept({ path: jsonPath("0.2.64"), method: "GET" })
.reply(502, "")
.times(3);

await expect(
manager.isNpmVersionAvailable(pkg, "0.2.64"),
Expand All @@ -180,9 +207,9 @@ describe("package-manager", () => {
});

it("returns true when NuGet lists the version", async () => {
nock(NUGET_HOST)
.get("/query")
.query(query)
mockAgent
.get(NUGET_HOST)
.intercept({ path: "/query", method: "GET", query })
.reply(200, {
data: [{ id: pkg, versions: [{ version: "0.2.64" }] }],
});
Expand All @@ -193,15 +220,22 @@ describe("package-manager", () => {
});

it("returns false when NuGet returns no matching package", async () => {
nock(NUGET_HOST).get("/query").query(query).reply(200, { data: [] });
mockAgent
.get(NUGET_HOST)
.intercept({ path: "/query", method: "GET", query })
.reply(200, { data: [] });

await expect(manager.isNpmVersionAvailable(pkg, "0.2.64")).resolves.toBe(
false,
);
});

it("throws when NuGet stays unreachable rather than reporting absent", async () => {
nock(NUGET_HOST).get("/query").query(query).times(3).reply(500);
mockAgent
.get(NUGET_HOST)
.intercept({ path: "/query", method: "GET", query })
.reply(500, "")
.times(3);

await expect(
manager.isNpmVersionAvailable(pkg, "0.2.64"),
Expand Down
Loading
Loading