@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
149 lines (145 loc) • 4.48 kB
JavaScript
;
var errors = require('@backstage/errors');
var dns = require('dns');
const PROTOCOL_SUFFIX = "+srv:";
class SrvResolvers {
#cache;
#cacheTtlMillis;
#resolveSrv;
constructor(options) {
this.#cache = /* @__PURE__ */ new Map();
this.#cacheTtlMillis = options?.cacheTtlMillis ?? 1e3;
this.#resolveSrv = options?.resolveSrv ?? ((host) => new Promise((resolve, reject) => {
dns.resolveSrv(host, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}));
}
isSrvUrl(url) {
try {
this.#parseSrvUrl(url);
return true;
} catch {
return false;
}
}
/**
* Get a resolver function for a given SRV form URL.
*
* @param url An SRV form URL, e.g. `http+srv://myplugin.services.region.example.net/api/myplugin`
* @returns A function that returns resolved URLs, e.g. `http://1234abcd.region.example.net:8080/api/myplugin`
*/
getResolver(url) {
const { protocol, host, path } = this.#parseSrvUrl(url);
return () => this.#resolveHost(host).then(
(resolved) => `${protocol}://${resolved}${path}`
);
}
/**
* Attempts to parse out the relevant parts of an SRV URL.
*/
#parseSrvUrl(url) {
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
throw new errors.InputError(
`SRV resolver expected a valid URL starting with http(s)+srv:// but got '${url}'`
);
}
if (!parsedUrl.protocol?.endsWith(PROTOCOL_SUFFIX) || !parsedUrl.hostname) {
throw new errors.InputError(
`SRV resolver expected a URL with protocol http(s)+srv:// but got '${url}'`
);
}
if (parsedUrl.port) {
throw new errors.InputError(
`SRV resolver URLs cannot contain a port but got '${url}'`
);
}
if (parsedUrl.username || parsedUrl.password) {
throw new errors.InputError(
`SRV resolver URLs cannot contain username or password but got '${url}'`
);
}
if (parsedUrl.search || parsedUrl.hash) {
throw new errors.InputError(
`SRV resolver URLs cannot contain search params or a hash but got '${url}'`
);
}
const protocol = parsedUrl.protocol.substring(
0,
parsedUrl.protocol.length - PROTOCOL_SUFFIX.length
);
const host = parsedUrl.hostname;
const path = parsedUrl.pathname.replace(/\/+$/, "");
if (!["http", "https"].includes(protocol)) {
throw new errors.InputError(
`SRV URLs must be based on http or https but got '${url}'`
);
}
return { protocol, host, path };
}
/**
* Resolves a single SRV record name to a host:port string.
*/
#resolveHost(host) {
let records = this.#cache.get(host);
if (!records) {
records = this.#resolveSrv(host).then(
(result) => {
if (!result.length) {
throw new errors.NotFoundError(`No SRV records found for ${host}`);
}
return result;
},
(err) => {
throw new errors.ForwardedError(`Failed SRV resolution for ${host}`, err);
}
);
this.#cache.set(host, records);
setTimeout(() => {
this.#cache.delete(host);
}, this.#cacheTtlMillis);
}
return records.then((rs) => {
const r = this.#pickRandomRecord(rs);
return `${r.name}:${r.port}`;
});
}
/**
* Among a set of records, pick one at random.
*
* This assumes that the set is not empty.
*
* Since this contract only ever returns a single record, the best it can do
* is to pick weighted-randomly among the highest-priority records. In order
* to be smarter than that, the caller would have to be able to make decisions
* on the whole set of records.
*/
#pickRandomRecord(allRecords) {
const lowestPriority = allRecords.reduce(
(acc, r) => Math.min(acc, r.priority),
Number.MAX_SAFE_INTEGER
);
const records = allRecords.filter((r) => r.priority === lowestPriority);
const totalWeight = records.reduce((acc, r) => acc + r.weight, 0);
const targetWeight = Math.random() * totalWeight;
let result = records[0];
let currentWeight = 0;
for (const record of records) {
currentWeight += record.weight;
if (targetWeight <= currentWeight) {
result = record;
break;
}
}
return result;
}
}
exports.SrvResolvers = SrvResolvers;
//# sourceMappingURL=SrvResolvers.cjs.js.map