forked from vscode-kubernetes-tools/vscode-kubernetes-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
explainer.ts
240 lines (224 loc) · 10.2 KB
/
explainer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
'use strict';
import request = require('request');
import * as kubernetes from '@kubernetes/client-node';
import * as pluralize from 'pluralize';
import { formatComplex, formatOne, Typed, formatType } from "./schema-formatting";
import { getActiveKubeconfig } from './components/config/config';
export function readSwagger(): Promise<any> {
const kc = new kubernetes.KubeConfig();
const kubeconfig = getActiveKubeconfig();
if (kubeconfig) {
kc.loadFromFile(kubeconfig);
} else {
kc.loadFromDefault();
}
return readSwaggerCore(kc);
}
function readSwaggerCore(kc: kubernetes.KubeConfig): Promise<any> {
const uri = `${kc.getCurrentCluster().server}/swagger.json`;
const opts: request.Options = {
url: uri,
};
kc.applyToRequest(opts);
return new Promise((resolve, reject) => {
request(uri, opts, (error, response, body) => {
if (error) {
reject(error);
return;
}
if (response.statusCode !== 200) {
reject(response);
return;
}
try {
const obj = JSON.parse(body);
resolve(obj);
} catch (ex) {
console.log(ex);
}
});
});
}
export function readExplanation(swagger: any, fieldsPath: string) {
const fields = fieldsPath.split('.');
const kindName = fields.shift();
const kindDef = findKindModel(swagger, kindName);
const text = chaseFieldPath(swagger, kindDef, kindName, fields);
return text;
}
function findKindModel(swagger: any, kindName: string): TypeModel {
// TODO: use apiVersion (e.g. v1, extensions/v1beta1) to help locate these
const v1def = findProperty(swagger.definitions, 'v1.' + kindName);
const v1beta1def = findProperty(swagger.definitions, 'v1beta1.' + kindName);
const kindDef = v1def || v1beta1def;
return kindDef;
}
function chaseFieldPath(swagger: any, currentProperty: TypeModel, currentPropertyName: string, fields: string[]) {
// What are our scenarios?
// 1. (ex: Deployment.[metadata]): We are at the end of the chain and
// are on a property with a $ref AND the $ref is of type 'object' and
// has a list of properties. List the NAME and DESCRIPTION of the current property
// plus the DESCRIPTION of the type (e.g. 'Standard object metadata.\n\n
// ObjectMeta is metadata that...'), followed by a list of properties
// of the type (name + type + description).
// 2. (ex: Deployment.[metadata].generation): We are in the midle of the chain,
// and are on a property with a $ref AND the $ref is of type 'object' and
// has a list of properties. Locate the property in the $ref corresponding to the NEXT
// element in the chain, and move to that.
// 2a. (ex: Deployment.[metadata].biscuits): We are in the middle of the chain,
// and are on a property with a $ref AND the $ref is of type 'object' and
// has a list of properties, BUT there is no property corresponding to the
// next element in the chain. Report an error; the kubectl message is
// "field 'biscuits' does not exist"
// 3. (ex: Deployment.metadata.[generation]): We are at the end of the chain
// and are on a property with a type and no $ref. List the NAME, TYPE and
// DESCRIPTION of the current property.
// 3a. (ex: Deployment.metadata.[generation].biscuits): We are NOT at the end of
// the chain, but are on a property with a type and no $ref. Treat as #3 and
// do not traverse (this is what kubectl does). Basically in #3 we are allowed
// to ignore the end-of-chain check.
// 4. (ex: Deployment.metadata.[annotations].*): We are in the middle of the chain,
// and are on a property WITHOUT a $ref but of type 'object'. This is an
// unstructured key-value store scenario. List the NAME, TYPE and DESCRIPTION
// of the current property.
// 5. (ex: Deployment.metadata.[creationTimestamp]): We are on a property with a $ref,
// BUT the type of the $ref is NOT 'object' and it does NOT have a list of properties.
// List the NAME of the property, the TYPE of the $ref and DESCRIPTION of the property.
// 6. (ex: [Deployment].metadata): We are in the middle of the chain, and are on a property
// WITHOUT a $ref, BUT it DOES have a list of properties. Locate the property in the list
// corresponding to the NEXT element in the chain, and move to that.
// 7. (ex: [Deployment]): We are at the end of the chain, and are on a property
// WITHOUT a $ref, BUT it DOES have a list of properties. List the NAME and DESCRIPTION
// of the current property, followed by a list of child properties.
//
// Algorithm:
// Are we on a property with a $ref?
// If YES:
// Does the $ref have a list of properties?
// If YES:
// Are we at the end of the chain?
// If YES:
// Case 1: List the NAME and DESCRIPTION of the current property, and the DESCRIPTION and CHILD PROPERTIES of the $ref.
// If NO:
// Does the $ref contain a property that matches the NEXT element in our chain?
// If YES:
// Case 2: Traverse to that property and recurse.
// If NO:
// Case 2a: Error: field does not exist
// If NO:
// Case 5: List the NAME of the current property, the TYPE of the $ref, and the DESCRIPTION of the current property.
// If NO:
// Does the current property have a list of properties?
// If YES:
// Are we at the end of the chain?
// If YES:
// Case 1: List the NAME and DESCRIPTION of the current property, and the CHILD PROPERTIES.
// If NO:
// Does the property list contain a property that matches the NEXT element in our chain?
// If YES:
// Case 2: Traverse to that property and recurse.
// If NO:
// Case 2a: Error: field does not exist
// If NO:
// Is the property of type 'object'?
// If YES:
// Case 4: List the NAME, TYPE and DESCRIPTION of the current property. (Ignore subsequent elements in the chain.)
// If NO:
// Case 3/3a: List the NAME, TYPE and DESCRIPTION of the current property. (Ignore subsequent elements in the chain.)
// [So cases 3, 3a and 4 are all the same really.]
const currentPropertyTypeRef = currentProperty.$ref || (currentProperty.items ? currentProperty.items.$ref : undefined);
if (currentPropertyTypeRef) {
const typeDefnPath: string[] = currentPropertyTypeRef.split('/');
typeDefnPath.shift();
const currentPropertyTypeInfo = findTypeDefinition(swagger, typeDefnPath);
if (currentPropertyTypeInfo) {
const typeRefProperties = currentPropertyTypeInfo.properties;
if (typeRefProperties) {
if (fields.length === 0) {
return formatComplex(currentPropertyName, currentProperty.description, currentPropertyTypeInfo.description, typeRefProperties);
} else {
const nextField = fields.shift();
const nextProperty = findProperty(typeRefProperties, nextField);
if (nextProperty) {
return chaseFieldPath(swagger, nextProperty, nextField, fields);
} else {
return explainError(nextField, 'field does not exist');
}
}
} else {
return formatOne(currentPropertyName, formatType(currentPropertyTypeInfo), currentProperty.description);
}
} else {
return explainError(currentPropertyTypeRef, 'unresolvable type reference');
}
} else {
const properties = currentProperty.properties;
if (properties) {
if (fields.length === 0) {
return formatComplex(currentPropertyName, currentProperty.description, undefined, properties);
} else {
const nextField = fields.shift();
const nextProperty = findProperty(properties, nextField);
if (nextProperty) {
return chaseFieldPath(swagger, nextProperty, nextField, fields);
} else {
return explainError(nextField, 'field does not exist');
}
}
} else {
return formatOne(currentPropertyName, formatType(currentProperty), currentProperty.description);
}
}
}
function explainError(header: string, error: string) {
return `**${header}:** ${error}`;
}
function singularizeVersionedName(name: string) {
const bits = name.split('.');
let lastBit = bits.pop();
lastBit = pluralize.singular(lastBit);
bits.push(lastBit);
return bits.join('.');
}
function findProperty(obj: any, name: string) {
const n = (name + "").toLowerCase();
for (const p in obj) {
const pinfo = obj[p];
if ((p + "").toLowerCase() === n) {
return pinfo;
}
const gvks = pinfo["x-kubernetes-group-version-kind"];
if (gvks && gvks.length && gvks.length > 0 && gvks[0]) {
const gvk = gvks[0];
const ver = gvk.version;
const kind = gvk.kind;
if (ver && kind) {
const vk = `${ver}.${kind}`;
if (vk.toLowerCase() === n) {
return pinfo;
}
}
}
}
const singname = singularizeVersionedName(name);
if (singname === name) {
return undefined;
} else {
return findProperty(obj, singname);
}
}
function findTypeDefinition(swagger: any, typeDefnPath: string[]): TypeModel | undefined {
let m = swagger;
for (const p of typeDefnPath) {
m = findProperty(m, p);
if (!m) {
return undefined;
}
}
return m;
}
// TODO: this isn't really a type model - it can be a type model (description + properties) *or* a property model (description + [type|$ref])
interface TypeModel extends Typed {
readonly description?: string;
readonly properties?: any;
}