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