Test connections to registries, if FF is enabled

This commit is contained in:
Michael B. Gale
2026-02-10 15:29:40 +00:00
parent c7eff3f0b1
commit 01ee641f14
4 changed files with 1709 additions and 1385 deletions
+13 -3
View File
@@ -6,7 +6,7 @@ import { pki } from "node-forge";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { Features } from "./feature-flags";
import { Feature, Features } from "./feature-flags";
import { KnownLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
@@ -22,6 +22,7 @@ import {
sendFailedStatusReport,
sendSuccessStatusReport,
} from "./start-proxy";
import { checkConnections } from "./start-proxy/reachability";
import { ActionName, sendUnhandledErrorStatusReport } from "./status-report";
import * as util from "./util";
@@ -112,7 +113,6 @@ async function run(startedAt: Date) {
// Initialise FFs, but only load them from disk if they are already available.
const repositoryNwo = getRepositoryNwo();
const gitHubVersion = await getGitHubVersion();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
features = new Features(
gitHubVersion,
repositoryNwo,
@@ -152,7 +152,17 @@ async function run(startedAt: Date) {
// Start the Proxy
const proxyBin = await getProxyBinaryPath(logger);
await startProxy(proxyBin, proxyConfig, proxyLogFilePath, logger);
const proxyInfo = await startProxy(
proxyBin,
proxyConfig,
proxyLogFilePath,
logger,
);
// Check that the private registries are reachable.
if (await features.getValue(Feature.StartProxyConnectionChecks)) {
await checkConnections(logger, proxyInfo);
}
// Report success if we have reached this point.
await sendSuccessStatusReport(
+98
View File
@@ -0,0 +1,98 @@
import test from "ava";
import * as sinon from "sinon";
import {
checkExpectedLogMessages,
setupTests,
withRecordingLoggerAsync,
} from "./../testing-utils";
import {
checkConnections,
ReachabilityBackend,
ReachabilityError,
} from "./reachability";
import { ProxyInfo, Registry } from "./types";
setupTests(test);
class MockReachabilityBackend implements ReachabilityBackend {
public async checkConnection(_registry: Registry): Promise<number> {
return 200;
}
}
const mavenRegistry: Registry = {
type: "maven_registry",
url: "https://repo.maven.apache.org/maven2/",
};
const nugetFeed: Registry = {
type: "nuget_feed",
url: "https://api.nuget.org/v3/index.json",
};
const proxyInfo: ProxyInfo = {
host: "127.0.0.1",
port: 1080,
cert: "",
registries: [mavenRegistry, nugetFeed],
};
test("checkConnections - basic functionality", async (t) => {
const backend = new MockReachabilityBackend();
const messages = await withRecordingLoggerAsync(async (logger) => {
const reachable = await checkConnections(logger, proxyInfo, backend);
t.is(reachable.size, proxyInfo.registries.length);
t.true(reachable.has(mavenRegistry));
t.true(reachable.has(nugetFeed));
});
checkExpectedLogMessages(t, messages, [
`Testing connection to ${mavenRegistry.url}`,
`Successfully tested connection to ${mavenRegistry.url}`,
`Testing connection to ${nugetFeed.url}`,
`Successfully tested connection to ${nugetFeed.url}`,
`Finished testing connections`,
]);
});
test("checkConnections - excludes failed status codes", async (t) => {
const backend = new MockReachabilityBackend();
sinon
.stub(backend, "checkConnection")
.onSecondCall()
.throws(new ReachabilityError(nugetFeed, 400));
const messages = await withRecordingLoggerAsync(async (logger) => {
const reachable = await checkConnections(logger, proxyInfo, backend);
t.is(reachable.size, 1);
t.true(reachable.has(mavenRegistry));
t.false(reachable.has(nugetFeed));
});
checkExpectedLogMessages(t, messages, [
`Testing connection to ${mavenRegistry.url}`,
`Successfully tested connection to ${mavenRegistry.url}`,
`Testing connection to ${nugetFeed.url}`,
`Connection test to ${nugetFeed.url} failed. (400)`,
`Finished testing connections`,
]);
});
test("checkConnections - handles other exceptions", async (t) => {
const backend = new MockReachabilityBackend();
sinon
.stub(backend, "checkConnection")
.onSecondCall()
.throws(new Error("Some generic error"));
const messages = await withRecordingLoggerAsync(async (logger) => {
const reachable = await checkConnections(logger, proxyInfo, backend);
t.is(reachable.size, 1);
t.true(reachable.has(mavenRegistry));
t.false(reachable.has(nugetFeed));
});
checkExpectedLogMessages(t, messages, [
`Testing connection to ${mavenRegistry.url}`,
`Successfully tested connection to ${mavenRegistry.url}`,
`Testing connection to ${nugetFeed.url}`,
`Connection test to ${nugetFeed.url} failed: Some generic error`,
`Finished testing connections`,
]);
});
+128
View File
@@ -0,0 +1,128 @@
import * as https from "https";
import { HttpsProxyAgent } from "https-proxy-agent";
import { Logger } from "../logging";
import { getErrorMessage } from "../util";
import { ProxyInfo, Registry } from "./types";
export class ReachabilityError extends Error {
constructor(
public readonly registry: Registry,
public readonly statusCode?: number | undefined,
) {
const statusStr = ReachabilityError.getStatusStr(statusCode);
super(`Connection test to ${registry.url} failed.${statusStr}`);
}
private static getStatusStr(statusCode: number | undefined) {
if (statusCode === undefined) return "";
return ` (${statusCode})`;
}
}
/**
* Abstracts over the backend for the reachability checks,
* to allow actual networking to be replaced with stubs.
*/
export interface ReachabilityBackend {
/**
* Performs a test HTTP request to the specified `registry`. Resolves to the status code,
* if a successful status code was obtained. Otherwise throws
*
* @param registry The registry to try and reach.
* @returns The successful status code (in the `<400` range).
*/
checkConnection: (registry: Registry) => Promise<number>;
}
class NetworkReachabilityBackend implements ReachabilityBackend {
private agent: https.Agent;
constructor(
private readonly logger: Logger,
private readonly proxy: ProxyInfo,
) {
this.agent = new HttpsProxyAgent(`http://${proxy.host}:${proxy.port}`);
}
public async checkConnection(registry: Registry): Promise<number> {
return new Promise((resolve, reject) => {
const req = https.request(
registry.url as string,
{ agent: this.agent, method: "HEAD", ca: this.proxy.cert },
(res) => {
this.logger.info(`Got a response: ${res.statusCode}`);
res.destroy();
if (res.statusCode !== undefined && res.statusCode < 400) {
resolve(res.statusCode);
} else {
reject(new ReachabilityError(registry, res.statusCode));
}
},
);
req.on("error", (e) => {
this.logger.error(e);
reject(new ReachabilityError(registry));
});
req.end();
});
}
}
/**
* Determines which configured registries can be reached by performing test requests to them.
*
* @param logger The logger to use.
* @param proxy Information about the proxy, including the configured registries.
* @param backend Optionally for testing, a `ReachabilityBackend` to use.
* @returns The set of registries which passed the checks.
*/
export async function checkConnections(
logger: Logger,
proxy: ProxyInfo,
backend?: ReachabilityBackend,
): Promise<Set<Registry>> {
const result: Set<Registry> = new Set();
// Don't do anything if there are no registries.
if (proxy.registries.length === 0) return result;
try {
// Initialise a networking backend if no backend was provided.
if (backend === undefined) {
backend = new NetworkReachabilityBackend(logger, proxy);
}
for (const registry of proxy.registries) {
try {
logger.debug(`Testing connection to ${registry.url}...`);
const statusCode = await backend.checkConnection(registry);
logger.info(
`Successfully tested connection to ${registry.url} (${statusCode})`,
);
result.add(registry);
} catch (e) {
if (e instanceof ReachabilityError && e.statusCode !== undefined) {
logger.error(
`Connection test to ${registry.url} failed. (${e.statusCode})`,
);
}
logger.error(
`Connection test to ${registry.url} failed: ${getErrorMessage(e)}`,
);
}
}
logger.debug(`Finished testing connections to private registries.`);
} catch (e) {
logger.error(
`Failed to test connections to private registries: ${getErrorMessage(e)}`,
);
}
return result;
}