github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/shared/services/applications-service.ts (about) 1 import * as deepMerge from 'deepmerge'; 2 import {Observable} from 'rxjs'; 3 import {map, repeat, retry} from 'rxjs/operators'; 4 5 import * as models from '../models'; 6 import {isValidURL} from '../utils'; 7 import requests from './requests'; 8 9 interface QueryOptions { 10 fields: string[]; 11 exclude?: boolean; 12 selector?: string; 13 appNamespace?: string; 14 } 15 16 function optionsToSearch(options?: QueryOptions) { 17 if (options) { 18 return {fields: (options.exclude ? '-' : '') + options.fields.join(','), selector: options.selector || '', appNamespace: options.appNamespace || ''}; 19 } 20 return {}; 21 } 22 23 export class ApplicationsService { 24 constructor() {} 25 26 public list(projects: string[], options?: QueryOptions): Promise<models.ApplicationList> { 27 return requests 28 .get('/applications') 29 .query({projects, ...optionsToSearch(options)}) 30 .then(res => res.body as models.ApplicationList) 31 .then(list => { 32 list.items = (list.items || []).map(app => this.parseAppFields(app)); 33 return list; 34 }); 35 } 36 37 public get(name: string, appNamespace: string, refresh?: 'normal' | 'hard'): Promise<models.Application> { 38 const query: {[key: string]: string} = {}; 39 if (refresh) { 40 query.refresh = refresh; 41 } 42 if (appNamespace) { 43 query.appNamespace = appNamespace; 44 } 45 return requests 46 .get(`/applications/${name}`) 47 .query(query) 48 .then(res => this.parseAppFields(res.body)); 49 } 50 51 public getApplicationSyncWindowState(name: string, appNamespace: string): Promise<models.ApplicationSyncWindowState> { 52 return requests 53 .get(`/applications/${name}/syncwindows`) 54 .query({name, appNamespace}) 55 .then(res => res.body as models.ApplicationSyncWindowState); 56 } 57 58 public ociMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number): Promise<models.OCIMetadata> { 59 let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/ocimetadata`).query({appNamespace}); 60 if (sourceIndex !== null) { 61 r = r.query({sourceIndex}); 62 } 63 if (versionId !== null) { 64 r = r.query({versionId}); 65 } 66 return r.then(res => res.body as models.OCIMetadata); 67 } 68 69 public revisionMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number | null, versionId: number | null): Promise<models.RevisionMetadata> { 70 let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/metadata`).query({appNamespace}); 71 if (sourceIndex !== null) { 72 r = r.query({sourceIndex}); 73 } 74 if (versionId !== null) { 75 r = r.query({versionId}); 76 } 77 return r.then(res => res.body as models.RevisionMetadata); 78 } 79 80 public revisionChartDetails(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number | null): Promise<models.ChartDetails> { 81 let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/chartdetails`).query({appNamespace}); 82 if (sourceIndex !== null) { 83 r = r.query({sourceIndex}); 84 } 85 if (versionId !== null) { 86 r = r.query({versionId}); 87 } 88 return r.then(res => res.body as models.ChartDetails); 89 } 90 91 public resourceTree(name: string, appNamespace: string): Promise<models.ApplicationTree> { 92 return requests 93 .get(`/applications/${name}/resource-tree`) 94 .query({appNamespace}) 95 .then(res => res.body as models.ApplicationTree); 96 } 97 98 public watchResourceTree(name: string, appNamespace: string): Observable<models.ApplicationTree> { 99 return requests 100 .loadEventSource(`/stream/applications/${name}/resource-tree?appNamespace=${appNamespace}`) 101 .pipe(map(data => JSON.parse(data).result as models.ApplicationTree)); 102 } 103 104 public managedResources(name: string, appNamespace: string, options: {id?: models.ResourceID; fields?: string[]} = {}): Promise<models.ResourceDiff[]> { 105 return requests 106 .get(`/applications/${name}/managed-resources`) 107 .query(`appNamespace=${appNamespace.toString()}`) 108 .query({...options.id, fields: (options.fields || []).join(',')}) 109 .then(res => (res.body.items as any[]) || []) 110 .then(items => { 111 items.forEach(item => { 112 if (item.liveState) { 113 item.liveState = JSON.parse(item.liveState); 114 } 115 if (item.targetState) { 116 item.targetState = JSON.parse(item.targetState); 117 } 118 if (item.predictedLiveState) { 119 item.predictedLiveState = JSON.parse(item.predictedLiveState); 120 } 121 if (item.normalizedLiveState) { 122 item.normalizedLiveState = JSON.parse(item.normalizedLiveState); 123 } 124 }); 125 return items as models.ResourceDiff[]; 126 }); 127 } 128 129 public getManifest(name: string, appNamespace: string, revision: string): Promise<models.ManifestResponse> { 130 return requests 131 .get(`/applications/${name}/manifests`) 132 .query({name, revision, appNamespace}) 133 .then(res => res.body as models.ManifestResponse); 134 } 135 136 public updateSpec(appName: string, appNamespace: string, spec: models.ApplicationSpec): Promise<models.ApplicationSpec> { 137 return requests 138 .put(`/applications/${appName}/spec`) 139 .query({appNamespace}) 140 .send(spec) 141 .then(res => res.body as models.ApplicationSpec); 142 } 143 144 public update(app: models.Application, query: {validate?: boolean} = {}): Promise<models.Application> { 145 return requests 146 .put(`/applications/${app.metadata.name}`) 147 .query(query) 148 .send(app) 149 .then(res => this.parseAppFields(res.body)); 150 } 151 152 public create(app: models.Application): Promise<models.Application> { 153 // Namespace may be specified in the app name. We need to parse and 154 // handle it accordingly. 155 if (app.metadata.name.includes('/')) { 156 const nns = app.metadata.name.split('/', 2); 157 app.metadata.name = nns[1]; 158 app.metadata.namespace = nns[0]; 159 } 160 return requests 161 .post(`/applications`) 162 .send(app) 163 .then(res => this.parseAppFields(res.body)); 164 } 165 166 public delete(name: string, appNamespace: string, propagationPolicy: string): Promise<boolean> { 167 let cascade = true; 168 if (propagationPolicy === 'non-cascading') { 169 propagationPolicy = ''; 170 cascade = false; 171 } 172 return requests 173 .delete(`/applications/${name}`) 174 .query({ 175 cascade, 176 propagationPolicy, 177 appNamespace 178 }) 179 .send({}) 180 .then(() => true); 181 } 182 183 public watch(query?: {name?: string; resourceVersion?: string; projects?: string[]; appNamespace?: string}, options?: QueryOptions): Observable<models.ApplicationWatchEvent> { 184 const search = new URLSearchParams(); 185 if (query) { 186 if (query.name) { 187 search.set('name', query.name); 188 } 189 if (query.resourceVersion) { 190 search.set('resourceVersion', query.resourceVersion); 191 } 192 if (query.appNamespace) { 193 search.set('appNamespace', query.appNamespace); 194 } 195 } 196 if (options) { 197 const searchOptions = optionsToSearch(options); 198 search.set('fields', searchOptions.fields); 199 search.set('selector', searchOptions.selector); 200 search.set('appNamespace', searchOptions.appNamespace); 201 query?.projects?.forEach(project => search.append('projects', project)); 202 } 203 const searchStr = search.toString(); 204 const url = `/stream/applications${(searchStr && '?' + searchStr) || ''}`; 205 return requests 206 .loadEventSource(url) 207 .pipe(repeat()) 208 .pipe(retry()) 209 .pipe(map(data => JSON.parse(data).result as models.ApplicationWatchEvent)) 210 .pipe( 211 map(watchEvent => { 212 watchEvent.application = this.parseAppFields(watchEvent.application); 213 return watchEvent; 214 }) 215 ); 216 } 217 218 public sync( 219 name: string, 220 appNamespace: string, 221 revision: string, 222 prune: boolean, 223 dryRun: boolean, 224 strategy: models.SyncStrategy, 225 resources: models.SyncOperationResource[], 226 syncOptions?: string[], 227 retryStrategy?: models.RetryStrategy 228 ): Promise<boolean> { 229 return requests 230 .post(`/applications/${name}/sync`) 231 .send({ 232 appNamespace, 233 revision, 234 prune: !!prune, 235 dryRun: !!dryRun, 236 strategy, 237 resources, 238 syncOptions: syncOptions ? {items: syncOptions} : null, 239 retryStrategy 240 }) 241 .then(() => true); 242 } 243 244 public rollback(name: string, appNamespace: string, id: number, prune?: boolean): Promise<boolean> { 245 return requests 246 .post(`/applications/${name}/rollback`) 247 .send({id, appNamespace, prune}) 248 .then(() => true); 249 } 250 251 public getDownloadLogsURL( 252 applicationName: string, 253 appNamespace: string, 254 namespace: string, 255 podName: string, 256 resource: {group: string; kind: string; name: string}, 257 containerName: string 258 ): string { 259 const search = this.getLogsQuery({namespace, appNamespace, podName, resource, containerName, follow: false}); 260 search.set('download', 'true'); 261 return `api/v1/applications/${applicationName}/logs?${search.toString()}`; 262 } 263 264 public getContainerLogs(query: { 265 applicationName: string; 266 appNamespace: string; 267 namespace: string; 268 podName: string; 269 resource: {group: string; kind: string; name: string}; 270 containerName: string; 271 tail?: number; 272 follow?: boolean; 273 sinceSeconds?: number; 274 untilTime?: string; 275 filter?: string; 276 matchCase?: boolean; 277 previous?: boolean; 278 }): Observable<models.LogEntry> { 279 const {applicationName} = query; 280 const search = this.getLogsQuery(query); 281 const entries = requests.loadEventSource(`/applications/${applicationName}/logs?${search.toString()}`).pipe(map(data => JSON.parse(data).result as models.LogEntry)); 282 let first = true; 283 return new Observable(observer => { 284 const subscription = entries.subscribe( 285 entry => { 286 if (entry.last) { 287 first = true; 288 observer.complete(); 289 subscription.unsubscribe(); 290 } else { 291 observer.next({...entry, first}); 292 first = false; 293 } 294 }, 295 err => { 296 first = true; 297 observer.error(err); 298 }, 299 () => { 300 first = true; 301 observer.complete(); 302 } 303 ); 304 return () => subscription.unsubscribe(); 305 }); 306 } 307 308 public getResource(name: string, appNamespace: string, resource: models.ResourceNode): Promise<models.State> { 309 return requests 310 .get(`/applications/${name}/resource`) 311 .query({ 312 name: resource.name, 313 appNamespace, 314 namespace: resource.namespace, 315 resourceName: resource.name, 316 version: resource.version, 317 kind: resource.kind, 318 group: resource.group || '' // The group query param must be present even if empty. 319 }) 320 .then(res => res.body as {manifest: string}) 321 .then(res => JSON.parse(res.manifest) as models.State); 322 } 323 324 public getResourceActions(name: string, appNamespace: string, resource: models.ResourceNode): Promise<models.ResourceAction[]> { 325 return requests 326 .get(`/applications/${name}/resource/actions`) 327 .query({ 328 appNamespace, 329 namespace: resource.namespace, 330 resourceName: resource.name, 331 version: resource.version, 332 kind: resource.kind, 333 group: resource.group 334 }) 335 .then(res => { 336 const actions = (res.body.actions as models.ResourceAction[]) || []; 337 actions.sort((actionA, actionB) => actionA.name.localeCompare(actionB.name)); 338 return actions; 339 }); 340 } 341 342 public runResourceAction( 343 name: string, 344 appNamespace: string, 345 resource: models.ResourceNode, 346 action: string, 347 resourceActionParameters: models.ResourceActionParam[] 348 ): Promise<models.ResourceAction[]> { 349 return requests 350 .post(`/applications/${name}/resource/actions/v2`) 351 .send( 352 JSON.stringify({ 353 appNamespace, 354 namespace: resource.namespace, 355 resourceName: resource.name, 356 version: resource.version, 357 kind: resource.kind, 358 group: resource.group, 359 resourceActionParameters: resourceActionParameters, 360 action 361 }) 362 ) 363 .then(res => (res.body.actions as models.ResourceAction[]) || []); 364 } 365 366 public patchResource(name: string, appNamespace: string, resource: models.ResourceNode, patch: string, patchType: string): Promise<models.State> { 367 return requests 368 .post(`/applications/${name}/resource`) 369 .query({ 370 name: resource.name, 371 appNamespace, 372 namespace: resource.namespace, 373 resourceName: resource.name, 374 version: resource.version, 375 kind: resource.kind, 376 group: resource.group || '', // The group query param must be present even if empty. 377 patchType 378 }) 379 .send(JSON.stringify(patch)) 380 .then(res => res.body as {manifest: string}) 381 .then(res => JSON.parse(res.manifest) as models.State); 382 } 383 384 public deleteResource(applicationName: string, appNamespace: string, resource: models.ResourceNode, force: boolean, orphan: boolean): Promise<any> { 385 return requests 386 .delete(`/applications/${applicationName}/resource`) 387 .query({ 388 name: resource.name, 389 appNamespace, 390 namespace: resource.namespace, 391 resourceName: resource.name, 392 version: resource.version, 393 kind: resource.kind, 394 group: resource.group || '', // The group query param must be present even if empty. 395 force, 396 orphan 397 }) 398 .send() 399 .then(() => true); 400 } 401 402 public events(applicationName: string, appNamespace: string): Promise<models.Event[]> { 403 return requests 404 .get(`/applications/${applicationName}/events`) 405 .query({appNamespace}) 406 .send() 407 .then(res => (res.body as models.EventList).items || []); 408 } 409 410 public resourceEvents( 411 applicationName: string, 412 appNamespace: string, 413 resource: { 414 namespace: string; 415 name: string; 416 uid: string; 417 } 418 ): Promise<models.Event[]> { 419 return requests 420 .get(`/applications/${applicationName}/events`) 421 .query({ 422 appNamespace, 423 resourceUID: resource.uid, 424 resourceNamespace: resource.namespace, 425 resourceName: resource.name 426 }) 427 .send() 428 .then(res => (res.body as models.EventList).items || []); 429 } 430 431 public terminateOperation(applicationName: string, appNamespace: string): Promise<boolean> { 432 return requests 433 .delete(`/applications/${applicationName}/operation`) 434 .query({appNamespace}) 435 .send() 436 .then(() => true); 437 } 438 439 public getLinks(applicationName: string, namespace: string): Promise<models.LinksResponse> { 440 return requests 441 .get(`/applications/${applicationName}/links`) 442 .query({namespace}) 443 .send() 444 .then(res => res.body as models.LinksResponse); 445 } 446 447 public getResourceLinks(applicationName: string, appNamespace: string, resource: models.ResourceNode): Promise<models.LinksResponse> { 448 return requests 449 .get(`/applications/${applicationName}/resource/links`) 450 .query({ 451 name: resource.name, 452 appNamespace, 453 namespace: resource.namespace, 454 resourceName: resource.name, 455 version: resource.version, 456 kind: resource.kind, 457 group: resource.group || '' // The group query param must be present even if empty. 458 }) 459 .send() 460 .then(res => { 461 const links = res.body as models.LinksResponse; 462 const items: models.LinkInfo[] = []; 463 (links?.items || []).forEach(link => { 464 if (isValidURL(link.url)) { 465 items.push(link); 466 } 467 }); 468 links.items = items; 469 return links; 470 }); 471 } 472 473 private getLogsQuery(query: { 474 namespace: string; 475 appNamespace: string; 476 podName: string; 477 resource: {group: string; kind: string; name: string}; 478 containerName: string; 479 tail?: number; 480 follow?: boolean; 481 sinceSeconds?: number; 482 untilTime?: string; 483 filter?: string; 484 matchCase?: boolean; 485 previous?: boolean; 486 }): URLSearchParams { 487 const {appNamespace, containerName, namespace, podName, resource, tail, sinceSeconds, untilTime, filter, previous, matchCase} = query; 488 let {follow} = query; 489 if (follow === undefined || follow === null) { 490 follow = true; 491 } 492 const search = new URLSearchParams(); 493 search.set('appNamespace', appNamespace); 494 search.set('container', containerName); 495 search.set('namespace', namespace); 496 search.set('follow', follow.toString()); 497 if (podName) { 498 search.set('podName', podName); 499 } else { 500 search.set('group', resource.group); 501 search.set('kind', resource.kind); 502 search.set('resourceName', resource.name); 503 } 504 if (tail) { 505 search.set('tailLines', tail.toString()); 506 } 507 if (untilTime) { 508 search.set('untilTime', untilTime); 509 } 510 if (filter) { 511 search.set('filter', filter); 512 } 513 if (previous) { 514 search.set('previous', previous.toString()); 515 } 516 if (matchCase) { 517 search.set('matchCase', matchCase.toString()); 518 } 519 // The API requires that this field be set to a non-empty string. 520 if (sinceSeconds) { 521 search.set('sinceSeconds', sinceSeconds.toString()); 522 } else { 523 search.set('sinceSeconds', '0'); 524 } 525 return search; 526 } 527 528 private parseAppFields(data: any): models.Application { 529 data = deepMerge( 530 { 531 apiVersion: 'argoproj.io/v1alpha1', 532 kind: 'Application', 533 spec: { 534 project: 'default' 535 }, 536 status: { 537 resources: [], 538 summary: {} 539 } 540 }, 541 data 542 ); 543 544 return data as models.Application; 545 } 546 547 public async getApplicationSet(name: string, namespace: string): Promise<models.ApplicationSet> { 548 return requests 549 .get(`/applicationsets/${name}`) 550 .query({appsetNamespace: namespace}) 551 .then(res => res.body as models.ApplicationSet); 552 } 553 554 public async listApplicationSets(): Promise<models.ApplicationSetList> { 555 return requests.get(`/applicationsets`).then(res => res.body as models.ApplicationSetList); 556 } 557 }