Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Add functionality to operate on Secrets #504

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/cli/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off',
'space-before-function-paren': 'off'
'space-before-function-paren': 'off',
'@typescript-eslint/member-delimiter-style': 'off'
}
}
30 changes: 30 additions & 0 deletions apps/cli/src/commands/secret.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import BaseCommand from '@/commands/base.command'
import CreateSecret from '@/commands/secret/create.secret'
import DeleteSecret from '@/commands/secret/delete.secret'
import ListSecret from '@/commands/secret/list.secret'
import FetchSecretRevisions from '@/commands/secret/revisions.secret'
import UpdateSecret from '@/commands/secret/update.secret'
import RollbackSecret from '@/commands/secret/rollback.secret'
import GetSecret from '@/commands/secret/get.secret'

export default class SecretCommand extends BaseCommand {
getName(): string {
return 'secret'
}

getDescription(): string {
return 'Manages the secrets on keyshade'
}

getSubCommands(): BaseCommand[] {
return [
new CreateSecret(),
new DeleteSecret(),
new GetSecret(),
new ListSecret(),
new FetchSecretRevisions(),
new UpdateSecret(),
new RollbackSecret()
]
}
}
129 changes: 129 additions & 0 deletions apps/cli/src/commands/secret/create.secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import BaseCommand from '@/commands/base.command'
import { text } from '@clack/prompts'
import ControllerInstance from '@/util/controller-instance'
import { Logger } from '@/util/logger'
import {
type CommandActionData,
type CommandArgument,
type CommandOption
} from '@/types/command/command.types'

export default class CreateSecret extends BaseCommand {
getName(): string {
return 'create'
}

getDescription(): string {
return 'Creates a secret'
}

getArguments(): CommandArgument[] {
return [
{
name: '<Project Slug>',
description: 'Slug of the project under which you want to create'
}
]
}

getOptions(): CommandOption[] {
return [
{
short: '-n',
long: '--name <string>',
description: 'Name of the secret. Must be unique across the project'
},
{
short: '-d',
long: '--note <string>',
description: 'A note describing the usage of the secret.'
},
{
short: '-r',
long: '--rotate-after <string>',
description:
' The duration in days after which the value of the secret should be rotated. Accepted values are `24`, `168`, `720`, `8769` and `never`. Defaults to `never`.'
},
{
short: '-e',
long: '--entries [entries...]',
description:
'An array of key-value pair (value and environmentSlug) for the secret.'
}
]
}

async action({ args, options }: CommandActionData): Promise<void> {
const { name, note, rotateAfter, entries } = await this.parseInput(options)
const [projectSlug] = args

if (!projectSlug) {
Logger.error('Project slug is required')
return
}

const { data, error, success } =
await ControllerInstance.getInstance().secretController.createSecret(
{
name,
note,
rotateAfter,
entries,
projectSlug
},
this.headers
)

if (success) {
Logger.info(`Secret ${data.name} (${data.slug}) created successfully!`)
Logger.info(`Created at ${data.createdAt}`)
Logger.info(`Updated at ${data.updatedAt}`)
} else {
Logger.error(`Failed to create secret: ${error.message}`)
}
}

private async parseInput(options: CommandActionData['options']): Promise<{
name: string
note?: string
rotateAfter?: '24' | '168' | '720' | '8760' | 'never'
entries: Array<{ value: string; environmentSlug: string }>
}> {
let { name, note, rotateAfter } = options
const { entries } = options

if (!name) {
name = await text({
message: 'Enter the name of secret',
placeholder: 'My Secret'
})
}

if (!entries) {
throw new Error('Entries is required')
}

if (!note) {
note = name
}

const parsedEntries = entries.map((entry) => {
const entryObj: { value: string; environmentSlug: string } = {
value: '',
environmentSlug: ''
}
entry.split(' ').forEach((pair) => {
const [key, value] = pair.split('=')
entryObj[key] = value
})
return entryObj
})

return {
name,
note,
rotateAfter,
entries: parsedEntries
}
}
}
44 changes: 44 additions & 0 deletions apps/cli/src/commands/secret/delete.secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type {
CommandActionData,
CommandArgument
} from '@/types/command/command.types'
import BaseCommand from '@/commands/base.command'
import { Logger } from '@/util/logger'
import ControllerInstance from '@/util/controller-instance'

