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

add env metadata command #481

Merged
merged 3 commits into from
May 9, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ oclif.manifest.json

packages/*/dist
packages/*/tsconfig.tsbuildinfo

.history
10 changes: 8 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"eslint.packageManager": "yarn",
"eslint.lintTask.enable": true,
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
}
},
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
Expand All @@ -29,5 +32,8 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
},
"files.exclude": {
".history/**": true,
},
}
3 changes: 2 additions & 1 deletion packages/cli-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export { HookName, HookFunc, HooksListeners, Hooks } from './lib/hooks.js'
export { PluginContext, PluginInitContext } from './lib/plugins/context.js'
export { errorToJson } from './lib/errors.js'
export {
composeFlags, pluginFlags, envIdFlags, tunnelServerFlags, urlFlags, buildFlags, tableFlags, parseBuildFlags,
composeFlags, pluginFlags, envIdFlags, tunnelServerFlags, parseTunnelServerFlags,
urlFlags, buildFlags, tableFlags, parseBuildFlags,
} from './lib/common-flags/index.js'
export { formatFlagsToArgs, parseFlags, ParsedFlags } from './lib/flags.js'
export { initHook } from './hooks/init/load-plugins.js'
Expand Down
17 changes: 1 addition & 16 deletions packages/cli-common/src/lib/common-flags/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EOL } from 'os'
import { DEFAULT_PLUGINS } from '../plugins/default-plugins.js'

export * from './build-flags.js'
export * from './tunnel-server-flags.js'

export const tableFlags = mapValues(ux.table.flags(), f => ({ ...f, helpGroup: 'OUTPUT' })) as ReturnType<typeof ux.table['flags']>

Expand Down Expand Up @@ -69,22 +70,6 @@ export const envIdFlags = {
...projectFlag,
} as const

export const tunnelServerFlags = {
'tunnel-url': Flags.string({
summary: 'Tunnel url, specify ssh://hostname[:port] or ssh+tls://hostname[:port]',
char: 't',
default: 'ssh+tls://livecycle.run' ?? process.env.PREVIEW_TUNNEL_OVERRIDE,
}),
'tls-hostname': Flags.string({
summary: 'Override TLS server name when tunneling via HTTPS',
required: false,
}),
'insecure-skip-verify': Flags.boolean({
summary: 'Skip TLS or SSH certificate verification',
default: false,
}),
} as const

