-
Notifications
You must be signed in to change notification settings - Fork 273
/
Copy pathserve.ts
258 lines (222 loc) · 8.22 KB
/
serve.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Command, CommandResult, CommandParams } from "./base"
import { startServer } from "../server/server"
import { IntegerParameter, StringsParameter } from "../cli/params"
import { printEmoji, printHeader } from "../logger/util"
import { dedent } from "../util/string"
import { CommandLine } from "../cli/command-line"
import { GardenInstanceManager } from "../server/instance-manager"
import chalk from "chalk"
import { getCloudDistributionName, sleep } from "../util/util"
import { Log } from "../logger/log-entry"
import { findProjectConfig } from "../config/base"
import { CloudApiTokenRefreshError, getGardenCloudDomain } from "../cloud/api"
import { uuidv4 } from "../util/random"
import { Garden } from "../garden"
import { GardenPluginReference } from "../plugin/plugin"
import { CommandError, ParameterError, isEAddrInUseException, isErrnoException } from "../exceptions"
export const defaultServerPort = 9777
export const serveArgs = {}
export const serveOpts = {
port: new IntegerParameter({
help: `The port number for the server to listen on (defaults to ${defaultServerPort} if available).`,
}),
cmd: new StringsParameter({ help: "(Only used by dev command for now)", hidden: true }),
}
export type ServeCommandArgs = typeof serveArgs
export type ServeCommandOpts = typeof serveOpts
export class ServeCommand<
A extends ServeCommandArgs = ServeCommandArgs,
O extends ServeCommandOpts = ServeCommandOpts,
R = any,
> extends Command<A, O, R> {
name = "serve"
help = "Starts the Garden Core API server for the current project and environment."
override cliOnly = true
override streamEvents = true
override hidden = true
override noProject = true
protected _manager?: GardenInstanceManager
protected commandLine?: CommandLine
protected sessionId?: string
protected plugins?: GardenPluginReference[]
override description = dedent`
Starts the Garden Core API server for the current project, and your selected environment+namespace.
Note: You must currently run one server per environment and namespace.
`
override arguments = <A>serveArgs
override options = <O>serveOpts
override printHeader({ log }) {
printHeader(log, "Garden API Server", "🌐")
}
override terminate() {
super.terminate()
this.server?.close().catch(() => {})
}
override maybePersistent() {
return true
}
override allowInDevCommand() {
return false
}
protected setProps(sessionId: string, plugins: GardenPluginReference[]) {
this.sessionId = sessionId
this.plugins = plugins
}
async action({
garden,
log,
opts,
cli,
}: CommandParams<ServeCommandArgs, ServeCommandOpts>): Promise<CommandResult<R>> {
const sessionId = garden.sessionId
this.setProps(sessionId, cli?.plugins || [])
const projectConfig = await findProjectConfig({ log, path: garden.projectRoot })
let defaultGarden: Garden | undefined
const manager = this.getManager(log, undefined)
manager.defaultProjectRoot = projectConfig?.path || process.cwd()
manager.defaultEnv = opts.env
if (projectConfig) {
// Try loading the default Garden instance based on found project config, to populate autocompleter etc.
try {
defaultGarden = await manager.getGardenForRequest({
projectConfig,
globalConfigStore: garden.globalConfigStore,
log,
args: {},
opts: {},
sessionId,
environmentString: opts.env,
})
if (this.commandLine) {
this.commandLine.cwd = defaultGarden.projectRoot
}
} catch (error) {
log.warn(`Unable to load Garden project found at ${projectConfig.path}: ${error}`)
}
}
const cloudDomain = getGardenCloudDomain(projectConfig?.domain)
try {
this.server = await startServer({
log,
manager,
port: opts.port,
defaultProjectRoot: manager.defaultProjectRoot || process.cwd(),
serveCommand: this,
})
} catch (err) {
if (isEAddrInUseException(err)) {
throw new ParameterError({
message: dedent`
Port ${opts.port} is already in use, possibly by another Garden server process.
Either terminate the other process, or choose another port using the --port parameter.
`,
})
} else if (isErrnoException(err)) {
throw new CommandError({
message: `Unable to start server: ${err.message}`,
code: err.code,
})
}
throw err
}
try {
const cloudApi = await manager.getCloudApi({ log, cloudDomain, globalConfigStore: garden.globalConfigStore })
if (!cloudApi) {
await garden.emitWarning({
key: "web-app",
log,
message: chalk.green(
`🌿 Explore logs, past commands, and your dependency graph in the Garden web App. Log in with ${chalk.cyan(
"garden login"
)}.`
),
})
}
if (projectConfig && cloudApi && defaultGarden) {
let projectId = projectConfig?.id
if (!projectId) {
const cloudProject = await cloudApi.getProjectByName(projectConfig.name)
projectId = cloudProject?.id
}
if (projectId && defaultGarden) {
const session = await cloudApi.registerSession({
parentSessionId: undefined,
projectId,
// Use the process (i.e. parent command) session ID for the serve command session
sessionId: manager.sessionId,
commandInfo: garden.commandInfo,
localServerPort: this.server.port,
environment: defaultGarden.environmentName,
namespace: defaultGarden.namespace,
isDevCommand: true,
})
if (session?.shortId) {
const distroName = getCloudDistributionName(cloudDomain)
const livePageUrl = cloudApi.getLivePageUrl({ shortId: session.shortId })
const msg = dedent`${printEmoji("🌸", log)}Connected to ${distroName} ${printEmoji("🌸", log)}
Follow the link below to stream logs, run commands, and more from your web dashboard ${printEmoji(
"👇",
log
)} \n\n${chalk.cyan(livePageUrl)}\n`
log.info(chalk.white(msg))
}
}
}
} catch (err) {
if (err instanceof CloudApiTokenRefreshError) {
const distroName = getCloudDistributionName(cloudDomain)
log.warn(dedent`
${chalk.yellow(`Unable to authenticate against ${distroName} with the current session token.`)}
The dashboard will not be available until you authenticate again. Please try logging out with
${chalk.bold("garden logout")} and back in again with ${chalk.bold("garden login")}.
`)
} else {
// Unhandled error when creating the cloud api
throw err
}
}
return new Promise((resolve, reject) => {
this.server!.on("close", () => {
resolve({})
})
this.server!.on("error", (err: unknown) => {
reject(err)
})
// Errors are handled in the method
this.reload(log)
.then(async () => {
if (this.commandLine) {
for (const cmd of opts.cmd || []) {
await this.commandLine.typeCommand(cmd)
await sleep(1000)
}
}
this.commandLine?.flashSuccess(chalk.white.bold(`Dev console is ready to go! 🚀`))
this.commandLine?.enable()
})
// Errors are handled in the method
.catch(() => {})
})
}
getManager(log: Log, initialSessionId: string | undefined): GardenInstanceManager {
if (!this._manager) {
this._manager = GardenInstanceManager.getInstance({
log,
sessionId: this.sessionId || initialSessionId || uuidv4(),
serveCommand: this,
plugins: this.plugins || [],
})
}
return this._manager
}
async reload(log: Log) {
await this.getManager(log, undefined).reload(log)
}
}