Skip to content

Commit

Permalink
Validate endpoints for source and target and add default ports if mis…
Browse files Browse the repository at this point in the history
…sing

Signed-off-by: Tanner Lewis <[email protected]>
  • Loading branch information
lewijacn committed Nov 7, 2024
1 parent bcd38c6 commit 3d8a2ce
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -424,12 +424,40 @@ function parseAuth(json: any): ClusterAuth | null {
return null; // Invalid auth type
}

// Validate a proper url string is provided and return an url string which contains a protocol, host name, and port.
// If a port is not provided, the default protocol port (e.g. 443, 80) will be explicitly added
export function validateAndReturnFormattedHttpURL(urlString: string) {
// URL will throw error if the urlString is invalid
const url = new URL(urlString);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error(`Invalid url protocol for endpoint: ${urlString} was expecting 'http' or 'https'`)
}
if (url.pathname !== "/") {
throw new Error(`Provided endpoint: ${urlString} must not contain a path: ${url.pathname}`)
}
// URLs that contain the default protocol port (e.g. 443, 80) will not show in the URL toString()
let formattedUrlString = url.toString()
if (formattedUrlString.endsWith("/")) {
formattedUrlString = formattedUrlString.slice(0, -1)
}
if (!url.port) {
if (url.protocol === "http:") {
formattedUrlString = formattedUrlString.concat(":80")
}
else {
formattedUrlString = formattedUrlString.concat(":443")
}
}
return formattedUrlString
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function parseClusterDefinition(json: any): ClusterYaml {
const endpoint = json.endpoint
let endpoint = json.endpoint
if (!endpoint) {
throw new Error('Missing required field in cluster definition: endpoint')
}
endpoint = validateAndReturnFormattedHttpURL(endpoint)
const version = json.version;
const auth = parseAuth(json.auth)
if (!auth) {
Expand All @@ -452,18 +480,18 @@ export function isRegionGovCloud(region: string): boolean {
*
* This allows us to create a private ECR repo for any image allowing us to have a consistent
* experience across VPCs and regions (e.g. running within VPC in gov-cloud with no internet access)
*
*
* This works by creating a temp Dockerfile with only the FROM with the param imageName and
* using that Dockerfile with cdk.assets to create a local Docker image asset.
*
* @param {string} imageName - The name of the Docker image to save as a tarball and use in CDK.
* @returns {ContainerImage} - A `ContainerImage` object representing the Docker image asset.
*/
*/
export function makeLocalAssetContainerImage(scope: Construct, imageName: string): ContainerImage {
const sanitizedImageName = imageName.replace(/[^a-zA-Z0-9-_]/g, '_');
const tempDir = mkdtempSync(join(tmpdir(), 'docker-build-' + sanitizedImageName));
const dockerfilePath = join(tempDir, 'Dockerfile');

let imageHash = null;
try {
// Update the image if it is not a local image
Expand All @@ -481,7 +509,7 @@ export function makeLocalAssetContainerImage(scope: Construct, imageName: string
CdkLogger.error('Error fetching the actual hash for the image: ' + imageName + ' Error: ' + error);
throw new Error('Error fetching the image hash for the image: ' + imageName + ' Error: ' + error);
}

const dockerfileContent = `
FROM ${imageName}
`;
Expand Down
32 changes: 2 additions & 30 deletions deployment/cdk/opensearch-service-migration/lib/network-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,6 @@ export class NetworkStack extends Stack {
public readonly albTargetProxyTG: IApplicationTargetGroup;
public readonly albSourceClusterTG: IApplicationTargetGroup;

// Validate a proper url string is provided and return an url string which contains a protocol, host name, and port.
// If a port is not provided, the default protocol port (e.g. 443, 80) will be explicitly added
static validateAndReturnFormattedHttpURL(urlString: string) {
// URL will throw error if the urlString is invalid
const url = new URL(urlString);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error(`Invalid url protocol for target endpoint: ${urlString} was expecting 'http' or 'https'`)
}
if (url.pathname !== "/") {
throw new Error(`Provided target endpoint: ${urlString} must not contain a path: ${url.pathname}`)
}
// URLs that contain the default protocol port (e.g. 443, 80) will not show in the URL toString()
let formattedUrlString = url.toString()
if (formattedUrlString.endsWith("/")) {
formattedUrlString = formattedUrlString.slice(0, -1)
}
if (!url.port) {
if (url.protocol === "http:") {
formattedUrlString = formattedUrlString.concat(":80")
}
else {
formattedUrlString = formattedUrlString.concat(":443")
}
}
return formattedUrlString
}

private validateVPC(vpc: IVpc) {
let uniqueAzPrivateSubnets: string[] = []
if (vpc.privateSubnets.length > 0) {
Expand Down Expand Up @@ -114,7 +87,7 @@ export class NetworkStack extends Stack {
InterfaceVpcEndpointAwsService.XRAY, // X-Ray Traces
isStackInGovCloud(this) ?
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM_FIPS : // EFS Control Plane GovCloud
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM, // EFS Control Plane
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM, // EFS Control Plane

];
interfaceEndpoints.forEach(service => createInterfaceVpcEndpoint(service));
Expand Down Expand Up @@ -288,9 +261,8 @@ export class NetworkStack extends Stack {
});

if (props.targetClusterEndpoint) {
const formattedClusterEndpoint = NetworkStack.validateAndReturnFormattedHttpURL(props.targetClusterEndpoint);
const deployId = props.addOnMigrationDeployId ? props.addOnMigrationDeployId : props.defaultDeployId;
createMigrationStringParameter(this, formattedClusterEndpoint, {
createMigrationStringParameter(this, props.targetClusterEndpoint, {
stage: props.stage,
defaultDeployId: deployId,
parameter: MigrationSSMParameter.OS_CLUSTER_ENDPOINT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
parseClusterDefinition,
validateFargateCpuArch,
parseArgsToDict,
appendArgIfNotInExtraArgs
appendArgIfNotInExtraArgs,
validateAndReturnFormattedHttpURL
} from "../lib/common-utilities";
import {describe, test, expect} from '@jest/globals';

Expand Down Expand Up @@ -244,7 +245,7 @@ describe('validateFargateCpuArch', () => {

test('parseClusterDefinition with basic auth parameters', () => {
const clusterDefinition = {
endpoint: 'https://target-cluster',
endpoint: 'https://target-cluster:443',
version: 'ES_7.10',
auth: {
type: 'basic',
Expand All @@ -263,7 +264,7 @@ describe('validateFargateCpuArch', () => {

test('parseClusterDefinition with no auth', () => {
const clusterDefinition = {
endpoint: 'XXXXXXXXXXXXXXXXXXXXXX',
endpoint: 'https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443',
auth: {"type": "none"}
}
const parsed = parseClusterDefinition(clusterDefinition);
Expand All @@ -274,7 +275,7 @@ describe('validateFargateCpuArch', () => {

test('parseClusterDefinition with sigv4 auth', () => {
const clusterDefinition = {
endpoint: 'XXXXXXXXXXXXXXXXXXXXXX',
endpoint: 'https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443',
auth: {
type: 'sigv4',
region: 'us-east-1',
Expand All @@ -288,4 +289,57 @@ describe('validateFargateCpuArch', () => {
expect(parsed.auth.sigv4?.region).toBe(clusterDefinition.auth.region);
expect(parsed.auth.sigv4?.serviceSigningName).toBe(clusterDefinition.auth.serviceSigningName);
})

test('Test valid https imported target cluster endpoint with port is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid https imported target cluster endpoint with no port is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid http imported target cluster endpoint with no port is formatted correctly', () => {
const inputTargetEndpoint = "http://vpc-domain-abcdef.us-east-1.es.amazonaws.com"
const expectedFormattedEndpoint = "http://vpc-domain-abcdef.us-east-1.es.amazonaws.com:80"
const actualFormatted = validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid imported target cluster endpoint ending in slash is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com/"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid imported target cluster endpoint having port and ending in slash is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test target cluster endpoint with no protocol throws error', () => {
const inputTargetEndpoint = "vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/"
const validateAndFormatURL = () => validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});

test('Test target cluster endpoint with path throws error', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/indexes"
const validateAndFormatURL = () => validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});

test('Test invalid target cluster endpoint throws error', () => {
const inputTargetEndpoint = "vpc-domain-abcdef"
const validateAndFormatURL = () => validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});
})
Original file line number Diff line number Diff line change
Expand Up @@ -159,57 +159,4 @@ describe('NetworkStack Tests', () => {
// Verify the total number of VPC Endpoints
networkTemplate.resourceCountIs('AWS::EC2::VPCEndpoint', expectedEndpoints.length);
});

test('Test valid https imported target cluster endpoint with port is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid https imported target cluster endpoint with no port is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid http imported target cluster endpoint with no port is formatted correctly', () => {
const inputTargetEndpoint = "http://vpc-domain-abcdef.us-east-1.es.amazonaws.com"
const expectedFormattedEndpoint = "http://vpc-domain-abcdef.us-east-1.es.amazonaws.com:80"
const actualFormatted = NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid imported target cluster endpoint ending in slash is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com/"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test valid imported target cluster endpoint having port and ending in slash is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const actualFormatted = NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(actualFormatted).toEqual(expectedFormattedEndpoint)
});

test('Test target cluster endpoint with no protocol throws error', () => {
const inputTargetEndpoint = "vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/"
const validateAndFormatURL = () => NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});

test('Test target cluster endpoint with path throws error', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443/indexes"
const validateAndFormatURL = () => NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});

test('Test invalid target cluster endpoint throws error', () => {
const inputTargetEndpoint = "vpc-domain-abcdef"
const validateAndFormatURL = () => NetworkStack.validateAndReturnFormattedHttpURL(inputTargetEndpoint)
expect(validateAndFormatURL).toThrow()
});
});

0 comments on commit 3d8a2ce

Please sign in to comment.