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  }