Skip to content

✨ YAML/Markdown runbook support in Mission Explorer with API group → CNCF project mapping#4045

Merged
clubanderson merged 3 commits intomainfrom
feat/yaml-md-mission-support
Apr 1, 2026
Merged

✨ YAML/Markdown runbook support in Mission Explorer with API group → CNCF project mapping#4045
clubanderson merged 3 commits intomainfrom
feat/yaml-md-mission-support

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

  • Adds YAML (.yaml/.yml) and Markdown (.md) file support to the Mission Explorer, allowing users to import runbooks and K8s manifests from public/private GitHub repos
  • Introduces an API group → CNCF project cross-reference mapping (50+ CRD API groups) that detects which CNCF projects are referenced in user YAML files
  • Detected projects are matched to console-kb install missions, enabling "holistic missions" that combine user YAML with community install/fix missions
  • Files that can't be directly parsed as missions show an unstructured preview with detected K8s resources, CNCF project badges, and an import action

Key files

File Purpose
apiGroupMapping.ts Maps CRD API groups (ray.io, karmada.io, argoproj.io, etc.) to CNCF projects, tags, and install mission filenames
fileParser.ts Parses YAML (single/multi-document, CRs, MissionExport format) and Markdown (frontmatter, ## sections, fenced code blocks) into MissionExport
composer.ts Composes holistic missions: user YAML + matched console-kb installers with replace/supplement logic
UnstructuredFilePreview.tsx Preview UI for unstructured files showing detected API groups, CNCF projects, content analysis, and import action

How it works

User imports configs/ray-cluster.yaml from their repo
  → Parser detects: apiVersion: ray.io/v1alpha1, kind: RayCluster
  → lookupProject("ray.io") → { project: "kuberay", installMission: "install-kuberay.json" }
  → UI shows: "Detected: KubeRay — install mission available"
  → Composer can merge: [Install KubeRay steps] → [Apply RayCluster CR]
  → Mission appears in Mission Control as a deployable payload

Compatibility with #4033 / console-kb #1828

The solution→fixer rename PRs are compatible. This PR:

  • Does not hardcode missionClass: 'solution' anywhere
  • Uses installMission filenames (not directory paths) in the mapping
  • Delegates to validateMissionExport() for normalization

Test plan

  • Import a .yaml file containing a RayCluster CR → verify KubeRay detected
  • Import a multi-document YAML with CRs from different projects → verify all detected
  • Import a .md runbook with ## Step headings and fenced code → verify steps extracted
  • Import a MissionExport in YAML format → verify direct import (no conversion needed)
  • Drag-drop a .yaml file → verify accepted (was previously filtered to .json only)
  • Verify YAML files show orange FileCode icon, MD files show green FileText icon
  • Verify build (npm run build) and type check (tsc --noEmit) pass cleanly

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>
Copilot AI review requested due to automatic review settings April 1, 2026 00:21
@kubestellar-prow kubestellar-prow bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 1, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 1, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit eaa0a26
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69cc66c0a195cd000785b32d
😎 Deploy Preview https://deploy-preview-4045.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow bot added needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Apr 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 } : {}),
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
...(format === 'markdown' ? { description: content } : {}),
...(format === 'markdown' ? { markdown: content } : {}),

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +219
const mission: MissionExport = {
version: 'kc-mission-v1',
title: title as string,
description: (description ?? '') as string,
type: inferMissionType(title as string, description as string),
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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),

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +306

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`,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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`,

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +306

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`,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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`,

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +91
// 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)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +126
// Build prerequisite names list
const prerequisites = [
...(userMission.prerequisites || []),
...supplementaryMissions.map((m) => m.title),
]
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +176
/**
* 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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +83
/**
* 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)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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>
@kubestellar-prow kubestellar-prow bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 1, 2026
@clubanderson clubanderson merged commit 3d54093 into main Apr 1, 2026
16 of 19 checks passed
@kubestellar-prow kubestellar-prow bot deleted the feat/yaml-md-mission-support branch April 1, 2026 00:29
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants