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  }