export default class DeleteSecret extends BaseCommand {
getName(): string {
return 'delete'
}

getDescription(): string {
return 'Deletes a secret'
}

getArguments(): CommandArgument[] {
return [
{
name: '<Secret Slug>',
description: 'Slug of the secret that you want to delete.'
}
]
}

async action({ args }: CommandActionData): Promise<void> {
const [secretSlug] = args

const { error, success } =
await ControllerInstance.getInstance().secretController.deleteSecret(
{
secretSlug
},
this.headers
)

if (success) {
Logger.info(`Secret ${secretSlug} deleted successfully!`)
} else {
Logger.error(`Failed to delete secret: ${error.message}`)
}
}
}
75 changes: 75 additions & 0 deletions apps/cli/src/commands/secret/get.secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {
CommandActionData,
CommandArgument,
CommandOption
} from '@/types/command/command.types'
import BaseCommand from '@/commands/base.command'
import ControllerInstance from '@/util/controller-instance'
import { Logger } from '@/util/logger'

export default class GetSecret extends BaseCommand {
getName(): string {
return 'get'
}

getDescription(): string {
return 'Get all secrets under a project'
}

getArguments(): CommandArgument[] {
return [
{
name: '<Project Slug>',
description: 'Slug of the project whose secrets you want.'
}
]
}

getOptions(): CommandOption[] {
return [
{
short: '-d',
long: '--decrypt-value',
description:
'Set this to true if the project contains the private key. If set to true, the values of the secret will be in plaintext format'
}
]
}

async action({ args, options }: CommandActionData): Promise<void> {
const [projectSlug] = args
const { decryptValue } = await this.parseInput(options)
const { data, error, success } =
await ControllerInstance.getInstance().secretController.getAllSecretsOfProject(
{
projectSlug,
decryptValue
},
this.headers
)

if (success) {
const secrets = data.items

if (secrets.length > 0) {
data.items.forEach((item: any) => {
const secret = item.secret
Logger.info(`- ${secret.name} (${secret.slug})`)
})
} else {
Logger.info('No secrets found')
}
} else {
Logger.error(`Failed fetching secrets: ${error.message}`)
}
}

private async parseInput(options: CommandActionData['options']): Promise<{
decryptValue: boolean
}> {
const { decryptValue = false } = options // defaults to false
return {
decryptValue
}
}
}
56 changes: 56 additions & 0 deletions apps/cli/src/commands/secret/list.secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {
CommandActionData,
CommandArgument
} from '@/types/command/command.types'
import BaseCommand from '@/commands/base.command'
import ControllerInstance from '@/util/controller-instance'
import { Logger } from '@/util/logger'

export default class ListSecret extends BaseCommand {
getName(): string {
return 'list'
}

getDescription(): string {
return 'List all secrets under a project and environment'
}

getArguments(): CommandArgument[] {
return [
{
name: '<Project Slug>',
description: 'Slug of the project whose secrets you want.'
},
{
name: '<Environment Slug>',
description: 'Slug of the environment whose secrets you want.'
}
]
}

async action({ args }: CommandActionData): Promise<void> {
const [projectSlug, environmentSlug] = args
const { data, error, success } =
await ControllerInstance.getInstance().secretController.getAllSecretsOfEnvironment(
{
projectSlug,
environmentSlug
},
this.headers
)

if (success) {
console.log(data)
const secrets = data
if (secrets.length > 0) {
data.forEach((secret: any) => {
Logger.info(`- ${secret.name} (${secret.value})`)
})
} else {
Logger.info('No secrets found')
}
} else {
Logger.error(`Failed fetching secrets: ${error.message}`)
}
}
}
Loading