Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into urls-wait-for-all-ser…
Browse files Browse the repository at this point in the history
…vices
  • Loading branch information
Roy Razon committed May 9, 2024
2 parents 869a403 + 46a4a63 commit 04e4fdd
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 74 deletions.
1 change: 1 addition & 0 deletions .github/workflows/npm-publish-canary.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
cache: yarn

- run: yarn
- run: yarn build

- name: Set NPM token
env:
Expand Down
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
@@ -1,5 +1,5 @@
import { Args, Flags } from '@oclif/core'
import { buildFlags, parseBuildFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { buildFlags, parseBuildFlags, parseTunnelServerFlags, tableFlags, text, tunnelServerFlags } from '@preevy/cli-common'
import { editUrl, tunnelNameResolver } from '@preevy/common'
import {
ComposeModel,
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

0 comments on commit 04e4fdd

Please sign in to comment.