@metaplay/metaplay-auth 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/auth.ts CHANGED
@@ -20,7 +20,6 @@ const tokenEndpoint = `${baseURL}/oauth2/token`
20
20
  const wellknownApi = new WellknownApi(new Configuration({
21
21
  basePath: baseURL,
22
22
  }))
23
- const tokenList: string[] = ['id_token', 'access_token', 'refresh_token'] // List of compulsory tokens
24
23
 
25
24
  /**
26
25
  * A helper function which generates a code verifier and challenge for exchaning code from Ory server.
@@ -111,7 +110,9 @@ export async function loginAndSaveTokens () {
111
110
  try {
112
111
  logger.debug(`Received callback request with code ${code}. Preparing to exchange for tokens...`)
113
112
  const tokens = await getTokensWithAuthorizationCode(state, redirectUri, verifier, code)
114
- await saveTokens(tokens)
113
+
114
+ // Only save access_token, id_token, and refresh_token
115
+ await saveTokens({ access_token: tokens.access_token, id_token: tokens.id_token, refresh_token: tokens.refresh_token })
115
116
 
116
117
  console.log('You are now logged in and can call the other commands.')
117
118
 
@@ -140,6 +141,40 @@ export async function loginAndSaveTokens () {
140
141
  void open(authorizationUrl)
141
142
  }
142
143
 
144
+ export async function machineLoginAndSaveTokens (clientId: string, clientSecret: string) {
145
+ // Get a fresh access token from Metaplay Auth.
146
+ const params = new URLSearchParams()
147
+ params.set('grant_type', 'client_credentials')
148
+ params.set('client_id', clientId)
149
+ params.set('client_secret', clientSecret)
150
+ params.set('scope', 'openid email profile offline_access')
151
+
152
+ const res = await fetch(tokenEndpoint, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/x-www-form-urlencoded',
156
+ },
157
+ body: params.toString(),
158
+ })
159
+
160
+ // Return type checked manually by Teemu on 2024-3-7.
161
+ const tokens = await res.json() as { access_token: string, token_type: string, expires_in: number, scope: string }
162
+
163
+ logger.debug('Received machine authentication tokens, saving them for future use...')
164
+
165
+ await saveTokens({ access_token: tokens.access_token })
166
+
167
+ const userInfoResponse = await fetch('https://portal.metaplay.dev/api/external/userinfo', {
168
+ headers: {
169
+ Authorization: `Bearer ${tokens.access_token}`
170
+ },
171
+ })
172
+
173
+ const userInfo = await userInfoResponse.json() as { given_name: string, family_name: string }
174
+
175
+ console.log(`You are now logged in with machine user ${userInfo.given_name} ${userInfo.family_name} (clientId=${clientId}) and can execute the other commands.`)
176
+ }
177
+
143
178
  /**
144
179
  * Refresh access and ID token with a refresh token.
145
180
  */