export const urlFlags = {
'include-access-credentials': Flags.boolean({
summary: 'Include access credentials for basic auth for each service URL',
Expand Down
24 changes: 24 additions & 0 deletions packages/cli-common/src/lib/common-flags/tunnel-server-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Flags } from '@oclif/core'
import { InferredFlags } from '@oclif/core/lib/interfaces'

export const tunnelServerFlags = {
'tunnel-url': Flags.string({
summary: 'Tunnel url, specify ssh://hostname[:port] or ssh+tls://hostname[:port]',
char: 't',
default: 'ssh+tls://livecycle.run' ?? process.env.PREVIEW_TUNNEL_OVERRIDE,
}),
'tls-hostname': Flags.string({
summary: 'Override TLS server name when tunneling via HTTPS',
required: false,
}),
'insecure-skip-verify': Flags.boolean({
summary: 'Skip TLS or SSH certificate verification',
default: false,
}),
} as const

export const parseTunnelServerFlags = (flags: Omit<InferredFlags<typeof tunnelServerFlags>, 'json'>) => ({
url: flags['tunnel-url'],
tlsServerName: flags['tls-hostname'],
insecureSkipVerify: flags['insecure-skip-verify'],
})
144 changes: 144 additions & 0 deletions packages/cli/src/commands/env/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Flags, ux } from '@oclif/core'
import { envIdFlags, parseTunnelServerFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { TunnelOpts, addBaseComposeTunnelAgentService, findComposeTunnelAgentUrl, findEnvId, getTunnelNamesToServicePorts, getUserCredentials, jwtGenerator, profileStore, queryEnvMetadata, readMetadata } from '@preevy/core'
import { tunnelNameResolver } from '@preevy/common'
import { inspect } from 'util'
import DriverCommand from '../../driver-command.js'
import { connectToTunnelServerSsh } from '../../tunnel-server-client.js'

type MetadataSource = 'agent' | 'driver'
type UnknownMetadata = Record<string, unknown>

// eslint-disable-next-line no-use-before-define
export default class EnvMetadataCommand extends DriverCommand<typeof EnvMetadataCommand> {
static description = 'Show metadata for a preview environment'
static enableJsonFlag = true

static flags = {
...envIdFlags,
...tunnelServerFlags,
source: Flags.custom<'driver' | 'agent'>({
summary: 'Show metadata from the driver, the agent, or the driver if the agent is not available',
default: ['agent', 'driver'],
multiple: true,
delimiter: ',',
multipleNonGreedy: true,
})(),
'fetch-timeout': Flags.integer({
default: 2500,
summary: 'Timeout for fetching metadata from the agent in milliseconds',
}),
} as const

async getComposeTunnelAgentUrl(
envId: string,
tunnelOpts: TunnelOpts,
tunnelingKey: string | Buffer,
) {
const expectedTunnels = getTunnelNamesToServicePorts(
addBaseComposeTunnelAgentService({ name: '' }),
tunnelNameResolver({ envId }),
)

const { client: tunnelServerSshClient } = await connectToTunnelServerSsh({
tunnelOpts,
profileStore: profileStore(this.store),
tunnelingKey,
log: this.logger,
})

try {
const expectedTunnelUrls = await tunnelServerSshClient.execTunnelUrl(Object.keys(expectedTunnels))

const expectedServiceUrls = Object.entries(expectedTunnels)
.map(([tunnel, { name, port }]) => ({ name, port, url: expectedTunnelUrls[tunnel] }))

return findComposeTunnelAgentUrl(expectedServiceUrls)
} finally {
void tunnelServerSshClient.end()
}
}

#envId: string | undefined
async envId() {
if (!this.#envId) {
const { flags } = this
this.#envId = await findEnvId({
userSpecifiedEnvId: flags.id,
userSpecifiedProjectName: flags.project,
userModel: () => this.ensureUserModel(),
log: this.logger,
})
}
return this.#envId
}

async getMetadataFromDriver() {
return await this.withConnection(await this.envId(), readMetadata)
}

async getMetadataFromAgent() {
const pStore = profileStore(this.store).ref
const tunnelingKey = await pStore.tunnelingKey()
const composeTunnelServiceUrl = await this.getComposeTunnelAgentUrl(
await this.envId(),
parseTunnelServerFlags(this.flags),
tunnelingKey,
)
const credentials = await getUserCredentials(jwtGenerator(tunnelingKey))
// eslint-disable-next-line @typescript-eslint/return-await
return await queryEnvMetadata({
composeTunnelServiceUrl,
credentials,
fetchTimeout: this.flags['fetch-timeout'],
retryOpts: { retries: 2 },
})
}

metadataFactories: Record<MetadataSource, () => Promise<UnknownMetadata>> = {
driver: this.getMetadataFromDriver.bind(this),
agent: this.getMetadataFromAgent.bind(this),
}

async getMetatdata() {
const { flags: { source: sources } } = this
const errors: { source: MetadataSource; error: unknown }[] = []
for (const source of sources) {
try {
this.logger.debug(`Fetching metadata from ${source}`)
return {
// eslint-disable-next-line no-await-in-loop
metadata: await this.metadataFactories[source](),
errors,
source,
}
} catch (err) {
errors.push({ source, error: err })
}
}

return { errors }
}

async run(): Promise<unknown> {
const { metadata, source: metadataSource, errors } = await this.getMetatdata()

if (!metadata) {
throw new Error(`Could not get metadata: ${inspect(errors)}`)
}

if (errors.length) {
for (const { source: errorSource, error } of errors) {
this.logger.warn(`Error fetching metadata from ${errorSource}: ${error}`)
}
}

if (this.jsonEnabled()) {
return { ...metadata, _source: metadataSource }
}

this.logger.info(`Metadata from ${text.code(metadataSource)}`)
this.logger.info(inspect(metadata, { depth: null, colors: text.supportsColor !== false }))
return undefined
}
}
8 changes: 2 additions & 6 deletions packages/cli/src/commands/proxy/connect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Args, Flags } from '@oclif/core'
import { jwkThumbprint, commands, profileStore, withSpinner, SshConnection, machineId, validateEnvId, normalizeEnvId, EnvId } from '@preevy/core'
import { tableFlags, text, tunnelServerFlags, urlFlags } from '@preevy/cli-common'
import { parseTunnelServerFlags, tableFlags, text, tunnelServerFlags, urlFlags } from '@preevy/cli-common'
import { inspect } from 'util'
import { formatPublicKey } from '@preevy/common'
import { spawn } from 'child_process'
Expand Down Expand Up @@ -58,11 +58,7 @@ export default class Connect extends ProfileCommand<typeof Connect> {
const pStoreRef = pStore.ref

const tunnelingKey = await pStoreRef.tunnelingKey()
const tunnelOpts = {
url: flags['tunnel-url'],
tlsServerName: flags['tls-hostname'],
insecureSkipVerify: flags['insecure-skip-verify'],
}
const tunnelOpts = parseTunnelServerFlags(flags)
const composeProject = args['compose-project']
let envId:EnvId
if (flags.id) {
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/commands/up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
telemetryEmitter,
withSpinner,
} from '@preevy/core'
import { buildFlags, parseBuildFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { buildFlags, parseBuildFlags, parseTunnelServerFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { inspect } from 'util'
import { editUrl, tunnelNameResolver } from '@preevy/common'
import MachineCreationDriverCommand from '../machine-creation-driver-command.js'
Expand Down Expand Up @@ -143,11 +143,7 @@ export default class Up extends MachineCreationDriverCommand<typeof Up> {
)
const thumbprint = await jwkThumbprint(tunnelingKey)

const tunnelOpts = {
url: flags['tunnel-url'],
tlsServerName: flags['tls-hostname'],
insecureSkipVerify: flags['insecure-skip-verify'],
}
const tunnelOpts = parseTunnelServerFlags(flags)

const { expectedServiceUrls, hostKey } = await fetchTunnelServerDetails({
log: this.logger,
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/commands/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'fs'
import yaml from 'yaml'
import { Args, ux, Interfaces } from '@oclif/core'
import { FlatTunnel, Logger, TunnelOpts, addBaseComposeTunnelAgentService, commands, findComposeTunnelAgentUrl, findEnvId, getTunnelNamesToServicePorts, profileStore } from '@preevy/core'
import { HooksListeners, PluginContext, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { HooksListeners, PluginContext, parseTunnelServerFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { asyncReduce } from 'iter-tools-es'
import { tunnelNameResolver } from '@preevy/common'
import { connectToTunnelServerSsh } from '../tunnel-server-client.js'
Expand Down Expand Up @@ -110,11 +110,7 @@ export default class Urls extends ProfileCommand<typeof Urls> {
log,
})

const tunnelOpts = {
url: flags['tunnel-url'],
tlsServerName: flags['tls-hostname'],
insecureSkipVerify: flags['insecure-skip-verify'],
}
const tunnelOpts = parseTunnelServerFlags(flags)

const pStore = profileStore(this.store).ref

Expand Down
14 changes: 8 additions & 6 deletions packages/cli/src/driver-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ abstract class DriverCommand<T extends typeof Command> extends ProfileCommand<T>
protected flags!: Flags<T>
protected args!: Args<T>

public async init(): Promise<void> {
await super.init()
this.#driverName = this.flags.driver ?? this.preevyConfig?.driver as DriverName ?? this.profile.driver as DriverName
}

#driverName: DriverName | undefined
get driverName() : DriverName {
if (!this.#driverName) {
throw new Error("Driver wasn't specified")
const driverName = this.flags.driver
?? this.preevyConfig?.driver as DriverName
?? this.profile.driver as DriverName

if (!driverName) {
throw new Error("Driver wasn't specified")
}
this.#driverName = driverName
}
return this.#driverName
}
Expand Down
Loading
Loading