✨ YAML/Markdown runbook support in Mission Explorer with API group → CNCF project mapping#4045
Conversation
Add support for importing YAML (.yaml/.yml) and Markdown (.md) files from public/private GitHub repos in the Mission Explorer. Files are analyzed for Kubernetes Custom Resources, and their API groups are mapped to CNCF projects via a new cross-reference mapping to determine which console-kb install missions they can replace or augment. New files: - apiGroupMapping.ts: Maps 50+ CRD API groups to CNCF projects - fileParser.ts: YAML/MD parser with CR detection and structured mission conversion - composer.ts: Holistic mission composition combining user YAML with matched community install missions - UnstructuredFilePreview.tsx: Preview UI for files that need AI conversion, showing detected projects and raw content Modified files: - MissionBrowser.tsx: Extended file filters, wired parser into selectNode and processLocalFile flows - DirectoryListing.tsx: File-type-aware icons (orange for YAML, green for MD, blue for JSON) - matcher.ts: API group expansion for scoring cluster-matched missions - types.ts: Added sourceFormat and detectedApiGroups to metadata - Mission Control types: Added importedMission and replacesInstallMission to PayloadProject - SolutionDefinitionPanel: Shows "your YAML" badge on projects with imported missions Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
✅ Deploy Preview for kubestellarconsole ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
👋 Hey @clubanderson — thanks for opening this PR!
This is an automated message. |
There was a problem hiding this comment.
Pull request overview
Adds YAML/Markdown import support to the Mission Explorer so users can bring in runbooks/manifests, detect referenced CNCF projects via CRD API groups, and preview/import non-mission files.
Changes:
- Introduces YAML/Markdown parsing (
fileParser.ts) and an API-group→CNCF project cross-reference (apiGroupMapping.ts). - Updates Mission Browser UX to accept
.yaml/.yml/.md, parse them on selection/upload, and show an unstructured preview when needed. - Adds a holistic mission composer (
composer.ts) and small UI/type extensions to surface “your YAML” in mission-control context.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/lib/missions/types.ts | Extends Mission metadata with sourceFormat and detectedApiGroups. |
| web/src/lib/missions/matcher.ts | Expands cluster keyword matching using API-group mapping (new logic). |
| web/src/lib/missions/fileParser.ts | New YAML/Markdown/JSON parser producing structured missions or unstructured previews. |
| web/src/lib/missions/composer.ts | New “holistic mission” composition helper (user mission + installer prerequisites). |
| web/src/lib/missions/apiGroupMapping.ts | New CRD API-group→CNCF project mapping + helpers. |
| web/src/components/missions/UnstructuredFilePreview.tsx | New UI to preview/import unstructured YAML/Markdown with detected projects. |
| web/src/components/missions/MissionBrowser.tsx | Accepts/loads YAML+MD, routes parsing to fileParser, and renders preview UI. |
| web/src/components/missions/browser/DirectoryListing.tsx | Shows distinct icons/colors for JSON vs YAML vs Markdown. |
| web/src/components/mission-control/types.ts | Adds optional importedMission and replacesInstallMission to payload project type. |
| web/src/components/mission-control/SolutionDefinitionPanel.tsx | Shows a “your YAML” badge when an imported mission is associated with a project. |
| title: `Apply ${fileName}`, | ||
| description: `Content imported from ${format === 'yaml' ? 'YAML' : 'Markdown'} file`, | ||
| ...(format === 'yaml' ? { yaml: content } : {}), | ||
| ...(format === 'markdown' ? { description: content } : {}), |
There was a problem hiding this comment.
In the generated steps object, description is assigned twice (once to a short summary, then overwritten with the full Markdown content when format === 'markdown'). This makes the step description unexpectedly become the entire file content. Consider using a different field (or only set description once) so the summary isn’t lost.
| ...(format === 'markdown' ? { description: content } : {}), | |
| ...(format === 'markdown' ? { markdown: content } : {}), |
| const mission: MissionExport = { | ||
| version: 'kc-mission-v1', | ||
| title: title as string, | ||
| description: (description ?? '') as string, | ||
| type: inferMissionType(title as string, description as string), |
There was a problem hiding this comment.
inferMissionType(title as string, description as string) can receive description as undefined at runtime (the cast is type-only), which will stringify to "undefined" and can affect keyword detection. Pass description ?? '' (or otherwise ensure a string) before calling inferMissionType.
| const mission: MissionExport = { | |
| version: 'kc-mission-v1', | |
| title: title as string, | |
| description: (description ?? '') as string, | |
| type: inferMissionType(title as string, description as string), | |
| const normalizedDescription = description ?? '' | |
| const mission: MissionExport = { | |
| version: 'kc-mission-v1', | |
| title: title as string, | |
| description: normalizedDescription, | |
| type: inferMissionType(title as string, normalizedDescription), |
|
|
||
| return { | ||
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | ||
| description: project | ||
| ? `Apply ${kind} resource for ${project.displayName}` | ||
| : `Apply ${kind} resource (${doc.apiVersion})`, | ||
| yaml: yaml.dump(doc, { indent: 2, lineWidth: -1 }), | ||
| command: `kubectl apply -f - <<'EOF'\n${yaml.dump(doc, { indent: 2, lineWidth: -1 })}EOF`, |
There was a problem hiding this comment.
The generated heredoc command relies on yaml.dump() ending with a trailing newline; otherwise EOF won’t be on its own line and kubectl apply -f - will fail. Add an explicit newline before the terminator (and ideally after EOF) so the command is always valid.
| return { | |
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | |
| description: project | |
| ? `Apply ${kind} resource for ${project.displayName}` | |
| : `Apply ${kind} resource (${doc.apiVersion})`, | |
| yaml: yaml.dump(doc, { indent: 2, lineWidth: -1 }), | |
| command: `kubectl apply -f - <<'EOF'\n${yaml.dump(doc, { indent: 2, lineWidth: -1 })}EOF`, | |
| const yamlText = yaml.dump(doc, { indent: 2, lineWidth: -1 }) | |
| return { | |
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | |
| description: project | |
| ? `Apply ${kind} resource for ${project.displayName}` | |
| : `Apply ${kind} resource (${doc.apiVersion})`, | |
| yaml: yamlText, | |
| command: `kubectl apply -f - <<'EOF'\n${yamlText}\nEOF\n`, |
|
|
||
| return { | ||
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | ||
| description: project | ||
| ? `Apply ${kind} resource for ${project.displayName}` | ||
| : `Apply ${kind} resource (${doc.apiVersion})`, | ||
| yaml: yaml.dump(doc, { indent: 2, lineWidth: -1 }), | ||
| command: `kubectl apply -f - <<'EOF'\n${yaml.dump(doc, { indent: 2, lineWidth: -1 })}EOF`, |
There was a problem hiding this comment.
yaml.dump(doc, ...) is called twice per step (once for step.yaml and again for the heredoc command). Cache the dumped YAML in a local variable to avoid duplicated work and guarantee the command and yaml fields stay identical.
| return { | |
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | |
| description: project | |
| ? `Apply ${kind} resource for ${project.displayName}` | |
| : `Apply ${kind} resource (${doc.apiVersion})`, | |
| yaml: yaml.dump(doc, { indent: 2, lineWidth: -1 }), | |
| command: `kubectl apply -f - <<'EOF'\n${yaml.dump(doc, { indent: 2, lineWidth: -1 })}EOF`, | |
| const docYaml = yaml.dump(doc, { indent: 2, lineWidth: -1 }) | |
| return { | |
| title: `Apply ${kind}${name ? ` "${name}"` : ''}`, | |
| description: project | |
| ? `Apply ${kind} resource for ${project.displayName}` | |
| : `Apply ${kind} resource (${doc.apiVersion})`, | |
| yaml: docYaml, | |
| command: `kubectl apply -f - <<'EOF'\n${docYaml}EOF`, |
| // Also expand from CRD API groups (e.g., "ray.io" → kuberay tags) | ||
| for (const r of cluster.resources) { | ||
| if (!r || !r.includes('.')) continue | ||
| const mapping = lookupProject(r) | ||
| if (mapping) { | ||
| expandedKeywords.add(mapping.project) | ||
| for (const tag of mapping.tags) expandedKeywords.add(tag) | ||
| } |
There was a problem hiding this comment.
cluster.resources (from useClusterContext) is currently populated with operator and Helm release names (e.g. "prometheus-operator", "cert-manager"), not CRD API group strings like "ray.io". Calling lookupProject(r) here will almost never match and adds work/confusion. Consider either feeding detected API groups into the cluster context explicitly, or removing this loop and relying on the existing RESOURCE_TO_PROJECTS expansion.
| // Build prerequisite names list | ||
| const prerequisites = [ | ||
| ...(userMission.prerequisites || []), | ||
| ...supplementaryMissions.map((m) => m.title), | ||
| ] |
There was a problem hiding this comment.
prerequisites concatenates userMission.prerequisites with supplementaryMissions.map(m => m.title) without deduplicating, so the same prerequisite can appear multiple times. Consider de-duping (and/or keeping stable order) before writing back to the composed mission.
| // Build prerequisite names list | |
| const prerequisites = [ | |
| ...(userMission.prerequisites || []), | |
| ...supplementaryMissions.map((m) => m.title), | |
| ] | |
| // Build prerequisite names list (deduplicated, stable order) | |
| const combinedPrerequisites = [ | |
| ...(userMission.prerequisites || []), | |
| ...supplementaryMissions.map((m) => m.title), | |
| ] | |
| const seenPrerequisites = new Set<string>() | |
| const prerequisites: string[] = [] | |
| for (const name of combinedPrerequisites) { | |
| if (!seenPrerequisites.has(name)) { | |
| seenPrerequisites.add(name) | |
| prerequisites.push(name) | |
| } | |
| } |
| /** | ||
| * Find a console-kb installer that matches a detected CNCF project. | ||
| * Matches on the `cncfProject` field or by install mission filename pattern. | ||
| */ | ||
| function findInstallerForProject( | ||
| project: ApiGroupMapping, | ||
| installers: MissionExport[], | ||
| ): MissionExport | null { | ||
| // Primary: match by cncfProject field | ||
| const byProject = installers.find( | ||
| (m) => m.cncfProject?.toLowerCase() === project.project.toLowerCase() | ||
| ) | ||
| if (byProject) return byProject | ||
|
|
||
| // Fallback: match by title containing the project name | ||
| const byTitle = installers.find( | ||
| (m) => m.title.toLowerCase().includes(project.project.toLowerCase()) | ||
| ) | ||
| if (byTitle) return byTitle |
There was a problem hiding this comment.
The comment says installers are matched “by install mission filename pattern”, but the implementation only matches on cncfProject or title.includes(project.project). Either update the comment to reflect reality or add the intended filename-based matching using project.installMission (if available in installer metadata).
| /** | ||
| * Parse a file into a MissionExport or an unstructured preview. | ||
| * | ||
| * Routes by file extension: | ||
| * - .json → JSON parse → validateMissionExport | ||
| * - .yaml/.yml → YAML parse → CR detection → structured or unstructured | ||
| * - .md → Markdown parse → extract steps from headings/code blocks | ||
| */ | ||
| export function parseFileContent(content: string, fileName: string): ParseResult { | ||
| const ext = getExtension(fileName) | ||
|
|
||
| switch (ext) { | ||
| case '.json': | ||
| return parseJsonFile(content) | ||
| case '.yaml': | ||
| case '.yml': | ||
| return parseYamlFile(content) | ||
| case '.md': | ||
| return parseMarkdownFile(content) | ||
| default: | ||
| // Unknown extension — try JSON first, then YAML | ||
| return parseWithFallback(content) | ||
| } |
There was a problem hiding this comment.
This PR adds substantial new parsing/composition logic (YAML multi-doc parsing, Markdown section/code-block extraction, API-group→project mapping), but there are no new unit tests covering these behaviors. Consider adding Vitest coverage (e.g., fileParser YAML multi-doc + CR wrapping, Markdown frontmatter + fenced blocks, and a couple of apiGroupMapping lookups) alongside existing web/src/lib/missions/__tests__/* tests.
34 new tests covering: - extractApiGroup: core/custom/subdomain API group extraction - lookupProject: exact match, subdomain match, unknown groups - deduplicateProjects: dedup by project name - parseFileContent (JSON): valid mission, K8s manifest wrap - parseFileContent (YAML): MissionExport format, single CR, multi-doc, core K8s, Karmada subdomain API groups - parseFileContent (MD): heading extraction, frontmatter, code blocks, mission type inference, unstructured fallback - composeHolisticMission: supplement/replace modes, tag merging, unmatched projects, multi-project composition Also fixes: - Added base istio.io mapping for subdomain detection - Fixed MD title extraction to prefer # heading over ## section Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Updated emitSolutionViewed → emitFixerViewed in the YAML/MD parser flow within MissionBrowser.tsx. All other changes auto-merged cleanly. Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
Summary
Key files
apiGroupMapping.tsfileParser.tscomposer.tsUnstructuredFilePreview.tsxHow it works
Compatibility with #4033 / console-kb #1828
The solution→fixer rename PRs are compatible. This PR:
missionClass: 'solution'anywhereinstallMissionfilenames (not directory paths) in the mappingvalidateMissionExport()for normalizationTest plan
.yamlfile containing aRayClusterCR → verify KubeRay detected.mdrunbook with## Stepheadings and fenced code → verify steps extracted.yamlfile → verify accepted (was previously filtered to .json only)npm run build) and type check (tsc --noEmit) pass cleanly