@@ -147,18 +182,22 @@ export async function extendCurrentSession (): Promise<void> {
147
182
  try {
148
183
  const tokens = await loadTokens()
149
184
 
150
- logger.debug('Validating access token...')
185
+ logger.debug('Check if access token still valid...')
151
186
  if (await validateToken(tokens.access_token)) {
152
187
  // Access token is not expired, return to the caller
153
- logger.debug('Access token is valid, return to the caller.')
188
+ logger.debug('Access token is valid, no need to refresh.')
154
189
  return
155
190
  }
156
191
 
157
- logger.debug('Access token is no longer valid, trying to extend the current session with a refresh token.')
192
+ // Check that the refresh_token exists (machine users don't have it)
193
+ if (!tokens.refresh_token) {
194
+ throw new Error('Cannot refresh an access_token without a refresh_token. With machine users, should just login again instead.')
195
+ }
158
196
 
197
+ logger.debug('Access token is no longer valid, trying to extend the current session with a refresh token.')
159
198
  const refreshedTokens = await extendCurrentSessionWithRefreshToken(tokens.refresh_token)
160
199
 
161
- await saveTokens(refreshedTokens)
200
+ await saveTokens({ access_token: refreshedTokens.access_token, id_token: refreshedTokens.id_token, refresh_token: refreshedTokens.refresh_token })
162
201
  } catch (error) {
163
202
  if (error instanceof Error) {
164
203
  console.error(error.message)
@@ -173,7 +212,7 @@ export async function extendCurrentSession (): Promise<void> {
173
212
  * @returns A promise that resolves to a new set of tokens.
174
213
  */
175
214
  async function extendCurrentSessionWithRefreshToken (refreshToken: string): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
176
- // TODO: similiar to the todo task in getTokensWithAuthorizationCode, http request can be handled by ory/client.
215
+ // TODO: similar to the todo task in getTokensWithAuthorizationCode, http request can be handled by ory/client.
177
216
  const params = new URLSearchParams({
178
217
  grant_type: 'refresh_token',
179
218
  refresh_token: refreshToken,
@@ -218,7 +257,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
218
257
  * @param code
219
258
  * @returns
220
259
  */
221
- async function getTokensWithAuthorizationCode (state: string, redirectUri: string, verifier: string, code: string): Promise<{ id_token: string, access_token: string, refresh_token: string } | {}> {
260
+ async function getTokensWithAuthorizationCode (state: string, redirectUri: string, verifier: string, code: string): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
222
261
  // TODO: the authorication code exchange flow might be better to be handled by ory/client, could check if there's any useful toosl there.
223
262
  try {
224
263
  const response = await fetch(tokenEndpoint, {
@@ -234,29 +273,22 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
234
273
  if (error instanceof Error) {
235
274
  logger.error(`Error exchanging code for tokens: ${error.message}`)
236
275
  }
237
-
238
- return {}
276
+ throw error
239
277
  }
240
278
  }
241
279
 
242
280
  /**
243
281
  * Load tokens from the local secret store.
244
282
  */
245
- export async function loadTokens (): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
283
+ export async function loadTokens (): Promise<{ id_token?: string, access_token: string, refresh_token?: string }> {
246
284
  try {
247
- const idToken = await getSecret('id_token')
248
- const accessToken = await getSecret('access_token')
249
- const refreshToken = await getSecret('refresh_token')
285
+ const tokens = await getSecret('tokens') as { id_token?: string, access_token: string, refresh_token?: string }
250
286
 
251
- if (idToken == null || accessToken == null || refreshToken == null) {
252
- throw new Error('Metaplay tokens not found. Are you logged in?')
287
+ if (!tokens) {
288
+ throw new Error('Unable to load tokens. You need to login first.')
253
289
  }
254
290
 
255
- return {
256
- id_token: idToken,
257
- access_token: accessToken,
258
- refresh_token: refreshToken
259
- }
291
+ return tokens
260
292
  } catch (error) {
261
293
  if (error instanceof Error) {
262
294
  throw new Error(`Error loading tokens: ${error.message}`)
@@ -273,23 +305,18 @@ export async function saveTokens (tokens: Record<string, string>): Promise<void>
273
305
  try {
274
306
  logger.debug('Received new tokens, verifying...')
275
307
 
276
- // Check if all tokens are present
277
- for (const tokenName of tokenList) {
278
- if (tokens[tokenName] === undefined) {
279
- throw new Error(`Metaplay token ${tokenName} not found. Please log in again and make sure all checkboxes of permissions are selected before proceeding.`)
280
- }
308
+ // All tokens must have an access_token (machine users only have it)
309
+ if (!tokens.access_token) {
310
+ throw new Error('Metaplay token has no access_token. Please log in again and make sure all checkboxes of permissions are selected before proceeding.')
281
311
  }
282
312
 
283
313
  logger.debug('Token verification completed, storing tokens...')
284
314
 
285
- await setSecret('id_token', tokens.id_token)
286
- await setSecret('access_token', tokens.access_token)
287
- await setSecret('refresh_token', tokens.refresh_token)
315
+ await setSecret('tokens', tokens)
288
316
 
289
317
  logger.debug('Tokens successfully stored.')
290
318
 
291
319
  await showTokenInfo(tokens.access_token)
292
- // await showTokenInfo(tokens.id_token)
293
320
  } catch (error) {
294
321
  if (error instanceof Error) {
295
322
  throw new Error(`Failed to save tokens: ${error.message}`)
@@ -303,12 +330,8 @@ export async function saveTokens (tokens: Record<string, string>): Promise<void>
303
330
  */
304
331
  export async function removeTokens (): Promise<void> {
305
332
  try {
306
- await removeSecret('id_token')
307
- logger.debug('Removed id_token.')
308
- await removeSecret('access_token')
309
- logger.debug('Removed access_token.')
310
- await removeSecret('refresh_token')
311
- logger.debug('Removed refresh_token.')
333
+ await removeSecret('tokens')
334
+ logger.debug('Removed tokens.')
312
335
  } catch (error) {
313
336
  if (error instanceof Error) {
314
337
  throw new Error(`Error removing tokens: ${error.message}`)
@@ -0,0 +1,260 @@
1
+ import { promises as fs, existsSync } from 'fs'
2
+ import { KubeConfig, CoreV1Api, V1Pod } from '@kubernetes/client-node'
3
+ import { logger } from './logging.js'
4
+ import { error } from 'console'
5
+
6
+ enum GameServerPodPhase {
7
+ Pending = 'Pending', // Still being deployed
8
+ Ready = 'Ready', // Successfully started and ready to accept traffic
9
+ Failed = 'Failed', // Failed to start
10
+ Unknown = 'Unknown', // Unknown status -- treat like Pending
11
+ }
12
+
13
+ interface GameServerPodStatus {
14
+ phase: GameServerPodPhase
15
+ message: string
16
+ details?: any
17
+ }
18
+
19
+ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
20
+ // Define label selector for gameserver
21
+ const labelSelector = 'app=metaplay-server'
22
+
23
+ try {
24
+ // Get gameserver pods in the namespace
25
+ const response = await k8sApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, labelSelector)
26
+
27
+ // Return pod statuses
28
+ return response.body.items
29
+ } catch (error) {
30
+ // \todo Better error handling ..
31
+ console.log('Failed to fetch pods from Kubernetes:', error)
32
+ throw new Error('Failed to fetch pods from Kubernetes')
33
+ }
34
+ }
35
+
36
+ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
37
+ const containerStatuses = pod.status?.containerStatuses
38
+ if (!containerStatuses || containerStatuses.length === 0) {
39
+ return { phase: GameServerPodPhase.Unknown, message: 'Unable to determine pod container statuses: pod.status.containerStatuses is empty' }
40
+ }
41
+
42
+ // Only one container allowed in the game server pod
43
+ if (containerStatuses.length !== 1) {
44
+ throw new Error(`Internal error: Expecting only one container in the game server pod, got ${containerStatuses.length}`)
45
+ }
46
+
47
+ // Handle missing container state
48
+ const containerStatus = containerStatuses[0]
49
+ console.log('Container status:', containerStatus)
50
+ const containerState = containerStatus.state
51
+ if (!containerState) {
52
+ return { phase: GameServerPodPhase.Unknown, message: 'Unable to get container state' }
53
+ }
54
+
55
+ // Check if container running & ready
56
+ const containerName = containerStatus.name
57
+ if (containerStatus.ready && containerState.running) {
58
+ return { phase: GameServerPodPhase.Ready, message: `Container ${containerName} is in ready phase and was started at ${containerState.running.startedAt}`, details: containerState.running }
59
+ }
60
+
61
+ // \note these may not be complete (or completely accurate)
62
+ const knownContainerFailureReasons = ['CrashLoopBackOff', 'Error', 'ImagePullBackOff', 'CreateContainerConfigError', 'OOMKilled', 'ContainerCannotRun', 'BackOff', 'InvalidImageName']
63
+ const knownContainerPendingReasons = ['Init', 'Pending', 'PodInitializing']
64
+
65
+ // Check if there's a previous terminated state (usually indicates a crash during server initialization)
66
+ const lastState = containerStatus.lastState
67
+ if (lastState) {
68
+ if (lastState.terminated) {
69
+ // Try to detecth why the previous launch failed
70
+ if (containerState.waiting) {
71
+ const reason = containerState.waiting.reason
72
+ if (knownContainerFailureReasons.includes(reason as string)) {
73
+ return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
74
+ } else if (knownContainerPendingReasons.includes(reason as string)) {
75
+ return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
76
+ } else {
77
+ return { phase: GameServerPodPhase.Unknown, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
78
+ }
79
+ } else if (containerState.running) {
80
+ // This happens when the container is still initializing
81
+ return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in running state`, details: containerState.running }
82
+ } else if (containerState.terminated) {
83
+ return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in terminated state`, details: containerState.terminated }
84
+ }
85
+
86
+ // Unable to determine launch failure reason, just return previous launch
87
+ return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} previous launch failed with exitCode=${lastState.terminated.exitCode} and reason=${lastState.terminated.reason}`, details: lastState.terminated }
88
+ }
89
+
90
+ // \todo handle containerState.running states (including various initialization states)
91
+ // \todo handle containerState.terminated states (what do these even mean)
92
+ }
93
+
94
+ console.log('Game server pod container in unknown state:', containerState)
95
+ return { phase: GameServerPodPhase.Unknown, message: 'Container in unknown state', details: containerState }
96
+ }
97
+
98
+ function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
99
+ const conditions = pod.status?.conditions
100
+ if (!conditions || conditions.length === 0) {
101
+ return { phase: GameServerPodPhase.Unknown, message: 'Unable to determine pod status: pod.status.conditions is empty', details: pod.status }
102
+ }
103
+
104
+ // Bail if 'PodScheduled' is not yet true
105
+ const condPodScheduled = conditions.find(cond => cond.type === 'PodScheduled')
106
+ if (condPodScheduled?.status !== 'True') {
107
+ return { phase: GameServerPodPhase.Pending, message: `Pod has not yet been scheduled on a node: ${condPodScheduled?.message}`, details: condPodScheduled }
108
+ }
109
+
110
+ // Bail if 'Initialized' not is yet true
111
+ const condInitialized = conditions.find(cond => cond.type === 'Initialized')
112
+ if (condInitialized?.status !== 'True') {
113
+ return { phase: GameServerPodPhase.Pending, message: `Pod has not yet been initialized: ${condInitialized?.message}`, details: condInitialized }
114
+ }
115
+
116
+ // Bail if 'ContainersReady' is not yet true
117
+ const condContainersReady = conditions.find(cond => cond.type === 'ContainersReady')
118
+ if (condContainersReady?.status !== 'True') {
119
+ if (condContainersReady?.reason === 'ContainersNotReady') {
120
+ return resolvePodContainersConditions(pod)
121
+ }
122
+
123
+ return { phase: GameServerPodPhase.Pending, message: `Pod containers are not yet ready: ${condContainersReady?.message}`, details: condContainersReady }
124
+ }
125
+
126
+ // Bail if 'Ready' is not yet true
127
+ const condReady = conditions.find(cond => cond.type === 'Ready')
128
+ if (condReady?.status !== 'True') {
129
+ return { phase: GameServerPodPhase.Pending, message: `Pod is not yet ready: ${condReady?.message}`, details: condReady }
130
+ }
131
+
132
+ // resolvePodContainersConditions(pod) // DEBUG DEBUG enable to print container state for running pods
133
+ return { phase: GameServerPodPhase.Ready, message: 'Pod is ready to serve traffic' }
134
+ }
135
+
136
+ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
137
+ // console.log('Pod.status:', JSON.stringify(pod.status, undefined, 2))
138
+
139
+ if (!pod.status) {
140
+ return { phase: GameServerPodPhase.Unknown, message: 'Unable to access pod.status from Kubernetes' }
141
+ }
142
+
143
+ // Handle status.phase
144
+ const podPhase = pod.status?.phase
145
+ switch (podPhase) {
146
+ case 'Pending':
147
+ // Pod not yet scheduled
148
+ return { phase: GameServerPodPhase.Pending, message: 'Pod is still in Pending phase' }
149
+
150
+ case 'Running':
151
+ // Pod has been scheduled and start -- note that the containers may have failed!
152
+ return resolvePodStatusConditions(pod)
153
+
154
+ case 'Succeeded': // Should not happen, the game server pods should never stop
155
+ case 'Failed': // Should not happen, the game server pods should never stop
156
+ case 'Unknown':
157
+ default:
158
+ return { phase: GameServerPodPhase.Unknown, message: `Invalid pod.status.phase: ${podPhase}` }
159
+ }
160
+ }
161
+
162
+ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
163
+ console.log('Fetching logs for pod..')
164
+ const podName = pod.metadata?.name
165
+ const namespace = pod.metadata?.namespace
166
+ const containerName = pod.spec?.containers[0].name // \todo Handle multiple containers?
167
+ if (!podName || !namespace || !containerName) {
168
+ throw new Error('Unable to determine pod metadata')
169
+ }
170
+
171
+ const pretty = 'True'
172
+ const previous = false
173
+ const tailLines = 100
174
+ const timestamps = true
175
+ try {
176
+ const response = await k8sApi.readNamespacedPodLog(podName, namespace, containerName, undefined, undefined, undefined, pretty, previous, undefined, tailLines, timestamps)
177
+ return response.body
178
+ } catch (error) {
179
+ // \todo Better error handling ..
180
+ console.log('Failed to fetch pod logs from Kubernetes:', error)
181
+ throw new Error('Failed to fetch pod logs from Kubernetes')
182
+ }
183
+ }
184
+
185
+ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
186
+ // console.log('Pod:', JSON.stringify(pod, undefined, 2))
187
+
188
+ // Classify game server status
189
+ const podStatus = resolvePodStatus(pod)
190
+
191
+ // If game server launch failed, get the error logs
192
+ if (podStatus.phase === GameServerPodPhase.Failed) {
193
+ const logs = await fetchPodLogs(k8sApi, pod)
194
+ console.log('Pod logs:\n' + logs)
195
+ }
196
+
197
+ console.log(`Pod ${pod.metadata?.name} status:`, podStatus)
198
+ return podStatus
199
+ }
200
+
201
+ async function delay (ms: number): Promise<void> {
202
+ return await new Promise<void>(resolve => setTimeout(resolve, ms))
203
+ }
204
+
205
+ export async function checkGameServerDeployment (namespace: string): Promise<number> {
206
+ logger.info(`Validating game server deployment in namespace ${namespace}`)
207
+
208
+ // Check that the KUBECONFIG environment variable exists
209
+ const kubeconfigPath = process.env.KUBECONFIG
210
+ if (!kubeconfigPath) {
211
+ throw new Error('The KUBECONFIG environment variable must be specified')
212
+ }
213
+
214
+ // Check that the kubeconfig file exists
215
+ if (!await existsSync(kubeconfigPath)) {
216
+ throw new Error(`The environment variable KUBECONFIG points to a file '${kubeconfigPath}' that doesn't exist`)
217
+ }
218
+
219
+ // Create Kubernetes API instance (with default kubeconfig)
220
+ const kc = new KubeConfig()
221
+ kc.loadFromFile(kubeconfigPath)
222
+ const k8sApi = kc.makeApiClient(CoreV1Api)
223
+
224
+ // Figure out when to stop
225
+ const startTime = Date.now()
226
+ const timeoutAt = startTime + 1 * 60 * 1000 // 5min
227
+
228
+ while (true) {
229
+ // Check pod states
230
+ const pods = await fetchGameServerPods(k8sApi, namespace)
231
+ const podStatus = await checkGameServerPod(k8sApi, pods[0]) // \todo Handle all pods
232
+
233
+ switch (podStatus.phase) {
234
+ case GameServerPodPhase.Ready:
235
+ console.log('Gameserver successfully started')
236
+ // \todo add further readiness checks -- ping endpoint, ping dashboard, other checks?
237
+ return 0
238
+
239
+ case GameServerPodPhase.Failed:
240
+ console.log('Gameserver failed to start')
241
+ return 1
242
+
243
+ case GameServerPodPhase.Pending:
244
+ console.log('Gameserver still starting')
245
+ break
246
+
247
+ case GameServerPodPhase.Unknown:
248
+ default:
249
+ console.log('Gameserver in unknown state')
250
+ break
251
+ }
252
+
253
+ if (Date.now() >= timeoutAt) {
254
+ logger.error('Timeout while waiting for gameserver to initialize')
255
+ return 124 // timeout
256
+ }
257
+
258
+ await delay(1000)
259
+ }
260
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
- import { loginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './auth.js'
3
+ import { loginAndSaveTokens, machineLoginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './auth.js'
4
4
  import { StackAPI } from './stackapi.js'
5
+ import { checkGameServerDeployment } from './deployment.js'
5
6
  import { setLogLevel } from './logging.js'
6
7
  import { exit } from 'process'
7
8
 
@@ -10,7 +11,7 @@ const program = new Command()
10
11
  program
11
12
  .name('metaplay-auth')
12
13
  .description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
13
- .version('1.1.5')
14
+ .version('1.1.7')
14
15
  .option('-d, --debug', 'enable debug output')
15
16
  .hook('preAction', (thisCommand) => {
16
17
  // Handle debug flag for all commands.
@@ -28,6 +29,34 @@ program.command('login')
28
29
  await loginAndSaveTokens()
29
30
  })
30
31
 
32
+ program.command('machine-login')
33
+ .description('login to the Metaplay cloud using a machine account (using credentials in environment variable METAPLAY_CREDENTIALS)')
34
+ .option('--dev-credentials', 'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!')
35
+ .action(async (options) => {
36
+ // Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
37
+ let credentials
38
+ if (options.devCredentials) {
39
+ credentials = options.devCredentials
40
+ } else {
41
+ credentials = process.env.METAPLAY_CREDENTIALS
42
+ if (!credentials || credentials === '') {
43
+ throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!')
44
+ }
45
+ }
46
+
47
+ // Parse the clientId and clientSecret from the credentials (separate by a '+' character)
48
+ // \note We can't be certain that the secret does not contain pluses so split at the first occurrence
49
+ const splitOffset = credentials.indexOf('+')
50
+ if (splitOffset === -1) {
51
+ throw new Error('Invalid format for credentials, you should copy-paste the value from the developer portal verbatim')
52
+ }
53
+ const clientId = credentials.substring(0, splitOffset)
54
+ const clientSecret = credentials.substring(splitOffset + 1)
55
+
56
+ // Login with machine user and save the tokens
57
+ await machineLoginAndSaveTokens(clientId, clientSecret)
58
+ })
59
+
31
60
  program.command('logout')
32
61
  .description('log out of your Metaplay account')
33
62
  .action(async () => {
@@ -50,7 +79,7 @@ program.command('show-tokens')
50
79
  .hook('preAction', async () => {
51
80
  await extendCurrentSession()
52
81
  })
53
- .action(async () => {
82
+ .action(async (options) => {
54
83
  try {
55
84
  // TODO: Could detect if not logged in and fail more gracefully?
56
85
  const tokens = await loadTokens()
@@ -81,7 +110,7 @@ program.command('get-kubeconfig')
81
110
  throw new Error('Could not determine a deployment to fetch the KubeConfigs from. You need to specify either a gameserver or an organization, project, and environment')
82
111
  }
83
112
 
84
- const stackApi = new StackAPI(tokens.id_token, tokens.access_token)
113
+ const stackApi = new StackAPI(tokens.access_token)
85
114
  if (options.stackApi) {
86
115
  stackApi.stack_api_base_uri = options.stackApi
87
116
  }
@@ -121,7 +150,7 @@ program.command('get-aws-credentials')
121
150
  throw new Error('Could not determine a deployment to fetch the AWS credentials from. You need to specify either a gameserver or an organization, project, and environment')
122
151
  }
123
152
 
124
- const stackApi = new StackAPI(tokens.id_token, tokens.access_token)
153
+ const stackApi = new StackAPI(tokens.access_token)
125
154
  if (options.stackApi) {
126
155
  stackApi.stack_api_base_uri = options.stackApi
127
156
  }
@@ -166,7 +195,7 @@ program.command('get-environment')
166
195
  throw new Error('Could not determine a deployment to fetch environment details from. You need to specify either a gameserver or an organization, project, and environment')
167
196
  }
168
197
 
169
- const stackApi = new StackAPI(tokens.id_token, tokens.access_token)
198
+ const stackApi = new StackAPI(tokens.access_token)
170
199
  if (options.stackApi) {
171
200
  stackApi.stack_api_base_uri = options.stackApi
172
201
  }
@@ -183,4 +212,24 @@ program.command('get-environment')
183
212
  }
184
213
  })
185
214
 
215
+ program.command('check-deployment')
216
+ .description('[experimental] check that a game server was successfully deployed, or print out useful error messages in case of failure')
217
+ .argument('[namespace]', 'kubernetes namespace of the deployment')
218
+ .action(async (namespace: string) => {
219
+ setLogLevel(0)
220
+
221
+ try {
222
+ if (!namespace) {
223
+ throw new Error('Must specify value for argument "namespace"')
224
+ }
225
+
226
+ // Run the checks and exit with success/failure exitCode depending on result
227
+ const exitCode = await checkGameServerDeployment(namespace)
228
+ exit(exitCode)
229
+ } catch (error) {
230
+ console.error(`Failed to check deployment status: ${error}`)
231
+ exit(1)
232
+ }
233
+ })
234
+
186
235
  void program.parseAsync()
@@ -65,7 +65,7 @@ function decrypt (text: string): string {
65
65
  return decrypted.toString()
66
66
  }
67
67
 
68
- export async function setSecret (key: string, value: string): Promise<void> {
68
+ export async function setSecret (key: string, value: any): Promise<void> {
69
69
  logger.debug(`Setting secret ${key}...`)
70
70
 
71
71
  const secrets = await loadSecrets()
@@ -75,7 +75,7 @@ export async function setSecret (key: string, value: string): Promise<void> {
75
75
  await fs.writeFile(filePath, content)
76
76
  }
77
77
 
78
- export async function getSecret (key: string): Promise<string | null> {
78
+ export async function getSecret (key: string): Promise<any | undefined> {
79
79
  logger.debug(`Getting secret ${key}...`)
80
80
 
81
81
  const secrets = await loadSecrets()
package/src/stackapi.ts CHANGED
@@ -16,21 +16,16 @@ interface GameserverId {
16
16
  }
17
17
 
18
18
  export class StackAPI {
19
- private readonly id_token: string
20
- private readonly access_token: string
19
+ private readonly accessToken: string
21
20
 
22
21
  private _stack_api_base_uri: string
23
22
 
24
- constructor (idToken: string | null, accessToken: string | null) {
25
- if (idToken == null) {
26
- throw new Error('id_token is missing')
27
- }
23
+ constructor (accessToken: string) {
28
24
  if (accessToken == null) {
29
- throw new Error('access_token is missing')
25
+ throw new Error('accessToken must be provided')
30
26
  }
31
27
 
32
- this.id_token = idToken
33
- this.access_token = accessToken
28
+ this.accessToken = accessToken
34
29
  this._stack_api_base_uri = 'https://infra.p1.metaplay.io/stackapi'
35
30
  }
36
31
 
@@ -62,13 +57,13 @@ export class StackAPI {
62
57
  const response = await fetch(url, {
63
58
  method: 'POST',
64
59
  headers: {
65
- Authorization: `Bearer ${this.id_token}`,
60
+ Authorization: `Bearer ${this.accessToken}`,
66
61
  'Content-Type': 'application/json'
67
62
  }
68
63
  })
69
64
 
70
65
  if (response.status !== 200) {
71
- throw new Error(`Failed to fetch AWS credetials: ${response.statusText}`)
66
+ throw new Error(`Failed to fetch AWS credentials: ${response.statusText}, response code=${response.status}`)
72
67
  }
73
68
 
74
69
  return await response.json()
@@ -94,7 +89,7 @@ export class StackAPI {
94
89
  const response = await fetch(url, {
95
90
  method: 'POST',
96
91
  headers: {
97
- Authorization: `Bearer ${this.id_token}`,
92
+ Authorization: `Bearer ${this.accessToken}`,
98
93
  'Content-Type': 'application/json'
99
94
  }
100
95
  })
@@ -125,7 +120,7 @@ export class StackAPI {
125
120
  const response = await fetch(url, {
126
121
  method: 'GET',
127
122
  headers: {
128
- Authorization: `Bearer ${this.id_token}`,
123
+ Authorization: `Bearer ${this.accessToken}`,
129
124
  'Content-Type': 'application/json'
130
125
  }
131
126
  })