Skip to content

Instantly share code, notes, and snippets.

@samrocksc
Last active September 6, 2021 05:59
Show Gist options
  • Save samrocksc/dfd3c83a617b1a4b8a38646c690c3288 to your computer and use it in GitHub Desktop.
Save samrocksc/dfd3c83a617b1a4b8a38646c690c3288 to your computer and use it in GitHub Desktop.
Writing a functional chained library to simplify Google Secret Manager
/* eslint-disable functional/functional-parameters */
/**
* This code was written to be followed along with from top to bottom. It is meant to show the value
* of having a functional interface, with object arity of one and controlling what goes into functions interface
* very strict fashion
*
* TODO: how do we make this chain easily? Commonality in needs and dependence inversion
* TODO: What if we need to add an over arching dependency?
*
* Why do i like function declaraitons more than class in stantiations?
* - A class is ultimately just an object
* - A Function is already a first class citizen in javascript, there was never a need for classes
* - A function can be instantiated asynchronously
* - A function simply mocks its inputs in a test
*
* Why do I like using type based objects versus an interfaced?
* - An interface is built on top of classes, which means they can be extended and mutated.
* - A type cannot be mutated, so you are always guaranteed whatever that type is.
* - A type is easily extendable.
* - Ultimately mutation is a very important thing to be wary of, using types decreases chances of dangerous mutation.
*/
import {
SecretManagerServiceClient,
protos,
} from '@google-cloud/secret-manager';
/**
* A declarative base input needed to bootstrap the primary library
*/
export type baseInput = {
readonly parent: string;
readonly client: SecretManagerServiceClient;
};
/**
* This is the required input for any function revolving around a secret. We will also
* want to pass in a list of secrets to know if the secret we are working with exists.
*/
export type requiredInput = baseInput & {
readonly secretId: string;
readonly secrets: readonly protos.google.cloud.secretmanager.v1.ISecret[];
};
/**
* A base output that will always be given from the instantiation of the manager
*/
type secretOutput = {
readonly create: () => unknown;
readonly get: () => Promise<protos.google.cloud.secretmanager.v1.ISecret>;
readonly delete: () => Promise<protos.google.protobuf.IEmpty | undefined>;
};
/**
* the core output for more general functions and then the secret specific
* functions
*/
export type baseOutput = {
readonly list: () => readonly protos.google.cloud.secretmanager.v1.ISecret[];
readonly secret: (secretId: string) => secretOutput;
};
/**
* the actual project builder itself.
*
* Example:
*
* ```
* const secretManager = await makeSecretsWithProject('project/xxxxx');
* await secretManager.list()
* ```
*
* NOTE: because we are using functions for our containers, we can actually
* use kind of a cool pattern of asynchronous instantiation that we wouldn't otherwise
* have with es6 classes
*/
export const makeSecretsWithProject = async (
parent: string,
): Promise<baseOutput> => {
const client = new SecretManagerServiceClient();
const secrets = await listSecrets({ parent, client });
return {
list: () => secrets,
// TODO: uh oh, we broke our rule of dependence inversion here?! Fix it!
secret: (secretId: string): secretOutput => {
const client = new SecretManagerServiceClient();
const input = { parent, secretId, client, secrets };
return {
create: createSecret(input),
get: async () => getSecret(input),
delete: async () => deleteSecret(input),
};
},
};
};
/**
* Returns the inherent list of projects auto retrieved when initialized.
* We want to make sure that we build from lowest needs to greater needs, this
* will allow us to expand quickly if we want to add functionality to a code
* base quickly and cleanly
*
* Example:
*
* ```
* const secretManager = await makeSecretsWithProject('project/xxxxx');
* console.log(secretManager.list())
* ```
*/
export const listSecrets = async (
input: baseInput,
): Promise<readonly protos.google.cloud.secretmanager.v1.ISecret[]> => {
const { client, parent } = input;
const [secrets] = await client.listSecrets({
parent: parent,
});
return secrets;
};
/**
* A simple getSecret function for retrieving a secret
*
* Example:
*
* ```
* const secretManager = await makeSecretsWithProject('project/xxxxx');
* console.log(await secretManager.secret('tester').create())
* ```
*/
export const createSecret = (
input: requiredInput,
) => async (): Promise<protos.google.cloud.secretmanager.v1.ISecret> => {
const { parent, client, secretId } = input;
const [secret] = await client.createSecret({
parent,
secretId,
secret: {
replication: {
automatic: {},
},
},
});
return secret;
};
/**
* A simple getSecret function for retrieving a secret
*
* Example:
*
* ```
* const secretManager = await makeSecretsWithProject('project/xxxxx');
* console.log(await secretManager.secret('tester).get())
* ```
*/
export const getSecret = async (
input: requiredInput,
): Promise<protos.google.cloud.secretmanager.v1.ISecret> => {
const { secretId, client, parent } = input;
const [secret] = await client.getSecret({
name: `${parent}/secrets/${secretId}`,
});
return secret;
};
/**
* A simple getSecret function for retrieving a secret
*
* Example:
*
* ```
* const secretManager = await makeSecretsWithProject('project/xxxxx');
* console.log(await secretManager.secret('tester').delete())
* ```
*/
export const deleteSecret = async (
input: requiredInput,
): Promise<protos.google.protobuf.IEmpty | undefined> => {
const { secretId, client, parent } = input;
// eslint-disable-next-line functional/no-expression-statement
console.log('hmm', await getSecret(input));
return client.deleteSecret({
name: `${parent}/secrets/${secretId}`,
});
};
// NOTE: this is just a playground to test functionality.
/* eslint-disable */
export const main = async () => {
const secretManager = await makeSecretsWithProject('projects/281563493995');
console.log(secretManager.list());
await secretManager.secret('tester').create();
console.log(await secretManager.secret('tester').get());
await secretManager.secret('tester').delete();
};
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment