Skip to content

Commit 94870f5

Browse files
Merge pull request #21 from platformercloud/hotfix/continue-import-with-fetch-failures
Continue import with fetch failures
2 parents ad2959c + de66e83 commit 94870f5

File tree

5 files changed

+126
-45
lines changed

5 files changed

+126
-45
lines changed

src/commands/import.ts

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import { flags } from '@oclif/command';
22
import { cli } from 'cli-ux';
33
import { Observable, of } from 'rxjs';
4-
import {
5-
concatMap,
6-
count,
7-
filter,
8-
mergeMap,
9-
tap,
10-
toArray,
11-
} from 'rxjs/operators';
4+
import { count, filter, mergeMap, tap, toArray } from 'rxjs/operators';
125
import Command from '../base-command';
6+
import { ensureTargetNamespace } from '../modules/apps/environment';
137
import { getClusterNamespaces, listClusters } from '../modules/cluster/api';
148
import {
159
getDefaultEnvironment,
@@ -29,7 +23,6 @@ import {
2923
import { writeHAR } from '../modules/util/fetch';
3024
import { tryValidateCommonFlags } from '../modules/util/validations';
3125
import chalk = require('chalk');
32-
import { ensureTargetNamespace } from '../modules/apps/environment';
3326

3427
export default class Apply extends Command {
3528
static description =
@@ -129,20 +122,18 @@ export default class Apply extends Command {
129122
);
130123
try {
131124
try {
132-
await of(...importGroups)
133-
.pipe(
134-
concatMap((group) => {
135-
const manifestOfGroup = group.getManifests();
136-
return applyManifests(
137-
manifestOfGroup,
138-
{ orgId, projectId, envId },
139-
{
140-
start: `Applying ${group.resourceTypes.description}`,
141-
}
142-
);
143-
})
144-
)
145-
.toPromise();
125+
for (const group of importGroups) {
126+
const manifestOfGroup = group.getManifests();
127+
await applyManifests(
128+
manifestOfGroup,
129+
{ orgId, projectId, envId },
130+
{
131+
start: `Applying ${group.resourceTypes.description}`,
132+
}
133+
);
134+
// if no apply failures occured, show warnings for fetch failures
135+
displayFetchFailures(group);
136+
}
146137
} catch (error) {
147138
// if error occurs, append msg to the running spinner
148139
cli.action.stop('Error occured');
@@ -315,10 +306,9 @@ async function printLogs(groups: ManifestImportGroup[]) {
315306
.pipe(
316307
filter((m) => m.state === ManifestState.SKIPPED),
317308
tap((manifest) => {
318-
const subTree = cli.tree();
319309
const kind = manifest.manifest.kind;
320310
const name = manifest.manifest.metadata.name;
321-
skippedTree.insert(`${kind} ${name}`, subTree);
311+
skippedTree.insert(`${kind} ${name}`);
322312
}),
323313
count(),
324314
tap((count) => {
@@ -331,6 +321,19 @@ async function printLogs(groups: ManifestImportGroup[]) {
331321
.toPromise();
332322
}
333323

324+
async function displayFetchFailures(group: ManifestImportGroup) {
325+
if (group.fetchFailures.length === 0) return;
326+
const tree = cli.tree();
327+
group.fetchFailures.forEach((failure) => {
328+
const subTree = cli.tree();
329+
tree.insert(chalk.yellow(`${failure.message}`), subTree);
330+
if (failure.cause) {
331+
subTree.insert(failure.cause);
332+
}
333+
});
334+
tree.display();
335+
}
336+
334337
function printSummary(statusArr: ManifestState[]) {
335338
const manifestCount = statusArr.length;
336339
const summary = new Map<ManifestState, number>();

src/modules/cluster/api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,10 @@ interface ClusterResourceQuery {
9696
apiVersion: string;
9797
kind: string;
9898
}
99-
interface ClusterResources {
99+
export interface ClusterResources {
100100
clusterID: string;
101101
payload: K8sObject[];
102102
}
103-
104103
/**
105104
* Query any resource on the Cluster
106105
* Tip: use kubectl api-versions and kubectl api-resources to figure out the exact

src/modules/errors/api-error.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ export default class APIError extends Error {
1313
get statusCode() {
1414
return this.response.status;
1515
}
16+
async data() {
17+
try {
18+
return await this.response.json();
19+
} catch (error) {
20+
return null;
21+
}
22+
}
1623
}

src/modules/gitops/manifest-group.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { defer, Observable, of } from 'rxjs';
1+
import { defer, EMPTY, Observable, of } from 'rxjs';
22
import {
3+
catchError,
34
filter,
45
map,
56
mergeAll,
@@ -9,7 +10,8 @@ import {
910
takeUntil,
1011
tap,
1112
} from 'rxjs/operators';
12-
import { queryResource } from '../cluster/api';
13+
import { ClusterResources, queryResource } from '../cluster/api';
14+
import APIError from '../errors/api-error';
1315
import { ImportType, ResourceType } from './manifest-import-types';
1416
import {
1517
ManifestObject,
@@ -28,6 +30,7 @@ interface ResourceQuery {
2830
export class ManifestImportGroup {
2931
resourceTypes: ImportType;
3032
#manifests: ManifestObject[] = [];
33+
fetchFailures: YamlFectchFailure[] = [];
3134
#observable: Observable<ManifestObject>;
3235
constructor(
3336
resourceTypes: ImportType,
@@ -38,6 +41,13 @@ export class ManifestImportGroup {
3841
this.resourceTypes = resourceTypes;
3942
this.#observable = of(...this.resourceTypes.types).pipe(
4043
mergeMap((r) => fetchResourcesOfType(query, r), 4),
44+
filter((v): v is ClusterResources => {
45+
if (v instanceof YamlFectchFailure) {
46+
this.fetchFailures.push(v);
47+
return false;
48+
}
49+
return true;
50+
}),
4151
takeUntil(skippedStateNotifier),
4252
map((v) => v.payload),
4353
mergeAll(),
@@ -70,16 +80,65 @@ function shouldImport(v: K8sObject, sourceNS: string) {
7080

7181
function fetchResourcesOfType(query: ResourceQuery, r: ResourceType) {
7282
const { orgId, projectId, clusterId, namespace } = query;
73-
return defer(() =>
74-
queryResource(
75-
{
76-
orgId,
77-
projectId,
78-
clusterId,
79-
apiVersion: r.apiVersion,
80-
kind: r.kind,
81-
},
82-
{ namespace }
83-
)
84-
).pipe(retry(2));
83+
return defer(async () => {
84+
try {
85+
const res = await queryResource(
86+
{
87+
orgId,
88+
projectId,
89+
clusterId,
90+
apiVersion: r.apiVersion,
91+
kind: r.kind,
92+
},
93+
{ namespace }
94+
);
95+
return res;
96+
} catch (err) {
97+
const data = err instanceof APIError ? await err.data() : null;
98+
const cause = isClusterResourcesFailure(data) ? data.error : null;
99+
throw new YamlFectchFailure(r, query, cause);
100+
}
101+
}).pipe(
102+
retry(2),
103+
catchError(async (err) => {
104+
if (err instanceof YamlFectchFailure) return err;
105+
return EMPTY;
106+
})
107+
);
108+
}
109+
110+
export class YamlFectchFailure extends Error {
111+
readonly resourceType: ResourceType;
112+
readonly resourceQuery: ResourceQuery;
113+
readonly cause: string | null;
114+
constructor(
115+
resourceType: ResourceType,
116+
resourceQuery: ResourceQuery,
117+
cause: string | null
118+
) {
119+
super(
120+
`Failed to fetch resources of kind : ${resourceType.kind} (${resourceType.apiVersion})`
121+
);
122+
this.cause = cause;
123+
this.resourceType = resourceType;
124+
this.resourceQuery = resourceQuery;
125+
}
126+
}
127+
128+
export interface ClusterResourcesFailure {
129+
clusterID: string;
130+
error: string;
131+
identifier: 'resource:get';
132+
requestID: string;
133+
status: 'fail';
134+
type: 'query';
135+
}
136+
export function isClusterResourcesFailure(
137+
data: any
138+
): data is ClusterResourcesFailure {
139+
if (typeof data !== 'object' || data === null) return false;
140+
if (!['status', 'error'].every((prop) => typeof data[prop] === 'string')) {
141+
return false;
142+
}
143+
return data['status'] === 'fail';
85144
}

src/modules/util/fetch.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import nodeFetch from 'node-fetch';
44
import { homedir } from 'os';
55
// @ts-ignore
66
import { createHarLog, withHar } from 'node-fetch-har';
7+
import { cli } from 'cli-ux';
78

89
const homeDir = homedir();
9-
const LOG_REQUESTS = false;
10+
const LOG_REQUESTS =
11+
process.env.DEBUG === '*' || process.env.DEBUG === 'REQUESTS';
1012

1113
export const { fetch, writeHAR } = (function () {
1214
if (!LOG_REQUESTS)
@@ -20,10 +22,21 @@ export const { fetch, writeHAR } = (function () {
2022
const fetch = withHar(nodeFetch, {
2123
onHarEntry: (entry: any) => har.push(entry),
2224
});
23-
2425
function writeHAR(fileName = `har-${new Date().toISOString()}.har`) {
25-
const filePath = path.join(homeDir, 'logs', fileName);
26-
fs.writeFileSync(filePath, JSON.stringify(createHarLog(har), null, 2));
26+
writeToHAR(fileName, har);
2727
}
2828
return { fetch, writeHAR };
2929
})();
30+
31+
function writeToHAR(fileName: string, logs: any[]) {
32+
const folderPath = path.join(homeDir, 'platformer-logs');
33+
const filePath = path.join(folderPath, fileName);
34+
try {
35+
fs.mkdirSync(folderPath, { recursive: true });
36+
} catch (error) {}
37+
try {
38+
fs.writeFileSync(filePath, JSON.stringify(createHarLog(logs), null, 2));
39+
} catch (error) {
40+
cli.error(`Failed to write to ${filePath}`);
41+
}
42+
}

0 commit comments

Comments
 (0)