Skip to content

Commit

Permalink
Don't request missing parameter store parameters
Browse files Browse the repository at this point in the history
Signed-off-by: Carl Gieringer <78054+carlgieringer@users.noreply.github.com>
  • Loading branch information
carlgieringer committed Jul 27, 2024
1 parent 055fee5 commit 6aa6022
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 13 deletions.
114 changes: 106 additions & 8 deletions howdju-service-common/lib/initializers/parameterStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {
Command,
DescribeParametersCommand,
GetParametersCommand,
} from "@aws-sdk/client-ssm";
import { toJson } from "howdju-common";

describe("parameterStore", () => {
describe("getParameterStoreConfig", () => {
afterEach(() => {
jest.clearAllMocks();
});
it("should return deserialized parameters", async () => {
// Arrange
jest.mock("@aws-sdk/client-ssm", () => {
Expand All @@ -11,15 +19,18 @@ describe("parameterStore", () => {
return {
GetParametersCommand: jest.requireActual("@aws-sdk/client-ssm")
.GetParametersCommand,
DescribeParametersCommand: jest.requireActual("@aws-sdk/client-ssm")
.DescribeParametersCommand,
SSMClient: jest.fn().mockImplementation(() => {
return ssmClient;
}),
};
});

const environment = "test";
const databaseConnectionInfoName = `/${environment}/database-connection-info`;
const expected = {
DATABASE_CONNECTION_INFO: {
"database-connection-info": {
host: "the-host",
database: "the-db-name",
username: "the-username",
Expand All @@ -30,13 +41,28 @@ describe("parameterStore", () => {
const ssmClient = new SSMClient();
// We mocked the constructor always to return the same instance, so mocking this one will
// mock the one in parameterStore.ts too.
ssmClient.send.mockResolvedValue({
Parameters: [
{
Name: "/test/DATABASE_CONNECTION_INFO",
Value: toJson(expected.DATABASE_CONNECTION_INFO),
},
],
ssmClient.send.mockImplementation((command: Command) => {
if (command instanceof GetParametersCommand) {
return {
Parameters: [
{
Name: databaseConnectionInfoName,
Value: toJson(expected["database-connection-info"]),
},
],
};
}
if (command instanceof DescribeParametersCommand) {
return {
Parameters: [
{
Name: databaseConnectionInfoName,
Type: "String",
},
],
};
}
throw new Error("Unexpected command type");
});

const { getParameterStoreConfig } =
Expand All @@ -48,5 +74,77 @@ describe("parameterStore", () => {
// Assert
expect(result).toEqual(expected);
});

it("only requests extant parameters", async () => {
// Arrange
jest.mock("@aws-sdk/client-ssm", () => {
const ssmClient = {
send: jest.fn(),
};
return {
GetParametersCommand: jest.requireActual("@aws-sdk/client-ssm")
.GetParametersCommand,
DescribeParametersCommand: jest.requireActual("@aws-sdk/client-ssm")
.DescribeParametersCommand,
SSMClient: jest.fn().mockImplementation(() => {
return ssmClient;
}),
};
});

const environment = "test";
const databaseConnectionInfoName = `/${environment}/database-connection-info`;
const expected = {
"database-connection-info": {
host: "the-host",
database: "the-db-name",
username: "the-username",
password: "the-password",
},
};
const { SSMClient } = jest.requireMock("@aws-sdk/client-ssm");
const ssmClient = new SSMClient();
// We mocked the constructor always to return the same instance, so mocking this one will
// mock the one in parameterStore.ts too.
ssmClient.send.mockImplementation((command: Command) => {
if (command instanceof GetParametersCommand) {
return {
Parameters: [
{
Name: databaseConnectionInfoName,
Value: toJson(expected["database-connection-info"]),
},
],
};
}
if (command instanceof DescribeParametersCommand) {
return {
Parameters: [
{
Name: databaseConnectionInfoName,
Type: "String",
},
],
};
}
throw new Error("Unexpected command type");
});

const { getParameterStoreConfig } =
jest.requireActual("./parameterStore");

// Act
await getParameterStoreConfig(environment);

// Assert
expect(ssmClient.send).toHaveBeenCalledTimes(2);
const getParametersCall = ssmClient.send.mock.calls.find(
([command]: [Command]) => command instanceof GetParametersCommand
);
const getParametersCommand = getParametersCall[0];
expect(getParametersCommand.input).toMatchObject({
Names: [databaseConnectionInfoName],
});
});
});
});
39 changes: 34 additions & 5 deletions howdju-service-common/lib/initializers/parameterStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
SSMClient,
GetParametersCommand,
DescribeParametersCommand,
Parameter,
} from "@aws-sdk/client-ssm";
import { PoolConfig } from "pg";

import { logger } from "howdju-common";
import { groupBy } from "lodash";

const client = new SSMClient();

Expand All @@ -30,13 +32,18 @@ export async function getParameterStoreConfig(
return {} as AsyncConfig;
}

const parameterNames = [
`/${environment}/${DATABASE_CONNECTION_INFO}`,
`/${environment}/${SCRAPING_ANT_API_KEY}`,
`/${environment}/${ZEN_ROWS_API_KEY}`,
];

// GetParametersCommand throws for missing parameters, so filter out the missing ones.
const extantParameterNames = await getExtantParameterNames(parameterNames);

const response = await client.send(
new GetParametersCommand({
Names: [
`/${environment}/${DATABASE_CONNECTION_INFO}`,
`/${environment}/${SCRAPING_ANT_API_KEY}`,
`/${environment}/${ZEN_ROWS_API_KEY}`,
],
Names: extantParameterNames,
WithDecryption: true,
})
);
Expand Down Expand Up @@ -82,6 +89,28 @@ export async function getParameterStoreConfig(
return pairs.reduce((acc, cur) => ({ ...acc, ...cur }), {}) as AsyncConfig;
}

async function getExtantParameterNames(
parameterNames: string[]
): Promise<string[]> {
const response = await client.send(
new DescribeParametersCommand({
ParameterFilters: [{ Key: "Name", Values: parameterNames }],
})
);
const foundParameterNames = new Set(
response.Parameters?.map((parameter) => parameter.Name) || []
);
const results = groupBy(parameterNames, (name) =>
foundParameterNames.has(name)
);
if (results.false?.length) {
logger.warn(
`Missing Parameter Store parameters: ${results.false.join(", ")}`
);
}
return results.true || [];
}

function extractName(fullName: string) {
const index = fullName.lastIndexOf("/");
if (index < 0) {
Expand Down

0 comments on commit 6aa6022

Please sign in to comment.