Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter relevant registries after validating the configuration #781

Merged
merged 1 commit into from
Sep 11, 2023
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
16 changes: 10 additions & 6 deletions extension/task/IDependabotConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ export interface IDependabotConfig {
/**
* Optional. Specify authentication details to access private package registries.
*/
registries?: IDependabotRegistry[];
registries?: Record<string, IDependabotRegistry>;
}

export interface IDependabotUpdate {
/**
* Location of package manifests.
* */
directory: string;
/**
* Package manager to use.
* */
packageEcosystem: string;
/**
* Location of package manifests.
* */
directory: string;
schedule?: IDependabotUpdateSchedule;
/**
* Customize which updates are allowed.
Expand Down Expand Up @@ -56,11 +56,15 @@ export interface IDependabotUpdate {
/**
* Whether to reject external code
*/
rejectExternalCode: boolean;
insecureExternalCodeExecution?: string;
/**
* Limit number of open pull requests for version updates.
*/
openPullRequestsLimit?: number;
/**
* Registries configured for this update.
*/
registries: string[];
/**
* Branch to create pull requests against.
*/
Expand Down
12 changes: 8 additions & 4 deletions extension/task/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as tl from "azure-pipelines-task-lib/task"
import { ToolRunner } from "azure-pipelines-task-lib/toolrunner"
import { IDependabotConfig, IDependabotUpdate } from "./IDependabotConfig";
import { IDependabotConfig, IDependabotRegistry, IDependabotUpdate } from "./IDependabotConfig";
import getSharedVariables from "./utils/getSharedVariables";
import { parseConfigFile } from "./utils/parseConfigFile";

Expand Down Expand Up @@ -88,7 +88,7 @@ async function run() {
}

// Set exception behaviour if true
if (update.rejectExternalCode === true) {
if (update.insecureExternalCodeExecution === "deny") {
dockerRunner.arg(["-e", 'DEPENDABOT_REJECT_EXTERNAL_CODE=true']);
}

Expand Down Expand Up @@ -130,8 +130,12 @@ async function run() {
}

// Set the extra credentials
if (config.registries != undefined && config.registries.length > 0) {
let extraCredentials = JSON.stringify(config.registries, (k, v) => v === null ? undefined : v);
if (config.registries != undefined && Object.keys(config.registries).length > 0) {
let selectedRegistries: IDependabotRegistry[] = [];
for (const reg of update.registries) {
selectedRegistries.push(config.registries[reg]);
}
let extraCredentials = JSON.stringify(selectedRegistries, (k, v) => v === null ? undefined : v);
dockerRunner.arg(["-e", `DEPENDABOT_EXTRA_CREDENTIALS=${extraCredentials}`]);
}

Expand Down
2 changes: 1 addition & 1 deletion extension/task/utils/getAzureDevOpsAccessToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function getAzureDevOpsAccessToken() {
debug(`Loading authorization for service connection ${serviceConnectionName}`);
return getEndpointAuthorizationParameter(serviceConnectionName, "AccessToken", false);
}

debug("No custom token provided. The SystemVssConnection's AccessToken shall be used.");
return getEndpointAuthorizationParameter(
"SystemVssConnection",
Expand Down
46 changes: 36 additions & 10 deletions extension/task/utils/parseConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,15 @@ async function parseConfigFile(variables: ISharedVariables): Promise<IDependabot
);
}

var dependabotConfig: IDependabotConfig = {
const updates = parseUpdates(config);
const registries = parseRegistries(config);
validateConfiguration(updates, registries);

return {
version: version,
updates: parseUpdates(config),
registries: parseRegistries(config),
updates: updates,
registries: registries,
};

return dependabotConfig;
}

function parseUpdates(config: any): IDependabotUpdate[] {
Expand All @@ -162,6 +164,7 @@ function parseUpdates(config: any): IDependabotUpdate[] {
directory: update["directory"],

openPullRequestsLimit: update["open-pull-requests-limit"],
registries: update["registries"] || [],

targetBranch: update["target-branch"],
vendor: update["vendor"] ? JSON.parse(update["vendor"]) : null,
Expand All @@ -170,7 +173,7 @@ function parseUpdates(config: any): IDependabotUpdate[] {
branchNameSeparator: update["pull-request-branch-name"]
? update["pull-request-branch-name"]["separator"]
: undefined,
rejectExternalCode: update["insecure-external-code-execution"] === "deny",
insecureExternalCodeExecution: update["insecure-external-code-execution"],

// We are well aware that ignore is not parsed here. It is intentional.
// The ruby script in the docker container does it automatically.
Expand Down Expand Up @@ -214,8 +217,8 @@ function parseUpdates(config: any): IDependabotUpdate[] {
return updates;
}

function parseRegistries(config: any): IDependabotRegistry[] {
var registries: IDependabotRegistry[] = [];
function parseRegistries(config: any): Record<string, IDependabotRegistry> {
var registries: Record<string, IDependabotRegistry> = {};

var rawRegistries = config["registries"];

Expand Down Expand Up @@ -243,7 +246,7 @@ function parseRegistries(config: any): IDependabotRegistry[] {
var type = rawType?.replace("-", "_");

var parsed: IDependabotRegistry = { type: type, };
registries.push(parsed);
registries[registryConfigKey] = parsed;

// handle special fields for 'hex-organization' types
if (type === 'hex_organization') {
Expand Down Expand Up @@ -311,6 +314,29 @@ function parseRegistries(config: any): IDependabotRegistry[] {
return registries;
}

function validateConfiguration(updates: IDependabotUpdate[], registries: Record<string, IDependabotRegistry>) {
const configured = Object.keys(registries);
const referenced: string[] = [];
for (const u of updates) referenced.push(...u.registries);

// ensure there are no configured registries that have not been referenced
const missingConfiguration = referenced.filter((el) => !configured.includes(el));
if (missingConfiguration.length > 0) {
throw new Error(
`Referenced registries: '${missingConfiguration.join(',')}' have not been configured in the root of dependabot.yml`
);
}

// ensure there are no registries referenced but not configured
const missingReferences = configured.filter((el) => !referenced.includes(el));
if (missingReferences.length > 0)
{
throw new Error(
`Registries: '${missingReferences.join(',')}' have not been referenced by any update`
);
}
}

const KnownRegistryTypes = [
"composer-repository",
"docker-registry",
Expand All @@ -325,4 +351,4 @@ const KnownRegistryTypes = [
"terraform-registry",
];

export { parseConfigFile, parseUpdates, parseRegistries, };
export { parseConfigFile, parseUpdates, parseRegistries, validateConfiguration, };
42 changes: 42 additions & 0 deletions extension/tests/utils/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: 'docker' # See documentation for possible values
directory: '/' # Location of package manifests
schedule:
interval: 'weekly'
time: '03:00'
day: 'sunday'
open-pull-requests-limit: 10
- package-ecosystem: 'npm' # See documentation for possible values
directory: '/client' # Location of package manifests
schedule:
interval: 'daily'
time: '03:15'
open-pull-requests-limit: 10
registries:
- reg1
- reg2
insecure-external-code-execution: 'deny'
ignore:
- dependency-name: 'react'
update-types: ['version-update:semver-major']
- dependency-name: 'react-dom'
update-types: ['version-update:semver-major']
- dependency-name: '@types/react'
update-types: ['version-update:semver-major']
- dependency-name: '@types/react-dom'
update-types: ['version-update:semver-major']
registries:
reg1:
type: nuget-feed
url: 'https://pkgs.dev.azure.com/dependabot/_packaging/dependabot/nuget/v3/index.json'
token: ':${{DEFAULT_TOKEN}}'
reg2:
type: npm-registry
url: 'https://pkgs.dev.azure.com/dependabot/_packaging/dependabot-npm/npm/registry/'
token: 'tingle-npm:${{DEFAULT_TOKEN}}'
91 changes: 78 additions & 13 deletions extension/tests/utils/parseConfigFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import { load } from "js-yaml";
import * as fs from "fs";
import * as path from "path";
import { parseRegistries } from "../../task/utils/parseConfigFile";
import { parseRegistries, parseUpdates, validateConfiguration } from "../../task/utils/parseConfigFile";
import { IDependabotRegistry, IDependabotUpdate } from "../../task/IDependabotConfig";

describe("Parse configuration file", () => {
it("Parsing works as expected", () => {
let config: any = load(fs.readFileSync('tests/utils/dependabot.yml', "utf-8"));
let updates = parseUpdates(config);
expect(updates.length).toBe(2);

// first
const first = updates[0];
expect(first.directory).toBe('/');
expect(first.packageEcosystem).toBe('docker');
expect(first.insecureExternalCodeExecution).toBe(undefined);
expect(first.registries).toEqual([]);

// second
const second = updates[1];
expect(second.directory).toBe('/client');
expect(second.packageEcosystem).toBe('npm');
expect(second.insecureExternalCodeExecution).toBe('deny');
expect(second.registries).toEqual(['reg1', 'reg2']);
});
});

describe("Parse registries", () => {
it("Parsing works as expected", () => {
let config: any = load(fs.readFileSync('tests/utils/sample-registries.yml', "utf-8"));
let registries = parseRegistries(config);
expect(registries.length).toBe(11);
expect(Object.keys(registries).length).toBe(11);

// composer-repository
var registry = registries[0];
var registry = registries['composer'];
expect(registry.type).toBe('composer_repository');
expect(registry.url).toBe('https://repo.packagist.com/example-company/');
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -27,7 +50,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);

// docker-registry
registry = registries[1];
registry = registries['dockerhub'];
expect(registry.type).toBe('docker_registry');
expect(registry.url).toBe(undefined);
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -44,7 +67,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(true);

// git
registry = registries[2];
registry = registries['github-octocat'];
expect(registry.type).toBe('git');
expect(registry.url).toBe('https://github.com');
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -61,7 +84,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);

// hex-organization
registry = registries[3];
registry = registries['github-hex-org'];
expect(registry.type).toBe('hex_organization');
expect(registry.url).toBe(undefined);
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -78,7 +101,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);

// hex-repository
registry = registries[4];
registry = registries['github-hex-repository'];
expect(registry.type).toBe('hex_repository');
expect(registry.url).toBe('https://private-repo.example.com');
expect(registry.registry).toBe(undefined);
Expand All @@ -94,7 +117,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);

// maven-repository
registry = registries[5];
registry = registries['maven-artifactory'];
expect(registry.type).toBe('maven_repository');
expect(registry.url).toBe('https://artifactory.example.com');
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -111,7 +134,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(true);

// npm-registry
registry = registries[6];
registry = registries['npm-github'];
expect(registry.type).toBe('npm_registry');
expect(registry.url).toBe(undefined);
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -128,7 +151,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(true);

// nuget-feed
registry = registries[7];
registry = registries['nuget-azure-devops'];
expect(registry.type).toBe('nuget_feed');
expect(registry.url).toBe('https://pkgs.dev.azure.com/contoso/_packaging/My_Feed/nuget/v3/index.json');
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -145,7 +168,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);

// python-index
registry = registries[8];
registry = registries['python-azure'];
expect(registry.type).toBe('python_index');
expect(registry.url).toBe(undefined);
expect(registry["index-url"]).toBe('https://pkgs.dev.azure.com/octocat/_packaging/my-feed/pypi/example');
Expand All @@ -162,7 +185,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(true);

// rubygems-server
registry = registries[9];
registry = registries['ruby-github'];
expect(registry.type).toBe('rubygems_server');
expect(registry.url).toBe('https://rubygems.pkg.github.com/octocat/github_api');
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -179,7 +202,7 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(false);

// terraform-registry
registry = registries[10];
registry = registries['terraform-example'];
expect(registry.type).toBe('terraform_registry');
expect(registry.url).toBe(undefined);
expect(registry["index-url"]).toBe(undefined);
Expand All @@ -196,3 +219,45 @@ describe("Parse registries", () => {
expect(registry["replaces-base"]).toBe(undefined);
});
});

describe("Validate registries", () => {
it("Validation works as expected", () => {
// let config: any = load(fs.readFileSync('tests/utils/dependabot.yml', "utf-8"));
// let updates = parseUpdates(config);
// expect(updates.length).toBe(2);

var updates: IDependabotUpdate[] = [
{
packageEcosystem: "npm",
directory: "/",
registries: ["dummy1", "dummy2"],
},
];

var registries: Record<string, IDependabotRegistry> = {
'dummy1': {
type: 'nuget',
url: "https://pkgs.dev.azure.com/contoso/_packaging/My_Feed/nuget/v3/index.json",
token: "pwd_1234567890",
},
'dummy2': {
type: "python-index",
url: "https://pkgs.dev.azure.com/octocat/_packaging/my-feed/pypi/example",
username: "[email protected]",
password: "pwd_1234567890",
"replaces-base": true,
},
};

// works as expected
validateConfiguration(updates, registries);

// fails: registry not referenced
updates[0].registries = [];
expect(() => validateConfiguration(updates, registries)).toThrow(`Registries: 'dummy1,dummy2' have not been referenced by any update`);

// fails: registrynot configured
updates[0].registries = ["dummy1", "dummy2", "dummy3",];
expect(() => validateConfiguration(updates, registries)).toThrow(`Referenced registries: 'dummy3' have not been configured in the root of dependabot.yml`);
});
});
Loading