Skip to content

Commit

Permalink
test: Convert OCI registry acceptance tests to unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
francescomari committed May 30, 2022
1 parent b2754c5 commit 4640825
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 377 deletions.
27 changes: 18 additions & 9 deletions src/cli/commands/test/iac/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import {
isIacShareResultsOptions,
} from './local-execution/assert-iac-options-flag';
import { hasFeatureFlag } from '../../../../lib/feature-flags';
import {
buildDefaultOciRegistry,
initRules,
} from './local-execution/rules/rules';
import {
cleanLocalCache,
getIacOrgSettings,
Expand All @@ -60,7 +64,6 @@ import config from '../../../../lib/config';
import { UnsupportedEntitlementError } from '../../../../lib/errors/unsupported-entitlement-error';
import * as ora from 'ora';
import { CustomError, FormattedCustomError } from '../../../../lib/errors';
import { initRules } from './local-execution/rules/rules';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand All @@ -78,6 +81,15 @@ export default async function(
validateTestOptions(options);
validateCredentials(options);

const orgPublicId = (options.org as string) ?? config.org;
const iacOrgSettings = await getIacOrgSettings(orgPublicId);

if (!iacOrgSettings.entitlements?.infrastructureAsCode) {
throw new UnsupportedEntitlementError('infrastructureAsCode');
}

const ociRegistryBuilder = () => buildDefaultOciRegistry(iacOrgSettings);

let testSpinner: ora.Ora | undefined;

const resultOptions: Array<Options & TestOptions> = [];
Expand All @@ -98,15 +110,12 @@ export default async function(
testSpinner = ora({ isSilent: options.quiet, stream: process.stdout });
}

const orgPublicId = (options.org as string) ?? config.org;
const iacOrgSettings = await getIacOrgSettings(orgPublicId);

if (!iacOrgSettings.entitlements?.infrastructureAsCode) {
throw new UnsupportedEntitlementError('infrastructureAsCode');
}

try {
const rulesOrigin = await initRules(iacOrgSettings, options);
const rulesOrigin = await initRules(
ociRegistryBuilder,
iacOrgSettings,
options,
);

testSpinner?.start(spinnerMessage);

Expand Down
59 changes: 19 additions & 40 deletions src/cli/commands/test/iac/local-execution/rules/oci-pull.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import * as registryClient from '@snyk/docker-registry-v2-client';
import { promises as fs } from 'fs';
import * as path from 'path';
import {
IaCErrorCodes,
ImageManifest,
ManifestConfig,
OCIPullOptions,
OCIRegistryURLComponents,
} from '../types';
import { IaCErrorCodes, OCIRegistryURLComponents } from '../types';
import { CustomError } from '../../../../../../lib/errors';
import { getErrorStringCode } from '../error-utils';
import { LOCAL_POLICY_ENGINE_DIR } from '../local-cache';
import * as Debug from 'debug';
import { createIacDir } from '../file-utils';
import { OciRegistry } from './oci-registry';
const debug = Debug('iac-oci-pull');

export const CUSTOM_RULES_TARBALL = 'custom-bundle.tar.gz';
Expand All @@ -36,50 +30,35 @@ export function extractOCIRegistryURLComponents(
}
return { registryBase: registryHost, repo, tag };
} catch {
// if one of the String functions in the try throws,
// we wrap it in our own error
throw new InvalidRemoteRegistryURLError(OCIRegistryURL);
}
}

/**
* Downloads an OCI Artifact from a remote OCI Registry and writes it to the disk.
* The artifact here is a custom rules bundle stored in a remote registry.
* In order to do that, it calls an external docker registry v2 client to get the manifests, the layers and then builds the artifact.
* Example: https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest
* @param OCIRegistryURL - the URL where the custom rules bundle is stored
* @param opt????? (optional) - object that holds the credentials and other metadata required for the registry-v2-client
* Downloads an OCI Artifact from a remote OCI Registry and writes it to the
* disk. The artifact here is a custom rules bundle stored in a remote registry.
* In order to do that, it calls an external docker registry v2 client to get
* the manifests, the layers and then builds the artifact. Example:
* https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest
*
* @param registry The client for accessing an OCI registry.
* @param repository The name of an OCI repository.
* @param tag The tag of an image in an OCI repository.
**/
export async function pull(
{ registryBase, repo, tag }: OCIRegistryURLComponents,
opt?: OCIPullOptions,
registry: OciRegistry,
repository: string,
tag: string,
): Promise<string> {
const manifest: ImageManifest = await registryClient.getManifest(
registryBase,
repo,
tag,
opt?.username,
opt?.password,
opt?.reqOptions,
);
if (manifest.schemaVersion !== 2) {
throw new InvalidManifestSchemaVersionError(
manifest.schemaVersion.toString(),
);
const { schemaVersion, layers } = await registry.getManifest(repository, tag);
if (schemaVersion !== 2) {
throw new InvalidManifestSchemaVersionError(schemaVersion.toString());
}
const manifestLayers: ManifestConfig[] = manifest.layers;
// We assume that we will always have an artifact of a single layer
if (manifestLayers.length > 1) {
if (layers.length > 1) {
debug('There were more than one layers found in the OCI Artifact.');
}
const blob = await registryClient.getLayer(
registryBase,
repo,
manifestLayers[0].digest,
opt?.username,
opt?.password,
opt?.reqOptions,
);
const { blob } = await registry.getLayer(repository, layers[0].digest);

try {
const downloadPath: string = path.join(
Expand Down
58 changes: 58 additions & 0 deletions src/cli/commands/test/iac/local-execution/rules/oci-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as registryClient from '@snyk/docker-registry-v2-client';

export type GetManifestResponse = {
schemaVersion: number;
layers: Layer[];
};

export type Layer = {
digest: string;
};

export type GetLayerResponse = {
blob: Buffer;
};

export interface OciRegistry {
getManifest(repository: string, tag: string): Promise<GetManifestResponse>;
getLayer(repository: string, digest: string): Promise<GetLayerResponse>;
}

export class RemoteOciRegistry implements OciRegistry {
private static options = {
acceptManifest: 'application/vnd.oci.image.manifest.v1+json',
acceptLayers: 'application/vnd.oci.image.layer.v1.tar+gzip',
};

constructor(
private registry: string,
private username?: string,
private password?: string,
) {}

getManifest(repository: string, tag: string): Promise<GetManifestResponse> {
return registryClient.getManifest(
this.registry,
repository,
tag,
this.username,
this.password,
RemoteOciRegistry.options,
);
}

async getLayer(
repository: string,
digest: string,
): Promise<GetLayerResponse> {
const blob = await registryClient.getLayer(
this.registry,
repository,
digest,
this.username,
this.password,
RemoteOciRegistry.options,
);
return { blob };
}
}
40 changes: 20 additions & 20 deletions src/cli/commands/test/iac/local-execution/rules/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import {
IaCErrorCodes,
IacOrgSettings,
IaCTestFlags,
layerContentType,
manifestContentType,
OCIRegistryURLComponents,
RulesOrigin,
} from '../types';
Expand All @@ -24,9 +22,11 @@ import {
customRulesMessage,
customRulesReportMessage,
} from '../../../../../../lib/formatters/iac-output';
import { OciRegistry, RemoteOciRegistry } from './oci-registry';
import { isValidUrl } from '../url-utils';

export async function initRules(
buildOciRegistry: () => OciRegistry,
iacOrgSettings: IacOrgSettings,
options: IaCTestFlags,
): Promise<RulesOrigin> {
Expand Down Expand Up @@ -67,7 +67,10 @@ export async function initRules(
if (!iacOrgSettings.entitlements?.iacCustomRulesEntitlement) {
throw new UnsupportedEntitlementPullError('iacCustomRulesEntitlement');
}
customRulesPath = await pullIaCCustomRules(iacOrgSettings);
customRulesPath = await pullIaCCustomRules(
buildOciRegistry,
iacOrgSettings,
);
rulesOrigin = RulesOrigin.Remote;
}

Expand Down Expand Up @@ -139,35 +142,32 @@ function getOCIRegistryURLComponents(
return getOCIRegistryURLComponentsFromEnv();
}

export function buildDefaultOciRegistry(settings: IacOrgSettings): OciRegistry {
const { registryBase } = getOCIRegistryURLComponents(settings);

const username = userConfig.get('oci-registry-username');
const password = userConfig.get('oci-registry-password');

return new RemoteOciRegistry(registryBase, username, password);
}

/**
* Pull and store the IaC custom-rules bundle from the remote OCI Registry.
*/
export async function pullIaCCustomRules(
buildOciRegistry: () => OciRegistry,
iacOrgSettings: IacOrgSettings,
): Promise<string> {
const ociRegistryURLComponents = getOCIRegistryURLComponents(iacOrgSettings);

const username = userConfig.get('oci-registry-username');
const password = userConfig.get('oci-registry-password');

const opt = {
username,
password,
reqOptions: {
acceptManifest: manifestContentType,
acceptLayer: layerContentType,
indexContentType: '',
},
};
const { repo, tag } = getOCIRegistryURLComponents(iacOrgSettings);

try {
return await pull(ociRegistryURLComponents, opt);
return await pull(buildOciRegistry(), repo, tag);
} catch (err) {
if (err.statusCode === 401) {
if ((err as any).statusCode === 401) {
throw new FailedToPullCustomBundleError(
'There was an authentication error. Incorrect credentials provided.',
);
} else if (err.statusCode === 404) {
} else if ((err as any).statusCode === 404) {
throw new FailedToPullCustomBundleError(
'The remote repository could not be found. Please check the provided registry URL.',
);
Expand Down
31 changes: 0 additions & 31 deletions src/cli/commands/test/iac/local-execution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,43 +356,12 @@ export interface TestReturnValue {
ignoreCount: number;
}

// https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest
export interface ImageManifest {
schemaVersion: number;
mediaType: string;
config: ManifestConfig;
layers: ManifestConfig[];
}

export interface ManifestConfig {
mediaType: string;
size: number;
digest: string; // unique content identifier
}

export interface Layer {
config: ManifestConfig;
blob: Buffer;
}

export interface OCIPullOptions {
username?: string;
password?: string;
// weak typing on the client
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reqOptions?: { accept?: string; indexContentType?: string };
imageSavePath?: string;
}

export interface OCIRegistryURLComponents {
registryBase: string;
repo: string;
tag: string;
}

export const manifestContentType = 'application/vnd.oci.image.manifest.v1+json';
export const layerContentType = 'application/vnd.oci.image.layer.v1.tar+gzip';

export enum PerformanceAnalyticsKey {
InitLocalCache = 'cache-init-ms',
FileLoading = 'file-loading-ms',
Expand Down
Loading

0 comments on commit 4640825

Please sign in to comment.