Last active
September 6, 2021 05:59
-
-
Save samrocksc/dfd3c83a617b1a4b8a38646c690c3288 to your computer and use it in GitHub Desktop.
Writing a functional chained library to simplify Google Secret Manager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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