Skip to content

Commit 47d49b7

Browse files
committed
Replacement for 'odo init' command #5788
CRW-10965 - Migrate the odo init command Fixes: #5788 Issue: https://redhat.atlassian.net/browse/CRW-10965 Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent b8265bb commit 47d49b7

13 files changed

Lines changed: 1395 additions & 563 deletions

File tree

Lines changed: 100 additions & 389 deletions
Large diffs are not rendered by default.

src/devfile/devfileResolver.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*-----------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See LICENSE file in the project root for license information.
4+
*-----------------------------------------------------------------------------------------------*/
5+
6+
import * as fs from 'fs/promises';
7+
import * as yaml from 'js-yaml';
8+
import path from 'path';
9+
import { DevfileInfo } from '../devfile-registry/devfileInfo';
10+
import { DevfileRegistry } from '../devfile-registry/devfileRegistryWrapper';
11+
12+
export class DevfileResolver {
13+
14+
private static parentCache = new Map<string, any>();
15+
16+
static invalidateCache() {
17+
this.parentCache.clear();
18+
}
19+
20+
async resolve(devfile: any): Promise<any> {
21+
const chain = await this.resolveParentChain(devfile);
22+
const mergedDevfile = this.mergeChain(chain);
23+
24+
return this.normalizeResolvedDevfile(mergedDevfile);
25+
}
26+
27+
private normalizeResolvedDevfile(devfile: any): any {
28+
const result = { ...devfile };
29+
if (result?.parent) {
30+
delete result.parent;
31+
}
32+
return result;
33+
}
34+
35+
async loadDevfile(path: string): Promise<any> {
36+
const raw = await fs.readFile(path, 'utf-8');
37+
return path.endsWith('.json') ? JSON.parse(raw) : yaml.load(raw);
38+
}
39+
40+
static async resolveDevfilePath(devfilePath: string): Promise<string | undefined> {
41+
try {
42+
const stats = await fs.stat(devfilePath);
43+
44+
if (stats.isFile()) {
45+
return devfilePath;
46+
}
47+
48+
if (stats.isDirectory()) {
49+
const candidates = ['devfile.yaml', 'devfile.yml', 'devfile.json'];
50+
51+
for (const file of candidates) {
52+
const fullPath = path.join(devfilePath, file);
53+
try {
54+
await fs.access(fullPath);
55+
return fullPath;
56+
} catch {
57+
// ignore
58+
}
59+
}
60+
}
61+
} catch {
62+
// ignore
63+
}
64+
return undefined;
65+
}
66+
67+
private async resolveParentChain(devfile: any): Promise<any[]> {
68+
const chain: any[] = [];
69+
const visited = new Set<string>();
70+
71+
let current = devfile;
72+
73+
while (current?.parent) {
74+
const key = this.getParentKey(current.parent);
75+
76+
if (visited.has(key)) {
77+
throw new Error(` ✗ Circular parent reference detected: ${key}`);
78+
}
79+
visited.add(key);
80+
81+
const parent = await this.fetchParentDevfile(current.parent);
82+
83+
chain.unshift(parent);
84+
current = parent;
85+
}
86+
87+
chain.push(devfile);
88+
89+
return chain;
90+
}
91+
92+
private normalizeParent(parent: any) {
93+
return {
94+
registry: (parent.registryUrl || 'https://registry.devfile.io').replace(/\/$/, ''),
95+
id: parent.id,
96+
version: parent.version || 'latest',
97+
};
98+
}
99+
100+
private getParentKey(parent: any): string {
101+
const p = this.normalizeParent(parent);
102+
return `${p.registry}|${p.id}|${p.version}`;
103+
}
104+
105+
private normalizeVersion(v?: string): string | undefined {
106+
if (!v) return undefined;
107+
108+
const parts = v.split('.');
109+
110+
// normalize 2.2 → 2.2.0
111+
if (parts.length === 2) {
112+
return `${parts[0]}.${parts[1]}.0`;
113+
}
114+
115+
return v;
116+
}
117+
118+
private resolveVersionEntry(stack: DevfileInfo, requested?: string) {
119+
const normalizedRequested = this.normalizeVersion(requested);
120+
121+
return (
122+
stack.versions.find(v =>
123+
this.normalizeVersion(v.version) === normalizedRequested
124+
)
125+
?? stack.versions.find(v => v.version === requested)
126+
?? stack.versions.find(v => v.default)
127+
?? stack.versions[0]
128+
);
129+
}
130+
131+
private async fetchFromRegistry(parent: any): Promise<any> {
132+
const p = this.normalizeParent(parent);
133+
134+
const registry = DevfileRegistry.Instance;
135+
const index = await registry.getDevfileInfoList(p.registry);
136+
const stack = index.find((s: DevfileInfo) => s.name === p.id);
137+
if (!stack) {
138+
throw new Error(`Stack not found: ${p.id}`);
139+
}
140+
141+
const versionEntry = this.resolveVersionEntry(stack, p.version);
142+
if (!versionEntry) {
143+
throw new Error(`Version not found: ${p.id}@${p.version}`);
144+
}
145+
146+
return registry.getRegistryDevfile(
147+
p.registry,
148+
p.id,
149+
versionEntry.version
150+
);
151+
}
152+
153+
private async fetchParentDevfile(parent: any): Promise<any> {
154+
const key = this.getParentKey(parent);
155+
156+
const cached = DevfileResolver.parentCache.get(key);
157+
if (cached) {
158+
return cached;
159+
}
160+
161+
try {
162+
const devfile = await this.fetchFromRegistry(parent);
163+
164+
DevfileResolver.parentCache.set(key, devfile);
165+
166+
return devfile;
167+
} catch (err: any) {
168+
throw new Error(` ✗ Failed to fetch parent devfile ${key}: ${err?.message || err}`);
169+
}
170+
}
171+
172+
private mergeChain(chain: any[]): any {
173+
return chain.reduce((acc, curr) => this.mergeDevfiles(acc, curr));
174+
}
175+
176+
private mergeDevfiles(parent: any, child: any): any {
177+
return {
178+
...parent,
179+
...child,
180+
metadata: this.deepMerge(parent.metadata, child.metadata),
181+
components: this.mergeByName(parent.components, child.components),
182+
commands: this.mergeById(parent.commands, child.commands),
183+
variables: this.deepMerge(parent.variables, child.variables),
184+
attributes: this.deepMerge(parent.attributes, child.attributes),
185+
events: this.deepMerge(parent.events, child.events),
186+
projects: child.projects ?? parent.projects,
187+
starterProjects: child.starterProjects ?? parent.starterProjects,
188+
};
189+
}
190+
191+
private getComponentType(comp: any): string | undefined {
192+
if (!comp) return undefined;
193+
194+
if (comp.container) return 'container';
195+
if (comp.kubernetes) return 'kubernetes';
196+
if (comp.image) return 'image';
197+
if (comp.volume) return 'volume';
198+
return undefined;
199+
}
200+
201+
private mergeComponents(parentComp: any, childComp: any): any {
202+
if (parentComp && childComp && !this.getComponentType(childComp) && this.getComponentType(parentComp)) {
203+
return parentComp;
204+
}
205+
206+
const parentType = this.getComponentType(parentComp);
207+
const childType = this.getComponentType(childComp);
208+
209+
if (!childType) {
210+
return parentComp ?? childComp;
211+
}
212+
213+
if (!parentComp) {
214+
return childComp;
215+
}
216+
217+
if (parentType && childType && parentType !== childType) {
218+
return childComp;
219+
}
220+
221+
const baseType = childType || parentType;
222+
223+
return {
224+
...parentComp,
225+
...childComp,
226+
...(baseType
227+
? {
228+
[baseType]: this.deepMerge(
229+
parentComp?.[baseType],
230+
childComp?.[baseType]
231+
)
232+
}
233+
: {})
234+
};
235+
}
236+
237+
private mergeByName(parent: any[] = [], child: any[] = []) {
238+
const map = new Map<string, any>();
239+
240+
for (const item of parent) {
241+
map.set(item.name, item);
242+
}
243+
244+
for (const item of child) {
245+
if (map.has(item.name)) {
246+
const merged = this.mergeComponents(map.get(item.name), item);
247+
map.set(item.name, merged);
248+
} else {
249+
map.set(item.name, item);
250+
}
251+
}
252+
253+
return Array.from(map.values());
254+
}
255+
256+
private mergeById(parent: any[] = [], child: any[] = []) {
257+
const map = new Map<string, any>();
258+
259+
for (const item of parent) {
260+
map.set(item.id, item);
261+
}
262+
263+
for (const item of child) {
264+
if (map.has(item.id)) {
265+
map.set(item.id, this.deepMerge(map.get(item.id), item));
266+
} else {
267+
map.set(item.id, item);
268+
}
269+
}
270+
271+
return Array.from(map.values());
272+
}
273+
274+
private deepMerge(target: any, source: any): any {
275+
if (!target) return source;
276+
if (!source) return target;
277+
278+
const result = { ...target };
279+
280+
for (const key of Object.keys(source)) {
281+
const srcVal = source[key];
282+
const tgtVal = target[key];
283+
284+
if (
285+
typeof srcVal === 'object' &&
286+
srcVal !== null &&
287+
!Array.isArray(srcVal)
288+
) {
289+
result[key] = this.deepMerge(tgtVal, srcVal);
290+
} else {
291+
result[key] = srcVal;
292+
}
293+
}
294+
295+
return result;
296+
}
297+
}

0 commit comments

Comments
 (0)