Skip to content

Commit

Permalink
1pass plugin improvements (#125)
Browse files Browse the repository at this point in the history
1password improvements!
- using sdk for service accounts, system cli with ambient auth otherwise
- use field ids instead of paths
- better error mesages
- better JSdoc comments
- update 1pass plugin docs

General improvements
- expose nested resolver branch resolution errors
- general cleanup of nested/branched resolvers
- adjust how plugins detect their current name/version

---------

Co-authored-by: Phil Miller <phil@dmno.dev>
  • Loading branch information
theoephraim and philmillman committed Aug 17, 2024
1 parent 4ae8cf8 commit abdaf0c
Show file tree
Hide file tree
Showing 16 changed files with 766 additions and 349 deletions.
7 changes: 7 additions & 0 deletions .changeset/big-swans-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dmno/encrypted-vault-plugin": patch
"@dmno/1password-plugin": patch
"dmno": patch
---

1password plugin improvements, related refactoring
46 changes: 30 additions & 16 deletions example-repo/.dmno/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ import { EncryptedVaultDmnoPlugin, EncryptedVaultTypes } from '@dmno/encrypted-v
const OnePassSecretsProd = new OnePasswordDmnoPlugin('1pass/prod', {
token: configPath('OP_TOKEN'),
envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',

// token: InjectPluginInputByType,
// token: 'asdf',
});
const OnePassSecretsDev = new OnePasswordDmnoPlugin('1pass', {
token: configPath('OP_TOKEN'),
envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=4u4klfhpldobgdxrcjwb2bqsta&h=dmnoinc.1password.com',
// token: InjectPluginInputByType,
// token: 'asdf',
});


const ProdVault = new EncryptedVaultDmnoPlugin('vault/prod', {
const EncryptedVaultSecrets = new EncryptedVaultDmnoPlugin('vault/prod', {
key: configPath('DMNO_VAULT_KEY'),
name: 'prod',
});
const NonProdVault = new EncryptedVaultDmnoPlugin('vault/dev', {
key: configPath('DMNO_VAULT_KEY'),
name: 'dev',
});
// const NonProdVault = new EncryptedVaultDmnoPlugin('vault/dev', {
// key: configPath('DMNO_VAULT_KEY'),
// name: 'dev',
// });



Expand All @@ -47,8 +48,24 @@ export default defineDmnoService({
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
},
OP_TOKEN_PROD: {
extends: OnePasswordTypes.serviceAccountToken,
// OP_TOKEN_PROD: {
// extends: OnePasswordTypes.serviceAccountToken,
// },

OP_ITEM_1: {
value: switchBy('DMNO_ENV', {
_default: OnePassSecretsDev.item(),
production: OnePassSecretsProd.item(),
}),
},
OP_ITEM_BY_ID: {
value: OnePassSecretsDev.itemById("ut2dftalm3ugmxc6klavms6tfq", "bphvvrqjegfmd5yoz4buw2aequ", "username"),
},
OP_ITEM_BY_LINK: {
value: OnePassSecretsDev.itemByLink("https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=bphvvrqjegfmd5yoz4buw2aequ&h=dmnoinc.1password.com", "helturjryuy73yjbnaovlce5fu"),
},
OP_ITEM_BY_REFERENCE: {
value: OnePassSecretsDev.itemByReference("op://dev test/example/username"),
},

SOME_API_KEY: {
Expand All @@ -58,8 +75,6 @@ export default defineDmnoService({
}),
},



DMNO_VAULT_KEY: {
extends: EncryptedVaultTypes.encryptionKey,
// required: true
Expand All @@ -71,16 +86,16 @@ export default defineDmnoService({
},

VAULT_ITEM_1: {
value: ProdVault.item(),
value: EncryptedVaultSecrets.item(),
},
VAULT_ITEM_WITH_SWITCH: {
value: switchByNodeEnv({
_default: NonProdVault.item(),
_default: EncryptedVaultSecrets.item(),
staging: switchBy('CONTEXT', {
'branch-preview': ProdVault.item(),
'pr-preview': ProdVault.item(),
'branch-preview': EncryptedVaultSecrets.item(),
'pr-preview': EncryptedVaultSecrets.item(),
}),
production: ProdVault.item()
production: EncryptedVaultSecrets.item()
}),
},

Expand All @@ -92,7 +107,6 @@ export default defineDmnoService({
extends: DmnoBaseTypes.email({
normalize: true,
}),
// required: true,
value: 'Test@test.com'
},

Expand Down
1 change: 1 addition & 0 deletions example-repo/packages/astro-web/.dmno/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default defineDmnoService({
source: 'api',
key: 'API_URL',
},
'SOME_API_KEY',
],
schema: {
OP_TOKEN: { extends: OnePasswordTypes.serviceAccountToken },
Expand Down
4 changes: 2 additions & 2 deletions example-repo/packages/astro-web/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ console.log('> secret value =', DMNO_CONFIG.SECRET_FOO);
console.log('> secret value in obj', { secret: DMNO_CONFIG.SECRET_FOO });
console.log('> secret value in array', ['secret', DMNO_CONFIG.SECRET_FOO ]);

console.log('\nthe secret on the next line should not');
console.log('> secret value =', unredact(DMNO_CONFIG.SECRET_FOO));
// console.log('\nthe secret on the next line should not');
// console.log('> secret value =', unredact(DMNO_CONFIG.SECRET_FOO));

// https://astro.build/config
export default defineConfig({
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/config-engine/config-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class CacheEntry {
this.encryptedValue = more?.encryptedValue;
}
async getEncryptedValue() {
return encrypt(CacheEntry.encryptionKey, this.value, CacheEntry.encryptionKeyName);
return encrypt(CacheEntry.encryptionKey, JSON.stringify(this.value), CacheEntry.encryptionKeyName);
}
// have to make this async because of the encryption call
async getJSON(): Promise<SerializedCacheEntry> {
Expand All @@ -283,9 +283,9 @@ class CacheEntry {
static async fromSerialized(itemKey: string, raw: SerializedCacheEntry) {
// currently this setup means the encryptedValue changes on every run...
// we could instead store the encryptedValue and reuse it if it has not changed
const value = await decrypt(CacheEntry.encryptionKey, raw.encryptedValue, CacheEntry.encryptionKeyName);
const valueStr = await decrypt(CacheEntry.encryptionKey, raw.encryptedValue, CacheEntry.encryptionKeyName);
// we are also tossing out the saved "usedBy" entries since we'll have new ones after this config run
return new CacheEntry(itemKey, value, {
return new CacheEntry(itemKey, JSON.parse(valueStr), {
updatedAt: new Date(raw.updatedAt),
encryptedValue: raw.encryptedValue,
});
Expand Down Expand Up @@ -628,7 +628,7 @@ export class DmnoWorkspace {
return this.valueCache[key].value;
}
}
async setCacheItem(key: string, value: string, usedBy?: string) {
async setCacheItem(key: string, value: any, usedBy?: string) {
if (this.cacheMode === 'skip') return undefined;
this.valueCache[key] = new CacheEntry(key, value, { usedBy });
}
Expand Down Expand Up @@ -952,9 +952,9 @@ export class ResolverContext {
async setCacheItem(key: string, value: ConfigValue) {
if (process.env.DISABLE_DMNO_CACHE) return;
if (value === undefined || value === null) return;
return this.service?.workspace.setCacheItem(key, value.toString(), this.itemFullPath);
return this.service?.workspace.setCacheItem(key, value, this.itemFullPath);
}
async getOrSetCacheItem(key: string, getValToWrite: () => Promise<string>) {
async getOrSetCacheItem(key: string, getValToWrite: () => Promise<ConfigValue>) {
if (!process.env.DISABLE_DMNO_CACHE) {
const cachedValue = await this.getCacheItem(key);
if (cachedValue) return cachedValue;
Expand Down Expand Up @@ -990,7 +990,7 @@ export abstract class DmnoConfigItemBase {

/** error encountered during resolution */
get resolutionError(): ResolutionError | undefined {
return this.valueResolver?.resolutionError;
return this.valueResolver?.selfOrChildResolutionError;
}

/** resolved value _after_ coercion logic applied */
Expand Down
25 changes: 17 additions & 8 deletions packages/core/src/config-engine/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,36 @@ export class DmnoError extends Error {

icon = '❌';

constructor(err: string | Error, readonly more?: {
tip?: string,
constructor(errOrMessage: string | Error, readonly more?: {
tip?: string | Array<string>,
err?: Error,
}) {
if (_.isError(err)) {
super(err.message);
this.originalError = err;
if (_.isError(errOrMessage)) {
super(errOrMessage.message);
this.originalError = errOrMessage;
this.icon = '💥';
} else {
super(err);
} else { // string
super(errOrMessage);
this.originalError = more?.err;
}
if (Array.isArray(more?.tip)) more.tip = more.tip.join('\n');
this.name = this.constructor.name;
}

get tip() {
if (!this.more?.tip) return undefined;
if (Array.isArray(this.more.tip)) return this.more.tip.join('\n');
return this.more.tip;
}

toJSON() {
return {
icon: this.icon,
type: this.type,
name: this.name,
message: this.message,
isUnexpected: this.isUnexpected,
...this.more,
...this.tip && { tip: this.tip },
};
}
}
Expand Down
27 changes: 14 additions & 13 deletions packages/core/src/config-engine/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,12 @@ export abstract class DmnoPlugin<

initializedPluginInstanceNames.push(instanceName);

// const callStack = new Error('').stack!.split('\n');
// const pluginDefinitionPath = callStack[2]
// .replace(/.*\(/, '')
// .replace(/:.*\)/, '');
// // special case for local dev when we have the plugins symlinked by pnpm
// if (pluginDefinitionPath.includes('/core/packages/plugins/')) {
// const pluginPackageName
// } else {

// }
// console.log(pluginDefinitionPath);
// ideally we would detect the current package name and version automatically here but I dont think it's possible
// instead we made static properties, which really should be abstract, but that is not supported
// so here we have some runtime checks to ensure they have been set
// see https://github.com/microsoft/TypeScript/issues/34516
if (!this.pluginPackageName) throw new Error('DmnoPlugin class must set `static pluginPackageName` prop');
if (!this.pluginPackageVersion) throw new Error('DmnoPlugin class must set `static pluginPackageVersion` prop');
}

/** name of the plugin itself - which is the name of the class */
Expand All @@ -234,10 +229,16 @@ export abstract class DmnoPlugin<
icon?: string;

static cliPath?: string;
get cliPath() {
// these 2 should be required, but TS currently does not support static abstract
static pluginPackageName: string;
static pluginPackageVersion: string;
private getStaticProp(key: 'cliPath' | 'pluginPackageName' | 'pluginPackageVersion') {
const PluginClass = this.constructor as typeof DmnoPlugin;
return PluginClass.cliPath;
return PluginClass[key];
}
get cliPath() { return this.getStaticProp('cliPath'); }
get pluginPackageName() { return this.getStaticProp('pluginPackageName')!; }
get pluginPackageVersion() { return this.getStaticProp('pluginPackageVersion')!; }

/**
* reference back to the service this plugin was initialized in
Expand Down
Loading

0 comments on commit abdaf0c

Please sign in to comment.