Skip to content

Commit

Permalink
Ignore non-versioned Features when generating outdated report (#731)
Browse files Browse the repository at this point in the history
* break test

* patch

* exit early if local Feature

* add non-versionable Features to config

* update test given devcontainers/templates#231

* sanity check negative case
  • Loading branch information
joshspicer authored Jan 25, 2024
1 parent 8dab2ac commit f507bb8
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 12 deletions.
6 changes: 6 additions & 0 deletions src/spec-configuration/containerCollectionsOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export function getRef(output: Log, input: string): OCIRef | undefined {
// Normalize input by downcasing entire string
input = input.toLowerCase();

// Invalid if first character is a dot
if (input.startsWith('.')) {
output.write(`Input '${input}' failed validation. Expected input to not start with '.'`, LogLevel.Error);
return;
}

const indexOfLastColon = input.lastIndexOf(':');
const indexOfLastAtCharacter = input.lastIndexOf('@');

Expand Down
16 changes: 8 additions & 8 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as os from 'os';

import { DevContainerConfig, DevContainerFeature, VSCodeCustomizations } from './configuration';
import { mkdirpLocal, readLocalFile, rmLocal, writeLocalFile, cpDirectoryLocal, isLocalFile } from '../spec-utils/pfs';
import { Log, LogLevel } from '../spec-utils/log';
import { Log, LogLevel, nullLog } from '../spec-utils/log';
import { request } from '../spec-utils/httpRequest';
import { fetchOCIFeature, tryGetOCIFeatureSet, fetchOCIFeatureManifestIfExistsFromUserIdentifier } from './containerFeaturesOCI';
import { uriToFsPath } from './configurationCommonUtils';
Expand Down Expand Up @@ -512,9 +512,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
}

export async function loadVersionInfo(params: ContainerFeatureInternalParams, config: DevContainerConfig) {
const { output } = params;

const userFeatures = updateDeprecatedFeaturesIntoOptions(userFeaturesToArray(config), output);
const userFeatures = userFeaturesToArray(config);
if (!userFeatures) {
return { features: {} };
}
Expand All @@ -525,8 +523,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co

await Promise.all(userFeatures.map(async userFeature => {
const userFeatureId = userFeature.userFeatureId;
const updatedFeatureId = getBackwardCompatibleFeatureId(output, userFeatureId);
const featureRef = getRef(output, updatedFeatureId);
const featureRef = getRef(nullLog, userFeatureId); // Filters out Feature identifiers that cannot be versioned (e.g. local paths, deprecated, etc..)
if (featureRef) {
const versions = (await getVersionsStrictSorted(params, featureRef))
?.reverse();
Expand All @@ -541,7 +538,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
wanted = versions.find(version => semver.satisfies(version, tag));
}
} else if (featureRef.digest && !wanted) {
const { type, manifest } = await getFeatureIdType(params, updatedFeatureId, undefined);
const { type, manifest } = await getFeatureIdType(params, userFeatureId, undefined);
if (type === 'oci' && manifest) {
const wantedFeature = await findOCIFeatureMetadata(params, manifest);
wanted = wantedFeature?.version;
Expand All @@ -561,7 +558,10 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
// Reorder Features to match the order in which they were specified in config
return {
features: userFeatures.reduce((acc, userFeature) => {
acc[userFeature.userFeatureId] = resolved[userFeature.userFeatureId];
const r = resolved[userFeature.userFeatureId];
if (r) {
acc[userFeature.userFeatureId] = r;
}
return acc;
}, {} as Record<string, any>)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
"features": {
"ghcr.io/devcontainers/features/git:1.0": "latest",
"ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": "latest",
"./features/mylocalfeature2": "latest",
"ghcr.io/devcontainers/features/github-cli": "latest",
"ghcr.io/devcontainers/features/azure-cli:0": "latest",
"ghcr.io/codspace/versioning/foo:0.3.1": "latest"
"ghcr.io/codspace/versioning/foo:0.3.1": "latest",
"./mylocalfeature": {},
"terraform": "latest",
"https://myfeatures.com/features.tgz": "latest"
}
}
29 changes: 28 additions & 1 deletion src/test/container-features/lockfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('Lockfile', function () {
}
});

it('outdated command', async () => {
it('outdated command with json output', async () => {
const workspaceFolder = path.join(__dirname, 'configs/lockfile-outdated-command');

const res = await shellExec(`${cli} outdated --workspace-folder ${workspaceFolder} --output-format json`);
Expand Down Expand Up @@ -125,6 +125,33 @@ describe('Lockfile', function () {
assert.strictEqual(foo.latestMajor, '2');
});

it('outdated command with text output', async () => {
const workspaceFolder = path.join(__dirname, 'configs/lockfile-outdated-command');

const res = await shellExec(`${cli} outdated --workspace-folder ${workspaceFolder} --output-format text`);
const response = res.stdout;
// Count number of lines of output
assert.strictEqual(response.split('\n').length, 7); // 5 valid Features + header + empty line

// Check that the header is present
assert.ok(response.includes('Current'), 'Current column is missing');
assert.ok(response.includes('Wanted'), 'Wanted column is missing');
assert.ok(response.includes('Latest'), 'Latest column is missing');

// Check that the features are present
// The version values are checked for correctness in the json variant of this test
assert.ok(response.includes('ghcr.io/devcontainers/features/git'), 'git Feature is missing');
assert.ok(response.includes('ghcr.io/devcontainers/features/git-lfs'), 'git-lfs Feature is missing');
assert.ok(response.includes('ghcr.io/devcontainers/features/github-cli'), 'github-cli Feature is missing');
assert.ok(response.includes('ghcr.io/devcontainers/features/azure-cli'), 'azure-cli Feature is missing');
assert.ok(response.includes('ghcr.io/codspace/versioning/foo'), 'foo Feature is missing');

// Check that filtered Features are not present
assert.ok(!response.includes('mylocalfeature'));
assert.ok(!response.includes('terraform'));
assert.ok(!response.includes('myfeatures'));
});

it('upgrade command', async () => {
const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-command');

Expand Down
4 changes: 2 additions & 2 deletions src/test/container-templates/containerTemplatesOCI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ describe('fetchTemplate', async function () {
const files = await fetchTemplate({ output, env: process.env }, selectedTemplate, dest);
assert.ok(files);
// Expected:
// ./environment.yml, ./.devcontainer/.env, ./.devcontainer/Dockerfile, ./.devcontainer/devcontainer.json, ./.devcontainer/docker-compose.yml, ./.devcontainer/noop.txt
assert.strictEqual(files.length, 6);
// ./environment.yml, ./.devcontainer/.env, ./.devcontainer/Dockerfile, ./.devcontainer/devcontainer.json, ./.devcontainer/docker-compose.yml, ./.devcontainer/noop.txt, ./.github/dependabot.yml
assert.strictEqual(files.length, 7);

// Read file modified by templated value
const dockerfile = (await readLocalFile(path.join(dest, '.devcontainer', 'Dockerfile'))).toString();
Expand Down

0 comments on commit f507bb8

Please sign in to comment.