microsoft/qdk
Publicmirrored from https://github.com/microsoft/qdkAvailable
source/vscode/src/azure/networkRequests.ts
377lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | import { log } from "qsharp-lang"; |
| 5 | import { EventType, getUserAgent, sendTelemetryEvent } from "../telemetry"; |
| 6 | |
| 7 | export async function azureRequest( |
| 8 | uri: string, |
| 9 | token: string, |
| 10 | associationId?: string, |
| 11 | method = "GET", |
| 12 | body?: string, |
| 13 | ) { |
| 14 | const headers: [string, string][] = [ |
| 15 | ["Content-Type", "application/json"], |
| 16 | ["x-ms-useragent", getUserAgent()], |
| 17 | ]; |
| 18 | |
| 19 | // For API simpilcity and storage back-compat, any api key is passed via the 'token' field. |
| 20 | if (token.startsWith("apiKey=")) { |
| 21 | headers.push(["x-ms-quantum-api-key", token.substring(7)]); |
| 22 | } else { |
| 23 | headers.push(["Authorization", `Bearer ${token}`]); |
| 24 | } |
| 25 | |
| 26 | try { |
| 27 | log.debug(`Fetching ${uri} with method ${method}`); |
| 28 | log.trace("Request headers & body: ", headers, body); |
| 29 | const response = await fetch(uri, { |
| 30 | headers, |
| 31 | method, |
| 32 | body, |
| 33 | }); |
| 34 | |
| 35 | if (!response.ok) { |
| 36 | log.error("Azure request failed", response); |
| 37 | if (associationId) { |
| 38 | sendTelemetryEvent( |
| 39 | EventType.AzureRequestFailed, |
| 40 | { |
| 41 | reason: `request to azure returned code ${response.status}`, |
| 42 | associationId, |
| 43 | }, |
| 44 | {}, |
| 45 | ); |
| 46 | } |
| 47 | throw await getAzureQuantumError(response); |
| 48 | } |
| 49 | |
| 50 | log.debug(`Got response ${response.status} ${response.statusText}`); |
| 51 | const result = await response.json(); |
| 52 | log.trace("Response value: ", result); |
| 53 | |
| 54 | return result; |
| 55 | } catch (e) { |
| 56 | if (associationId) { |
| 57 | sendTelemetryEvent( |
| 58 | EventType.AzureRequestFailed, |
| 59 | { reason: `request to azure failed to return`, associationId }, |
| 60 | {}, |
| 61 | ); |
| 62 | } |
| 63 | log.error(`Failed to fetch ${uri}: ${e}`); |
| 64 | throw new Error(getErrorMessage(e)); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // Different enough to above to warrant it's own function |
| 69 | export async function storageRequest( |
| 70 | uri: string, |
| 71 | method: string, |
| 72 | token?: string, |
| 73 | proxy?: string, |
| 74 | extraHeaders?: [string, string][], |
| 75 | body?: string | Uint8Array, |
| 76 | associationId?: string, |
| 77 | ) { |
| 78 | const headers: [string, string][] = [ |
| 79 | ["x-ms-version", "2023-01-03"], |
| 80 | ["x-ms-date", new Date().toUTCString()], |
| 81 | ["x-ms-useragent", getUserAgent()], |
| 82 | ]; |
| 83 | if (token) { |
| 84 | // For API simpilcity and storage back-compat, any api key is passed via the 'token' field. |
| 85 | if (token.startsWith("apiKey=")) { |
| 86 | headers.push(["x-ms-quantum-api-key", token.substring(7)]); |
| 87 | } else { |
| 88 | headers.push(["Authorization", `Bearer ${token}`]); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | if (extraHeaders?.length) headers.push(...extraHeaders); |
| 93 | if (proxy) { |
| 94 | log.debug(`Setting x-proxy-to header to ${uri}`); |
| 95 | headers.push(["x-proxy-to", uri]); |
| 96 | uri = proxy; |
| 97 | } |
| 98 | try { |
| 99 | log.debug(`Fetching ${uri} with method ${method}`); |
| 100 | const response = await fetch(uri, { method, headers, body }); |
| 101 | if (!response.ok) { |
| 102 | log.error("Storage request failed", response); |
| 103 | if (associationId) { |
| 104 | sendTelemetryEvent( |
| 105 | EventType.StorageRequestFailed, |
| 106 | { |
| 107 | reason: `request to storage on azure returned code ${response.status}`, |
| 108 | associationId, |
| 109 | }, |
| 110 | {}, |
| 111 | ); |
| 112 | } |
| 113 | throw await getAzureStorageError(response); |
| 114 | } |
| 115 | log.debug(`Got response ${response.status} ${response.statusText}`); |
| 116 | return response; |
| 117 | } catch (e) { |
| 118 | log.error(`Failed to fetch ${uri}: ${e}`); |
| 119 | if (associationId) { |
| 120 | sendTelemetryEvent( |
| 121 | EventType.StorageRequestFailed, |
| 122 | { |
| 123 | reason: `request to storage on azure failed to return`, |
| 124 | associationId, |
| 125 | }, |
| 126 | {}, |
| 127 | ); |
| 128 | } |
| 129 | throw new Error(getErrorMessage(e)); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | class AzureError extends Error { |
| 134 | constructor(message: string) { |
| 135 | super(message); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | async function getAzureQuantumError(response: Response): Promise<AzureError> { |
| 140 | let error: { code: string; message: string } | undefined = undefined; |
| 141 | try { |
| 142 | const json = await response.json(); |
| 143 | // Extract the error data if it conforms to the Azure Quantum error schema defined in |
| 144 | // https://github.com/Azure/azure-rest-api-specs/blob/957fd518388828b31126417415b04f859b95c586/specification/quantum/data-plane/Microsoft.Quantum/preview/2022-09-12-preview/quantum.json#L1186 |
| 145 | if (json && json.error && json.error.code && json.error.message) { |
| 146 | error = json.error; |
| 147 | } |
| 148 | } catch { |
| 149 | /* empty */ |
| 150 | } |
| 151 | |
| 152 | let message; |
| 153 | if (error) { |
| 154 | message = `Azure Quantum request failed with status ${response.status}.\n${error.code}: ${error.message}`; |
| 155 | } else { |
| 156 | message = `Azure Quantum request failed with status ${response.status}.`; |
| 157 | } |
| 158 | return new AzureError(message); |
| 159 | } |
| 160 | |
| 161 | function getAzureStorageError(response: Response): AzureError { |
| 162 | // Azure Storage appears to uses headers and xml responses to communicate error data, |
| 163 | // but we have not seen yet these in practice. |
| 164 | // https://github.com/Azure/azure-rest-api-specs/blob/eb06c34581dc6f56868ee9cc811a51f0e1a50770/specification/storage/data-plane/Microsoft.BlobStorage/preview/2021-12-02/blob.json#L75C30-L75C30 |
| 165 | return new AzureError( |
| 166 | `Storage request failed with status ${response.status}.`, |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | // Generate a user friendly error message |
| 171 | function getErrorMessage(e: any): string { |
| 172 | if (e instanceof AzureError) { |
| 173 | return e.message; |
| 174 | } else if (e instanceof Error) { |
| 175 | return `Request failed: ${e.message}`; |
| 176 | } else { |
| 177 | return `Request failed.`; |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | export class AzureUris { |
| 182 | static readonly apiVersion = "2020-01-01"; |
| 183 | static readonly mgmtEndpoint = "https://management.azure.com"; |
| 184 | |
| 185 | static tenants() { |
| 186 | // https://learn.microsoft.com/en-us/rest/api/resources/tenants/list |
| 187 | return `${this.mgmtEndpoint}/tenants?api-version=${this.apiVersion}`; |
| 188 | } |
| 189 | |
| 190 | static subscriptions() { |
| 191 | // https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list |
| 192 | return `${this.mgmtEndpoint}/subscriptions?api-version=${this.apiVersion}`; |
| 193 | } |
| 194 | |
| 195 | static workspaces(subscriptionId: string) { |
| 196 | // https://github.com/Azure/azure-rest-api-specs/blob/main/specification/quantum/resource-manager/Microsoft.Quantum/preview/2022-01-10-preview/quantum.json#L221 |
| 197 | return `${this.mgmtEndpoint}/subscriptions/${subscriptionId}/providers/Microsoft.Quantum/workspaces?api-version=2022-01-10-preview`; |
| 198 | } |
| 199 | |
| 200 | static listKeys( |
| 201 | subscriptionId: string, |
| 202 | resourceGroupName: string, |
| 203 | workspaceName: string, |
| 204 | ) { |
| 205 | // https://github.com/Azure/azure-rest-api-specs/blob/main/specification/quantum/resource-manager/Microsoft.Quantum/preview/2023-11-13-preview/quantum.json#L419 |
| 206 | return `${this.mgmtEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Quantum/workspaces/${workspaceName}/listKeys?api-version=2023-11-13-preview`; |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | export class QuantumUris { |
| 211 | readonly apiVersion = "2022-09-12-preview"; |
| 212 | |
| 213 | constructor( |
| 214 | public endpoint: string, // e.g. "https://westus.quantum.azure.com" |
| 215 | public id: string, // e.g. "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/quantumResourcegroup/providers/Microsoft.Quantum/Workspaces/quantumworkspace1" |
| 216 | ) {} |
| 217 | |
| 218 | quotas() { |
| 219 | return `${this.endpoint}${this.id}/quotas?api-version=${this.apiVersion}`; |
| 220 | } |
| 221 | |
| 222 | providerStatus() { |
| 223 | return `${this.endpoint}${this.id}/providerStatus?api-version=${this.apiVersion}`; |
| 224 | } |
| 225 | |
| 226 | jobs(jobId?: string) { |
| 227 | return !jobId |
| 228 | ? `${this.endpoint}${this.id}/jobs?api-version=${this.apiVersion}` |
| 229 | : `${this.endpoint}${this.id}/jobs/${jobId}?api-version=${this.apiVersion}`; |
| 230 | } |
| 231 | |
| 232 | // Needs to POST an application/json payload such as: {"containerName": "job-073064ed-2a47-11ee-b8e7-010101010000","blobName":"outputData"} |
| 233 | sasUri() { |
| 234 | return `${this.endpoint}${this.id}/storage/sasUri?api-version=${this.apiVersion}`; |
| 235 | } |
| 236 | |
| 237 | storageProxy() { |
| 238 | return `${this.endpoint}${this.id}/storage/proxy?api-version=${this.apiVersion}`; |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | export class StorageUris { |
| 243 | // Requests use a Shared Access Signature. See https://learn.microsoft.com/en-us/rest/api/storageservices/service-sas-examples |
| 244 | |
| 245 | // x-ms-date header should be present in format: Sun, 06 Nov 1994 08:49:37 GMT |
| 246 | // See https://learn.microsoft.com/en-us/rest/api/storageservices/representation-of-date-time-values-in-headers |
| 247 | |
| 248 | readonly apiVersion = "2023-01-03"; // Pass as x-ms-version header (see https://learn.microsoft.com/en-us/rest/api/storageservices/versioning-for-the-azure-storage-services#authorize-requests-by-using-azure-ad-shared-key-or-shared-key-lite) |
| 249 | |
| 250 | // Same for PUT, with a status code of 201 if successful |
| 251 | getContainer(storageAccount: string, container: string, sas: string) { |
| 252 | return `https://${storageAccount}.blob.core.windows.net/${container}?restype=container&${sas}`; |
| 253 | } |
| 254 | |
| 255 | // Same for DELETE, with a status code of 202 if successful |
| 256 | // Also same URI for PUT, but must include the following headers: |
| 257 | // - x-ms-blob-type: BlockBlob |
| 258 | // - Content-Length: <n> |
| 259 | // It will return 201 if created. |
| 260 | getBlob( |
| 261 | storageAccount: string, |
| 262 | container: string, |
| 263 | blob: string, |
| 264 | sas: string, |
| 265 | ) { |
| 266 | return `https://${storageAccount}.blob.core.windows.net/${container}/${blob}?${sas}`; |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | // Put all the Response types in a namespace for easy importing |
| 271 | |
| 272 | // eslint-disable-next-line @typescript-eslint/no-namespace |
| 273 | export namespace ResponseTypes { |
| 274 | export type Tenant = { |
| 275 | id: string; |
| 276 | tenantId: string; |
| 277 | displayName: string; |
| 278 | }; |
| 279 | |
| 280 | export type TenantList = { |
| 281 | value: Array<Tenant>; |
| 282 | }; |
| 283 | |
| 284 | export type Subscription = { |
| 285 | id: string; |
| 286 | subscriptionId: string; |
| 287 | tenantId: string; |
| 288 | displayName: string; |
| 289 | }; |
| 290 | |
| 291 | export type SubscriptionList = { |
| 292 | value: Array<Subscription>; |
| 293 | }; |
| 294 | |
| 295 | export type Provider = { |
| 296 | providerId: string; |
| 297 | providerSku: string; |
| 298 | provisioningState: string; // e.g. 'Succeeded' |
| 299 | resourceUsageId: string; |
| 300 | }; |
| 301 | |
| 302 | export type Workspace = { |
| 303 | id: string; |
| 304 | name: string; |
| 305 | location: string; |
| 306 | properties: { |
| 307 | providers: Array<Provider>; |
| 308 | provisioningState: string; |
| 309 | storageAccount: string; // e.g. "/subscriptions/<guid>/resourceGroups/<name>/providers/Microsoft.Storage/storageAccounts/<id>" |
| 310 | endpointUri: string; // e.g. "https://<workspace-name>.westus.quantum.azure.com". Note: workspace-name should be removed. |
| 311 | }; |
| 312 | }; |
| 313 | |
| 314 | export type WorkspaceList = { |
| 315 | value: Array<Workspace>; |
| 316 | }; |
| 317 | |
| 318 | export type Quota = { |
| 319 | scope: string; |
| 320 | providerId: string; |
| 321 | period: string; |
| 322 | holds: number; |
| 323 | utilization: number; |
| 324 | limit: number; |
| 325 | }; |
| 326 | |
| 327 | export type QuotaList = { |
| 328 | nextLink: string | null; |
| 329 | value: Array<Quota>; |
| 330 | }; |
| 331 | |
| 332 | export type Target = { |
| 333 | id: string; |
| 334 | currentAvailability: "Available" | "Degraded" | "Unavailable"; |
| 335 | averageQueueTime: number; // minutes |
| 336 | statusPage: string; // url |
| 337 | }; |
| 338 | |
| 339 | export type ProviderStatus = { |
| 340 | id: string; |
| 341 | currentAvailability: "Available" | "Degraded" | "Unavailable"; |
| 342 | targets: Array<Target>; |
| 343 | }; |
| 344 | |
| 345 | export type ProviderStatusList = { |
| 346 | nextLink: string | null; |
| 347 | value: Array<ProviderStatus>; |
| 348 | }; |
| 349 | |
| 350 | export type Job = { |
| 351 | id: string; |
| 352 | name: string; |
| 353 | target: string; |
| 354 | containerUri: string; |
| 355 | inputDataUri: string; |
| 356 | outputDataUri: string; |
| 357 | inputDataFormat: string; |
| 358 | outputDataFormat: string; |
| 359 | inputParams: any; |
| 360 | status: "Waiting" | "Executing" | "Succeeded" | "Failed" | "Cancelled"; |
| 361 | creationTime: string; |
| 362 | beginExecutionTime: string; |
| 363 | endExecutionTime: string; |
| 364 | cancellationTime?: string; |
| 365 | costEstimate?: any; |
| 366 | errorData?: any; |
| 367 | }; |
| 368 | |
| 369 | export type JobList = { |
| 370 | nextLink: string | null; |
| 371 | value: Array<Job>; |
| 372 | }; |
| 373 | |
| 374 | export type SasUri = { |
| 375 | sasUri: string; |
| 376 | }; |
| 377 | } |
| 378 | |